const A='/api/';

const DEF={
 monkey:{n:'Monkey',m:8,e:'🐒',img:'assets/candidates/monkey.png'},rabbit:{n:'Rabbit',m:6,e:'🐇',img:'assets/candidates/rabbit.png'},lion:{n:'Lion',m:12,e:'🦁',img:'assets/candidates/lion.png'},panda:{n:'Panda',m:8,e:'🐼',img:'assets/candidates/panda.png'},
 swallow:{n:'Swallow',m:6,e:'🐦',img:'assets/candidates/swallow.png'},pigeon:{n:'Pigeon',m:8,e:'🕊️',img:'assets/candidates/pigeon.png'},peacock:{n:'Peacock',m:8,e:'🦚',img:'assets/candidates/peacock.png'},eagle:{n:'Eagle',m:12,e:'🦅',img:'assets/candidates/eagle.png'},
 shark:{n:'Shark',m:24,e:'🦈',img:'assets/candidates/shark.png'},frog:{n:'Golden Shark',m:100,e:'🦈',img:'assets/candidates/frog.png'},beast_group:{n:'Beast',m:2,e:'🐒'},bird_group:{n:'Bird',m:2,e:'🦅'},
 pay_all:{n:'Pay All',m:'',e:'🎁'},take_all:{n:'Take All',m:'',e:'💣'}
};

const TRACK=[
 'monkey','rabbit','rabbit','rabbit','frog','swallow','swallow','swallow','pigeon',
 'pigeon','pigeon','take_all','peacock','peacock',
 'peacock','eagle','eagle','eagle','shark','lion','lion','lion','panda',
 'panda','panda','pay_all','monkey','monkey'
];

const $=s=>document.querySelector(s);
const $$=s=>[...document.querySelectorAll(s)];

let token=localStorage.zoo_token||'';
let authMode=localStorage.zoo_auth_mode||'';
let round=null;
let queuedRound=null;
let phase='BOOT';
let chip=10;
let roundBet=0;
let lastRoundBets=[];
let busy=false;
let bettingOpen=false;
let localTimer=null;
let heartbeat=null;
let heartbeatBusy=false;
let historyHeartbeat=null;
let historyBusy=false;
let lastWinnerTrackIndex=null;
let transitionStarted=false;
let wheelTrail=[];
let betDeadlinePerf=0;
let displayedBetSecond=null;
let pendingDrawResult=null;
let inactivityWarningKey='';
let inactivityKickShown=false;
const pendingBetAreas=new Set();

const wheelVisualStyle=document.createElement('style');
wheelVisualStyle.id='luckzooWheelVisualLayerV2';
wheelVisualStyle.textContent=`
  .track-cell > .wheel-visual-layer{
    position:absolute!important;
    inset:0!important;
    width:100%!important;
    height:100%!important;
    display:block!important;
    pointer-events:none!important;
    box-sizing:border-box!important;
    border:0!important;
    border-radius:0!important;
    opacity:0;
    z-index:20!important;
    transition:none!important;
    animation:none!important;
    will-change:opacity,background,box-shadow;
  }
  .track-cell > .icon,
  .track-cell > .caption{
    position:relative!important;
    z-index:21!important;
  }
`;
document.head.appendChild(wheelVisualStyle);


function sleep(ms){return new Promise(r=>setTimeout(r,ms))}

function wheelWait(ms){
 return new Promise(resolve=>{
   const started=performance.now();
   function frame(now){
     if(now-started>=ms)resolve();
     else requestAnimationFrame(frame);
   }
   requestAnimationFrame(frame);
 });
}

function toast(t){
 const x=$('#toast');
 x.textContent=t;
 x.style.display='block';
 clearTimeout(x._t);
 x._t=setTimeout(()=>x.style.display='none',1600);
}

function showInactivityModal(message,kicked=false){
 if(document.getElementById('luckzooInactivityModal'))return;

 const overlay=document.createElement('div');
 overlay.id='luckzooInactivityModal';
 overlay.style.cssText='position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.72);display:flex;align-items:center;justify-content:center;padding:18px;isolation:isolate;pointer-events:auto';

 const box=document.createElement('div');
 box.style.cssText='position:relative;z-index:2;width:min(560px,94vw);background:linear-gradient(180deg,#008b56,#005f48);border:2px solid #18f39a;border-radius:18px;box-shadow:0 12px 45px rgba(0,0,0,.55);overflow:hidden;text-align:center;color:#fff';

 box.innerHTML=`
  <div style="padding:17px 20px;border-bottom:2px solid #18f39a;font-size:30px;font-weight:900;color:#fff51b">Tips</div>
  <div style="padding:62px 30px 54px;font-size:24px;line-height:1.35">${message}</div>
  <div style="padding:0 30px 24px">
   <button id="luckzooInactivityOk" style="min-width:220px;padding:14px 32px;border-radius:32px;border:2px solid #ffe36a;background:linear-gradient(#ff9d16,#ffd52d);font-size:25px;font-weight:900;cursor:pointer">OK</button>
  </div>`;

 overlay.appendChild(box);
 document.documentElement.appendChild(overlay);

 document.getElementById('luckzooInactivityOk').onclick=()=>{
   overlay.remove();
   if(kicked)location.replace('/lobby/');
 };
}

