// game-blackjack.jsx — Blackjack · ~99% RTP under basic strategy · dealer h17
// Cards drawn i.i.d. uniform over 13 ranks (infinite-deck approximation; card
// counting is not exploitable).
// Rules: dealer hits soft 17 (H17) and stands on all hard 17+. Naturals pay 7:5 except on splits.
// Splits allowed once on equal-value pairs. Double allowed on any 2-card hand
// (including post-split). The bet input stays fixed across rounds — double
// and split debit additional stakes from the bag without mutating `bet`.
//
// §5.71 — server-resolved (final Phase 1 game). Two endpoints:
//   POST /play/blackjack/start — deals 4 cards, checks naturals.
//   POST /play/blackjack/action {action} — hit / stand / double / split.
// Server owns the per-card chain (sha256(chainHex:nonce) → unbiasedMod 13),
// the H17 dealer rule, and all hand resolution. When the last player hand
// resolves, the server plays the dealer and returns the final state in the
// same response. Browser receives {status, hands, dealerUp|dealer, ...} and
// animates new cards as they arrive. busyRef still guards against re-entry
// during in-flight requests.
//
// Implementation notes:
//   • Phase enum: idle → dealing → player → dealer → done.
//   • UI uses fixed min-heights on every section so the page height never
//     changes mid-round. The result frame is always rendered (placeholder
//     when no last round), and the player-hand grid is always two columns.
//   • Local drawCardFromHex + pullCard removed (server owns card derivation).
//     bjVal / bjHandTotal / bjIsSoft retained for the per-hand totals shown
//     under each card stack.

const { useState: useStateBJ, useEffect: useEffectBJ, useRef: useRefBJ } = React;

// BJ_SUITS / BJ_RANKS were used by the local drawCardFromHex + pullCard
// pre-§5.71. Server owns derivation now; cards arrive with their {r, s}
// already populated, so the browser only needs bjVal/bjHandTotal/bjIsSoft
// for the per-hand totals shown under each card stack.
function bjVal(r){ if (r==='A') return 11; if (['J','Q','K'].includes(r)) return 10; return +r; }
function bjHandTotal(hand) {
  let total = hand.reduce((s,c)=>s + bjVal(c.r), 0);
  let aces = hand.filter(c=>c.r==='A').length;
  while (total > 21 && aces > 0) { total -= 10; aces--; }
  return total;
}
// Soft = at least one ace still counted as 11 in the optimal total. Used for
// the H17 dealer rule (hit on soft 17, stand on hard 17).
function bjIsSoft(hand) {
  let hard = 0; let hasAce = false;
  for (const c of hand) {
    hard += (c.r === 'A') ? 1 : bjVal(c.r);
    if (c.r === 'A') hasAce = true;
  }
  return hasAce && hard + 10 <= 21;
}

