/* global React, ReactDOM */
const { useState, useRef, useCallback, useEffect, useMemo } = React;

// =============================================================================
// Constants
// =============================================================================
const MIN_BET_DEFAULT = 0.10;
const MAX_BET_DEFAULT = 100;
const MAX_SKIPS_DEFAULT = 10;

const RANKS = ['2','3','4','5','6','7','8','9','10','J','Q','K','A'];
const SUITS = ['hearts','diamonds','clubs','spades'];
const SUIT_SYMBOLS = {
  hearts:   '♥',
  diamonds: '♦',
  clubs:    '♣',
  spades:   '♠',
};
const RANK_VALUES = {
  '2':2, '3':3, '4':4, '5':5, '6':6, '7':7,
  '8':8, '9':9, '10':10, 'J':11, 'Q':12, 'K':13, 'A':14,
};

// =============================================================================
// Pure helpers
// =============================================================================
function drawCard() {
  const arr = new Uint32Array(2);
  crypto.getRandomValues(arr);
  return { rank: RANKS[arr[0] % 13], suit: SUITS[arr[1] % 4] };
}

function higherDesc(rank) {
  const v = RANK_VALUES[rank];
  if (v === 14) return 'ACE OR SAME';
  const idx = RANKS.indexOf(rank);
  const nextRank = RANKS[idx + 1];
  if (v === 13) return `${nextRank} BEING THE HIGHEST`;
  return `${nextRank} OR HIGHER`;
}

function lowerDesc(rank) {
  const v = RANK_VALUES[rank];
  if (v === 2) return '2 OR SAME';
  const idx = RANKS.indexOf(rank);
  const prevRank = RANKS[idx - 1];
  if (v === 3) return `${prevRank} BEING THE LOWEST`;
  return `${prevRank} OR LOWER`;
}