function applyInactivityState(j){
 const s=j?.inactivity;
 if(!s)return;

 if(s.kicked){
   bettingOpen=false;
   if(!inactivityKickShown){
     inactivityKickShown=true;
     showInactivityModal(
       'Due to inactivity, you have been removed from the current game session.',
       true
     );
   }
   return;
 }

 const n=Number(s.inactive_rounds||0);
 const step=Number(s.warning_step||0);

 if(s.warning && (step===1 || step===2)){
   const key=n+':'+step;
   if(inactivityWarningKey!==key){
     inactivityWarningKey=key;
     const message=step===2
       ? 'You are inactive. Place a bet now to remain in the current game session. This is your final warning round.'
       : 'You are inactive. Place a bet to remain in the current game session.';
     showInactivityModal(message,false);
   }
 }

 if(n===0){
   inactivityWarningKey='';
   inactivityKickShown=false;
 }
}

async function api(f,o={}){
 o.headers=Object.assign({'Content-Type':'application/json'},o.headers||{});
 if(token){
   if(authMode==='guest')o.headers['X-Guest-Session']=token;
   else o.headers.Authorization='Bearer '+token;
 }
 if(!o.method||o.method==='GET')o.cache='no-store';
 const r=await fetch(A+f,o);
 const j=await r.json();
 if(!r.ok||j.ok===false)throw Error(j.error||'Request failed');
 return j;
}

function playableBalance(w){
 return Number(w?.cash_balance||0)+Number(w?.bonus_balance||0);
}

function setBalance(v){
 const n=Number(v||0).toFixed(2);
 $('#bal').textContent=n;
 $('#topBal').textContent=n;
}


async function syncPlayerIdentity(){
 try{
   const j=await api('account_center.php?ts='+Date.now());
   const a=j?.account||{};
   const uid=Number(a.user_id||0);

   let label=authMode==='registered'?(String(localStorage.getItem('lz_display_name')||'').trim()||'Player'):'Player';

   if(a.auth_mode==='guest' || a.registered===false){
     const custom=String(a.display_name||localStorage.getItem('lz_display_name')||'').trim();
     if(custom)localStorage.setItem('lz_display_name',custom);
     label=custom||(uid?`Guest${uid}`:'Guest');
   }else{
     const custom=String(a.display_name||'').trim();
     if(custom)localStorage.setItem('lz_display_name',custom);
     const internal=String(a.internal_login_id||'').trim();

     if(custom){
       label=custom;
     }else if(internal){
       label=`ID ${internal}`;
     }else if(uid){
       label=`Player #${uid}`;
     }else{
       label='Player';
     }
   }

   const el=$('#accountName');
   if(el)el.textContent=label;

   // Keep wallet synchronized from the same authenticated response as identity.
   if(j?.wallet)setBalance(playableBalance(j.wallet));

   return a;
 }catch(e){
   // Do not interrupt the game if profile metadata temporarily fails.
   const el=$('#accountName');
   if(el){
     if(authMode==='registered' && el.textContent.trim()==='Guest')el.textContent='Player';
     if(authMode==='guest' && !el.textContent.trim())el.textContent='Guest';
   }
   return null;
 }
}

function setPhase(p,n=''){
 phase=p;
 bettingOpen=(p==='BETTING');
 $('#phase').textContent=p;
 $('#timer').textContent=n;
 $$('[data-area]').forEach(b=>{
   b.disabled=!bettingOpen;
   b.classList.toggle('bet-locked',!bettingOpen);
 });
 $('#rebet').disabled=!bettingOpen;
 $('#rebet').classList.toggle('bet-locked',!bettingOpen);
}

