// game-roulette.jsx — European single-zero roulette · 97.3% RTP / 2.7% house edge
//
// §5.68 — server-resolved. Browser sends the placed `bets` map + clientSeed
// to /play/roulette and receives {pocket, payout, hex, nonce}. Wheel
// rotation animation + bet-key registration + per-bet/table cap validation
// stay browser-side (server re-validates server-side as defence-in-depth).
// Local `checkBet` is retained for the result display only — server is the
// authority on actual payouts.

const { useState: useStateR, useEffect: useEffectR, useRef: useRefR } = React;

// European wheel order (single 0)
const WHEEL = [0,32,15,19,4,21,2,25,17,34,6,27,13,36,11,30,8,23,10,5,24,16,33,1,20,14,31,9,22,18,29,7,28,12,35,3,26];
const REDS = new Set([1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36]);

function colorOf(n){ if (n===0) return 'green'; return REDS.has(n) ? 'red' : 'black'; }

// Per-denomination chip colors. Each chip-size button doubles as the legend
// — the button shows the chip in its actual color, so the player learns the
// mapping without a separate panel.
const CHIP_COLORS = {
  0.01: '#ececec',  // white  (lowest)
  0.1:  '#ef4444',  // red
  0.25: '#3b82f6',  // blue
  0.5:  '#22c55e',  // green
  1:    '#f97316',  // orange
  2:    '#a855f7',  // purple (highest)
};
function chipColorFor(v) { return CHIP_COLORS[v] || '#f5a623'; }
// Light fills (white, gold) need dark text; everything else gets white text.
const LIGHT_CHIP_FILLS = new Set(['#ececec', '#f5a623']);
function chipTextColor(v) { return LIGHT_CHIP_FILLS.has(chipColorFor(v)) ? '#0a0a0a' : '#fff'; }
function chipLabel(v) {
  if (v === undefined) return 'RUN!';
  return v < 1 ? v.toString().replace(/^0/, '') : v.toString();
}