function fmt(n) {
  return '$' + Number(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}

const isRed = (suit) => suit === 'hearts' || suit === 'diamonds';

// =============================================================================
// Audio
// =============================================================================
class AudioEngine {
  constructor() {
    this.ctx = null;
    this.enabled = true;
    this.cardBuffer = null;
  }
  getCtx() {
    if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)();
    return this.ctx;
  }
  toggle() { this.enabled = !this.enabled; return this.enabled; }
  async loadCardSound() {
    try {
      const ctx = this.getCtx();
      const res = await fetch('card.wav');
      const buf = await res.arrayBuffer();
      this.cardBuffer = await ctx.decodeAudioData(buf);
    } catch (e) { /* ignore */ }
  }
  play(freq, duration, type = 'sine', vol = 0.15) {
    if (!this.enabled) return;
    try {
      const ctx = this.getCtx();
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.type = type;
      osc.frequency.value = freq;
      gain.gain.setValueAtTime(vol, ctx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
      osc.connect(gain); gain.connect(ctx.destination);
      osc.start(); osc.stop(ctx.currentTime + duration);
    } catch (e) { /* ignore */ }
  }
  sndCard() {
    if (!this.enabled) return;
    try {
      const ctx = this.getCtx();
      if (this.cardBuffer) {
        const source = ctx.createBufferSource();
        source.buffer = this.cardBuffer;
        const gain = ctx.createGain();
        gain.gain.value = 0.3;
        source.connect(gain); gain.connect(ctx.destination);
        source.start();
      } else {
        this.play(800, 0.05, 'triangle', 0.1);
      }
    } catch (e) { /* ignore */ }
  }
  sndBet() { this.play(250, 0.12, 'sine', 0.08); }
  sndWin() {
    this.play(660, 0.15, 'sine', 0.12);
    setTimeout(() => this.play(880, 0.15, 'sine', 0.12), 80);
    setTimeout(() => this.play(1100, 0.25, 'sine', 0.14), 160);
    setTimeout(() => this.play(1320, 0.35, 'sine', 0.10), 260);
  }
  sndBigWin() {
    this.play(660, 0.15, 'sine', 0.15);
    setTimeout(() => this.play(880, 0.15, 'sine', 0.15), 80);
    setTimeout(() => this.play(1100, 0.2, 'sine', 0.15), 160);
    setTimeout(() => this.play(1320, 0.2, 'sine', 0.15), 240);
    setTimeout(() => this.play(1540, 0.4, 'sine', 0.18), 320);
    setTimeout(() => this.play(1760, 0.5, 'sine', 0.12), 420);
  }
  sndLose() {
    this.play(200, 0.25, 'sine', 0.08);
    setTimeout(() => this.play(160, 0.3, 'sine', 0.06), 100);
  }
  sndCashout() {
    this.play(500, 0.1, 'sine', 0.1);
    setTimeout(() => this.play(700, 0.1, 'sine', 0.1), 60);
    setTimeout(() => this.play(900, 0.15, 'sine', 0.12), 120);
  }
  sndSkip() { this.sndCard(); }
}

// =============================================================================
// RGS API client
// =============================================================================
const API_BASE = (typeof window !== 'undefined' && window.RGS_API_BASE) || '';
const GAME_UUID = (typeof window !== 'undefined' && window.HILO_GAME_UUID) || 'hilo_965';

let TOKEN = null;

async function api(path, opts = {}) {
  const headers = { ...(opts.headers || {}) };
  if (TOKEN) headers['Authorization'] = 'Bearer ' + TOKEN;
  if (opts.body !== undefined) headers['Content-Type'] = 'application/json';
  const r = await fetch(API_BASE + path, {
    method: opts.method || 'GET',
    headers,
    body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
  });
  const data = await r.json().catch(() => ({}));
  if (data && data.error_code) {
    const err = new Error(data.error_description || data.error_code);
    err.errorCode = data.error_code;
    throw err;
  }
  if (!r.ok) throw new Error('HTTP ' + r.status);
  return data;
}

async function initDemo() {
  const r = await api('/api/v1/init-demo', { method: 'POST', body: { game_uuid: GAME_UUID } });
  const u = new URL(r.url, 'http://x');
  TOKEN = u.searchParams.get('token');
  if (!TOKEN) throw new Error('init-demo: missing token');
  return TOKEN;
}
const getState   = () => api('/api/v1/state');
const placeBet   = (amount) => api('/api/v1/bet', { method: 'POST', body: { amount: Number(amount).toFixed(2) } });
const actGuess   = (id, direction) => api(`/api/v1/round/${id}/action`, { method: 'POST', body: { type: 'guess', direction } });
const actSkip    = (id) => api(`/api/v1/round/${id}/action`, { method: 'POST', body: { type: 'skip' } });
const apiCashout = (id) => api(`/api/v1/round/${id}/cashout`, { method: 'POST', body: {} });

const cardFromResponse = (c) => {
  if (!c || typeof c !== 'object' || !c.rank || !c.suit) return null;
  return { rank: c.rank, suit: c.suit };
};

// =============================================================================
// GameInfoModal
// =============================================================================
function GameInfoModal({ open, onClose, minBet, maxBet, maxSkips, rtp }) {
  const rtpPct = (parseFloat(rtp) * 100).toFixed(1);
  const houseEdgePct = ((1 - parseFloat(rtp)) * 100).toFixed(1);

  return (
    <div
      className={`modal-overlay${open ? ' show' : ''}`}
      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
    >
      <div className="modal">
        <button className="modal-close" onClick={onClose}>&times;</button>
        <h2>Game Info</h2>
        <div className="modal-payout-box">
          <span className="label">Bet Range</span>
          <span className="value">${minBet.toFixed(2)} — ${maxBet.toFixed(2)}</span>
        </div>
        <div className="modal-payout-box">
          <span className="label">Max Skips</span>
          <span className="value">{maxSkips} per round</span>
        </div>
        <div className="modal-payout-box">
          <span className="label">RTP</span>
          <span className="value">{rtpPct}%</span>
        </div>
        <div className="info-box">
          <span className="info-icon">{'♠'}</span>
          <p>
            HiLo is a card guessing game. A card is dealt and you predict whether
            the next card will be Higher, Lower, or the Same. Correct guesses
            multiply your winnings — cash out anytime or keep pushing!
          </p>
        </div>
        <h3>How to Play</h3>
        <ol>
          <li><strong>Place your bet</strong> — enter the amount you want to wager.</li>
          <li><strong>A card is dealt</strong> — 2 (low) through Ace (high).</li>
          <li><strong>Choose Higher or Same / Lower or Same</strong> — riskier guesses pay higher multipliers. Same rank always wins.</li>
          <li><strong style={{ color: '#0ECC68' }}>Correct!</strong> — your multiplier grows. Cash out or continue.</li>
          <li><strong style={{ color: '#ED4163' }}>Wrong!</strong> — you lose your entire bet.</li>
          <li><strong>Skip</strong> — don{"'"}t like the odds? Skip up to {maxSkips} times per round.</li>
        </ol>
        <h3>Card Ranking</h3>
        <div className="info-box">
          <span className="info-icon">{'♦'}</span>
          <p>
            2 is the <strong>lowest</strong> card. Ace is the <strong>highest</strong>.
            Order: 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, A.
            Each draw uses an infinite deck — every card has the same probability regardless
            of previous draws.
          </p>
        </div>
        <h3>House Edge &amp; RTP</h3>
        <div className="info-box">
          <span className="info-icon">{'📊'}</span>
          <p>
            <strong>RTP: {rtpPct}%</strong> — House edge is {houseEdgePct}%.
            Multipliers are derived from <code>RTP / probability</code>. Every outcome is
            provably fair — tap Fair Play to verify.
          </p>
        </div>
      </div>
    </div>
  );
}

// =============================================================================
// ProvablyFairModal
// =============================================================================
function ProvablyFairModal({ open, onClose, seedHash }) {
  const [activeTab, setActiveTab] = useState('seeds');
  const [clientSeed, setClientSeed] = useState(() => 'hilo_' + Math.random().toString(36).slice(2, 10));
  const [copied, setCopied] = useState(null);

  const handleCopy = (text, label) => {
    if (navigator.clipboard) navigator.clipboard.writeText(text);
    setCopied(label);
    setTimeout(() => setCopied(null), 1500);
  };
  const handleRegen = () => setClientSeed('hilo_' + Math.random().toString(36).slice(2, 10));

  const displayHash = seedHash || 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';

  return (
    <div
      className={`modal-overlay${open ? ' show' : ''}`}
      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
    >
      <div className="modal">
        <button className="modal-close" onClick={onClose}>&times;</button>
        <h2>Provably Fair</h2>
        <p className="pf-desc">
          This game uses <strong>Provably Fair</strong> technology to generate each
          card draw. You can verify that every outcome is fair and unmanipulated.
        </p>

        <div className="pf-toggle-row">
          <button
            className={`pf-toggle-btn${activeTab === 'seeds' ? ' active' : ''}`}
            onClick={() => setActiveTab('seeds')}
          >Seeds</button>
          <button
            className={`pf-toggle-btn${activeTab === 'verify' ? ' active' : ''}`}
            onClick={() => setActiveTab('verify')}
          >Verify</button>
        </div>

        {activeTab === 'seeds' && (
          <React.Fragment>
            <div className="pf-section">
              <div className="pf-label">
                <span className="pf-icon">{'🖥'}</span> Client Seed
              </div>
              <div className="pf-sublabel">Generated on your side — you control this</div>
              <div className="pf-input-row">
                <input type="text" value={clientSeed} onChange={(e) => setClientSeed(e.target.value)} />
                <button className="pf-btn-sm" title="Copy" onClick={() => handleCopy(clientSeed, 'client')}>
                  {copied === 'client' ? '✓' : 'CP'}
                </button>
                <button className="pf-btn-sm" title="Regenerate" onClick={handleRegen}>R</button>
              </div>
            </div>
            <div className="pf-section">
              <div className="pf-label">
                <span className="pf-icon">{'🔒'}</span> Server Seed SHA256
              </div>
              <div className="pf-sublabel">Committed before you bet — revealed after</div>
              <div className="pf-hash-box">{displayHash}</div>
              <div className="pf-cp-row">
                <button className="pf-btn-sm" title="Copy" onClick={() => handleCopy(displayHash, 'hash')}>
                  {copied === 'hash' ? '✓' : 'CP'}
                </button>
              </div>
              <p className="pf-note">
                This hash is committed <strong>before</strong> you bet. After the round,
                the server seed is revealed so you can verify SHA256(seed) matches.
              </p>
            </div>
          </React.Fragment>
        )}

        {activeTab === 'verify' && (
          <div className="pf-how">
            <div className="pf-how-title">How HiLo Provably Fair Works</div>
            <ol>
              <li>Server generates a secret seed and shows you its SHA256 hash</li>
              <li>You set your client seed (your influence on the randomness)</li>
              <li>You place a bet and a starting card is dealt</li>
              <li>Each card draw: HMAC-SHA256(server_seed, &quot;hilo:&quot; + client_seed + &quot;:&quot; + step) → rank + suit</li>
              <li>You choose Higher, Lower, or Same — if correct, multiplier compounds</li>
              <li>Cash out anytime to lock in winnings</li>
              <li>Server reveals the seed — verify SHA256 matches the pre-committed hash</li>
            </ol>
          </div>
        )}
      </div>
    </div>
  );
}

// =============================================================================
// HiLoApp
// =============================================================================
function HiLoApp() {
  const [loading, setLoading] = useState(true);

  // server-driven
  const [phase, setPhase] = useState('connecting');
  const [balance, setBalance] = useState(0);
  const [currency, setCurrency] = useState('USD');
  const [minBet, setMinBet] = useState(MIN_BET_DEFAULT);
  const [maxBet, setMaxBet] = useState(MAX_BET_DEFAULT);
  const [maxSkips, setMaxSkips] = useState(MAX_SKIPS_DEFAULT);
  const [rankInfo, setRankInfo] = useState({});
  const [rtp, setRtp] = useState('0.965');
  const [seedHash, setSeedHash] = useState('');

  const [activeRoundId, setActiveRoundId] = useState(null);
  const [betAmount, setBetAmount] = useState(0);
  const [totalMultiplier, setTotalMultiplier] = useState(1);
  const [skipsUsed, setSkipsUsed] = useState(0);

  // ui
  const [betStr, setBetStr] = useState('1.00');
  const [currentCard, setCurrentCard] = useState(null);
  const [streak, setStreak] = useState([]);
  const [lastResult, setLastResult] = useState(null);
  const [lastPayout, setLastPayout] = useState(0);
  const [showCashout, setShowCashout] = useState(false);
  const [isAnimating, setIsAnimating] = useState(false);
  const [cardAnim, setCardAnim] = useState('');
  const [prevCard, setPrevCard] = useState(null);
  const [gameHistory, setGameHistory] = useState([]);
  const [gameInfoOpen, setGameInfoOpen] = useState(false);
  const [pfModalOpen, setPfModalOpen] = useState(false);
  const [soundOn, setSoundOn] = useState(true);
  const [alert, setAlert] = useState(null);
  const [freeGrant, setFreeGrant] = useState(null);
  const [freeBetSummary, setFreeBetSummary] = useState(null);
  const [freeWelcome, setFreeWelcome] = useState(false);
  const [faved, setFaved] = useState(localStorage.getItem('fav_hilo') === 'true');

  const audioRef = useRef(null);
  const freeBetTrackerRef = useRef(null);

  const showAlertMsg = useCallback((msg) => {
    setAlert(msg);
    setTimeout(() => setAlert(null), 2500);
  }, []);

  const fetchFreeRounds = useCallback(async () => {
    // --- ?demo-free mock preview mode ---
    if (window.location.search.indexOf('demo-free') !== -1) {
      setFreeGrant(function(prev) {
        if (!prev) {
          // First call: return mock grant, init tracker, show welcome
          var mock = { _mock: true, id: 'mock', game_uuid: GAME_UUID, bet_amount: '5.00', rounds_total: 5, rounds_used: 0, rounds_left: 5, status: 'active' };
          freeBetTrackerRef.current = { totalWinnings: 0, totalBet: 0, rounds: 0 };
          setFreeWelcome(true);
          return mock;
        }
        // Subsequent calls: decrement rounds_left
        var nextLeft = prev.rounds_left - 1;
        var nextUsed = prev.rounds_used + 1;
        if (nextLeft <= 0) {
          // All rounds used: trigger summary modal, clear tracker
          if (freeBetTrackerRef.current && freeBetTrackerRef.current.rounds > 0) {
            setFreeBetSummary({ ...freeBetTrackerRef.current });
          }
          freeBetTrackerRef.current = null;
          return null;
        }
        return { ...prev, rounds_left: nextLeft, rounds_used: nextUsed };
      });
      return;
    }
    // --- end demo-free mock ---

    try {
      const data = await api('/api/v1/freerounds');
      const grants = data && Array.isArray(data.grants) ? data.grants : [];
      const active = grants.find(function(g) {
        return g.status === 'active' && g.game_uuid === GAME_UUID && g.rounds_left > 0;
      });
      if (active) {
        if (!freeBetTrackerRef.current) {
          freeBetTrackerRef.current = { totalWinnings: 0, totalBet: 0, rounds: 0 };
          setFreeWelcome(true);
        }
      } else {
        if (freeBetTrackerRef.current && freeBetTrackerRef.current.rounds > 0) {
          setFreeBetSummary({ ...freeBetTrackerRef.current });
          freeBetTrackerRef.current = null;
        }
      }
      setFreeGrant(active || null);
    } catch (e) {
      // Demo sessions or missing endpoint - gracefully ignore
      setFreeGrant(null);
    }
  }, []);

  // Boot
  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        await initDemo();
        const state = await getState();
        if (cancelled) return;
        setBalance(parseFloat(state.balance));
        setCurrency(state.currency);
        if (state.config) {
          if (state.config.min_bet) setMinBet(parseFloat(state.config.min_bet));
          if (state.config.max_bet) setMaxBet(parseFloat(state.config.max_bet));
          if (state.config.rtp) setRtp(state.config.rtp);
        }
        if (state.next_seed_hash) setSeedHash(state.next_seed_hash);
        if (state.game_data) {
          if (typeof state.game_data.max_skips === 'number') setMaxSkips(state.game_data.max_skips);
          if (Array.isArray(state.game_data.rank_info)) {
            const map = {};
            for (const r of state.game_data.rank_info) map[r.rank] = r;
            setRankInfo(map);
          }
        }
        setCurrentCard(drawCard());
        setPhase('idle');
        await new Promise(r => setTimeout(r, 2500));
        setLoading(false);
        fetchFreeRounds();
      } catch (e) {
        if (!cancelled) {
          showAlertMsg('Failed to connect: ' + e.message);
          setPhase('idle');
          setCurrentCard(drawCard());
          setLoading(false);
        }
      }
    })();
    return () => { cancelled = true; };
  }, [showAlertMsg, fetchFreeRounds]);

  useEffect(() => {
    const audio = new AudioEngine();
    audio.loadCardSound();
    audioRef.current = audio;
  }, []);

  const getBet = useCallback(() => {
    const b = parseFloat(betStr);
    if (isNaN(b) || b < minBet) return minBet;
    if (b > maxBet) return maxBet;
    return Math.floor(b * 100) / 100;
  }, [betStr, minBet, maxBet]);

  const ri = currentCard ? rankInfo[currentCard.rank] : undefined;
  const hMult = ri ? parseFloat(ri.higher_multiplier) : 0;
  const lMult = ri ? parseFloat(ri.lower_multiplier) : 0;
  const hProb = ri ? parseFloat(ri.higher_probability) : 0;
  const lProb = ri ? parseFloat(ri.lower_probability) : 0;
  const hDescStr = currentCard ? higherDesc(currentCard.rank) : '';
  const lDescStr = currentCard ? lowerDesc(currentCard.rank) : '';

  const profit = useMemo(
    () => Math.floor(betAmount * totalMultiplier * 100) / 100,
    [betAmount, totalMultiplier]
  );

  const transitionCard = useCallback((nextCard, cb) => {
    setIsAnimating(true);
    setPrevCard(currentCard);
    setCurrentCard(nextCard);
    setCardAnim('deal');
    if (audioRef.current) audioRef.current.sndCard();
    setTimeout(() => {
      setCardAnim('');
      setPrevCard(null);
      setIsAnimating(false);
      cb();
    }, 650);
  }, [currentCard]);

  const startGame = useCallback(async () => {
    if (phase === 'connecting' || isAnimating) return;
    const rawBet = parseFloat(betStr);
    const bet = freeGrant ? parseFloat(freeGrant.bet_amount) : getBet();
    if (!freeGrant && rawBet > maxBet) { showAlertMsg(`Max bet is $${maxBet.toFixed(2)}`); return; }
    if (!freeGrant && rawBet > balance) { showAlertMsg('Insufficient balance'); return; }
    try {
      if (audioRef.current) audioRef.current.sndBet();
      const r = await placeBet(bet);
      const gd = r.game_data || {};
      const card = cardFromResponse(gd.current_card);
      if (!card) { showAlertMsg('Server returned invalid card'); return; }
      setBalance(parseFloat(r.balance));
      if (r.next_seed_hash) setSeedHash(r.next_seed_hash);
      setActiveRoundId(r.round_id);
      setBetAmount(bet);
      setTotalMultiplier(1);
      setSkipsUsed(0);
      if (typeof gd.max_skips === 'number') setMaxSkips(gd.max_skips);
      setCurrentCard(card);
      setStreak([{ card, guess: 'start', correct: true }]);
      setLastResult(null);
      setShowCashout(false);
      setPrevCard(null);
      setPhase('playing');
    } catch (e) {
      showAlertMsg(e.message || 'Bet failed');
    }
  }, [phase, isAnimating, getBet, betStr, maxBet, balance, showAlertMsg, freeGrant]);

  const applyTerminal = useCallback((r, win) => {
    setBalance(parseFloat(r.balance));
    if (r.next_seed_hash) setSeedHash(r.next_seed_hash);
    const payout = parseFloat(r.total_payout);
    setLastPayout(payout);
    setLastResult(win ? 'win' : 'lose');
    setActiveRoundId(null);
    setPhase('result');
    if (audioRef.current) {
      if (win) audioRef.current.sndCashout(); else audioRef.current.sndLose();
    }
    const mult = totalMultiplier;
    setGameHistory((prev) => [{ won: win, payout, mult }, ...prev].slice(0, 50));
    if (freeBetTrackerRef.current) {
      var updated = { totalWinnings: freeBetTrackerRef.current.totalWinnings + (win ? payout : 0), totalBet: freeBetTrackerRef.current.totalBet + betAmount, rounds: freeBetTrackerRef.current.rounds + 1 };
      freeBetTrackerRef.current = updated;
    }
    fetchFreeRounds();
  }, [totalMultiplier, betAmount, fetchFreeRounds]);

  const makeGuess = useCallback(async (guess) => {
    if (phase !== 'playing' || !currentCard || isAnimating || !activeRoundId) return;
    try {
      const r = await actGuess(activeRoundId, guess);
      const gd = r.game_data || {};
      const newCard = cardFromResponse(gd.new_card);
      if (!newCard) { showAlertMsg('Server returned invalid card'); return; }
      const correct = gd.correct === true;
      const stepMult = parseFloat(gd.step_multiplier || '0');
      const newTotal = parseFloat(gd.total_multiplier || '0');

      transitionCard(newCard, () => {
        setStreak((prev) => [...prev, { card: newCard, guess, correct, mult: stepMult }]);
        if (correct && !r.finished) {
          setTotalMultiplier(newTotal);
          setShowCashout(true);
          if (audioRef.current) {
            if (newTotal >= 10) audioRef.current.sndBigWin();
            else audioRef.current.sndWin();
          }
        } else if (correct && r.finished) {
          setTotalMultiplier(newTotal);
          applyTerminal(r, true);
        } else {
          applyTerminal(r, false);
        }
      });
    } catch (e) {
      showAlertMsg(e.message || 'Guess failed');
    }
  }, [phase, currentCard, isAnimating, activeRoundId, transitionCard, showAlertMsg, applyTerminal]);

  const cashout = useCallback(async () => {
    if (phase !== 'playing' || !showCashout || isAnimating || !activeRoundId) return;
    try {
      const r = await apiCashout(activeRoundId);
      applyTerminal(r, true);
    } catch (e) {
      showAlertMsg(e.message || 'Cashout failed');
    }
  }, [phase, showCashout, isAnimating, activeRoundId, applyTerminal, showAlertMsg]);

  const newGame = useCallback(() => {
    setPhase('idle'); setStreak([]);
    setLastResult(null); setShowCashout(false); setTotalMultiplier(1);
    setPrevCard(null); setCardAnim(''); setSkipsUsed(0);
  }, []);

  const skipCard = useCallback(async () => {
    if (isAnimating) return;
    if (phase !== 'playing') {
      if (phase === 'result') newGame();
      const nextCard = drawCard();
      if (audioRef.current) audioRef.current.sndSkip();
      setIsAnimating(true);
      setPrevCard(currentCard);
      setCurrentCard(nextCard);
      setCardAnim('deal');
      setTimeout(() => {
        setCardAnim(''); setPrevCard(null); setIsAnimating(false);
      }, 650);
      return;
    }
    if (skipsUsed >= maxSkips) { showAlertMsg(`Max ${maxSkips} skips per round`); return; }
    if (!activeRoundId) return;
    try {
      const r = await actSkip(activeRoundId);
      const gd = r.game_data || {};
      const newCard = cardFromResponse(gd.new_card);
      if (!newCard) { showAlertMsg('Server returned invalid card'); return; }
      if (audioRef.current) audioRef.current.sndSkip();
      transitionCard(newCard, () => {
        setStreak((prev) => [...prev, { card: newCard, guess: 'skip', correct: true }]);
        setSkipsUsed(typeof gd.skips_used === 'number' ? gd.skips_used : skipsUsed + 1);
      });
    } catch (e) {
      showAlertMsg(e.message || 'Skip failed');
    }
  }, [phase, currentCard, skipsUsed, maxSkips, isAnimating, activeRoundId, transitionCard, newGame, showAlertMsg]);

  const handlePrimary = () => {
    if (phase === 'idle' || phase === 'result') {
      if (phase === 'result') newGame();
      startGame();
    } else if (phase === 'playing' && showCashout && !isAnimating) {
      cashout();
    }
  };

  const primaryLabel =
    phase === 'connecting' ? 'CONNECTING…' :
    phase === 'playing' && showCashout ? `CASH OUT ${fmt(profit)}` :
    phase === 'playing' ? 'PLAYING...' :
    freeGrant ? 'FREE ROUND' : 'BET';
  const primaryDisabled =
    phase === 'connecting' ||
    (phase === 'playing' && (!showCashout || isAnimating));

  if(loading) return (
    <div className="loading-screen">
      <div className="loading-content">
        <img className="loading-logo" src="logo-mybc.svg" alt="MYBC"/>
        <h1 style={{fontSize:24,fontWeight:900,color:"#FFFFFF",margin:"12px 0 4px",fontFamily:"var(--font-primary)",letterSpacing:1}}>Hi-Lo</h1>
        <div className="loading-bar-wrap"><div className="loading-bar"/></div>
        <p className="loading-text">Loading your experience</p>
      </div>
    </div>
  );

  return (
    <React.Fragment>
      <div className="app">
        <div className="header">
          <div className="header-left">
            <div className="game-name">
              <img className="ico" src="icon-cards.svg" alt="HiLo" style={{height:20,width:20,filter:"brightness(0) invert(1)"}}/>
              <span>HiLo</span>
            </div>
          </div>
          <div className="header-bal-mobile">
            <div className="bet-balance-icon">$</div>
            <span className="bet-balance-amount">{Number(balance).toLocaleString('en-US', {minimumFractionDigits:2, maximumFractionDigits:2})}</span>
          </div>
          <div className="header-right">
            <div className="fairplay" onClick={() => setPfModalOpen(true)}>Fair Play</div>
            <div className="info" onClick={() => setGameInfoOpen(true)}>i</div>
          </div>
        </div>

        <div className="history-bar">
          {streak.length === 0 ? (
            <span className="history-empty">No cards yet</span>
          ) : streak.map((entry, i) => (
            <div key={i} className={`trail-group ${i === streak.length - 1 ? 'trail-new' : ''}`}>
              <div className={`trail-tag ${entry.guess} ${!entry.correct ? 'bust' : ''}`}>
                {entry.guess === 'start' && 'Start'}
                {entry.guess === 'skip' && 'Skip'}
                {entry.guess === 'higher' && (
                  <React.Fragment>
                    <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="4">
                      <path d="M12 19V5M5 12l7-7 7 7"/>
                    </svg>
                    {entry.mult ? entry.mult.toFixed(2) : '0.00'}x
                  </React.Fragment>
                )}
                {entry.guess === 'lower' && (
                  <React.Fragment>
                    <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="4">
                      <path d="M12 5v14M19 12l-7 7-7-7"/>
                    </svg>
                    {entry.mult ? entry.mult.toFixed(2) : '0.00'}x
                  </React.Fragment>
                )}
              </div>
              <div className="trail-row">
                <div className={`trail-chip ${isRed(entry.card.suit) ? 'tc-red' : 'tc-black'} ${!entry.correct ? 'trail-bust' : ''}`}>
                  <span className="tc-suit">{SUIT_SYMBOLS[entry.card.suit]}</span>
                  <span className="tc-rank">{entry.card.rank}</span>
                </div>
                {i < streak.length - 1 && (
                  <span className="trail-arrow">
                    <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
                      <path d="M13 5l7 7-7 7M6 5l7 7-7 7"/>
                    </svg>
                  </span>
                )}
              </div>
            </div>
          ))}
        </div>

        <main className="board">
          <div className="arc-bg" />

          <div className="side-option">
            <button className="option-card" onClick={() => makeGuess('lower')} disabled={phase !== 'playing' || isAnimating}>
              <div className="opt-icon">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
                  <path d="M12 5v14M19 12l-7 7-7-7"/>
                </svg>
              </div>
              <span className="opt-label">LOWER</span>
              <span className="opt-sub">or Same</span>
              <span className="opt-mult">{currentCard ? `x${lMult.toFixed(2)}` : '—'}</span>
            </button>
            <span className="opt-desc">{currentCard ? lDescStr : ' '}</span>
          </div>

          <div className="center-col">
            <div className="deck-wrap">
              <div className="card-frame">
                <div className={`main-card ${currentCard ? (isRed(currentCard.suit) ? 'mc-red' : 'mc-black') : 'mc-empty'} ${cardAnim === 'enter' ? 'card-enter' : ''}`}>
                  {currentCard ? (
                    <React.Fragment>
                      <span className="mc-rank">{currentCard.rank}</span>
                      <span className="mc-suit">{SUIT_SYMBOLS[currentCard.suit]}</span>
                    </React.Fragment>
                  ) : (
                    <span className="mc-placeholder">?</span>
                  )}
                </div>
                {prevCard && cardAnim === 'deal' && (
                  <div className={`main-card card-out ${isRed(prevCard.suit) ? 'mc-red' : 'mc-black'}`}>
                    <span className="mc-rank">{prevCard.rank}</span>
                    <span className="mc-suit">{SUIT_SYMBOLS[prevCard.suit]}</span>
                  </div>
                )}
              </div>

              <div className="deck-edges">
                <div className="deck-edge" />
                <div className="deck-edge" />
                <div className="deck-edge" />
                <div className="deck-edge" />
                <div className="deck-edge" />
              </div>

              <button className="skip-btn-icon" onClick={skipCard} disabled={isAnimating || (phase === 'playing' && skipsUsed >= maxSkips)} title="Skip Card">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
                  <path d="M13 5l7 7-7 7M6 5l7 7-7 7"/>
                </svg>
              </button>
            </div>

            <button className="skip-btn-text" onClick={skipCard} disabled={isAnimating || (phase === 'playing' && skipsUsed >= maxSkips)}>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
                <path d="M13 5l7 7-7 7M6 5l7 7-7 7"/>
              </svg>
              Skip Card {phase === 'playing' ? `(${maxSkips - skipsUsed})` : ''}
            </button>
          </div>

          <div className="side-option">
            <button className="option-card" onClick={() => makeGuess('higher')} disabled={phase !== 'playing' || isAnimating}>
              <div className="opt-icon">
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
                  <path d="M12 19V5M5 12l7-7 7 7"/>
                </svg>
              </div>
              <span className="opt-label">HIGHER</span>
              <span className="opt-sub">or Same</span>
              <span className="opt-mult">{currentCard ? `x${hMult.toFixed(2)}` : '—'}</span>
            </button>
            <span className="opt-desc">{currentCard ? hDescStr : ' '}</span>
          </div>

          <div className="bg-decor">
            <div className="bg-card bg-card-1">{SUIT_SYMBOLS.spades}</div>
            <div className="bg-card bg-card-2">{SUIT_SYMBOLS.hearts}</div>
            <div className="bg-card bg-card-3">{SUIT_SYMBOLS.diamonds}</div>
            <div className="bg-card bg-card-4">{SUIT_SYMBOLS.clubs}</div>
          </div>

          <div className={`result-badge ${phase === 'result' && lastResult ? lastResult : 'hidden'}`}>
            {lastResult === 'win' ? `CASHED OUT ${fmt(lastPayout)}` : lastResult === 'lose' ? 'BUST!' : ' '}
          </div>
        </main>

        <div className="bottom-panel">
          {freeGrant && (
            <div className="free-bet-banner">
              <span className="free-bet-icon">{'🎁'}</span>
              <div className="free-bet-info">
                <div className="free-bet-title">FREE ROUNDS</div>
                <div className="free-bet-count">{freeGrant.rounds_left} of {freeGrant.rounds_total} remaining</div>
                <div className="free-bet-progress">
                  <div className="free-bet-progress-fill" style={{ width: ((freeGrant.rounds_used / freeGrant.rounds_total) * 100) + '%' }} />
                </div>
              </div>
            </div>
          )}
          <div className="prob-row">
            <button className="prob-btn prob-lower" onClick={() => makeGuess('lower')} disabled={phase !== 'playing' || isAnimating}>
              <span className="prob-text">Lower / Same</span>
              <span className="prob-pct">
                <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
                  <path d="M12 5v14M19 12l-7 7-7-7"/>
                </svg>
                {currentCard ? `${(lProb * 100).toFixed(2)}%` : '—'}
              </span>
            </button>
            <button className="prob-btn prob-higher" onClick={() => makeGuess('higher')} disabled={phase !== 'playing' || isAnimating}>
              <span className="prob-pct">
                <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
                  <path d="M12 19V5M5 12l7-7 7 7"/>
                </svg>
                {currentCard ? `${(hProb * 100).toFixed(2)}%` : '—'}
              </span>
              <span className="prob-text">Higher / Same</span>
            </button>
          </div>

          <div className="bet-row bet-compact-row">
            <div className="bet-balance-row">
              <div className="bet-balance-icon">$</div>
              <div className="bet-balance-text"><span className="bet-balance-label">Balance</span><span className="bet-balance-amount">{Number(balance).toLocaleString('en-US', {minimumFractionDigits:2, maximumFractionDigits:2})}</span></div>
            </div>
            <div className={`input-row bet-input-row${freeGrant ? ' locked' : ''}`}>
              <span className="currency">$</span>
              <input
                type="number"
                value={freeGrant ? parseFloat(freeGrant.bet_amount).toFixed(2) : betStr}
                onChange={(e) => setBetStr(e.target.value)}
                disabled={phase === 'playing' || phase === 'connecting' || !!freeGrant}
                min={minBet}
                max={maxBet}
                step="0.10"
              />
              <div className="chips">
                <button className="chip" onClick={() => setBetStr(Math.max(minBet, getBet() / 2).toFixed(2))} disabled={phase === 'playing' || phase === 'connecting' || !!freeGrant}>{'½'}</button>
                <button className="chip" onClick={() => setBetStr(Math.min(getBet() * 2, maxBet).toFixed(2))} disabled={phase === 'playing' || phase === 'connecting' || !!freeGrant}>2x</button>
                <button className="chip" onClick={() => setBetStr(Math.min(maxBet, balance).toFixed(2))} disabled={phase === 'playing' || phase === 'connecting' || !!freeGrant}>Max</button>
              </div>
            </div>

            {phase === 'playing' && showCashout ? (
              <button className="place-btn cashout-mode desktop-cashout" onClick={cashout} disabled={isAnimating}>
                CASH OUT {fmt(profit)}
              </button>
            ) : (
              <button
                className={`place-btn${freeGrant && phase !== 'playing' ? ' free-mode' : ''}`}
                onClick={phase !== 'playing' ? handlePrimary : undefined}
                disabled={phase === 'connecting' || phase === 'playing'}
              >
                {phase === 'connecting' ? 'CONNECTING…' : freeGrant ? 'FREE ROUND' : 'BET'}
              </button>
            )}
          </div>
          <button
            className={`cashout-btn${phase === 'playing' && showCashout ? ' cashout-ready' : ''}`}
            onClick={cashout}
            disabled={!(phase === 'playing' && showCashout) || isAnimating}
          >
            {phase === 'playing' && showCashout ? `CASH OUT ${fmt(profit)}` : 'CASH OUT'}
          </button>
          <div style={{display:'none'}}>
            <div className="streak-box">
              <span className="stat-label">Multiplier</span>
              <span className="stat-val accent">{totalMultiplier > 1 ? `${totalMultiplier.toFixed(2)}x` : '0x'}</span>
            </div>
          </div>
        </div>

        <div className="bottom">
          <div className="bottom-icons">
            <div className={`ic sound-toggle${!soundOn ? ' muted' : ''}`} title="Toggle Sound"
              onClick={() => {
                const on = audioRef.current ? audioRef.current.toggle() : true;
                setSoundOn(on);
              }}>
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
                <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
                <path className="sound-waves" d="M15.54 8.46a5 5 0 0 1 0 7.07M19.07 4.93a10 10 0 0 1 0 14.14" style={{ display: soundOn ? undefined : 'none' }} />
              </svg>
            </div>
            <div className={`ic${faved ? ' faved' : ''}`} title="Favorite" onClick={() => { const next = !faved; setFaved(next); localStorage.setItem('fav_hilo', next); }}>
              <svg width="18" height="18" viewBox="0 0 24 24" fill={faved ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2">
                <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
              </svg>
            </div>
          </div>
          <div className="bottom-logo">MYBC</div>
        </div>
      </div>

      {alert && !gameInfoOpen && !pfModalOpen && (
        <div className="alert-toast">
          <span className="alert-icon">{'⚠'}</span>
          {alert}
        </div>
      )}

      <GameInfoModal open={gameInfoOpen} onClose={() => setGameInfoOpen(false)}
        minBet={minBet} maxBet={maxBet} maxSkips={maxSkips} rtp={rtp} />
      <ProvablyFairModal open={pfModalOpen} onClose={() => setPfModalOpen(false)} seedHash={seedHash} />

      {freeWelcome && freeGrant && (
        <div className="modal-overlay show" onClick={() => setFreeWelcome(false)}>
          <div className="modal free-bet-welcome" onClick={(e) => e.stopPropagation()}>
            <div className="fbw-icon">{'\uD83C\uDF81'}</div>
            <h2>Free Rounds Available!</h2>
            <p className="fbw-desc">You have been awarded free rounds for this game. The bet amount is fixed at <strong>{fmt(parseFloat(freeGrant.bet_amount))}</strong> per round.</p>
            <div className="fbw-count">
              <span className="fbw-count-num">{freeGrant.rounds_total}</span>
              <span className="fbw-count-label">Free Rounds</span>
            </div>
            <button className="fbw-btn" onClick={() => setFreeWelcome(false)}>START PLAYING</button>
          </div>
        </div>
      )}

      {freeBetSummary && (
        <div className="modal-overlay show" onClick={() => setFreeBetSummary(null)}>
          <div className="modal free-bet-summary" onClick={(e) => e.stopPropagation()}>
            <button className="modal-close" onClick={() => setFreeBetSummary(null)}>&times;</button>
            <div className="fbs-icon">{'\uD83C\uDF81'}</div>
            <h2>Free Rounds Complete!</h2>
            <div className="fbs-stats">
              <div className="fbs-stat">
                <span className="fbs-stat-label">Rounds Played</span>
                <span className="fbs-stat-val">{freeBetSummary.rounds}</span>
              </div>
            </div>
            <div className="fbs-winnings">
              <span className="fbs-winnings-label">Total Won</span>
              <span className="fbs-winnings-val">{fmt(freeBetSummary.totalWinnings)}</span>
            </div>
            <button className="fbs-btn" onClick={() => setFreeBetSummary(null)}>CONTINUE PLAYING</button>
          </div>
        </div>
      )}
    </React.Fragment>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<HiLoApp />);