function buildTrack(){
 const tr=$('#track');
 tr.innerHTML='';
 const cw=100/9,rh=100/7,boxes=[];

 for(let c=0;c<9;c++)boxes.push({left:c*cw,top:0,width:cw,height:rh});
 for(let r=1;r<=5;r++)boxes.push({left:8*cw,top:r*rh,width:cw,height:rh});
 for(let c=8;c>=0;c--)boxes.push({left:c*cw,top:6*rh,width:cw,height:rh});
 for(let r=5;r>=1;r--)boxes.push({left:0,top:r*rh,width:cw,height:rh});

 boxes.forEach((b,i)=>{
   const k=TRACK[i],d=DEF[k],e=document.createElement('div');
   e.className='track-cell '+(['frog','shark','pay_all','take_all'].includes(k)?'special ':'')+(k==='frog'?'golden-shark':'');
   e.dataset.key=k;e.dataset.i=i;
   e.style.left=b.left+'%';e.style.top=b.top+'%';e.style.width=b.width+'%';e.style.height=b.height+'%';
   const trackImg=k==='shark'
      ? 'assets/candidates/shark-wheel.png'
      : k==='frog'
        ? 'assets/candidates/goldenshark-wheel.png'
        : d.img;
    const icon=trackImg?`<img src="${trackImg}" alt="">`:d.e;
   const cap='';
   e.innerHTML=`<span class="wheel-visual-layer" aria-hidden="true"></span><span class="icon">${icon}</span><span class="caption">${cap}</span>`;
   tr.appendChild(e);
 });
}

function clearTrack(){
 wheelTrail=[];
 $$('.track-cell').forEach(e=>{
   e.classList.remove('winner-lock','wheel-trail-1','wheel-trail-2','wheel-trail-3','wheel-trail-4','wheel-trail-5','wheel-trail-6');
   const v=e.querySelector('.wheel-visual-layer');
   if(v){
     v.style.opacity='0';
     v.style.background='transparent';
     v.style.borderColor='transparent';
     v.style.boxShadow='none';
   }
 });
 const h=$('#trackHighlight');
 if(h){h.classList.add('hide');h.classList.remove('winner-stop')}
}


function paintWheelTrail(cell,stop=false){
  if(!cell)return;

  wheelTrail=wheelTrail.filter(x=>x&&document.body.contains(x)&&x!==cell);
  wheelTrail.push(cell);
  while(wheelTrail.length>6)wheelTrail.shift();

  // Remove trail classes and RESET only the dedicated overlay child.
  // Nothing on the real box, icon or caption is repositioned.
  $$('.track-cell').forEach(c=>{
    c.classList.remove(
      'wheel-trail-1','wheel-trail-2','wheel-trail-3',
      'wheel-trail-4','wheel-trail-5','wheel-trail-6'
    );
    const v=c.querySelector('.wheel-visual-layer');
    if(v){
      v.style.opacity='0';
      v.style.background='transparent';
      v.style.borderColor='transparent';
      v.style.boxShadow='none';
    }
  });

  const visual=[
    {o:.88,b:'transparent',bg:'rgba(214,166,18,.32)',sh:'inset 0 0 16px rgba(255,222,92,.28)'},
    {o:.72,b:'transparent',bg:'rgba(203,155,14,.24)',sh:'inset 0 0 13px rgba(255,218,82,.22)'},
    {o:.58,b:'transparent',bg:'rgba(192,146,12,.18)',sh:'inset 0 0 11px rgba(255,212,72,.17)'},
    {o:.44,b:'transparent',bg:'rgba(181,137,10,.13)',sh:'inset 0 0 9px rgba(255,205,62,.13)'},
    {o:.32,b:'transparent',bg:'rgba(170,128,8,.09)',sh:'inset 0 0 7px rgba(255,198,54,.10)'},
    {o:.22,b:'transparent',bg:'rgba(160,120,7,.06)',sh:'inset 0 0 5px rgba(255,190,48,.07)'}
  ];

  const len=wheelTrail.length;
  wheelTrail.forEach((c,i)=>{
    const age=len-1-i;
    const cls='wheel-trail-'+(age+1);
    c.classList.add(cls);

    const v=c.querySelector('.wheel-visual-layer');
    const s=visual[age]||visual[visual.length-1];
    if(v){
      v.style.opacity=String(s.o);
      v.style.background=s.bg;
      v.style.borderColor='transparent';
      v.style.boxShadow=s.sh;
    }
  });

  if(stop)wheelTrail=[...wheelTrail];
}

function moveHighlight(cell,stop=false){
  if(!cell)return;

  // Keep the old inner moving rectangle OFF.
  // The only visible hotspot is the dedicated overlay painted on the genuine
  // 28 outer boxes.
  const hi=$('#trackHighlight');
  if(hi){
    hi.classList.add('hide');
    hi.classList.remove('winner-stop');
  }

  paintWheelTrail(cell,stop);
}

function setPlayerBetMarksClosed(closed){
 const marks=$('#betMarks');
 if(!marks)return;
 marks.classList.toggle('bet-marks-closed',!!closed);
 marks.querySelectorAll('.bet-mark').forEach(m=>m.classList.toggle('bet-mark-closed',!!closed));
}