function RoulettePage({ palette: M, wallet, fairness, onConnect }) {
  const [bets, setBets] = useStateR({}); // key:type, val:amount
  const [chip, setChip] = useStateR(0.1);
  const [spinning, setSpinning] = useStateR(false);
  const [last, setLast] = useStateR(null); // {pocket, payout}
  const [angle, setAngle] = useStateR(0);
  const [history, setHistory] = useStateR([]);
  const [error, setError] = useBetError();
  const [errorTitle, setErrorTitle] = useStateR('INSUFFICIENT BAG');
  // Synchronous re-entry lock (audit HIGH-9).
  const busyRef = useRefR(false);

  function clearBets() { setBets({}); }
  const totalStake = Object.values(bets).reduce((s,b)=>s + b.amt, 0);

  // Per-coin max applies to the TOTAL stake across all chips on the felt
  // (10 SOL ≈ 1684.2 USDC/USDT).
  const MAX_BET_SOL = 10;
  const maxBet = maxBetForCoin(MAX_BET_SOL, wallet.activeCoin);

  // Per-bet-type caps (audit HIGH-8). Total max payout = stake × (mult+1):
  //   straight 35:1 → 1 SOL × 36 = 36 SOL liability per pocket
  //   dozens/columns 2:1 → 5 SOL × 3 = 15 SOL liability
  //   even-money 1:1 → 10 SOL × 2 = 20 SOL liability
  // Without per-pocket caps, a 10 SOL straight pays 360 SOL — disproportionate
  // to the rest of the table.
  const PER_BET_CAP_SOL = {
    straight: 1,
    dozen: 5,
    evenMoney: 10,
  };
  function perBetCap(key) {
    if (key.startsWith('n')) return maxBetForCoin(PER_BET_CAP_SOL.straight, wallet.activeCoin);
    if (key === 'd1' || key === 'd2' || key === 'd3') return maxBetForCoin(PER_BET_CAP_SOL.dozen, wallet.activeCoin);
    return maxBetForCoin(PER_BET_CAP_SOL.evenMoney, wallet.activeCoin);
  }

  function placeBet(key, label) {
    if (spinning) return;
    // Reject the chip up-front if it would push the felt past the table cap —
    // prevents the user from stacking past the limit and only finding out at
    // spin time.
    if (totalStake + chip > maxBet + 1e-9) {
      setErrorTitle('TABLE LIMIT');
      setError(`max table stake on roulette is ${maxBet.toFixed(2)} ${wallet.activeCoin} (current ${totalStake.toFixed(2)}) — adding a ${chip} ${wallet.activeCoin} chip would exceed it`);
      return;
    }
    // Per-bet-type cap check. Straight numbers are most liability-dense, so
    // they have the lowest cap.
    const cap = perBetCap(key);
    const onThisBet = (bets[key]?.amt || 0) + chip;
    if (onThisBet > cap + 1e-9) {
      setErrorTitle('BET LIMIT');
      setError(`max stake on ${label} is ${cap.toFixed(2)} ${wallet.activeCoin}`);
      return;
    }
    setError(null);
    // Track per-chip placements so the felt can render a color-coded stack
    // matching each individual click's denomination (item 4 — chip legend).
    setBets(b => ({ ...b, [key]: {
      amt: (b[key]?.amt || 0) + chip,
      count: (b[key]?.count || 0) + 1,
      placements: [...((b[key]?.placements) || []), chip],
      label,
    } }));
  }

  async function spin() {
    if (busyRef.current) return;
    if (spinning) return;
    if (totalStake === 0) {
      setErrorTitle('NO BETS PLACED');
      setError('place at least one chip on the table before spinning');
      return;
    }
    if (totalStake > maxBet) {
      setErrorTitle('TABLE LIMIT');
      setError(`max table stake on roulette is ${maxBet.toFixed(2)} ${wallet.activeCoin}; lower your bets or remove chips`);
      return;
    }
    if (!wallet.canPlay) { onConnect(); return; }
    const bag = wallet.balance[wallet.activeCoin] || 0;
    if (totalStake > bag) {
      setErrorTitle('INSUFFICIENT BAG');
      setError(`total stake ${totalStake.toFixed(3)} ${wallet.activeCoin} exceeds bag (${bag.toFixed(3)} ${wallet.activeCoin} available)`);
      return;
    }
    busyRef.current = true;
    setError(null);
    setSpinning(true); setLast(null);
    if (!wallet.debit(wallet.activeCoin, totalStake)) {
      busyRef.current = false;
      setSpinning(false);
      setErrorTitle('INSUFFICIENT BAG');
      setError(`bag changed under the bet -- try again`);
      return;
    }
    playRouletteSpinSound();

    // §5.68: server owns RNG + pocket selection + payout evaluation.
    // We pass a clean {key: amt} map (drop placements/count/label — server
    // doesn't need them) and receive {pocket, payout, hex, nonce}.
    const betsForServer = {};
    Object.entries(bets).forEach(([key, b]) => { betsForServer[key] = b.amt; });

    let server;
    try {
      server = await callServerGame('/play/roulette', {
        bets: betsForServer,
        clientSeed: fairness.clientSeed,
        coin: wallet.activeCoin,
        user: wallet.username,
      });
    } catch (err) {
      wallet.credit(wallet.activeCoin, totalStake, 'roulette-refund-' + Date.now());
      stopRouletteSpinSound();
      setSpinning(false);
      busyRef.current = false;
      setErrorTitle('SERVER ERROR');
      setError('server error: ' + (err && err.message ? err.message : String(err)));
      return;
    }

    const pocket = Number(server.pocket);
    const payout = Number(server.payout) || 0;
    const serverHex = String(server.hex || '');
    const wheelIdx = WHEEL.indexOf(pocket);

    // Animate wheel rotation. We compute the rotation as a delta added to the
    // current angle (instead of an absolute value) so EVERY spin animates 6+
    // full turns over the 3s transition, regardless of the wheel's resting
    // position from the previous spin.
    setAngle(prev => {
      // Wedge wheelIdx's center sits at (wheelIdx + 0.5) * (360/37) clockwise
      // from the top initially. CSS rotate is CW, so to put that wedge's CENTER
      // (not its leading edge) under the pointer at the top, we rotate the
      // wheel by -((wheelIdx + 0.5) * 360/37) — i.e., 360 minus that value mod 360.
      const wedge = 360 / 37;
      const targetMod = ((360 - (wheelIdx + 0.5) * wedge) % 360 + 360) % 360;
      const prevMod = ((prev % 360) + 360) % 360;
      let forward = (targetMod - prevMod) % 360;
      if (forward < 0) forward += 360;
      return prev + 360 * 6 + forward;
    });

    // Audio stops at 3.0s (200ms before the wheel finishes settling) so it
    // ends on a clean note rather than getting cut off mid-tone at the result.
    setTimeout(() => stopRouletteSpinSound(), 3000);
    await new Promise(res => setTimeout(res, 3200));

    // Compute straightHit locally for the sound routing — derivable from
    // the bets set + the server-returned pocket. Server's `payout` is the
    // authoritative total.
    let straightHit = false;
    Object.keys(bets).forEach(key => {
      if (key.startsWith('n') && checkBet(key, pocket) > 0) straightHit = true;
    });
    if (payout > 0) wallet.credit(wallet.activeCoin, payout, serverHex || ('roulette-cash-' + Date.now()));

    // Snapshot the round's totalStake into `last` so the result line stays
    // frozen at the lost amount even after the player starts placing new
    // chips for the next round (which would otherwise mutate totalStake).
    setLast({ pocket, payout, stake: totalStake, coin: wallet.activeCoin, hex: serverHex.slice(0,12) });
    wallet.logPlay({ game:'Roulette', bet: totalStake, payout, win: payout>totalStake, coin: wallet.activeCoin, hex: serverHex.slice(0,16), pocket });
    // NOTE: server wrote /plays already (rules step8 blocks browser writes for Roulette).
    setHistory(h => [pocket, ...h].slice(0, 16));
    setSpinning(false);
    busyRef.current = false;
    // Chips stay on the felt across spins — "let it ride" pattern. The
    // player clears manually via the [clear bets] button, or adds to
    // existing bets, or re-spins with the same layout.
    // Straight-number hits get the streak fanfare (same sound blackjack
    // uses on a natural 21). Everything else routes to the standard win/
    // loss sound. Any winning bet triggers the win variant, even if other
    // bets on the same spin lost and the round netted negative.
    if (straightHit) playStreakSound();
    else playOutcomeSound(payout > 0);
  }

  function checkBet(key, pocket) {
    if (key === 'red') return colorOf(pocket)==='red' ? 1 : 0;
    if (key === 'black') return colorOf(pocket)==='black' ? 1 : 0;
    if (key === 'odd') return (pocket > 0 && pocket%2 === 1) ? 1 : 0;
    if (key === 'even') return (pocket > 0 && pocket%2 === 0) ? 1 : 0;
    if (key === 'low') return (pocket >= 1 && pocket <= 18) ? 1 : 0;
    if (key === 'high') return (pocket >= 19 && pocket <= 36) ? 1 : 0;
    if (key === 'd1') return (pocket >= 1 && pocket <= 12) ? 2 : 0;
    if (key === 'd2') return (pocket >= 13 && pocket <= 24) ? 2 : 0;
    if (key === 'd3') return (pocket >= 25 && pocket <= 36) ? 2 : 0;
    if (key.startsWith('n')) return (+key.slice(1) === pocket) ? 35 : 0;
    return 0;
  }

  // Chip size presets (coin-flip-style row of selector buttons). Filter to
  // only show chips at or above the active coin's minimum (0.01 SOL / 1 USDC/USDT).
  const CHIP_SIZES = [0.01, 0.1, 0.25, 0.5, 1, 2].filter(v => v >= coinMinBet(wallet.activeCoin));

  // Auto-clamp the selected chip when the active coin changes — otherwise a
  // 0.1 chip lingers as selected after switching to USDC/USDT and the user
  // can't drop chips because the chip falls below min.
  useEffectR(() => {
    if (chip < coinMinBet(wallet.activeCoin)) setChip(CHIP_SIZES[0]);
  }, [wallet.activeCoin]); // eslint-disable-line

  return (
    <div className="rc-page" style={{flex:1, padding:'18px 26px 40px', overflowY:'auto'}}>
      {/* zoom: 0.8 scales the entire roulette page (visual and layout) to 80%
          of its original size — same browser-supported approach used for
          most casino layouts that need to fit a smaller viewport. The
          rc-roulette-zoom class resets zoom to 1 on mobile so the felt
          isn't double-shrunk. */}
      <div className="rc-roulette-zoom" style={{zoom: 0.8}}>
      <h1 className="rc-h-page" style={{fontSize:48, color:M.red, margin:'4px 0', textShadow:`3px 3px 0 ${M.yellow}`}}>ROULETTE</h1>
      <div className="rc-h-sub" style={{color:M.ink2, fontSize:18, marginBottom:14}}>
        <span style={{color:M.green}}>97.3% RTP</span> · single-zero european · <span style={{color:M.hot}}>2.7% edge</span> · max table {maxBet.toFixed(2)} {wallet.activeCoin} · max per-number 1 (max win 36)
      </div>

      <div className="rc-roulette-playmode">
        <PlayModeBar palette={M} wallet={wallet} onConnect={onConnect}/>
      </div>
      <BetErrorToast error={error} onClose={()=>setError(null)} palette={M} title={errorTitle}/>

      {/* Layout mirrors coin flip: left column is the bet/control panel,
          right column is the action stage. History sits under the chips
          panel on the left. */}
      <div className="rc-game-grid" style={{display:'grid', gridTemplateColumns:'320px 1fr', gap:18, alignItems:'start'}}>
        {/* LEFT: chips panel + history below */}
        <div style={{display:'flex', flexDirection:'column', gap:14}}>
          <Frame title="chips" accent={M.yellow} className="rc-roulette-chips">
            {/* Greyout the chip controls during the spin (matches flip pattern). */}
            <div className="rc-bet-panel" style={{opacity: spinning ? 0.4 : 1, pointerEvents: spinning ? 'none' : 'auto', transition: 'opacity 0.15s'}}>
              <div className="rc-bet-inputs">
                {/* Chip size selector — each button shows the chip in its
                    actual color, doubling as the legend for the on-felt stacks. */}
                <div style={{fontSize:13, color:M.ink2, marginBottom:4}}>chip size ({wallet.activeCoin}) · color = denomination</div>
                <div style={{display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap:6, marginBottom:14}}>
                  {CHIP_SIZES.map(v => (
                    <button key={v} onClick={()=>setChip(v)} disabled={spinning} style={{
                      display:'flex', alignItems:'center', gap:6, justifyContent:'center',
                      background: chip===v ? M.yellow : M.bg,
                      color:      chip===v ? M.bg     : M.yellow,
                      border:`2px solid ${M.yellow}`, fontFamily:'inherit', fontSize:14, padding:'6px 0',
                      cursor: spinning?'not-allowed':'pointer', fontWeight:700,
                      boxShadow: chip===v ? `3px 3px 0 ${M.bg}` : 'none', letterSpacing:'0.05em',
                    }}>
                      <RouletteChip size={28} value={v}/>
                      <span>{v}</span>
                    </button>
                  ))}
                </div>

                <div style={{fontSize:13, color:M.ink2, marginBottom:4}}>total stake</div>
                <div style={{fontSize:24, color:M.green, fontWeight:700, marginBottom:14}}>{totalStake.toFixed(3)} {wallet.activeCoin}</div>

                {Object.entries(bets).length > 0 && (
                  <div style={{maxHeight:120, overflowY:'auto', fontSize:13, marginBottom:8, fontFamily:'JetBrains Mono, monospace'}}>
                    {Object.entries(bets).map(([k,b])=>(
                      <div key={k} style={{display:'flex', justifyContent:'space-between', padding:'2px 0', color:M.ink2}}>
                        <span style={{color:M.yellow}}>{b.label}</span>
                        <span>{b.amt.toFixed(3)}</span>
                      </div>
                    ))}
                  </div>
                )}

                {/* Desktop [clear bets] — lives inside the chips Frame above
                    the SPIN button. Hidden on mobile; the mobile twin below
                    the standalone SPIN takes over so [clear bets] sits below
                    SPIN as a paired action. */}
                <button className="rc-hide-mobile" onClick={clearBets} disabled={totalStake===0||spinning} style={{
                  width:'100%', background:'transparent', color:M.red, border:`2px solid ${M.red}`,
                  fontFamily:'inherit', fontSize:16, padding:'6px 0',
                  cursor: (totalStake===0||spinning)?'not-allowed':'pointer', marginBottom:8,
                  opacity: (totalStake===0||spinning)?0.5:1,
                }}>[clear bets]</button>
              </div>
              {/* Desktop SPIN — lives inside the chips Frame. Hidden on mobile;
                  the mobile-only twin below the wheel takes over so SPIN sits
                  above the "bet on the felt" Frame as a standalone action. */}
              <button className="rc-bet-action rc-hide-mobile" onClick={spin} disabled={spinning||totalStake===0} style={{
                width:'100%', background: spinning||totalStake===0 ? M.ink2 : M.hot, color:M.bg,
                border:`3px solid ${M.bg}`, fontFamily:'inherit', fontSize:20, fontWeight:700, padding:'12px 0',
                cursor: spinning?'wait':(totalStake===0?'not-allowed':'pointer'),
                boxShadow:`6px 6px 0 ${M.green}`, letterSpacing:'0.05em',
              }}>{spinning?'spinning…':'► SPIN'}</button>
            </div>
          </Frame>

          {/* History — desktop twin, lives below the chips Frame in the left
              column (original location). Hidden on mobile; the right-column
              twin below takes over and rides above the wheel. */}
          <Frame title="history" accent={M.cyan} className="rc-hide-mobile">
            <div style={{display:'flex', gap:4, flexWrap:'wrap'}}>
              {history.length===0 && <span style={{color:M.ink2, fontSize:14}}>no spins yet</span>}
              {history.map((n,i)=>{
                const c = colorOf(n);
                return <div key={i} style={{width:28, height:28, display:'flex', alignItems:'center', justifyContent:'center', background:c==='red'?M.red:c==='green'?M.green:'#222', color:c==='black'?M.ink:M.bg, fontSize:14, fontWeight:700, border:`1px solid ${M.green}33`}}>{n}</div>;
              })}
            </div>
          </Frame>
        </div>

        {/* RIGHT: history (mobile twin) + wheel + felt. The history Frame here
            is rc-mobile-only — visible only at ≤768px — and uses rc-history
            (order: -1) so it sits above the wheel on phones. Desktop sees only
            the left-column twin above. */}
        <div style={{display:'flex', flexDirection:'column', gap:14}}>
          <Frame title="history" accent={M.cyan} className="rc-mobile-only rc-history rc-roulette-history-m">
            <div style={{display:'flex', gap:4, flexWrap:'wrap'}}>
              {history.length===0 && <span style={{color:M.ink2, fontSize:14}}>no spins yet</span>}
              {history.map((n,i)=>{
                const c = colorOf(n);
                return <div key={i} style={{width:28, height:28, display:'flex', alignItems:'center', justifyContent:'center', background:c==='red'?M.red:c==='green'?M.green:'#222', color:c==='black'?M.ink:M.bg, fontSize:14, fontWeight:700, border:`1px solid ${M.green}33`}}>{n}</div>;
              })}
            </div>
          </Frame>
          {/* Wheel */}
          <Frame title="the wheel" accent={M.red} className="rc-wheel-frame" style={{display:'flex', flexDirection:'column', alignItems:'center', minHeight:400}}>
            <div className="rc-wheel" style={{position:'relative', width:360, height:360, maxWidth:'100%', aspectRatio:'1 / 1', margin:'18px auto 0'}}>
              <style>{`@keyframes pulse { 50% { transform: scale(1.05); } }`}</style>
              <svg width="100%" height="100%" viewBox="-100 -100 200 200" preserveAspectRatio="xMidYMid meet" style={{
                display:'block', borderRadius:'50%',
                transform: `rotate(${angle}deg)`,
                transition: spinning ? 'transform 3s cubic-bezier(0.2,0.8,0.2,1)' : 'none',
                filter: `drop-shadow(0 0 16px ${M.red}aa)`,
                shapeRendering:'geometricPrecision',
              }}>
                <circle cx="0" cy="0" r="96" fill={M.yellow}/>
                <g>{WHEEL.map((n,i)=>{
                  const c = colorOf(n);
                  const col = c==='red'?M.red:c==='black'?'#0a0a0a':M.green;
                  const a1 = (i/37)*2*Math.PI - Math.PI/2;
                  const a2 = ((i+1)/37)*2*Math.PI - Math.PI/2;
                  const r = 90;
                  const x1=Math.cos(a1)*r, y1=Math.sin(a1)*r;
                  const x2=Math.cos(a2)*r, y2=Math.sin(a2)*r;
                  const am = (a1+a2)/2;
                  const lr = 76;
                  const lx = Math.cos(am)*lr, ly = Math.sin(am)*lr;
                  const labelRot = (am*180/Math.PI) + 90;
                  return (
                    <g key={i}>
                      <path d={`M0,0 L${x1.toFixed(3)},${y1.toFixed(3)} A${r},${r} 0 0 1 ${x2.toFixed(3)},${y2.toFixed(3)} Z`} fill={col}/>
                      <text x={lx.toFixed(2)} y={ly.toFixed(2)}
                            transform={`rotate(${labelRot.toFixed(2)} ${lx.toFixed(2)} ${ly.toFixed(2)})`}
                            textAnchor="middle" fontFamily='"VT323", monospace' fontSize="9" fontWeight="700"
                            fill="#fff" dominantBaseline="middle">{n}</text>
                    </g>
                  );
                })}</g>
                <circle cx="0" cy="0" r="90" fill="none" stroke="#0a0a0a" strokeWidth="0.6"/>
              </svg>
              {/* center hub */}
              <div style={{position:'absolute', top:'50%', left:'50%', transform:'translate(-50%,-50%)', width:90, height:90, borderRadius:'50%', background:M.bg, border:`3px solid ${M.yellow}`, display:'flex', alignItems:'center', justifyContent:'center', color:M.yellow, fontSize:24, fontWeight:700}}>RUN!</div>
              {/* pointer */}
              <div style={{position:'absolute', top:-14, left:'50%', transform:'translateX(-50%)', width:0, height:0, borderLeft:'16px solid transparent', borderRight:'16px solid transparent', borderTop:`26px solid ${M.yellow}`}}/>
            </div>
            {/* Result slot — fixed height so the felt below doesn't jump when result shows/hides.
                flex-start anchors content to the top so the larger result block doesn't shove
                the "spinning…" prompt position when it replaces the placeholder. */}
            <div className="rc-resolve-slot-col" style={{marginTop:14, textAlign:'center', minHeight:72, display:'flex', flexDirection:'column', justifyContent:'flex-start'}}>
              {last && !spinning && (
                <>
                  <div style={{fontSize:32, fontWeight:700, color: colorOf(last.pocket)==='red'?M.red:colorOf(last.pocket)==='green'?M.green:M.ink, textShadow:`2px 2px 0 ${M.bg}`, lineHeight:1}}>
                    {last.pocket} {colorOf(last.pocket).toUpperCase()}
                  </div>
                  <div style={{fontSize:18, color: last.payout>0?M.green:M.red, marginTop:4}}>
                    {last.payout>0 ? `+${last.payout.toFixed(2)} ${last.coin}` : `−${last.stake.toFixed(2)} ${last.coin}`}
                  </div>
                </>
              )}
              {spinning && (
                <div style={{fontSize:18, color:M.yellow, opacity:0.8}}>spinning…</div>
              )}
            </div>
          </Frame>

          {/* Mobile-only SPIN twin — standalone (not inside any Frame) so it
              sits directly above the "bet on the felt" Frame as a separate
              action. The desktop twin lives inside the chips Frame and is
              hidden on mobile via rc-hide-mobile. */}
          <button className="rc-mobile-only rc-roulette-action" onClick={spin} disabled={spinning||totalStake===0} style={{
            width:'100%', background: spinning||totalStake===0 ? M.ink2 : M.hot, color:M.bg,
            border:`3px solid ${M.bg}`, fontFamily:'inherit', fontSize:20, fontWeight:700, padding:'12px 0',
            cursor: spinning?'wait':(totalStake===0?'not-allowed':'pointer'),
            boxShadow:`6px 6px 0 ${M.green}`, letterSpacing:'0.05em',
          }}>{spinning?'spinning…':'► SPIN'}</button>

          {/* Mobile-only [clear bets] twin — sits directly below the standalone
              SPIN so the action pair is visible together above the felt. The
              desktop twin lives inside the chips Frame above SPIN. */}
          <button className="rc-mobile-only rc-roulette-clear" onClick={clearBets} disabled={totalStake===0||spinning} style={{
            width:'100%', background:'transparent', color:M.red, border:`2px solid ${M.red}`,
            fontFamily:'inherit', fontSize:16, padding:'6px 0', marginTop:6,
            cursor: (totalStake===0||spinning)?'not-allowed':'pointer',
            opacity: (totalStake===0||spinning)?0.5:1,
          }}>[clear bets]</button>

          {/* Felt */}
          <Frame title="bet on the felt" accent={M.green}>
            {/* Greyout the felt during the spin — chips can't be dropped while
                the wheel is settling. The rc-felt-shrink class compresses the
                whole felt grid to 50% on mobile (zoom CSS); desktop unaffected. */}
            <div className="rc-felt-shrink" style={{opacity: spinning ? 0.4 : 1, pointerEvents: spinning ? 'none' : 'auto', transition: 'opacity 0.15s'}}>
            {/* Numbers grid: 0 + 3×12 on desktop; on mobile the layout flips to a
                vertical 3-wide × 12-row table with the 0 as a full-width header. */}
            <div className="rc-felt-numbers" style={{display:'flex', gap:4, overflowX:'auto'}}>
              <div className="rc-felt-zero">
                <FeltCell label="0" onClick={()=>placeBet('n0','0')} bet={bets['n0']} bg={M.green} M={M} tall/>
              </div>
              <div className="rc-felt-grid" style={{display:'grid', gridTemplateColumns:'repeat(12, 1fr)', gap:4, flex:1, minWidth:340}}>
                {Array.from({length:36}, (_,i)=>i+1).map(n => {
                  const c = colorOf(n);
                  return <FeltCell key={n} label={n.toString()} onClick={()=>placeBet(`n${n}`, n.toString())} bet={bets[`n${n}`]} bg={c==='red'?M.red:'#222'} M={M}/>;
                })}
              </div>
            </div>
            {/* Outside bets */}
            <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:4, marginTop:4}}>
              <FeltCell label="1ST 12" onClick={()=>placeBet('d1','1st 12')} bet={bets['d1']} bg={M.panel} M={M} small/>
              <FeltCell label="2ND 12" onClick={()=>placeBet('d2','2nd 12')} bet={bets['d2']} bg={M.panel} M={M} small/>
              <FeltCell label="3RD 12" onClick={()=>placeBet('d3','3rd 12')} bet={bets['d3']} bg={M.panel} M={M} small/>
            </div>
            {/* Source order is the desktop layout: [1-18, EVEN, RED, BLACK, ODD, 19-36].
                The mobile 3-col split needs [BLACK, EVEN, RED] over [1-18, ODD, 19-36],
                so the mobile rule (.rc-roulette-outside in index.html) swaps BLACK↔1-18
                via CSS `order:` rather than restructuring the DOM. */}
            <div className="rc-roulette-outside" style={{display:'grid', gridTemplateColumns:'repeat(6, 1fr)', gap:4, marginTop:4}}>
              <FeltCell label="1-18" onClick={()=>placeBet('low','1-18')} bet={bets['low']} bg={M.panel} M={M} small/>
              <FeltCell label="EVEN" onClick={()=>placeBet('even','even')} bet={bets['even']} bg={M.panel} M={M} small/>
              <FeltCell label="RED" onClick={()=>placeBet('red','red')} bg={M.red} bet={bets['red']} M={M} small/>
              <FeltCell label="BLACK" onClick={()=>placeBet('black','black')} bet={bets['black']} bg="#222" M={M} small/>
              <FeltCell label="ODD" onClick={()=>placeBet('odd','odd')} bet={bets['odd']} bg={M.panel} M={M} small/>
              <FeltCell label="19-36" onClick={()=>placeBet('high','19-36')} bet={bets['high']} bg={M.panel} M={M} small/>
            </div>
            </div>
          </Frame>
        </div>
      </div>

      <RouletteStatsPanel palette={M} wallet={wallet}/>
      <HashPanel palette={M} fairness={fairness}/>
      </div>
    </div>
  );
}

