// game-slots.jsx — slots with NORMAL (3-reel) and PRO (6-reel) modes.
//
// Normal mode tiers (priority high → low):
//   1) Three-of-a-kind   — same exact symbol on all 3 reels  (sym.mult)
//   2) Three-of-a-family — 3 symbols all from the same family (FAMILY_MULT)
//   3) Pair              — any two reels showing the same exact symbol (sym.pairMult)
//
// Pro mode tiers (priority high → low):
//   1) Six-of-a-kind     — sym.pro.mult6
//   2) Five-of-a-kind    — sym.pro.mult5
//   3) Four-of-a-kind    — sym.pro.mult4
//   4) Three-of-a-kind   — sym.pro.mult3
//   5) Pair              — sym.pro.pair
// Highest-paying matching count anywhere on the 6 reels wins.
//
// Pair multipliers in both modes intentionally include the 0.2/0.3/0.4/0.5
// and 1.1/1.2/1.3/1.4/1.5 values across the 10 symbols.
//
// §5.67 — server-resolved. The Worker owns the symbol weights, paytable,
// PRO_FORCED jackpot table, and CONSOLATION_TABLE. POST /play/slots returns
// {targets, mult, kind, label, payout} from one server call; the browser
// uses `targets` for the reel reveal animation and `mult/kind/payout` for
// the result display + wallet credit. The local `evaluateWin`/`evaluateProWin`
// + paytable below stay for the on-screen win-symbols display (so the result
// row knows which glyphs to draw) — server is authoritative on the actual
// outcome, browser-side evaluation is a presentational mirror.

const { useState: useStateSL, useEffect: useEffectSL, useRef: useRefSL } = React;

const SLOT_SYMBOLS = [
  // s=glyph, mult=3-of-a-kind (normal), pairMult=2-of-a-kind (normal),
  // family=group, color=palette key, label=name,
  // pro={ mult6, mult5, mult4, mult3, pair } = pro-mode multipliers per match count.
  // Normal-mode paytable. New hollow-red 7 sits at the top as the rarest
  // jackpot symbol; the solid red 7 stays as the previous-tier jackpot.
  // `hollow: true` triggers the outline render (color via WebkitTextStroke).
  // Pro mode payouts capped at 200× anywhere on the table — the hollow 7
  // 6-of-a-kind is the only path to the cap.
  { s:'7', mult:20,  pairMult:2,    family:'7',     color:'red',    label:'MEGA JACKPOT', hollow:true,
    pro: { mult6:200, mult5:120, mult4:35, mult3:8, pair:2 } },
  { s:'7', mult:15,  pairMult:1.5,  family:'7',     color:'red',    label:'JACKPOT',
    pro: { mult6:150, mult5:80, mult4:20,  mult3:5,   pair:1.5 } },
  { s:'$', mult:12,  pairMult:1.4,  family:'money', color:'yellow', label:'big win',
    pro: { mult6:150, mult5:35, mult4:10,  mult3:3,   pair:1.4 } },
  { s:'★', mult:10,  pairMult:1.3,  family:'star',  color:'cyan',   label:'star',
    pro: { mult6:80,  mult5:20, mult4:6,   mult3:2,   pair:1.3 } },
  { s:'♥', mult:8,   pairMult:1.2,  family:'heart', color:'hot',    label:'heart',
    pro: { mult6:70,  mult5:15, mult4:5,   mult3:1.5, pair:1.2 } },
  { s:'◆', mult:7,   pairMult:1.1,  family:'gem',   color:'green',  label:'gem',
    pro: { mult6:60,  mult5:13, mult4:4,   mult3:1.4, pair:1.1 } },
  // Bottom 5 symbols: pro 3-of-a-kind pays a small per-symbol amount (0.1×–0.3×)
  // so even high-weight reels produce visible wins instead of zeros. Trimmed
  // from the symbolic 0.1–0.5 ladder to leave headroom for the random
  // consolation (CONSOLATION_TABLE) without pushing pro-mode RTP past 100%.
  { s:'¢', mult:6,   pairMult:0.5,  family:'money', color:'green',  label:'coin',
    pro: { mult6:50,  mult5:8,  mult4:3,   mult3:0.3,  pair:0.5 } },
  { s:'♢', mult:5,   pairMult:0.4,  family:'gem',   color:'cyan',   label:'cracked gem',
    pro: { mult6:20,  mult5:5,  mult4:2,   mult3:0.25, pair:0.4 } },
  { s:'♡', mult:4,   pairMult:0.3,  family:'heart', color:'hot',    label:'lil heart',
    pro: { mult6:15,  mult5:4,  mult4:1.5, mult3:0.2,  pair:0.3 } },
  { s:'¤', mult:3,   pairMult:0.2,  family:'money', color:'yellow', label:'penny',
    pro: { mult6:12,  mult5:3,  mult4:1.4, mult3:0.15, pair:0.25 } },
  { s:'⋆', mult:2.5, pairMult:0.1,  family:'star',  color:'cyan',   label:'spark',
    pro: { mult6:10,  mult5:2.5,mult4:1.3, mult3:0.1,  pair:0.2 } },
];