function markBet(area,amt){
 const b=document.querySelector(`[data-area="${area}"]`);
 if(!b)return;
 const marks=$('#betMarks');
 const board=$('#board');
 if(!marks||!board)return;

 let m=marks.querySelector(`.bet-mark[data-bet-area="${area}"]`);
 const total=Number(m?.dataset.betTotal||0)+Number(amt||0);

 // Keep ONE visible amount for this player's own stake on the selected area.
 // During BETTING it looks like a chip; after BETTING CLOSED it becomes a
 // highlighted amount while remaining on the same candidate.
 if(!m){
   const br=board.getBoundingClientRect(),r=b.getBoundingClientRect();
   // getBoundingClientRect() returns rendered pixels. On Android/WebView the
   // game board can be CSS-scaled, while absolute left/top values are resolved
   // in the board's unscaled coordinate space. Convert rendered coordinates
   // back to board-local CSS pixels so each wager stays on its own candidate.
   const sx=br.width ? board.clientWidth/br.width : 1;
   const sy=br.height ? board.clientHeight/br.height : 1;
   m=document.createElement('div');
   m.className='bet-mark';
   m.dataset.betArea=area;
   m.style.left=((r.left-br.left+r.width*.68)*sx)+'px';
   m.style.top=((r.top-br.top+r.height*.28)*sy)+'px';
   marks.appendChild(m);
 }
 m.dataset.betTotal=String(total);
 m.textContent=total>=1000?((total%1000===0?total/1000:(total/1000).toFixed(1))+'K'):String(total);
 m.classList.toggle('bet-mark-closed',phase!=='BETTING');
}

async function wallet(){
 try{
   const w=await api('wallet.php?session_token='+encodeURIComponent(token)+'&ts='+Date.now());
   if(w.wallet)setBalance(playableBalance(w.wallet));
 }catch(e){}
}

function historyStamp(r){
 const n=Number(r.round_id??r.id??r.round??0);
 if(Number.isFinite(n)&&n>0)return n;
 const dt=Date.parse(r.created_at??r.finished_at??r.result_at??r.updated_at??'');
 return Number.isFinite(dt)?dt:0;
}

function trackIndexForWinner(roundId,winner){
 const matches=[];
 TRACK.forEach((k,i)=>{if(k===winner)matches.push(i)});
 if(!matches.length)return null;
 const rid=Math.abs(Number(roundId)||0);
 return matches[rid%matches.length];
}

async function history(){
 if(historyBusy)return;
 historyBusy=true;
 try{
   const j=await api('recent_winners.php?ts='+Date.now()),h=$('#history');
   const rows=[...(j.winners||[])].sort((a,b)=>historyStamp(b)-historyStamp(a));

   if(lastWinnerTrackIndex===null && rows.length){
     const latest=rows[0];
     const recovered=trackIndexForWinner(
       latest.round_id??latest.id??latest.round,
       latest.winner
     );
     if(recovered!==null)lastWinnerTrackIndex=recovered;
   }

   h.innerHTML='';
   rows.slice(0,10).forEach(r=>{
     const d=DEF[r.winner]||{n:r.winner,m:'',e:'•'},el=document.createElement('div');
     el.className='hist';
     el.dataset.roundId=String(r.round_id??r.id??'');
     el.innerHTML=`<span class="hi">${d.img?`<img src="${d.img}" alt="">`:d.e}</span><span><b>${d.n}</b><small>${d.m?'x'+d.m:''}</small></span>`;
     h.appendChild(el);
   });
 }catch(e){}
 finally{historyBusy=false;}
}

function prependConfirmedWinner(roundId,winner){
 const h=$('#history');
 if(!h)return;
 const rid=String(roundId);
 const old=[...h.querySelectorAll('.hist')].find(x=>x.dataset.roundId===rid);
 if(old){h.prepend(old);return}
 const d=DEF[winner]||{n:winner,m:'',e:'•'},el=document.createElement('div');
 el.className='hist';
 el.dataset.roundId=rid;
 el.innerHTML=`<span class="hi">${d.img?`<img src="${d.img}" alt="">`:d.e}</span><span><b>${d.n}</b><small>${d.m?'x'+d.m:''}</small></span>`;
 h.prepend(el);
 while(h.children.length>10)h.lastElementChild.remove();
}

async function getResult(roundId){
 const wantedRoundId=Number(roundId);
 if(!Number.isFinite(wantedRoundId)||wantedRoundId<=0)return null;

 const controller=new AbortController();
 // 750ms was too aggressive on a real server and could repeatedly abort a
 // perfectly healthy result request. Give PHP/MySQL enough time to finish.
 const timeout=setTimeout(()=>controller.abort(),3000);

 try{
   const r=await fetch(
     A+'round_status.php?round_id='+wantedRoundId+'&ts='+Date.now(),
     {cache:'no-store',signal:controller.signal}
   );

   if(!r.ok)return null;

   const j=await r.json();
   const result=j?.round||null;

   // Never animate a result belonging to another/newer round.
   if(
     !j?.ok
     || !result?.winner
     || Number(result.id)!==wantedRoundId
   )return null;

   return result;
 }catch(e){
   return null;
 }finally{
   clearTimeout(timeout);
 }
}