// Roulette stats — derived from wallet.history filtered to game==='Roulette'.
function RouletteStatsPanel({ palette: M, wallet }) {
  const coin = wallet.activeCoin;
  const spins = (wallet.history || []).filter(h => h.game === 'Roulette' && h.coin === coin);
  const total = spins.length;
  const wins = spins.filter(s => s.win).length;
  const winRate = total ? (wins / total) * 100 : 0;
  const wagered = spins.reduce((s, x) => s + (x.bet || 0), 0);
  const netPL = spins.reduce((s, x) => s + ((x.payout || 0) - (x.bet || 0)), 0);
  const bestWin = spins.reduce((m, x) => Math.max(m, (x.payout || 0) - (x.bet || 0)), 0);
  const greens = spins.filter(s => s.pocket === 0).length;
  const dp = 2;
  const fmt = n => `${n>=0?'+':''}${n.toFixed(dp)} ${coin}`;
  const fmtAbs = n => `${n.toFixed(dp)} ${coin}`;
  const tile = (label, value, color) => (
    <div style={{textAlign:'center', padding:'4px 0'}}>
      <div style={{fontSize:11, color:M.ink2, textTransform:'uppercase', letterSpacing:'0.05em'}}>{label}</div>
      <div style={{fontSize:22, color, fontWeight:700, lineHeight:1.1, marginTop:4, fontFamily:'JetBrains Mono, monospace'}}>{value}</div>
    </div>
  );
  return (
    <Frame title="roulette stats" accent={M.cyan} className="rc-roulette-stats" style={{marginTop:18}}>
      {total === 0 ? (
        <div style={{color:M.ink2, fontSize:14, padding:'4px 0'}}>{'>'} no spins yet in {coin}. drop some chips.</div>
      ) : (
        <>
          <div className="rc-stat-6" style={{display:'grid', gridTemplateColumns:'repeat(6, 1fr)', gap:14}}>
            {tile('spins',         total,                            M.green)}
            {tile('win rate',      `${winRate.toFixed(1)}%`,         M.cyan)}
            {tile('total wagered', fmtAbs(wagered),                  M.yellow)}
            {tile('net P/L',       fmt(netPL),                       netPL >= 0 ? M.green : M.red)}
            {tile('best win',      `+${bestWin.toFixed(dp)} ${coin}`,M.green)}
            {tile('greens hit',    `${greens}`,                      M.hot)}
          </div>
          <LastRoundsTable M={M} rounds={spins} headers={['pocket', 'bet', 'payout', 'P/L']} renderRow={s => {
            const pl = (s.payout || 0) - (s.bet || 0);
            const plColor = pl > 0 ? M.green : pl === 0 ? M.yellow : M.red;
            const plText  = pl > 0 ? `+${pl.toFixed(dp)}` : pl === 0 ? '±0.00' : `−${Math.abs(pl).toFixed(dp)}`;
            const pocketColor = s.pocket === 0 ? M.green : (colorOf(s.pocket) === 'red' ? M.red : M.ink);
            return [
              { value: `${s.pocket} ${colorOf(s.pocket).toUpperCase()}`, color: pocketColor, fontWeight: 700 },
              { value: (s.bet || 0).toFixed(dp),                         color: M.ink },
              { value: s.payout > 0 ? (s.payout || 0).toFixed(dp) : '—', color: s.payout > 0 ? M.green : M.ink2 },
              { value: plText,                                           color: plColor, fontWeight: 700 },
            ];
          }}/>
        </>
      )}
    </Frame>
  );
}