// Family payouts calibrated for ~97% RTP in normal mode (verified via sim-rtp.ps1).
// Family hits are by far the most common payout tier, so this is the main RTP lever.
const FAMILY_MULT = { money: 14, gem: 16, heart: 16, star: 18 };
// 7-family payout depends on hollow-vs-solid composition (handled in evaluateWin).
//   2 hollow + 1 solid → 9×
//   2 solid + 1 hollow → 8×
const SEVEN_FAMILY_MULT_HOLLOW_HEAVY = 9;
const SEVEN_FAMILY_MULT_SOLID_HEAVY  = 8;
const FAMILY_INFO = {
  money: { label:'money family', symbols:['$','¢','¤'], color:'yellow' },
  gem:   { label:'gem family',   symbols:['◆','♢'],     color:'green'  },
  heart: { label:'heart family', symbols:['♥','♡'],     color:'hot'    },
  star:  { label:'star family',  symbols:['★','⋆'],     color:'cyan'   },
  '7':   { label:'red 7 family', symbols:['7','7'],     color:'red'    },
};

// 11 entries (hollow 7 added at index 0). Sums to 100.
const WEIGHTS = [1, 3, 5, 8, 8, 8, 14, 16, 16, 12, 9];

// Random consolation — applied when the natural reels return no winning combo.
// Rolled against an extra fairness float (floats[7]). Cumulative thresholds:
//   12% → 0.1×    (recover 10%)
//    8% → 0.2×
//    5% → 0.3×
//    3% → 0.4×
//    2% → 0.5×    (recover half)
//   rest → full loss
// ~30% of natural-loss spins return a partial refund instead of zero.
const CONSOLATION_TABLE = [
  { upTo: 0.12, mult: 0.1 },
  { upTo: 0.20, mult: 0.2 },
  { upTo: 0.25, mult: 0.3 },
  { upTo: 0.28, mult: 0.4 },
  { upTo: 0.30, mult: 0.5 },
];
function pickConsolation(f) {
  const hit = CONSOLATION_TABLE.find(c => f < c.upTo);
  return hit ? hit.mult : 0;
}

function evaluateWin(targets) {
  // Normal mode (3 reels). Priority: 3-of-a-kind > 3-of-family > pair > none.
  // `symbols` is the array of winning slot symbols (used by the result display).
  if (targets[0] === targets[1] && targets[1] === targets[2]) {
    const sym = SLOT_SYMBOLS[targets[0]];
    if (sym.mult > 0) {
      return { mult: sym.mult, kind: 'three-of-a-kind', symbols: [sym, sym, sym], label: sym.label };
    }
  }
  const families = targets.map(i => SLOT_SYMBOLS[i].family);
  if (families[0] === families[1] && families[1] === families[2]) {
    const f = families[0];
    const symList = targets.map(i => SLOT_SYMBOLS[i]);
    if (f === '7') {
      // Mixed hollow + solid 7s. Pure 3-of-a-kind already returned above,
      // so we know it's a mix. Majority hollow → 9×, majority solid → 8×.
      const hollowCount = symList.filter(s => s.hollow).length;
      const mult = hollowCount >= 2 ? SEVEN_FAMILY_MULT_HOLLOW_HEAVY : SEVEN_FAMILY_MULT_SOLID_HEAVY;
      return { mult, kind: 'three-of-a-family', symbols: symList, label: FAMILY_INFO['7'].label };
    }
    return { mult: FAMILY_MULT[f], kind: 'three-of-a-family', symbols: symList, label: FAMILY_INFO[f].label };
  }
  let pairIdx = -1;
  if (targets[0] === targets[1]) pairIdx = targets[0];
  else if (targets[1] === targets[2]) pairIdx = targets[1];
  else if (targets[0] === targets[2]) pairIdx = targets[0];
  if (pairIdx !== -1) {
    const sym = SLOT_SYMBOLS[pairIdx];
    if (sym.pairMult > 0) {
      return { mult: sym.pairMult, kind: 'pair', symbols: [sym, sym], label: `pair of ${sym.label}` };
    }
  }
  return { mult: 0, kind: 'none', symbols: [], label: 'no match' };
}