async function waitForRoundResult(roundId){
 const wantedRoundId=Number(roundId);
 if(!Number.isFinite(wantedRoundId)||wantedRoundId<=0)return null;

 // Keep one continuous DRAWING wait instead of repeatedly dropping busy /
 // transitionStarted and recursively restarting drawPhase(). This prevents
 // overlapping draw loops when the API is temporarily slow.
 while(true){
   const data=await getResult(wantedRoundId);
   if(data)return data;

   // If the browser has already switched to another round, this draw is stale.
   if(round && Number(round.id)!==wantedRoundId)return null;

   await sleep(180);
 }
}

function startLocalBetTimer(milliseconds){
 clearInterval(localTimer);

 const ms=Math.max(0,Number(milliseconds)||0);
 betDeadlinePerf=performance.now()+ms;
 displayedBetSecond=null;

 function paint(){
   if(phase!=='BETTING')return;

   const remaining=Math.max(0,betDeadlinePerf-performance.now());
   const sec=Math.ceil(remaining/1000);

   if(sec!==displayedBetSecond){
     displayedBetSecond=sec;
     $('#timer').textContent=sec;
   }

   bettingOpen=remaining>250;

   if(remaining<=0){
     clearInterval(localTimer);
     beginBettingClosed();
   }
 }

 setPlayerBetMarksClosed(false);
 setPhase('BETTING',Math.ceil(ms/1000));
 paint();
 localTimer=setInterval(paint,100);
}

function beginBettingClosed(){
 if(transitionStarted||busy||!round)return;

 transitionStarted=true;
 clearInterval(localTimer);
 bettingOpen=false;
 setPlayerBetMarksClosed(true);
 pendingDrawResult=null;

 const wantedRoundId=Number(round.id);

 // Resolve winner + rng_number while CLOSED counts 3,2,1.
 const prefetch=async()=>{
   const deadline=performance.now()+2800;
   while(!pendingDrawResult && performance.now()<deadline){
     const r=await getResult(wantedRoundId);
     if(r){
       pendingDrawResult=r;
       break;
     }
     await sleep(90);
   }
 };
 prefetch().catch(()=>{});

 const started=performance.now();
 let shown=null;
 setPhase('BETTING CLOSED',3);

 localTimer=setInterval(()=>{
   const remain=Math.max(0,3000-(performance.now()-started));
   const sec=Math.ceil(remain/1000);

   if(sec!==shown){
     shown=sec;
     setPhase('BETTING CLOSED',sec);
   }

   if(remain<=0){
     clearInterval(localTimer);
     setPhase('BETTING CLOSED',0);
     drawPhase(wantedRoundId);
   }
 },50);
}

async function flyWinnerFromTrack(target,winner){
 if(!target)return;
 const d=DEF[winner]||{e:'•'},board=$('#board'),br=board.getBoundingClientRect(),tr=target.getBoundingClientRect();
 const f=document.createElement('div');
 f.className='winner-flight';
 f.innerHTML=d.img?`<img src="${d.img}" alt="">`:d.e;
 f.style.left=(tr.left-br.left+tr.width/2)+'px';
 f.style.top=(tr.top-br.top+tr.height/2)+'px';
 board.appendChild(f);
 f.getBoundingClientRect();
 f.classList.add('go');
 await sleep(620);
 f.remove();
}