function BlackjackPage({ palette: M, wallet, fairness, onConnect }) {
  const { bet, setBet, betStr, setBetStr } = useBetInput(0.1);
  const [phase, setPhase] = useStateBJ('idle'); // idle | dealing | player | dealer | done
  // hands: [{cards, stake, status, doubled}]. status ∈ active|stood|busted|natural
  const [hands, setHands] = useStateBJ([]);
  const [activeIdx, setActiveIdx] = useStateBJ(0);
  const [dealer, setDealer] = useStateBJ([]);
  const [hideHole, setHideHole] = useStateBJ(true);
  const [last, setLast] = useStateBJ(null);
  const [error, setError] = useBetError();

  // §5.71: round identity. Server holds the per-card chain + dealer hole card
  // + hands state keyed by ${uid}:${roundId}. roundHexRef populated from the
  // start response for the result-row hash display.
  const roundIdRef  = useRefBJ(null);
  const roundHexRef = useRefBJ('');
  // Busy ref blocks re-entry while an action is awaiting the server response.
  const busyRef     = useRefBJ(false);

  const MIN_BET = coinMinBet(wallet.activeCoin);
  // Per-coin opening-bet cap (100 SOL ≈ 16842 USDC/USDT).
  const MAX_BET_SOL = 100;
  const maxBet = maxBetForCoin(MAX_BET_SOL, wallet.activeCoin);
  // Aggregate round-stake cap (audit HIGH-7). Without this, split+double on
  // both hands commits up to 4× the opening bet (= 400 SOL on a 100 SOL
  // "max bet"), letting a single round exceed the disclosed max-bet cap.
  // 4× is the tightest cap that still allows the full split+double-on-both
  // play; the per-action stake added via debit can be checked against this.
  const MAX_ROUND_STAKE = maxBet * 4;

  // Clamp the bet into [MIN_BET, maxBet] when switching coins so USDC/USDT
  // (1-unit floor) don't leave the 0.1 SOL default below the minimum.
  useEffectBJ(() => {
    if (bet < MIN_BET) setBet(MIN_BET);
    else if (bet > maxBet) setBet(maxBet);
  }, [wallet.activeCoin]); // eslint-disable-line

  // Apply a server response. Two shapes:
  //   { status:'player', hands, dealerUp, activeIdx }
  //   { status:'done',   hands, dealer, results, totalPayout, totalStake, hex? }
  function applyServer(server) {
    const newHands = Array.isArray(server.hands) ? server.hands : [];
    setHands(newHands);
    setActiveIdx(Number(server.activeIdx) || 0);
    if (server.status === 'done') {
      // Final state — dealer hand is fully revealed (incl. hole + any draws).
      // Resolve payouts using the server's `results` array.
      const dealerFinal = Array.isArray(server.dealer) ? server.dealer : [];
      setDealer(dealerFinal);
      setHideHole(false);
      const totalPayout = Number(server.totalPayout) || 0;
      const totalStake  = Number(server.totalStake) || newHands.reduce((s, h) => s + (h.stake || 0), 0);
      const results = Array.isArray(server.results) ? server.results : [];
      const hex = String(server.hex || roundHexRef.current || '');
      if (totalPayout > 0) wallet.credit(wallet.activeCoin, totalPayout, 'bj-cash-' + (roundIdRef.current || Date.now()));
      setLast({ results, totalPayout, totalStake, dTot: Number(server.dTot) || bjHandTotal(dealerFinal), hex: hex.slice(0, 12) });
      wallet.logPlay({
        game:'Blackjack', bet: totalStake, payout: totalPayout,
        win: totalPayout > totalStake, coin: wallet.activeCoin,
        hex: hex.slice(0, 16),
        outcome: results.length === 1 ? results[0].outcome : 'split',
        hands: results.length,
      });
      // NOTE: server wrote /plays (rules step11 blocks browser writes for Blackjack).
      roundIdRef.current = null;
      setPhase('done');
      const hasNaturalBJ = results.some(r => r.outcome === 'bj-win');
      if (hasNaturalBJ) playStreakSound();
      else playOutcomeSound(totalPayout > totalStake);
      return;
    }
    // status === 'player' — only the up card is visible.
    if (server.dealerUp) setDealer([server.dealerUp]);
    setHideHole(true);
    setPhase('player');
  }

  async function deal() {
    if (busyRef.current) return;
    if (phase !== 'idle' && phase !== 'done') return;
    if (!wallet.canPlay) { onConnect(); return; }
    const bag = wallet.balance[wallet.activeCoin] || 0;
    if (bet < MIN_BET) {
      setError(`minimum bet is ${MIN_BET} ${wallet.activeCoin}`);
      return;
    }
    if (bet > maxBet) {
      setError(`max bet on blackjack is ${maxBet.toFixed(2)} ${wallet.activeCoin}`);
      return;
    }
    if (bet > bag) {
      setError(`bet ${bet.toFixed(3)} ${wallet.activeCoin} exceeds bag (${bag.toFixed(3)} ${wallet.activeCoin} available)`);
      return;
    }
    busyRef.current = true;
    setError(null);
    setPhase('dealing');
    setLast(null); setHands([]); setDealer([]); setHideHole(true); setActiveIdx(0);
    if (!wallet.debit(wallet.activeCoin, bet)) {
      busyRef.current = false;
      setPhase('idle');
      setError(`bag changed under the bet -- try again`);
      return;
    }

    let server;
    try {
      server = await callServerGame('/play/blackjack/start', {
        bet,
        clientSeed: fairness.clientSeed,
        coin: wallet.activeCoin,
        user: wallet.username,
      });
    } catch (err) {
      wallet.credit(wallet.activeCoin, bet, 'bj-refund-' + Date.now());
      busyRef.current = false;
      setPhase('idle');
      setError('server error: ' + (err && err.message ? err.message : String(err)));
      return;
    }

    roundIdRef.current = server.roundId;
    roundHexRef.current = String(server.serverHash || '');

    // Brief staggered animation to mirror the pre-§5.71 dealing feel:
    // first player card, dealer up, second player card, dealer hole placeholder.
    const playerHand = (Array.isArray(server.hands) && server.hands[0]) ? server.hands[0] : null;
    const pCards = playerHand ? playerHand.cards : [];
    const dealerUp = server.dealerUp || null;
    setTimeout(()=>{ setHands([{ ...(playerHand || {}), cards: pCards.slice(0, 1) }]); playHitSound(); }, 200);
    setTimeout(()=>{ if (dealerUp) setDealer([dealerUp]); playHitSound(); }, 500);
    setTimeout(()=>{ setHands([playerHand || { cards: [], stake: bet, status: 'active', doubled: false }]); playHitSound(); }, 800);
    setTimeout(()=>{ playHitSound(); /* hole-card thump; visual remains hidden */ }, 1100);
    setTimeout(() => {
      busyRef.current = false;
      // Server may have already resolved on a natural — apply the full state
      // (this reveals the hole card + dealer hand + results).
      applyServer(server);
    }, 1300);
  }

  // Player-action guard — only allow on an active hand during the player phase.
  function canActOnActive() {
    if (phase !== 'player') return false;
    const h = hands[activeIdx];
    return !!h && h.status === 'active';
  }

  // Generic server-action wrapper. Handles 404 (round lost → refund total
  // committed stake) and animates new state via applyServer.
  async function sendAction(action) {
    if (!roundIdRef.current) return;
    let server;
    try {
      server = await callServerGame('/play/blackjack/action', {
        roundId: roundIdRef.current,
        action,
      });
    } catch (err) {
      const msg = err && err.message ? err.message : String(err);
      if (msg.includes('round not found')) {
        const committed = hands.reduce((s, h) => s + (h.stake || 0), 0);
        if (committed > 0) wallet.credit(wallet.activeCoin, committed, 'bj-refund-' + (roundIdRef.current || Date.now()));
        roundIdRef.current = null;
        setPhase('idle');
        setHideHole(false);
        setError('round lost (server reset); refunded ' + committed.toFixed(wallet.activeCoin === 'SOL' ? 3 : 2) + ' ' + wallet.activeCoin);
        return;
      }
      setError('server error: ' + msg);
      return;
    }
    // If the action ended the round, the server returned the full dealer
    // hand + results. Phase the reveal: brief delay so the player sees the
    // hole flip + any dealer draws land before the result row appears.
    if (server.status === 'done') {
      setHideHole(false);
      setPhase('dealer');
      const dealerFinal = Array.isArray(server.dealer) ? server.dealer : [];
      // Animate dealer draws one by one (skip the first 2 — already on table).
      const extraDraws = Math.max(0, dealerFinal.length - 2);
      for (let i = 0; i < extraDraws; i++) {
        setTimeout(() => {
          setDealer(dealerFinal.slice(0, 2 + i + 1));
          playHitSound();
        }, 500 * (i + 1));
      }
      setTimeout(() => applyServer(server), 500 * extraDraws + 300);
      return;
    }
    applyServer(server);
  }

  async function hit() {
    if (busyRef.current) return;
    if (!canActOnActive()) return;
    busyRef.current = true;
    playHitSound();
    try { await sendAction('hit'); }
    finally { busyRef.current = false; }
  }

  async function stand() {
    if (busyRef.current) return;
    if (!canActOnActive()) return;
    busyRef.current = true;
    try { await sendAction('stand'); }
    finally { busyRef.current = false; }
  }

  async function double() {
    if (busyRef.current) return;
    if (!canActOnActive()) return;
    const idx = activeIdx;
    const h = hands[idx];
    if (h.cards.length !== 2) return;
    const bag = wallet.balance[wallet.activeCoin] || 0;
    if (h.stake > bag) {
      setError(`double would need ${h.stake.toFixed(3)} ${wallet.activeCoin} but bag has ${bag.toFixed(3)} ${wallet.activeCoin}`);
      return;
    }
    const currentCommitted = hands.reduce((s, x) => s + x.stake, 0);
    if (currentCommitted + h.stake > MAX_ROUND_STAKE + 1e-9) {
      setError(`double would exceed max round stake ${MAX_ROUND_STAKE.toFixed(2)} ${wallet.activeCoin}`);
      return;
    }
    busyRef.current = true;
    try {
      if (!wallet.debit(wallet.activeCoin, h.stake)) {
        setError(`bag changed under the double -- try again`);
        return;
      }
      playHitSound();
      await sendAction('double');
    } finally {
      busyRef.current = false;
    }
  }

  const canSplit = (() => {
    if (phase !== 'player') return false;
    if (hands.length !== 1) return false;
    const h = hands[0];
    if (!h || h.cards.length !== 2) return false;
    if (h.status !== 'active') return false;
    if (bjVal(h.cards[0].r) !== bjVal(h.cards[1].r)) return false;
    const bag = wallet.balance[wallet.activeCoin] || 0;
    return bet <= bag;
  })();

  async function split() {
    if (busyRef.current) return;
    if (!canSplit) return;
    const currentCommitted = hands.reduce((s, x) => s + x.stake, 0);
    if (currentCommitted + bet > MAX_ROUND_STAKE + 1e-9) {
      setError(`split would exceed max round stake ${MAX_ROUND_STAKE.toFixed(2)} ${wallet.activeCoin}`);
      return;
    }
    busyRef.current = true;
    try {
      if (!wallet.debit(wallet.activeCoin, bet)) {
        setError(`bag changed under the split -- try again`);
        return;
      }
      playHitSound();
      await sendAction('split');
    } finally {
      busyRef.current = false;
    }
  }

  const playing = phase === 'dealing' || phase === 'player' || phase === 'dealer';
  const activeHand = hands[activeIdx];
  // Desktop: always 2 slots — the empty one becomes a muted "split for second hand" placeholder.
  // Mobile: the placeholder is hidden via .rc-bj-empty-slot { display: none } so only filled slots show.
  const handSlots = [hands[0] || null, hands[1] || null];

  return (
    <div className="rc-page" style={{flex:1, padding:'18px 26px 40px', overflowY:'auto'}}>
      <h1 className="rc-h-page" style={{fontSize:48, color:M.cyan, margin:'4px 0', textShadow:`3px 3px 0 ${M.hot}`}}>BLACKJACK</h1>
      <div className="rc-h-sub" style={{color:M.ink2, fontSize:18, marginBottom:14}}>
        <span style={{color:M.green}}>~99% RTP</span> (basic strategy) · i.i.d. card model · dealer hits soft 17 · 7:5 BJ · split + double · max bet {maxBet.toFixed(2)} {wallet.activeCoin} (max round stake {MAX_ROUND_STAKE.toFixed(2)})
      </div>

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

      <div className="rc-game-grid" style={{display:'grid', gridTemplateColumns:'320px 1fr', gap:18, alignItems:'start'}}>
        {/* Bet column — fixed min-height so button-set swaps don't shrink it */}
        <Frame title="bet" accent={M.green} className="rc-bj-bet-frame rc-blackjack-bet" style={{minHeight:480}}>
         <div className="rc-bet-panel">
          {/* Greyout the bet input area while a hand is active. The action
              area below stays live since the player needs HIT/STAND/etc.
              during the 'player' phase. */}
          <div className="rc-bet-inputs" style={{opacity: playing ? 0.4 : 1, pointerEvents: playing ? 'none' : 'auto', transition: 'opacity 0.15s'}}>
            <div style={{fontSize:14, color:M.ink2, marginBottom:4}}>bet ({wallet.activeCoin})</div>
            <input type="number" step={0.01} value={betStr} onChange={e=>setBetStr(e.target.value)} disabled={playing} style={{
              width:'100%', background:M.bg, border:`2px solid ${M.green}`, color:M.green, padding:'8px 10px', fontFamily:'inherit', fontSize:22, outline:'none',
            }}/>
            <div style={{marginTop:6}}>
              <QuickBetButtons current={bet} onPick={v=>setBet(v)} palette={M} disabled={playing}/>
            </div>
            <div style={{display:'flex', gap:4, marginTop:6}}>
              <button onClick={()=>setBet(b=>Math.max(MIN_BET, +(b/2).toFixed(6)))} disabled={playing} style={btnSec(M)}>½</button>
              <button onClick={()=>setBet(b=>+Math.min(b*2, maxBet).toFixed(6))} disabled={playing} style={btnSec(M)}>2×</button>
              <button onClick={()=>setBet(+(Math.min(wallet.balance[wallet.activeCoin] || 0, maxBet)).toFixed(6))} disabled={playing} title={`max bet: ${maxBet.toFixed(2)} ${wallet.activeCoin}`} style={btnSec(M)}>max</button>
            </div>
          </div>

          {/* Action area — fixed min-height so the button set swap doesn't
              change the bet column's overall height. */}
          <div className="rc-bet-action rc-bj-action" style={{borderTop:`2px dashed ${M.green}`, marginTop:14, paddingTop:14, display:'flex', flexDirection:'column', gap:6, minHeight:240}}>
            {(phase==='idle' || phase==='done') && (
              <button onClick={deal} style={{background:M.hot, color:M.bg, border:`3px solid ${M.bg}`, fontFamily:'inherit', fontSize:22, padding:'10px 0', fontWeight:700, cursor:'pointer', boxShadow:`5px 5px 0 ${M.green}`}}>► DEAL</button>
            )}
            {phase==='dealing' && (
              <div style={{textAlign:'center', color:M.yellow, fontSize:18, padding:8}}>dealing…</div>
            )}
            {phase==='player' && activeHand && (() => {
              const canDouble = activeHand.cards.length===2 && activeHand.stake <= wallet.balance[wallet.activeCoin];
              return (
                <>
                  <button onClick={hit} style={{background:M.green, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit', fontSize:22, padding:'8px 0', fontWeight:700, cursor:'pointer'}}>+ HIT</button>
                  <button onClick={stand} style={{background:M.cyan, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit', fontSize:22, padding:'8px 0', fontWeight:700, cursor:'pointer'}}>✋ STAND</button>
                  {/* DOUBLE and SPLIT are always rendered so the action area
                      stays the same height; disabled state is greyed out when
                      the action isn't available (wrong card count, can't
                      afford, or values don't match). */}
                  <button onClick={double} disabled={!canDouble} title={canDouble ? 'double down — one card and stand' : 'double only on a 2-card hand with enough bag'} style={{
                    background: canDouble ? M.yellow : M.bg,
                    color:      canDouble ? M.bg     : M.ink2,
                    border:`2px solid ${canDouble ? M.bg : M.ink2}`,
                    fontFamily:'inherit', fontSize:18, padding:'6px 0', fontWeight:700,
                    cursor: canDouble ? 'pointer' : 'not-allowed',
                    opacity: canDouble ? 1 : 0.5,
                  }}>2× DOUBLE</button>
                  <button onClick={split} disabled={!canSplit} title={canSplit ? 'split into two hands' : 'split needs a pair of equal-value cards on the opening hand'} style={{
                    background: canSplit ? M.hot : M.bg,
                    color:      canSplit ? M.bg  : M.ink2,
                    border:`2px solid ${canSplit ? M.bg : M.ink2}`,
                    fontFamily:'inherit', fontSize:18, padding:'6px 0', fontWeight:700,
                    cursor: canSplit ? 'pointer' : 'not-allowed',
                    opacity: canSplit ? 1 : 0.5,
                  }}>↔ SPLIT</button>
                </>
              );
            })()}
            {phase==='dealer' && <div style={{textAlign:'center', color:M.yellow, fontSize:18, padding:8}}>dealer playing…</div>}
            {hands.length > 1 && phase==='player' && (
              <div style={{marginTop:'auto', fontSize:13, color:M.ink2, lineHeight:1.4}}>
                {'>'} playing hand {activeIdx + 1} of {hands.length}
              </div>
            )}
          </div>
         </div>
        </Frame>

        {/* Right column — dealer, hands grid, result. Each section has a
            min-height so vertical layout stays stable across phases. */}
        <div style={{display:'flex', flexDirection:'column', gap:14}}>
          <Frame title="dealer" accent={M.hot} className="rc-blackjack-dealer">
            <BJHand cards={dealer} total={hideHole && dealer.length>=2 ? bjVal(dealer[0]?.r||'2') : bjHandTotal(dealer)} hideSecond={hideHole} M={M} accent={M.hot}/>
          </Frame>

          {/* Two-column hand grid on desktop with an empty placeholder slot
              when not split. On mobile the placeholder is CSS-hidden so only
              filled slots render. */}
          <div className="rc-2col" style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:14}}>
            {handSlots.map((h, i) => {
              const slotClass = i === 0 ? 'rc-bj-hand-you' : 'rc-bj-hand-split';
              if (!h) {
                return (
                  <Frame key={i} title={hands.length === 1 && i === 1 ? '—' : (i === 0 ? 'you' : '—')} accent={M.ink2} className={`rc-bj-empty-slot ${slotClass}`}>
                    <div style={{minHeight:110, opacity:0.4, display:'flex', alignItems:'center', justifyContent:'center', color:M.ink2, fontSize:14}}>
                      {i === 0 ? 'awaiting deal…' : (hands.length === 1 ? 'split for second hand' : '')}
                    </div>
                  </Frame>
                );
              }
              const isActive = phase==='player' && i === activeIdx;
              const accent = isActive ? M.green : M.cyan;
              const label = hands.length > 1 ? `hand ${i+1} · stake ${h.stake.toFixed(2)}${h.doubled?' (2×)':''}` : `you${h.doubled?' · doubled':''}`;
              return (
                <Frame key={i} title={label} accent={accent} className={slotClass}>
                  <BJHand cards={h.cards} total={bjHandTotal(h.cards)} M={M} accent={accent} status={h.status}/>
                </Frame>
              );
            })}
          </div>

          {/* Result frame — always rendered on desktop with a placeholder so
              the layout doesn't shift. On mobile the placeholder is CSS-hidden
              via .rc-bj-result-idle so it doesn't eat real estate while dealing
              / playing — the frame only appears when a round resolves. */}
          <Frame title="result" accent={M.yellow} className={`rc-bj-result ${(last && phase === 'done') ? '' : 'rc-bj-result-idle'}`}>
            <div className="rc-resolve-slot-col" style={{textAlign:'center', minHeight:160, display:'flex', flexDirection:'column', justifyContent:'flex-start'}}>
              {last && phase==='done' ? (
                <>
                  <div style={{fontSize:32, color: last.totalPayout > last.totalStake ? M.green : last.totalPayout === last.totalStake ? M.yellow : M.red, fontWeight:700, textShadow:`2px 2px 0 ${M.bg}`}}>
                    {last.totalPayout > last.totalStake ? '★ WIN ★' : last.totalPayout === last.totalStake ? '— PUSH —' : '✗ LOSS'}
                  </div>
                  <div style={{fontSize:14, color:M.ink2, marginTop:6}}>
                    {last.results.map((r, i) => {
                      const delta = r.payout - r.stake;
                      const deltaColor = delta > 0 ? M.green : delta === 0 ? M.yellow : M.red;
                      const deltaText = delta > 0 ? `+${delta.toFixed(2)}` : delta === 0 ? '±0.00' : `−${Math.abs(delta).toFixed(2)}`;
                      return (
                        <div key={i}>
                          {last.results.length>1 ? `hand ${i+1} ` : ''}{bjOutcomeLabel(r.outcome)} · you {r.pTot} vs dealer {last.dTot} ·{' '}
                          <span style={{color: deltaColor, fontWeight:700}}>{deltaText} {wallet.activeCoin}</span>
                        </div>
                      );
                    })}
                    {last.results.length > 1 && (() => {
                      const total = last.totalPayout - last.totalStake;
                      const tColor = total > 0 ? M.green : total === 0 ? M.yellow : M.red;
                      const tText = total > 0 ? `+${total.toFixed(2)}` : total === 0 ? '±0.00' : `−${Math.abs(total).toFixed(2)}`;
                      return (
                        <div style={{marginTop:6, color:M.ink, fontWeight:700}}>
                          total: <span style={{color:tColor}}>{tText} {wallet.activeCoin}</span>
                        </div>
                      );
                    })()}
                  </div>
                  <div style={{fontSize:13, color:M.ink2, marginTop:6, fontFamily:'JetBrains Mono, monospace'}}>hash: {last.hex}…</div>
                </>
              ) : (
                <div style={{color:M.ink2, fontSize:14}}>
                  {phase === 'idle' ? '> deal a hand to begin' :
                   phase === 'dealing' ? '> dealing the opening hand' :
                   phase === 'player' ? '> your move' :
                   phase === 'dealer' ? '> dealer playing' :
                   ''}
                </div>
              )}
            </div>
          </Frame>
        </div>
      </div>

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

function bjOutcomeLabel(o) {
  if (o === 'bj-win') return '★ BLACKJACK';
  if (o === 'win') return '★ win';
  if (o === 'dealer-bust') return '★ dealer bust';
  if (o === 'push') return '— push';
  if (o === 'loss') return '✗ loss';
  if (o === 'bust') return '✗ bust';
  if (o === 'bj-loss') return '✗ dealer BJ';
  return o;
}

function btnSec(M){ return { flex:1, background:M.bg, color:M.green, border:`1px solid ${M.green}`, fontFamily:'inherit', fontSize:14, padding:'4px 0', cursor:'pointer' }; }

function BJHand({ cards, total, hideSecond, M, accent, status }) {
  // Dealer doesn't receive a `status` prop (only player hands do), so fall back
  // to a numeric bust check so the dealer hand greys out the same way a busted
  // player hand does. Mirrors the §5.50 total-color fix on the line below.
  const dim = status === 'busted' || total > 21;
  return (
    <div style={{display:'flex', alignItems:'center', gap:14, minHeight:110, opacity: dim ? 0.55 : 1}}>
      <div style={{display:'flex', gap:6, flexWrap:'wrap'}}>
        {cards.length===0 && [0,1].map(i=><CardBack2 key={i} M={M}/>)}
        {cards.map((c,i)=>{
          if (i===1 && hideSecond) return <CardBack2 key={i} M={M}/>;
          return <BJCard key={i} card={c} M={M} accent={accent}/>;
        })}
      </div>
      <div style={{flex:1}}/>
      <div style={{fontSize:32, color: (status==='busted' || total > 21) ? M.red : M.green, fontWeight:700, fontFamily:'inherit'}}>{cards.length>0 ? total : '—'}</div>
    </div>
  );
}
function BJCard({ card, M, accent }) {
  const red = card.s==='♥' || card.s==='♦';
  return (
    <div style={{width:60, height:88, background:M.ink, color:red?'#cc0000':'#000', border:`2px solid ${accent}`, padding:6, position:'relative', fontFamily:'serif'}}>
      <div style={{position:'absolute', top:4, left:6, fontSize:14, fontWeight:700}}>{card.r}</div>
      <div style={{position:'absolute', top:'50%', left:'50%', transform:'translate(-50%,-50%)', fontSize:24}}>{card.s}</div>
    </div>
  );
}
function CardBack2({ M }) {
  return <div style={{width:60, height:88, background: M.hot, border:`2px solid ${M.ink2}`}}/>;
}

// Blackjack stats — derived from wallet.history filtered to game==='Blackjack'
// and the user's active coin. Mirrors the slots/flip stats panel layout.
function BlackjackStatsPanel({ palette: M, wallet }) {
  const coin = wallet.activeCoin;
  const rounds = (wallet.history || []).filter(h => h.game === 'Blackjack' && h.coin === coin);
  const total = rounds.length;
  const wins = rounds.filter(r => r.win).length;
  const winRate = total ? (wins / total) * 100 : 0;
  const wagered = rounds.reduce((s, r) => s + (r.bet || 0), 0);
  const netPL = rounds.reduce((s, r) => s + ((r.payout || 0) - (r.bet || 0)), 0);
  const bestWin = rounds.reduce((m, r) => Math.max(m, (r.payout || 0) - (r.bet || 0)), 0);
  const naturals = rounds.filter(r => r.outcome === 'bj-win').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="blackjack stats" accent={M.cyan} className="rc-blackjack-stats" style={{marginTop:18}}>
      {total === 0 ? (
        <div style={{color:M.ink2, fontSize:14, padding:'4px 0'}}>{'>'} no rounds yet in {coin}. play a hand.</div>
      ) : (
        <>
          <div className="rc-stat-6" style={{display:'grid', gridTemplateColumns:'repeat(6, 1fr)', gap:14}}>
            {tile('rounds',         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('blackjacks',     `${naturals}`,                    M.hot)}
          </div>
          <LastRoundsTable M={M} rounds={rounds} headers={['outcome', 'bet', 'payout', 'P/L']} renderRow={r => {
            const pl = (r.payout || 0) - (r.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 labelMap = {
              'bj-win':       { txt: 'BLACKJACK',   color: M.green  },
              'win':          { txt: 'WIN',         color: M.green  },
              'dealer-bust':  { txt: 'DEALER BUST', color: M.green  },
              'push':         { txt: 'PUSH',        color: M.yellow },
              'loss':         { txt: 'LOSS',        color: M.red    },
              'bust':         { txt: 'BUST',        color: M.red    },
              'bj-loss':      { txt: 'DEALER BJ',   color: M.red    },
              'split':        { txt: r.hands ? `SPLIT ×${r.hands}` : 'SPLIT', color: M.cyan },
            };
            const o = labelMap[r.outcome] || { txt: (r.outcome || '—').toUpperCase(), color: M.ink };
            return [
              { value: o.txt,                                              color: o.color, fontWeight: 700 },
              { value: (r.bet || 0).toFixed(dp),                           color: M.ink },
              { value: r.payout > 0 ? (r.payout || 0).toFixed(dp) : '—',   color: r.payout > 0 ? M.green : M.ink2 },
              { value: plText,                                             color: plColor, fontWeight: 700 },
            ];
          }}/>
        </>
      )}
    </Frame>
  );
}

Object.assign(window, { BlackjackPage });