function evaluateProWin(targets) {
  // Pro mode (6 reels): pay the BEST tier reachable across all symbols.
  // 3-of-a-kind is the floor — pairs never pay. Bottom-tier 3-of-a-kinds pay
  // small (0.1×–0.3×); spins with no winning combo at all roll the random
  // CONSOLATION_TABLE refund as a final fallback.
  const counts = {};
  targets.forEach(i => { counts[i] = (counts[i] || 0) + 1; });
  let best = null;
  for (const idx in counts) {
    const count = counts[idx];
    const sym = SLOT_SYMBOLS[+idx];
    const m = sym.pro;
    let mult = 0, kind = 'none';
    if (count >= 6)      { mult = m.mult6; kind = 'six-of-a-kind'; }
    else if (count === 5){ mult = m.mult5; kind = 'five-of-a-kind'; }
    else if (count === 4){ mult = m.mult4; kind = 'four-of-a-kind'; }
    else if (count === 3){ mult = m.mult3; kind = 'three-of-a-kind'; }
    if (mult > (best?.mult || 0)) best = { mult, kind, sym, count };
  }
  if (!best) return { mult: 0, kind: 'none', symbols: [], label: 'no match' };
  return {
    mult: best.mult, kind: best.kind,
    symbols: Array(best.count).fill(best.sym),
    label: `${best.count}× ${best.sym.label}`,
  };
}