async function drawPhase(roundId){
 if(busy||!roundId)return;
 busy=true;
 setPhase('DRAWING','');

 const cells=$$('.track-cell');

 let data=pendingDrawResult;
 pendingDrawResult=null;

 if(!data){
   data=await waitForRoundResult(roundId);
 }

 if(!data){
   // This can only happen if this draw became stale because the browser was
   // deliberately moved to another round. Do not start an overlapping loop.
   busy=false;
   transitionStarted=false;
   return;
 }

 const matches=cells.filter(c=>c.dataset.key===data.winner);
 const target=matches.length
   ? matches[Math.abs(Number(roundId))%matches.length]
   : null;

 if(!target){
   busy=false;
   transitionStarted=false;
   return;
 }

 const targetIndex=Number(target.dataset.i);

 /*
  * SAME SERVER RNG drives the visual route.
  * 3..7 complete laps + exact physical distance to the actual winner.
  * No fixed drawing seconds and no extra "fill time" laps.
  */
 const rng=Math.abs(Number(data.rng_number)||1);
 const fullLaps=3+(rng%5);       // 3,4,5,6,7
 const fastMs=18+(rng%5);        // 26..30ms, faster, still strict one-box-per-frame        // 44..48ms, deliberate box-by-box cadence

 // One genuine-style stop only:
 // fast body stays constant, then the LAST 6 physical boxes decelerate once.
 // Sum = 1445ms, matching the natural slowdown tail in wheel-drawing.wav.
 const SLOW=[90,125,175,245,340,470];

 let current=lastWinnerTrackIndex===null?27:lastWinnerTrackIndex;
 const direct=(targetIndex-current+28)%28;

 let totalSteps=(fullLaps*28)+direct;
 if(totalSteps<6)totalSteps+=28;
 const fastSteps=totalSteps-SLOW.length;

 // Start the ORIGINAL wheel recording exactly with the first visual wheel step.
 window.LuckZooAudio?.startWheelMotion?.();

 // Device-independent clock-based wheel timing. Slow Android/WebView frames
 // cannot stretch the round duration; missed visual steps are caught up.
 await new Promise(resolve=>{
   let shownSteps=0;
   let last=performance.now();
   let carry=0;

   function frame(now){
     carry+=Math.max(0,now-last);
     last=now;

     // IMPORTANT: move at most ONE physical box per rendered frame.
     // This prevents the mobile WebView from skipping several boxes visually
     // and gives a genuine 1-by-1 wheel feel.
     if(carry>=fastMs && shownSteps<fastSteps){
       carry-=fastMs;
       current=(current+1)%28;
       moveHighlight(cells[current],false);
       shownSteps++;
     }

     if(shownSteps>=fastSteps){
       resolve();
       return;
     }
     requestAnimationFrame(frame);
   }
   requestAnimationFrame(frame);
 });

 const slowTotal=SLOW.reduce((a,b)=>a+b,0);
 window.LuckZooAudio?.beginWheelSlowdown?.(slowTotal);

 await new Promise(resolve=>{
   const started=performance.now();
   const marks=[];
   let sum=0;
   for(const ms of SLOW){sum+=ms;marks.push(sum);}
   let shown=0;

   function frame(now){
     const elapsed=now-started;

     // Also keep the final slowdown strictly one physical box at a time.
     if(shown<marks.length && elapsed>=marks[shown]){
       current=(current+1)%28;
       moveHighlight(cells[current],false);
       shown++;
     }

     if(shown>=marks.length){
       resolve();
       return;
     }
     requestAnimationFrame(frame);
   }
   requestAnimationFrame(frame);
 });

 current=targetIndex;
 moveHighlight(target,true);
 target.classList.add('winner-lock');
 lastWinnerTrackIndex=targetIndex;

 const d=DEF[data.winner]||{n:data.winner,m:'',e:'•'};

 // Reaching the RNG winner immediately begins PRIZING.
 setPhase('PRIZING',4);

 flyWinnerFromTrack(target,data.winner).catch(()=>{});
 $('#winnerEmoji').innerHTML=d.img?`<img src="${d.img}" alt="">`:d.e;
 $('#winnerName').textContent=d.n+(d.m?' x'+d.m:'');
 $('#winner').classList.remove('hide');

 // Network work is background-only during PRIZING.
 getResult(roundId).catch(()=>{});
 wallet().catch(()=>{});
 setTimeout(()=>history().catch(()=>{}),350);
 setTimeout(()=>history().catch(()=>{}),1700);
 setTimeout(()=>history().catch(()=>{}),3000);

 const prizeStart=performance.now();
 let shown=null;

 while(true){
   const remain=Math.max(0,4000-(performance.now()-prizeStart));
   const sec=Math.ceil(remain/1000);

   if(sec!==shown){
     shown=sec;
     setPhase('PRIZING',sec);
   }

   if(remain<=0)break;
   await sleep(40);
 }

 setPhase('PRIZING',0);
 $('#winner').classList.add('hide');

 // Clear winner + previous five boxes immediately before the next betting round.
 clearTrack();

 history().catch(()=>{});
 wallet().catch(()=>{});

 busy=false;
 transitionStarted=false;

 // Only after PRIZING can a fresh 13-second betting round be created.
 await loadNextRound();
}

async function loadNextRound(){
 clearInterval(localTimer);

 try{
   queuedRound=null;

   const fresh=await api('current_round.php?ts='+Date.now());
   applyInactivityState(fresh);
   if(fresh?.inactivity?.kicked)return;
   const srv=fresh?.round||null;

   if(!srv){
     setTimeout(loadNextRound,120);
     return;
   }

   round=srv;
   roundBet=0;
   $('#roundBet').textContent='0';
   $('#betMarks').innerHTML='';
   lastRoundBets=[];
   clearTrack();

   $('#room').textContent=
     `${fresh.room.name} ${fresh.room.real_players}/${fresh.room.capacity}`;

   // Start the fresh betting clock before any optional background refresh.
   startLocalBetTimer(Number(round.client_display_ms||round.milliseconds_remaining||0));

   wallet().catch(()=>{});
   history().catch(()=>{});
 }catch(e){
   setTimeout(loadNextRound,120);
 }
}