function FeltCell({ label, onClick, bet, bg, M, small, tall }) {
  // Show a visible chip stack for each click. Cap visible chips so the
  // tower doesn't grow unboundedly tall; show "+N" badge if there are more.
  // Each rendered chip uses its own denomination's color (item 4).
  const MAX_VISIBLE = 6;
  const STACK_OFFSET = 6; // px each chip rises above the one below
  const placements = bet?.placements || [];
  const count = placements.length;
  // Stack the most recent placements on top; older ones are hidden under
  // the visible cap with the "+N" overflow badge.
  const visiblePlacements = placements.slice(-MAX_VISIBLE);
  const overflow = count > MAX_VISIBLE ? count - MAX_VISIBLE : 0;
  return (
    <button onClick={onClick} style={{
      background:bg, color:M.ink, border:`1px solid ${M.green}33`, fontFamily:'inherit',
      fontSize: small?13:16, fontWeight:700, padding:tall?'40px 8px':'10px 0', cursor:'pointer',
      position:'relative', minHeight: tall?'auto':36,
    }}>
      {label}
      {visiblePlacements.map((v, i) => (
        <span key={i} style={{
          position:'absolute',
          // Each subsequent chip rises STACK_OFFSET px above the one below it.
          top: `calc(50% - ${i * STACK_OFFSET}px)`,
          left: '50%',
          transform: 'translate(-50%, -50%)',
          pointerEvents: 'none',
          filter: `drop-shadow(0 2px 3px rgba(0,0,0,0.55))`,
          zIndex: i + 1,
        }}>
          <RouletteChip size={42} value={v}/>
        </span>
      ))}
      {overflow > 0 && (
        <span style={{
          position:'absolute',
          top: `calc(50% - ${(MAX_VISIBLE - 1) * STACK_OFFSET + 14}px)`,
          left: 'calc(50% + 14px)',
          background: M.bg, color: M.yellow,
          border: `1px solid ${M.yellow}`,
          fontSize: 10, fontWeight: 700,
          padding: '0 4px', lineHeight: 1.4,
          pointerEvents: 'none',
          zIndex: MAX_VISIBLE + 2,
        }}>+{overflow}</span>
      )}
    </button>
  );
}