function SlotsPage({ palette: M, wallet, fairness, onConnect }) {
  const [proMode, setProMode] = useStateSL(false);
  const reelsCount = proMode ? 6 : 3;
  const [reels, setReels] = useStateSL([0,1,2]);

  // Mobile-aware reel sizing — 6-reel PRO mode at 56px wide × 6 + gaps overflows
  // a phone Frame, so on phones we drop to a 'tiny' variant.
  const [isMobile, setIsMobile] = useStateSL(() =>
    typeof window !== 'undefined' && window.matchMedia
      ? window.matchMedia('(max-width: 768px)').matches : false
  );
  useEffectSL(() => {
    if (typeof window === 'undefined' || !window.matchMedia) return;
    const mq = window.matchMedia('(max-width: 768px)');
    const handler = e => setIsMobile(e.matches);
    if (mq.addEventListener) mq.addEventListener('change', handler);
    else mq.addListener(handler);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener('change', handler);
      else mq.removeListener(handler);
    };
  }, []);
  const reelSize = (isMobile && proMode) ? 'tiny' : (proMode ? 'small' : 'normal');
  const [spinning, setSpinning] = useStateSL(false);
  const { bet, setBet, betStr, setBetStr } = useBetInput(0.1);
  const [last, setLast] = useStateSL(null);
  const [error, setError] = useBetError();
  // Synchronous re-entry lock (audit HIGH-9). See game-flip.jsx for rationale.
  const busyRef = useRefSL(false);

  // Autospin: autoRef is the sync source of truth so the drive effect can
  // re-check after a setTimeout without reading stale React state. The
  // autoRemaining state mirror exists only to re-render the controls.
  const [autoRemaining, setAutoRemaining] = useStateSL(0);
  const autoRef = useRefSL(0);

  // Bet bounds (in the active coin). Both 3-reel and 6-reel modes share the
  // same range — keeps payouts predictable since multipliers cap at 200×.
  // Min: 0.01 SOL or 1 USDC/USDT. Max: 1 SOL equivalent (~168 USDC/USDT).
  const SLOT_MIN_BET = coinMinBet(wallet.activeCoin);
  const SLOT_MAX_BET = maxBetForCoin(1, wallet.activeCoin);

  // Reset reel display whenever mode flips so the visual reel count matches.
  // Clamp the bet into the active coin's [min, max] range when toggling modes
  // OR switching coins (USDC/USDT mode raises the floor to 1).
  useEffectSL(() => {
    setReels(Array.from({length: reelsCount}, (_, i) => i % SLOT_SYMBOLS.length));
    setLast(null);
    if (bet < SLOT_MIN_BET) setBet(SLOT_MIN_BET);
    else if (bet > SLOT_MAX_BET) setBet(SLOT_MAX_BET);
    // Mode or coin change cancels any pending autospin — bet bounds shifted,
    // safer to require an explicit restart than to keep firing under new rules.
    if (autoRef.current > 0) { autoRef.current = 0; setAutoRemaining(0); }
  }, [proMode, wallet.activeCoin]); // eslint-disable-line

  function startAuto(n) {
    if (busyRef.current || spinning) return;
    autoRef.current = n;
    setAutoRemaining(n);
  }
  function stopAuto() {
    autoRef.current = 0;
    setAutoRemaining(0);
  }
  function abortAutoOnFail() {
    if (autoRef.current > 0) stopAuto();
  }

  async function spin() {
    if (busyRef.current) return;
    if (spinning) return;
    if (!wallet.canPlay) { abortAutoOnFail(); onConnect(); return; }
    const bag = wallet.balance[wallet.activeCoin] || 0;
    if (bet < SLOT_MIN_BET) {
      setError(`minimum bet is ${SLOT_MIN_BET} ${wallet.activeCoin}`);
      abortAutoOnFail();
      return;
    }
    if (bet > SLOT_MAX_BET) {
      setError(`maximum bet is ${SLOT_MAX_BET} ${wallet.activeCoin}`);
      abortAutoOnFail();
      return;
    }
    if (bet > bag) {
      setError(`bet ${bet.toFixed(3)} ${wallet.activeCoin} exceeds bag (${bag.toFixed(3)} ${wallet.activeCoin} available)`);
      abortAutoOnFail();
      return;
    }
    busyRef.current = true;
    setError(null);
    setSpinning(true); setLast(null);
    if (!wallet.debit(wallet.activeCoin, bet)) {
      busyRef.current = false;
      setSpinning(false);
      setError(`bag changed under the bet -- try again`);
      abortAutoOnFail();
      return;
    }
    playSlotSpinSound();

    // §5.67: server owns the symbol pick + paytable evaluation. We send
    // {bet, proMode, clientSeed, coin, user} and get back {targets, mult,
    // kind, label, payout}. PRO_FORCED + CONSOLATION_TABLE live server-side
    // only; the browser no longer needs hashToFloats / WEIGHTS for outcome
    // resolution.
    let server;
    try {
      server = await callServerGame('/play/slots', {
        bet,
        proMode,
        clientSeed: fairness.clientSeed,
        coin: wallet.activeCoin,
        user: wallet.username,
      });
    } catch (err) {
      wallet.credit(wallet.activeCoin, bet, 'slots-refund-' + Date.now());
      stopSlotSpinSound();
      setSpinning(false);
      busyRef.current = false;
      setError('server error: ' + (err && err.message ? err.message : String(err)));
      abortAutoOnFail();
      return;
    }

    const targets   = Array.isArray(server.targets) ? server.targets : [];
    const mult      = Number(server.mult) || 0;
    const kind      = String(server.kind || 'none');
    const label     = String(server.label || (mult > 0 ? 'win' : 'no match'));
    const payout    = Number(server.payout) || 0;
    const serverHex = String(server.hex || '');

    // Reveal each reel evenly across the 2s spin window. Last reel lands
    // at exactly SPIN_MS so the final symbol arrives just before spinning
    // flips off and the result row renders.
    const SPIN_MS = 2000;
    for (let i = 0; i < reelsCount; i++) {
      const t = ((i + 1) / reelsCount) * SPIN_MS;
      const target = typeof targets[i] === 'number' ? targets[i] : 0;
      setTimeout(() => setReels(prev => { const x = [...prev]; x[i] = target; return x; }), t);
    }
    await new Promise(res => setTimeout(res, SPIN_MS));

    // Browser-side evaluator runs ONLY to extract the symbols glyphs for the
    // result-row display. Server is authoritative on mult/kind/payout. If the
    // server applied consolation, kind === 'consolation' and the symbols
    // array isn't used (the consolation render branch doesn't draw symbols).
    const localWin = proMode ? evaluateProWin(targets) : evaluateWin(targets);
    const symbols = (kind === 'consolation' || kind === 'none') ? [] : localWin.symbols;

    if (payout > 0) wallet.credit(wallet.activeCoin, payout, serverHex || ('slots-cash-' + Date.now()));
    wallet.logPlay({ game:'Slots', mode: proMode?'pro':'normal', bet, payout, win: payout>0, coin: wallet.activeCoin, hex: serverHex.slice(0,16), mult, kind });
    // NOTE: server wrote /plays already (rules step7 blocks browser writes for Slots).
    setLast({ targets, mult, kind, symbols, label, payout, hex: serverHex.slice(0,12) });
    setSpinning(false);
    busyRef.current = false;
    // Tick the autospin counter — drive effect schedules the next spin
    // once both the busy lock and spinning state have cleared.
    if (autoRef.current > 0) { autoRef.current--; setAutoRemaining(autoRef.current); }
    stopSlotSpinSound();
    // Top-tier hits get the jackpot fanfare:
    //   • 3-reel: hitting the 20× MEGA JACKPOT (hollow 7 three-of-a-kind)
    //   • 6-reel: hitting the 200× cap (hollow 7 six-of-a-kind)
    // Mid-tier wins (5×–9×) get the streak fanfare. Everything else (incl.
    // consolation) routes through the standard win/loss sound.
    const isJackpot = (!proMode && mult === 20) || (proMode && mult === 200);
    if (isJackpot) {
      playJackpotSound();
    } else if (mult >= 5 && mult <= 9) {
      playStreakSound();
    } else {
      playOutcomeSound(payout > 0);
    }
  }

  // Drive the autospin loop. After each spin completes (spinning flips back
  // to false and the busy lock clears), schedule the next spin with a brief
  // delay so the player can see the result. Reading autoRef inside the
  // timeout guards against a STOP click between schedule and fire.
  useEffectSL(() => {
    if (autoRemaining > 0 && !spinning) {
      const id = setTimeout(() => {
        if (autoRef.current > 0) spin();
      }, 600);
      return () => clearTimeout(id);
    }
  }, [autoRemaining, spinning]); // eslint-disable-line

  return (
    <div className="rc-page rc-slots-page" style={{flex:1, padding:'18px 26px 40px', overflowY:'auto'}}>
      <h1 className="rc-h-page" style={{fontSize:48, color:M.hot, margin:'4px 0', textShadow:`3px 3px 0 ${M.yellow}`}}>SLOTS</h1>
      <div className="rc-h-sub" style={{color:M.ink2, fontSize:18, marginBottom:14}}>
        {proMode
          ? <><span style={{color:M.green}}>97.8% RTP</span> · PRO 6-reel · 6/5/4/3-of-a-kind · bigger highs, no pair payouts</>
          : <><span style={{color:M.green}}>96.8% RTP</span> · 3-reel · 11 symbols · 3-of-a-kind / family / pair</>
        }
      </div>

      {/* Mode toggle */}
      <div style={{display:'flex', gap:8, marginBottom:14, alignItems:'center'}}>
        <span style={{fontSize:13, color:M.ink2, letterSpacing:'0.05em'}}>MODE:</span>
        {[
          { k:false, l:'NORMAL · 3 REELS' },
          { k:true,  l:'PRO · 6 REELS'    },
        ].map(opt => {
          const active = proMode === opt.k;
          return (
            <button key={String(opt.k)} onClick={()=>{ if (!spinning && !autoRemaining) setProMode(opt.k); }} disabled={spinning || autoRemaining > 0} style={{
              background: active ? M.hot : M.bg,
              color:      active ? M.bg  : M.hot,
              border:`2px solid ${M.hot}`, padding:'5px 14px', fontFamily:'inherit', fontSize:14, fontWeight:700,
              cursor: (spinning || autoRemaining > 0) ? 'not-allowed' : 'pointer', letterSpacing:'0.05em',
              boxShadow: active ? `3px 3px 0 ${M.bg}` : 'none',
            }}>{opt.l}</button>
          );
        })}
      </div>

      <PlayModeBar palette={M} wallet={wallet} onConnect={onConnect}/>
      <BetErrorToast error={error} onClose={()=>setError(null)} palette={M}/>

      <div className="rc-game-grid" style={{display:'grid', gridTemplateColumns:'320px 1fr', gap:18}}>
        <Frame title={proMode ? 'pro paytable' : 'paytable'} accent={M.cyan}>
          {proMode ? <ProPaytable M={M}/> : <NormalPaytable M={M}/>}
          <ConsolationRow M={M}/>
          <div style={{marginTop:12, fontSize:13, color:M.ink2, lineHeight:1.4}}>
            {proMode
              ? <>{'>'} highest match-count anywhere wins. payout scales with how many of the same symbol land.</>
              : <>{'>'} highest tier wins. families = same color group. pair = any 2 reels matching.</>}
          </div>
        </Frame>

        <Frame title="machine" accent={M.yellow} style={{minHeight:340, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center'}}>
          {/* Reels */}
          <div className={`rc-slots-reels${proMode ? ' rc-slots-reels-pro' : ''}`} style={{display:'flex', gap: reelSize==='tiny' ? 4 : proMode?6:10, padding:18, background:M.bg, border:`4px solid ${M.yellow}`, boxShadow:`6px 6px 0 ${M.hot}`}}>
            {reels.map((idx,i)=>(
              <Reel key={i} idx={idx} spinning={spinning} M={M} size={reelSize}/>
            ))}
          </div>

          {/* Result slot — fixed height so the SPIN button below doesn't shift.
              flex-start anchors content to the top so a tall WIN block doesn't
              push earlier content up when it replaces the shorter placeholder
              (mobile keeps `center` via .rc-resolve-slot-col rule). */}
          <div className="rc-resolve-slot-col" style={{marginTop:18, textAlign:'center', minHeight:78, display:'flex', flexDirection:'column', justifyContent:'flex-start'}}>
            {last && !spinning && (
              <>
                {last.payout > 0 && last.kind === 'consolation' ? (
                  <div style={{fontSize:32, color:M.yellow, fontWeight:700, textShadow:`2px 2px 0 ${M.bg}`, lineHeight:1}}>
                    ≈ {last.mult}× = +{last.payout.toFixed(wallet.activeCoin==='SOL'?3:2)} {wallet.activeCoin}
                  </div>
                ) : last.payout > 0 ? (
                  <div style={{display:'flex', alignItems:'center', justifyContent:'center', gap:8, color:M.green, fontWeight:700, textShadow:`2px 2px 0 ${M.bg}`, lineHeight:1}}>
                    <span style={{display:'inline-flex', gap:4}}>
                      {last.symbols.map((s, i) => <PaytableGlyph key={i} sym={s} M={M} size={28}/>)}
                    </span>
                    <span style={{fontSize:32}}>{last.mult}× = +{last.payout.toFixed(wallet.activeCoin==='SOL'?3:2)} {wallet.activeCoin}</span>
                  </div>
                ) : (
                  <div style={{fontSize:32, color:M.red, fontWeight:700, textShadow:`2px 2px 0 ${M.bg}`, lineHeight:1}}>✗ no match</div>
                )}
                {last.payout > 0 && (
                  <div style={{fontSize:14, color:M.yellow, marginTop:4, letterSpacing:'0.05em'}}>{last.label}</div>
                )}
                <div style={{fontSize:13, color:M.ink2, marginTop:4, fontFamily:'JetBrains Mono, monospace'}}>hash: {last.hex}…</div>
              </>
            )}
            {spinning && (
              <div style={{fontSize:16, color:M.yellow, opacity:0.8}}>spinning…</div>
            )}
          </div>

          {/* Greyout the bet panel during the spin (or autospin sequence) so
              the player can't change bet mid-spin (matches the flip page
              pattern). Autospin controls live outside this wrapper so STOP
              stays clickable while the panel is locked. */}
          <div className="rc-slots-bet rc-bet-panel" style={{display:'flex', flexDirection:'column', gap:8, marginTop:18, alignItems:'stretch', minWidth:340, opacity: (spinning || autoRemaining > 0) ? 0.4 : 1, pointerEvents: (spinning || autoRemaining > 0) ? 'none' : 'auto', transition: 'opacity 0.15s'}}>
            {/* Bet label on its own line above the input so the "· min–max"
                hint never overflows behind the number box at narrow widths. */}
            <div style={{display:'flex', alignItems:'baseline', gap:6, fontSize:13}}>
              <span style={{color:M.ink2}}>bet ({wallet.activeCoin})</span>
              <span style={{color:M.yellow}}>· {SLOT_MIN_BET}–{SLOT_MAX_BET}</span>
            </div>
            <div className="rc-slots-betspin" style={{display:'flex', gap:10, alignItems:'center'}}>
              <input type="number" step={0.01} min={SLOT_MIN_BET} max={SLOT_MAX_BET} value={betStr} onChange={e=>setBetStr(e.target.value)} disabled={spinning || autoRemaining > 0} style={{
                width:120, background:M.bg, border:`2px solid ${M.green}`, color:M.green, padding:'6px 8px', fontFamily:'inherit', fontSize:18, outline:'none',
              }}/>
              <button onClick={spin} disabled={spinning || autoRemaining > 0} style={{
                flex:1, background: (spinning || autoRemaining > 0) ? M.ink2 : M.hot,
                color:M.bg, border:`3px solid ${M.bg}`, fontFamily:'inherit', fontSize:22,
                padding:'8px 18px', fontWeight:700, cursor: (spinning || autoRemaining > 0) ? 'wait' : 'pointer',
                boxShadow:`4px 4px 0 ${M.green}`, minWidth:120, textAlign:'center',
              }}>► SPIN</button>
            </div>
            <QuickBetButtons
              values={[0.01, 0.1, 0.25, 0.5, 1]}
              current={bet} onPick={v=>setBet(v)} palette={M} disabled={spinning || autoRemaining > 0}
            />
          </div>

          {/* Autospin controls — sit outside the bet-panel greyout so STOP
              stays clickable while a sequence is running. AUTO ×N starts an
              N-spin sequence; the drive effect chains them with a brief gap. */}
          <div className="rc-slots-auto" style={{display:'flex', gap:8, marginTop:8, alignItems:'center', justifyContent:'center', minWidth:340, flexWrap:'wrap'}}>
            {autoRemaining === 0 ? (
              <>
                <span style={{fontSize:13, color:M.ink2, letterSpacing:'0.05em', marginRight:4}}>AUTO:</span>
                {[5, 10, 50, 100].map(n => (
                  <button key={n} onClick={()=>startAuto(n)} disabled={spinning} style={{
                    background:M.bg, color:M.cyan, border:`2px solid ${M.cyan}`, fontFamily:'inherit',
                    fontSize:14, padding:'4px 12px', fontWeight:700, letterSpacing:'0.05em',
                    cursor: spinning?'not-allowed':'pointer',
                  }}>× {n}</button>
                ))}
              </>
            ) : (
              <>
                <span style={{fontSize:13, color:M.yellow, letterSpacing:'0.05em', fontFamily:'JetBrains Mono, monospace'}}>
                  AUTO {autoRemaining} LEFT
                </span>
                <button onClick={stopAuto} style={{
                  background:M.red, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit',
                  fontSize:14, padding:'4px 14px', fontWeight:700, letterSpacing:'0.05em',
                  cursor:'pointer', boxShadow:`3px 3px 0 ${M.bg}`,
                }}>■ STOP</button>
              </>
            )}
          </div>
        </Frame>
      </div>

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

// Slots stats — derived from wallet.history filtered to game==='Slots' and the
// user's active coin. Mirrors the flip-stats panel layout.
function SlotsStatsPanel({ palette: M, wallet }) {
  const coin = wallet.activeCoin;
  const spins = (wallet.history || []).filter(h => h.game === 'Slots' && h.coin === coin);
  const total = spins.length;
  const hits = spins.filter(s => s.win).length;
  const hitRate = total ? (hits / 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 bestMult = spins.reduce((m, x) => Math.max(m, x.mult || 0), 0);
  // SOL allows 0.01 bets, and the consolation table includes 0.1× — so a
  // 0.001 SOL payout is possible. Use 3 decimals on SOL so those don't
  // round to 0.00. USDC/USDT min is 1, smallest win 0.1, so 2 is fine.
  const dp = coin === 'SOL' ? 3 : 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="slots stats" accent={M.cyan} style={{marginTop:18}}>
      {total === 0 ? (
        <div style={{color:M.ink2, fontSize:14, padding:'4px 0'}}>{'>'} no spins yet in {coin}. play a round.</div>
      ) : (
        <>
          <div className="rc-stat-6" style={{display:'grid', gridTemplateColumns:'repeat(6, 1fr)', gap:14}}>
            {tile('spins',          total,                          M.green)}
            {tile('hit rate',       `${hitRate.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('best multiplier',`${bestMult}×`,                 M.hot)}
          </div>
          <LastRoundsTable M={M} rounds={spins} limit={20} headers={['mode', 'mult', '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)}`;
            return [
              { value: s.mode === 'pro' ? 'PRO' : 'NORMAL', color: s.mode === 'pro' ? M.hot : M.green, fontWeight: 700 },
              { value: s.mult > 0 ? `${s.mult}×` : '—',     color: s.mult > 0 ? M.yellow : M.ink2 },
              { 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>
  );
}

// Per-row family multiplier (distinguishes hollow-heavy vs solid-heavy 7-fam rows).
function rowFamilyMult(s) {
  if (s.family === '7') {
    return s.hollow ? SEVEN_FAMILY_MULT_HOLLOW_HEAVY : SEVEN_FAMILY_MULT_SOLID_HEAVY;
  }
  return FAMILY_MULT[s.family] || 0;
}

// glyphStyle — render a slot glyph as solid color, or as an outlined hollow
// shape. Hollow uses CSS -webkit-text-stroke on a vector sans-serif font
// (VT323's bitmap pixels render filled when stroked, so we swap fonts only
// for hollow glyphs to get a clean outline).
function glyphStyle(sym, M, fs) {
  const c = M[sym.color] || M.ink;
  if (sym.hollow) {
    return {
      fontSize: fs,
      lineHeight: 1,
      fontWeight: 900,
      fontFamily: 'Arial, "Helvetica Neue", sans-serif',
      color: 'transparent',
      WebkitTextFillColor: 'transparent',
      WebkitTextStrokeWidth: `${Math.max(1.5, fs / 18)}px`,
      WebkitTextStrokeColor: c,
    };
  }
  return {
    fontSize: fs,
    lineHeight: 1,
    fontWeight: 700,
    fontFamily: '"VT323", monospace',
    color: c,
  };
}

// Glyph wrapped in a fixed-size box so solid (VT323) and hollow (sans-serif)
// occupy the same vertical/horizontal footprint despite differing font metrics.
function PaytableGlyph({ sym, M, size }) {
  return (
    <span style={{
      display:'inline-flex', alignItems:'center', justifyContent:'center',
      width: size + 8, height: size + 4,
    }}>
      <span style={glyphStyle(sym, M, size)}>{sym.s}</span>
    </span>
  );
}

// Cell wrapper — flex-centered so each row's cells share a baseline and the
// dashed top border lines up across columns regardless of cell content height.
function rowCellStyle(M, extra) {
  return {
    display:'flex', alignItems:'center', justifyContent:'flex-end',
    minHeight: 32, padding:'0 4px',
    borderTop:`1px dashed ${M.green}22`,
    fontFamily:'JetBrains Mono, monospace', fontWeight:700, fontSize:14,
    ...extra,
  };
}

function NormalPaytable({ M }) {
  // Matrix layout mirroring the pro paytable: one row per symbol, columns
  // for 3-kind / family / pair. Per-row family value comes from the symbol's
  // family group (so 3 money symbols all show the same family number).
  const headerStyle = { fontSize:11, color:M.ink2, letterSpacing:'0.08em', textAlign:'right', padding:'2px 4px' };
  return (
    <div style={{display:'grid', gridTemplateColumns:'34px repeat(3, 1fr)', alignItems:'stretch', columnGap:4, rowGap:0}}>
      <span/>
      <span style={headerStyle}>3-kind</span>
      <span style={headerStyle}>family</span>
      <span style={headerStyle}>pair</span>
      {SLOT_SYMBOLS.map((s, idx) => {
        const fam = rowFamilyMult(s);
        return (
          <React.Fragment key={`${idx}-${s.s}-${s.hollow?'h':'s'}`}>
            <span style={rowCellStyle(M, { justifyContent:'center' })}><PaytableGlyph sym={s} M={M} size={26}/></span>
            <span style={rowCellStyle(M, { color: s.mult > 0 ? M.green : M.ink2 })}>{s.mult > 0 ? `${s.mult}×` : '—'}</span>
            <span style={rowCellStyle(M, { color: fam > 0 ? M.yellow : M.ink2 })}>{fam > 0 ? `${fam}×` : '—'}</span>
            <span style={rowCellStyle(M, { color: s.pairMult > 0 ? M.hot : M.ink2 })}>{s.pairMult > 0 ? `${s.pairMult}×` : '—'}</span>
          </React.Fragment>
        );
      })}
    </div>
  );
}

// Consolation tier list — derived from CONSOLATION_TABLE so probabilities and
// payouts stay in sync with the spin logic. Renders below the paytable in both
// modes since the random refund applies to any no-match spin.
function ConsolationRow({ M }) {
  let prev = 0;
  const tiers = CONSOLATION_TABLE.map(t => {
    const pct = (t.upTo - prev) * 100;
    prev = t.upTo;
    return { mult: t.mult, pct };
  }).slice().reverse();
  return (
    <div style={{marginTop:14, paddingTop:10, borderTop:`1px dashed ${M.yellow}55`}}>
      <div style={{fontSize:11, color:M.ink2, letterSpacing:'0.08em', marginBottom:6}}>
        NEAR MISS · NO WINNING COMBO
      </div>
      <div style={{display:'grid', gridTemplateColumns:'repeat(5, 1fr)', gap:4, fontFamily:'JetBrains Mono, monospace'}}>
        {tiers.map(t => (
          <div key={t.mult} style={{textAlign:'center', padding:'4px 0', border:`1px solid ${M.yellow}33`, background:M.bg}}>
            <div style={{fontSize:16, color:M.yellow, fontWeight:700, lineHeight:1}}>{t.mult}×</div>
            <div style={{fontSize:11, color:M.ink2, marginTop:3}}>{t.pct.toFixed(0)}%</div>
          </div>
        ))}
      </div>
    </div>
  );
}

function ProPaytable({ M }) {
  // Compact 5-column grid: SYMBOL | 6× | 5× | 4× | 3×
  const headerStyle = { fontSize:11, color:M.ink2, letterSpacing:'0.08em', textAlign:'right', padding:'2px 4px' };
  return (
    <div style={{display:'grid', gridTemplateColumns:'34px repeat(4, 1fr)', alignItems:'stretch', columnGap:4, rowGap:0}}>
      <span/>
      <span style={headerStyle}>6×</span>
      <span style={headerStyle}>5×</span>
      <span style={headerStyle}>4×</span>
      <span style={headerStyle}>3×</span>
      {SLOT_SYMBOLS.map((s, idx)=>{
        const cell = (val, color) => (
          <span style={rowCellStyle(M, { color: val > 0 ? color : M.ink2 })}>{val > 0 ? `${val}×` : '—'}</span>
        );
        return (
          <React.Fragment key={`${idx}-${s.s}-${s.hollow?'h':'s'}`}>
            <span style={rowCellStyle(M, { justifyContent:'center' })}><PaytableGlyph sym={s} M={M} size={26}/></span>
            {cell(s.pro.mult6, M.hot)}
            {cell(s.pro.mult5, M.yellow)}
            {cell(s.pro.mult4, M.cyan)}
            {cell(s.pro.mult3, M.green)}
          </React.Fragment>
        );
      })}
    </div>
  );
}

function Reel({ idx, spinning, M, size }) {
  const sym = SLOT_SYMBOLS[idx];
  // Three sizes: normal (3-reel desktop/mobile), small (6-reel desktop),
  // tiny (6-reel mobile — 6 reels at 42px fit a phone Frame).
  const w  = size === 'tiny' ? 42 : size === 'small' ? 56 : 90;
  const h  = size === 'tiny' ? 60 : size === 'small' ? 80 : 120;
  const fs = size === 'tiny' ? 32 : size === 'small' ? 44 : 72;
  const blurFs = size === 'tiny' ? 26 : size === 'small' ? 38 : 60;
  return (
    <div style={{
      width:w, height:h, background:M.panel, border:`2px solid ${M.green}`,
      display:'flex', alignItems:'center', justifyContent:'center', position:'relative', overflow:'hidden',
    }}>
      <style>{`@keyframes reelSpin { from{transform:translateY(0)} to{transform:translateY(-1200px)} }`}</style>
      {spinning ? (
        <div style={{display:'flex', flexDirection:'column', animation:'reelSpin 0.3s linear infinite'}}>
          {[...SLOT_SYMBOLS, ...SLOT_SYMBOLS, ...SLOT_SYMBOLS].map((s,i)=>(
            <span key={i} style={{...glyphStyle(s, M, blurFs), height:h, lineHeight:`${h}px`, textAlign:'center'}}>{s.s}</span>
          ))}
        </div>
      ) : (
        <span style={glyphStyle(sym, M, fs)}>{sym.s}</span>
      )}
    </div>
  );
}

Object.assign(window, { SlotsPage });