async function syncServer(){
 if(!token||!round||phase!=='BETTING'||heartbeatBusy)return;

 heartbeatBusy=true;
 try{
   stageGameLoad(34,'Connecting to game server…','Synchronizing current round…');
   const j=await api('current_round.php?ts='+Date.now());
   stageGameLoad(62,'Round synchronized','Loading player data…');
   applyInactivityState(j);
   if(j?.inactivity?.kicked)return;

   if(j?.room){
     $('#room').textContent=
       `${j.room.name} ${j.room.real_players}/${j.room.capacity}`;
   }

   // Never modify #timer here. One monotonic clock owns visible betting time.
   if(!j?.round || Number(j.round.id)!==Number(round.id)){
     bettingOpen=false;
     beginBettingClosed();
   }
 }catch(e){
   if((betDeadlinePerf-performance.now())<=250){
     bettingOpen=false;
     beginBettingClosed();
   }
 }finally{
   heartbeatBusy=false;
 }
}

function makeBetRequestId(){
 if(globalThis.crypto?.randomUUID)return crypto.randomUUID();
 return 'bet-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,14);
}

async function place(area,amt){
 if(!bettingOpen||phase!=='BETTING'||!round)return;

 // Prevent an accidental repeated tap on the same area while the first request
 // is still waiting for the server. Other areas remain usable.
 if(pendingBetAreas.has(area))return;

 const requestId=makeBetRequestId();
 pendingBetAreas.add(area);

 try{
   const j=await api('place_bet.php',{
     method:'POST',
     body:JSON.stringify({
       round_id:Number(round.id),
       area,
       amount:amt,
       request_id:requestId
     })
   });

   // An idempotent replay is already represented on-screen by the original
   // successful request, so never add the local mark twice.
   if(j?.bet?.idempotent_replay){
     if(j.wallet)setBalance(playableBalance(j.wallet));
     return;
   }

   roundBet+=amt;
   $('#roundBet').textContent=roundBet;
   markBet(area,amt);
   lastRoundBets.push([area,amt]);

   inactivityWarningKey='';
   inactivityKickShown=false;

   if(j.wallet)setBalance(playableBalance(j.wallet));
   toast(`${DEF[area].n} +${amt}`);
 }catch(e){
   const msg=String(e.message||'');
   if(/betting|closed|time|round.*end/i.test(msg)){
     bettingOpen=false;
     beginBettingClosed();
     return;
   }
   toast(msg);
 }finally{
   pendingBetAreas.delete(area);
 }
}

$$('[data-area]').forEach(b=>b.onclick=()=>place(b.dataset.area,chip));

$$('[data-chip]').forEach(b=>b.onclick=()=>{
 chip=Number(b.dataset.chip);
 $$('[data-chip]').forEach(x=>x.classList.toggle('active',x===b));
});

$('#rebet').onclick=async()=>{
 if(!bettingOpen||phase!=='BETTING')return;
 if(!lastRoundBets.length)return toast('No previous bets');
 const copy=[...lastRoundBets];
 lastRoundBets=[];
 for(const [a,v] of copy)await place(a,v);
};

$('#historyBtn').onclick=history;

$('#guest').onclick=async()=>{
 try{
   const j=await api('guest_create.php',{method:'POST',body:'{}'});
   token=j.guest.session_token;authMode='guest';
   localStorage.zoo_token=token;localStorage.zoo_auth_mode=authMode;
   if(j.wallet)setBalance(playableBalance(j.wallet));
   enter();
 }catch(e){toast(e.message)}
};

$('#loginShow').onclick=()=>$('#login').classList.remove('hide');
$('#loginClose').onclick=()=>$('#login').classList.add('hide');

$('#loginGo').onclick=async()=>{
 try{
   const j=await api('mobile_login.php',{method:'POST',body:JSON.stringify({phone:$('#phone').value,password:$('#pass').value})});
   token=j.user.session_token;authMode='registered';
   localStorage.zoo_token=token;localStorage.zoo_auth_mode=authMode;
   $('#accountName').textContent='Player';
   $('#login').classList.add('hide');
   enter();
 }catch(e){toast(e.message)}
};

let gameLoadTimer=null;
let gameLoadProgress=0;