// RouletteChip — same visual language as the coin-flip coin (colored disc,
// thick black ring, inner ring, value label). When `value` is supplied, the
// disc takes that denomination's color from CHIP_COLORS and the label shows
// the denomination ("1", ".25", etc.) instead of "RUN!".
function RouletteChip({ size = 28, value }) {
  const fill = chipColorFor(value);
  const textFill = chipTextColor(value);
  const label = chipLabel(value);
  // Shorter labels can use a bigger glyph — keeps tiny chips legible.
  const fontSize = label.length <= 2 ? 92 : label.length === 3 ? 72 : 56;
  return (
    <svg viewBox="0 0 200 200" width={size} height={size} style={{display:'block'}}>
      <circle cx="100" cy="100" r="96" fill="#0a0a0a"/>
      <circle cx="100" cy="100" r="88" fill={fill}/>
      <circle cx="100" cy="100" r="74" fill="none" stroke="#0a0a0a" strokeWidth="4"/>
      <circle cx="100" cy="100" r="70" fill={fill}/>
      <text x="100" y="100" textAnchor="middle" dominantBaseline="middle"
            alignmentBaseline="central" fontFamily='"VT323", monospace'
            fontSize={fontSize} fontWeight="700" fill={textFill}>{label}</text>
    </svg>
  );
}

Object.assign(window, { RoulettePage, RouletteChip });