function gameLoadCopy(p){
 if(p<18)return ['Starting Zoo Roulette…','Loading resources…'];
 if(p<42)return ['Loading game resources…','Preparing wheel and betting board…'];
 if(p<66)return ['Connecting to game server…','Synchronizing current round…'];
 if(p<84)return ['Syncing player data…','Loading wallet and history…'];
 return ['Preparing game…','Finalizing Zoo Roulette…'];
}
function paintGameLoad(p,status='',detail=''){
 gameLoadProgress=Math.max(0,Math.min(100,Math.round(p)));
 const bar=document.getElementById('lzGameLoadBar');
 const pct=document.getElementById('lzGameLoadPercent');
 const st=document.getElementById('lzGameLoadStatus');
 const dt=document.getElementById('lzGameLoadDetail');
 if(bar)bar.style.width=gameLoadProgress+'%';
 if(pct)pct.textContent=gameLoadProgress+'%';
 const c=gameLoadCopy(gameLoadProgress);
 if(st)st.textContent=status||c[0];
 if(dt)dt.textContent=detail||c[1];
}
function startGameLoad(){
 clearInterval(gameLoadTimer);
 gameLoadProgress=0;
 paintGameLoad(0,'Starting Zoo Roulette…','Loading resources…');
 gameLoadTimer=setInterval(()=>{
   if(gameLoadProgress>=92)return;
   const step=gameLoadProgress<30?3:gameLoadProgress<65?2:1;
   paintGameLoad(Math.min(92,gameLoadProgress+step));
 },120);
}
function stageGameLoad(p,status,detail=''){
 paintGameLoad(Math.max(gameLoadProgress,p),status,detail);
}

function releaseGameLoader(){
 clearInterval(gameLoadTimer);
 gameLoadTimer=null;
 paintGameLoad(100,'Ready','Entering Zoo Roulette…');
 const loadingCover=document.getElementById('lzGameLoadingCover');
 if(!loadingCover)return;
 setTimeout(()=>{
   requestAnimationFrame(()=>{
     requestAnimationFrame(()=>loadingCover.classList.add('lz-ready'));
   });
 },160);
}

async function enter(){
 startGameLoad();
 stageGameLoad(12,'Loading game resources…','Preparing wheel and betting board…');
 $('#entry').classList.add('hide');
 $('#game').classList.remove('hide');

 try{
   const j=await api('current_round.php?ts='+Date.now());
   applyInactivityState(j);

   // If the backend removes the player for inactivity, do not leave the
   // loading cover above the inactivity dialog.
   if(j?.inactivity?.kicked){
     releaseGameLoader();
     return;
   }

   round=j.round;
   $('#room').textContent=`${j.room.name} ${j.room.real_players}/${j.room.capacity}`;

   // These are supporting requests. A temporary profile/wallet/history
   // failure must never keep the entire game trapped behind the loader.
   stageGameLoad(72,'Syncing player data…','Loading wallet and history…');
   await Promise.allSettled([
     syncPlayerIdentity(),
     wallet(),
     history()
   ]);

   // The round is now valid and the board has had two paint frames to settle.
   // Reveal the genuine game even if one non-critical supporting request failed.
   stageGameLoad(94,'Preparing game…','Finalizing Zoo Roulette…');
   releaseGameLoader();
   // startLocalBetTimer() expects MILLISECONDS. The old code passed
   // seconds_remaining (for example 13), which meant ~13ms and could force
   // the UI into DRAWING while the backend still had ~13 seconds of betting.
   const initialBetMs=Number(
     round.client_display_ms
     ?? round.milliseconds_remaining
     ?? (Number(round.seconds_remaining||0)*1000)
   );
   startLocalBetTimer(initialBetMs);

   if(heartbeat)clearInterval(heartbeat);
   heartbeat=setInterval(syncServer,900);

   // History follows backend independently of the visual wheel.
   if(historyHeartbeat)clearInterval(historyHeartbeat);
   historyHeartbeat=setInterval(()=>{if(phase==='BETTING'&&!busy)history();},5000);
 }catch(e){
   toast(e.message);
   setTimeout(enter,900);
 }
}

buildTrack();
if(token&&authMode){
  enter();

  // Safety only: never leave a valid authenticated game on an endless loader.
  // Normal release happens inside enter() after current_round initialization.
  setTimeout(()=>{
    if(round)releaseGameLoader();
  },5000);
}


/* Help artwork ----------------------------------------------------------- */
(function(){
 const helpBtn=document.getElementById('helpBtn');
 const helpOverlay=document.getElementById('helpOverlay');
 const helpClose=document.getElementById('helpClose');
 if(!helpBtn||!helpOverlay||!helpClose)return;

 function openHelp(){
   helpOverlay.classList.remove('hide');
   helpOverlay.setAttribute('aria-hidden','false');
 }
 function closeHelp(){
   helpOverlay.classList.add('hide');
   helpOverlay.setAttribute('aria-hidden','true');
 }
 helpBtn.addEventListener('click',openHelp);
 helpClose.addEventListener('click',closeHelp);
 helpOverlay.addEventListener('click',e=>{
   if(e.target===helpOverlay)closeHelp();
 });
 document.addEventListener('keydown',e=>{
   if(e.key==='Escape'&&!helpOverlay.classList.contains('hide'))closeHelp();
 });
})();
