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

// =============================================================================
// Constants
// =============================================================================
const GAME_UUID = 'mines_965';
const GRID_SIZES = [25, 36, 49, 64];
const DEFAULT_GRID_SIZE = 25;
const MIN_MINES = 1;
const DEFAULT_MINES = 3;
const MIN_BET_DEFAULT = 1;
const MAX_BET_DEFAULT = 100;

function fmt(n) {
  return '$' + Number(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function gridCols(totalTiles) { return Math.round(Math.sqrt(totalTiles)); }
function maxMines(totalTiles) { return totalTiles - 1; }

// =============================================================================
// Audio
// =============================================================================
class AudioEngine {
  constructor() { this.ctx = null; this.enabled = true; }
  getCtx() { if (!this.ctx) this.ctx = new (window.AudioContext || window.webkitAudioContext)(); return this.ctx; }
  toggle() { this.enabled = !this.enabled; return this.enabled; }
  play(freq, dur, 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 + dur);
      osc.connect(gain); gain.connect(ctx.destination);
      osc.start(); osc.stop(ctx.currentTime + dur);
    } catch (e) {}
  }
  sndBet()     { this.play(250, 0.12, 'sine', 0.08); }
  sndReveal()  { this.play(600, 0.08, 'triangle', 0.1); setTimeout(() => this.play(800, 0.1, 'triangle', 0.08), 50); }
  sndMine() {
    if (!this.enabled) return;
    try {
      const ctx = this.getCtx(); const now = ctx.currentTime;
      const noiseLen = 0.4;
      const buf = ctx.createBuffer(1, ctx.sampleRate * noiseLen, ctx.sampleRate);
      const d = buf.getChannelData(0);
      for (let i = 0; i < d.length; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / d.length, 3);
      const noise = ctx.createBufferSource(); noise.buffer = buf;
      const ng = ctx.createGain(); ng.gain.setValueAtTime(0.35, now); ng.gain.exponentialRampToValueAtTime(0.001, now + noiseLen);
      const nf = ctx.createBiquadFilter(); nf.type = 'lowpass'; nf.frequency.setValueAtTime(3000, now); nf.frequency.exponentialRampToValueAtTime(200, now + noiseLen);
      noise.connect(nf); nf.connect(ng); ng.connect(ctx.destination); noise.start(now);
      const rumble = ctx.createOscillator(); rumble.type = 'sine'; rumble.frequency.setValueAtTime(60, now); rumble.frequency.exponentialRampToValueAtTime(20, now + 0.5);
      const rg = ctx.createGain(); rg.gain.setValueAtTime(0.3, now); rg.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
      rumble.connect(rg); rg.connect(ctx.destination); rumble.start(now); rumble.stop(now + 0.5);
      const hit = ctx.createOscillator(); hit.type = 'sawtooth'; hit.frequency.setValueAtTime(200, now); hit.frequency.exponentialRampToValueAtTime(30, now + 0.15);
      const hg = ctx.createGain(); hg.gain.setValueAtTime(0.25, now); hg.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
      hit.connect(hg); hg.connect(ctx.destination); hit.start(now); hit.stop(now + 0.15);
    } catch (e) {}
  }
  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); }
  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); }
}

// =============================================================================
// RGS Client
// =============================================================================
const API_BASE = (typeof window !== 'undefined' && window.RGS_API_BASE != null) ? window.RGS_API_BASE : '';
// Session token comes from the URL only. The frontend never creates its own
// demo session — a demo is started out-of-band via /api/v1/init-demo.
let TOKEN = new URLSearchParams(window.location.search).get('token') || null;

async function rgsFetch(path, opts = {}) {
  const 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;
}
const getState     = () => rgsFetch('/api/v1/state');
const placeBet     = (amount, grid_size, mine_count) => rgsFetch('/api/v1/bet', { method: 'POST', body: { amount: Number(amount).toFixed(2), grid_size, mine_count } });
const revealTileRPC = (roundId, tile) => rgsFetch(`/api/v1/round/${roundId}/action`, { method: 'POST', body: { tile } });
const cashoutRound  = (roundId) => rgsFetch(`/api/v1/round/${roundId}/cashout`, { method: 'POST', body: {} });

// =============================================================================
// SVG Components
// =============================================================================
function GemSVG({ className }) {
  return (
    <svg className={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
      <path d="M12 2L6 9H1L12 22L23 9H18L12 2Z" />
      <path d="M12 2L8 9H16L12 2Z" opacity="0.6" fill="rgba(255,255,255,0.3)" />
      <path d="M8 9L12 22L1 9H8Z" opacity="0.3" fill="rgba(255,255,255,0.15)" />
    </svg>
  );
}

// =============================================================================
// GameInfoModal
// =============================================================================
function GameInfoModal({ open, onClose, minBet, maxBet, rtp, mineCount, totalTiles }) {
  const rtpPct = (parseFloat(rtp) * 100).toFixed(1);
  const houseEdgePct = ((1 - parseFloat(rtp)) * 100).toFixed(1);
  const cols = gridCols(totalTiles);
  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)} &mdash; ${maxBet.toFixed(2)}</span>
        </div>
        <div className="modal-payout-box">
          <span className="label">Grid Size</span>
          <span className="value">{cols} x {cols} ({totalTiles} tiles)</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">{'\uD83D\uDCA3'}</span>
          <p>
            Mines is a grid-based game where you reveal tiles to find gems while
            avoiding hidden mines. Each gem increases your multiplier &mdash; cash out
            anytime or keep pushing for bigger wins!
          </p>
        </div>
        <h3>How to Play</h3>
        <ol>
          <li><strong>Set grid &amp; mines</strong> &mdash; choose grid size and how many mines to hide.</li>
          <li><strong>Place your bet</strong> &mdash; enter the amount you want to wager.</li>
          <li><strong>Click tiles</strong> &mdash; each safe tile reveals a gem and increases your multiplier.</li>
          <li><strong style={{ color: '#0ECC68' }}>Cash out</strong> &mdash; lock in your winnings at any time.</li>
          <li><strong style={{ color: '#ED4163' }}>Hit a mine</strong> &mdash; you lose your entire bet.</li>
        </ol>
        <h3>House Edge &amp; RTP</h3>
        <div className="info-box">
          <span className="info-icon">{'\uD83D\uDCCA'}</span>
          <p>
            <strong>RTP: {rtpPct}%</strong> &mdash; House edge is {houseEdgePct}%.
            More mines = higher risk = higher multipliers. Every outcome is
            provably fair &mdash; tap Fair Play to verify.
          </p>
        </div>
      </div>
    </div>
  );
}

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

  const handleCopy = (text, label) => {
    if (navigator.clipboard) navigator.clipboard.writeText(text);
    setCopied(label); setTimeout(() => setCopied(null), 1500);
  };

  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
          mine placements. 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">{'\uD83D\uDDA5'}</span> Client Seed</div>
              <div className="pf-sublabel">Generated on your side &mdash; 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" onClick={() => handleCopy(clientSeed, 'client')}>{copied === 'client' ? '\u2713' : 'CP'}</button>
                <button className="pf-btn-sm" onClick={() => setClientSeed('mines_' + Math.random().toString(36).slice(2, 10))}>R</button>
              </div>
            </div>
            <div className="pf-section">
              <div className="pf-label"><span className="pf-icon">{'\uD83D\uDD12'}</span> Server Seed SHA256</div>
              <div className="pf-sublabel">Committed before you bet &mdash; revealed after</div>
              <div className="pf-hash-box">{hash}</div>
              <div className="pf-cp-row">
                <button className="pf-btn-sm" onClick={() => handleCopy(hash, 'hash')}>{copied === 'hash' ? '\u2713' : 'CP'}</button>
              </div>
              <p className="pf-note">
                This hash is committed <strong>before</strong> you bet. After the game,
                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 Mines 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 select mine count</li>
              <li>Mine positions: HMAC-SHA256(server_seed, client_seed:nonce) mapped to grid positions</li>
              <li>You reveal tiles &mdash; each safe tile multiplies your winnings</li>
              <li>Cash out anytime to lock in winnings</li>
              <li>Server reveals the seed &mdash; verify SHA256 matches the pre-committed hash</li>
            </ol>
          </div>
        )}
      </div>
    </div>
  );
}

// =============================================================================
// NoSessionScreen — shown when opened without a session token.
// =============================================================================
function NoSessionScreen() {
  return (
    <div style={{display:'flex',alignItems:'center',justifyContent:'center',minHeight:'100vh',
      background:'#101015',color:'#c8c8d0',fontFamily:'system-ui,-apple-system,sans-serif',
      textAlign:'center',padding:'24px'}}>
      <div>
        <h2 style={{margin:'0 0 8px',fontSize:'20px',fontWeight:700,color:'#fff'}}>No active session</h2>
        <p style={{margin:0,fontSize:'14px',opacity:0.6}}>Open this game through your casino.</p>
      </div>
    </div>
  );
}

// =============================================================================
// MinesApp
// =============================================================================
function MinesApp() {
  // Server state
  const [connected, setConnected] = useState(false);
  const [noSession, setNoSession] = useState(false);
  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 [rtp, setRtp] = useState(0.965);
  const [seedHash, setSeedHash] = useState('');

  // Bet config
  const [betStr, setBetStr] = useState('10.00');
  const [gridSize, setGridSize] = useState(DEFAULT_GRID_SIZE);
  const [mineCount, setMineCount] = useState(DEFAULT_MINES);

  // Round state
  const [roundId, setRoundId] = useState(null);
  const [tiles, setTiles] = useState(() => Array(DEFAULT_GRID_SIZE).fill('hidden'));
  const [revealedCount, setRevealedCount] = useState(0);
  const [currentMult, setCurrentMult] = useState(null);
  const [nextMult, setNextMult] = useState(null);
  const [currentPayout, setCurrentPayout] = useState(null);
  const [betAmount, setBetAmount] = useState(0);
  const [inFlight, setInFlight] = useState(false);
  const [lastResult, setLastResult] = useState(null);
  const [boom, setBoom] = useState(false);

  // Free rounds
  const [freeGrant, setFreeGrant] = useState(null);
  const isFreeMode = freeGrant != null;

  // Free bet summary
  const [freeBetSummary, setFreeBetSummary] = useState(null);
  const [freeBetTracker, setFreeBetTracker] = useState(null);
  const freeBetTrackerRef = useRef(null);

  // UI
  const [soundOn, setSoundOn] = useState(true);
  const [alert, setAlert] = useState(null);
  const [gameInfoOpen, setGameInfoOpen] = useState(false);
  const [pfModalOpen, setPfModalOpen] = useState(false);
  const [freeWelcome, setFreeWelcome] = useState(false);
  const [faved, setFaved] = useState(localStorage.getItem('fav_mines') === 'true');

  const audioRef = useRef(null);
  const balRef = useRef(0);

  // Keep ref in sync with tracker state
  useEffect(() => { freeBetTrackerRef.current = freeBetTracker; }, [freeBetTracker]);

  useEffect(() => { audioRef.current = new AudioEngine(); }, []);
  useEffect(() => { balRef.current = balance; }, [balance]);

  const totalTiles = gridSize;
  const cols = gridCols(totalTiles);
  const maxM = maxMines(totalTiles);
  const gemsTotal = totalTiles - mineCount;
  const phase = roundId ? 'playing' : (lastResult ? 'result' : 'idle');
  const profit = currentPayout != null ? currentPayout : 0;

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

  // Fetch free rounds grants
  const fetchFreeRounds = useCallback(async () => {
    // Preview mode: ?demo-free in URL shows mock free bet UI
    if (window.location.search.indexOf('demo-free') !== -1) {
      setFreeGrant(function (prev) {
        if (prev && prev._mock) {
          var left = prev.rounds_left - 1;
          if (left <= 0) {
            // Free bets exhausted — show summary if tracker has data
            var tracker = freeBetTrackerRef.current;
            if (tracker && tracker.rounds > 0) {
              setFreeBetSummary({ totalWinnings: tracker.totalWinnings, totalBet: tracker.totalBet, rounds: tracker.rounds });
              setFreeBetTracker(null);
            }
            return null;
          }
          return Object.assign({}, prev, { rounds_left: left, rounds_used: prev.rounds_total - left });
        }
        // First time — initialize tracker and show welcome
        var active = { _mock: true, id: 'mock', game_uuid: GAME_UUID, bet_amount: '5.00', rounds_total: 5, rounds_used: 0, rounds_left: 5, status: 'active' };
        if (!prev && active) {
          setFreeWelcome(true);
        }
        setFreeBetTracker({ totalWinnings: 0, totalBet: 0, rounds: 0 });
        return active;
      });
      return;
    }
    try {
      const data = await rgsFetch('/api/v1/freerounds');
      const grants = (data && data.grants) || [];
      const active = grants.find(function (g) {
        return g.status === 'active' && g.rounds_left > 0 && g.game_uuid === GAME_UUID;
      });
      if (!active) {
        // No more grants — show summary if tracker has data
        var tracker = freeBetTrackerRef.current;
        if (tracker && tracker.rounds > 0) {
          setFreeBetSummary({ totalWinnings: tracker.totalWinnings, totalBet: tracker.totalBet, rounds: tracker.rounds });
          setFreeBetTracker(null);
        }
      }
      setFreeGrant(function (prev) {
        if (!prev && active) {
          // First grant appearing — initialize tracker and show welcome
          setFreeBetTracker({ totalWinnings: 0, totalBet: 0, rounds: 0 });
          setFreeWelcome(true);
        }
        return active || null;
      });
    } catch (e) {
      // Silently ignore — demo sessions return empty or error
      var tracker = freeBetTrackerRef.current;
      if (tracker && tracker.rounds > 0) {
        setFreeBetSummary({ totalWinnings: tracker.totalWinnings, totalBet: tracker.totalBet, rounds: tracker.rounds });
        setFreeBetTracker(null);
      }
      setFreeGrant(null);
    }
  }, []);

  // Grid size change (only when not playing)
  const handleGridSize = useCallback((size) => {
    if (roundId) return;
    setGridSize(size);
    setTiles(Array(size).fill('hidden'));
    setMineCount(prev => Math.min(prev, size - 1));
  }, [roundId]);

  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]);

  // Boot: require a session token from the URL, then load state.
  useEffect(() => {
    let cancelled = false;
    if (!TOKEN) { setNoSession(true); return; }
    (async () => {
      try {
        const state = await getState();
        if (cancelled) return;
        setBalance(parseFloat(state.balance));
        balRef.current = 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(parseFloat(state.config.rtp));
        }
        if (state.next_seed_hash) setSeedHash(state.next_seed_hash);
        setConnected(true);
        fetchFreeRounds();
      } catch (e) {
        if (!cancelled) showAlertMsg('Connect failed: ' + e.message);
      }
    })();
    return () => { cancelled = true; };
  }, [showAlertMsg, fetchFreeRounds]);

  // Start game
  const startGame = useCallback(async () => {
    if (!connected) { showAlertMsg('Not connected'); return; }
    const amt = getBet();
    if (!isFreeMode && amt > balRef.current) { showAlertMsg('Insufficient balance'); return; }
    if (mineCount < 1 || mineCount >= gridSize) { showAlertMsg('Invalid mine count'); return; }

    setInFlight(true);
    let resp;
    try { resp = await placeBet(amt, gridSize, mineCount); }
    catch (e) { setInFlight(false); showAlertMsg(e.message || 'Bet failed'); return; }
    setInFlight(false);

    if (audioRef.current) audioRef.current.sndBet();
    setBalance(parseFloat(resp.balance)); balRef.current = parseFloat(resp.balance);
    if (resp.next_seed_hash) setSeedHash(resp.next_seed_hash);
    setRoundId(resp.round_id);
    setBetAmount(isFreeMode ? parseFloat(freeGrant.bet_amount) : amt);
    setTiles(Array(gridSize).fill('hidden'));
    setRevealedCount(0);
    setCurrentMult(null);
    setCurrentPayout(null);
    setLastResult(null);
    const gd = resp.game_data || {};
    setNextMult(gd.next_multiplier || null);
  }, [connected, getBet, mineCount, gridSize, showAlertMsg, isFreeMode, freeGrant]);

  // Reveal tile
  const pickTile = useCallback(async (index) => {
    if (!roundId || inFlight) return;
    if (tiles[index] !== 'hidden') return;

    setInFlight(true);
    let resp;
    try { resp = await revealTileRPC(roundId, index); }
    catch (e) { setInFlight(false); showAlertMsg(e.message || 'Reveal failed'); return; }
    setInFlight(false);

    const gd = resp.game_data || {};
    const outcome = gd.outcome;

    if (outcome === 'mine') {
      // Boom
      if (audioRef.current) audioRef.current.sndMine();
      setBoom(true); setTimeout(() => setBoom(false), 600);
      const mines = new Set(gd.mines_revealed || []);
      setTiles(prev => prev.map((s, i) => {
        if (i === index) return 'mine';
        if (mines.has(i)) return 'mine-other';
        if (s === 'revealed') return 'revealed';
        return 'safe';
      }));
      setLastResult('lose');
      setRoundId(null);
      if (resp.balance != null) { const nb = parseFloat(resp.balance); if (isFinite(nb)) { setBalance(nb); balRef.current = nb; } }
      if (resp.next_seed_hash) setSeedHash(resp.next_seed_hash);
      // Track free bet loss
      if (freeBetTrackerRef.current) {
        var updated = { totalWinnings: freeBetTrackerRef.current.totalWinnings, totalBet: freeBetTrackerRef.current.totalBet + betAmount, rounds: freeBetTrackerRef.current.rounds + 1 };
        freeBetTrackerRef.current = updated;
        setFreeBetTracker(updated);
        fetchFreeRounds();
      }
      return;
    }

    // Safe tile
    if (audioRef.current) audioRef.current.sndReveal();
    const newRevealed = revealedCount + 1;
    setRevealedCount(newRevealed);
    setTiles(prev => { const n = [...prev]; n[index] = 'revealed'; return n; });

    if (resp.finished) {
      // All gems found — auto cashout
      const payout = parseFloat(resp.total_payout || gd.payout || 0);
      if (resp.balance != null) { const nb = parseFloat(resp.balance); if (isFinite(nb)) { setBalance(nb); balRef.current = nb; } }
      if (resp.next_seed_hash) setSeedHash(resp.next_seed_hash);
      if (audioRef.current) { audioRef.current.sndCashout(); setTimeout(() => audioRef.current.sndBigWin(), 200); }
      const mines = new Set(gd.mines_revealed || []);
      setTiles(prev => prev.map((s, i) => mines.has(i) ? 'mine-other' : (s === 'revealed' ? 'revealed' : 'safe')));
      setCurrentPayout(payout);
      setCurrentMult(gd.multiplier || gd.current_multiplier);
      setLastResult('win');
      setRoundId(null);
      // Track free bet auto-cashout win
      if (freeBetTrackerRef.current) {
        var updated = { totalWinnings: freeBetTrackerRef.current.totalWinnings + payout, totalBet: freeBetTrackerRef.current.totalBet + betAmount, rounds: freeBetTrackerRef.current.rounds + 1 };
        freeBetTrackerRef.current = updated;
        setFreeBetTracker(updated);
        fetchFreeRounds();
      }
      return;
    }

    setCurrentMult(gd.current_multiplier || null);
    setCurrentPayout(gd.current_payout ? parseFloat(gd.current_payout) : null);
    setNextMult(gd.next_multiplier || null);
    if (gd.current_multiplier >= 10 && audioRef.current) audioRef.current.sndBigWin();
  }, [roundId, inFlight, tiles, revealedCount, showAlertMsg, betAmount, fetchFreeRounds]);

  // Cashout
  const cashout = useCallback(async () => {
    if (!roundId || inFlight || revealedCount === 0) return;
    setInFlight(true);
    let resp;
    try { resp = await cashoutRound(roundId); }
    catch (e) { setInFlight(false); showAlertMsg(e.message || 'Cashout failed'); return; }
    setInFlight(false);

    const gd = resp.game_data || {};
    const payout = parseFloat(resp.total_payout || gd.payout || 0);
    if (resp.balance != null) { const nb = parseFloat(resp.balance); if (isFinite(nb)) { setBalance(nb); balRef.current = nb; } }
    if (resp.next_seed_hash) setSeedHash(resp.next_seed_hash);
    if (audioRef.current) audioRef.current.sndCashout();
    const mines = new Set(gd.mines_revealed || []);
    setTiles(prev => prev.map((s, i) => mines.has(i) ? 'mine-other' : (s === 'revealed' ? 'revealed' : 'safe')));
    setCurrentPayout(payout);
    setCurrentMult(gd.multiplier || gd.current_multiplier);
    setLastResult('win');
    setRoundId(null);
    // Track free bet cashout win
    if (freeBetTrackerRef.current) {
      var updated = { totalWinnings: freeBetTrackerRef.current.totalWinnings + payout, totalBet: freeBetTrackerRef.current.totalBet + betAmount, rounds: freeBetTrackerRef.current.rounds + 1 };
      freeBetTrackerRef.current = updated;
      setFreeBetTracker(updated);
      fetchFreeRounds();
    }
  }, [roundId, inFlight, revealedCount, showAlertMsg, betAmount, fetchFreeRounds]);

  // New game (reset to idle)
  const newGame = useCallback(() => {
    setLastResult(null);
    setTiles(Array(gridSize).fill('hidden'));
    setRevealedCount(0);
    setBetAmount(0);
    setCurrentMult(null);
    setCurrentPayout(null);
    setNextMult(null);
  }, [gridSize]);

  // Tile rendering
  const getTileClass = (state) => {
    if (state === 'revealed') return 'tile tile-revealed';
    if (state === 'safe')     return 'tile tile-revealed';
    if (state === 'mine')     return 'tile tile-mine';
    if (state === 'mine-other') return 'tile tile-mine-other';
    return 'tile';
  };

  const getTileContent = (state) => {
    if (state === 'safe')      return <GemSVG className="tile-icon tile-gem-dim" />;
    if (state === 'revealed')  return <GemSVG className="tile-icon tile-gem" />;
    if (state === 'mine')      return <span className="tile-icon tile-bomb">{'\uD83D\uDCA3'}</span>;
    if (state === 'mine-other') return <span className="tile-icon tile-bomb-other">{'\uD83D\uDCA3'}</span>;
    return null;
  };

  // Slider background
  const sliderPct = maxM > MIN_MINES ? ((mineCount - MIN_MINES) / (maxM - MIN_MINES)) * 100 : 0;
  const sliderBg = `linear-gradient(to right, var(--accent) 0%, var(--accent) ${sliderPct}%, var(--red) ${sliderPct}%, var(--red) 100%)`;

  if (noSession) return <NoSessionScreen />;

  return (
    <React.Fragment>
      <div className="app">
        {/* HEADER */}
        <div className="header">
          <div className="header-left">
            <div className="game-name">
              <svg className="ico" width="20" height="20" viewBox="0 0 24 24" fill="var(--accent)" xmlns="http://www.w3.org/2000/svg">
                <path d="M12 2L6 9H1L12 22L23 9H18L12 2Z" />
                <path d="M12 2L8 9H16L12 2Z" opacity="0.6" fill="rgba(255,255,255,0.3)" />
                <path d="M8 9L12 22L1 9H8Z" opacity="0.3" fill="rgba(255,255,255,0.15)" />
              </svg>
              <span>Mines</span>
            </div>
          </div>
          <div className="header-balance">
            <span className="header-bal-icon">{'\uD83D\uDCB0'}</span>
            <span className="header-bal-value">{fmt(balance)}</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>

        {/* MAIN BODY: sidebar + grid */}
        <div className="game-body">
          {/* LEFT SIDEBAR */}
          <aside className="sidebar">
            {/* Free bet banner */}
            {isFreeMode && (
              <div className="ctrl-group ctrl-free">
                <div className="free-bet-banner">
                  <span className="free-bet-icon">{'\uD83C\uDF81'}</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_left / freeGrant.rounds_total) * 100) + '%' }}></div>
                    </div>
                  </div>
                </div>
              </div>
            )}

            {/* Action buttons */}
            <div className="ctrl-action">
              {phase === 'idle' && (
                <button className={'place-btn' + (isFreeMode ? ' free-mode' : '')} onClick={startGame} disabled={inFlight}>
                  {inFlight ? 'PLACING...' : (isFreeMode ? 'FREE ROUND' : 'BET')}
                </button>
              )}
              {phase === 'playing' && revealedCount === 0 && (
                <button className="place-btn" disabled>PICK A TILE</button>
              )}
              {phase === 'playing' && revealedCount > 0 && (
                <button className="place-btn cashout-mode" onClick={cashout} disabled={inFlight}>
                  CASH OUT {currentPayout != null ? fmt(currentPayout) : ''}
                </button>
              )}
              {phase === 'result' && (
                <button className={'place-btn' + (isFreeMode ? ' free-mode' : '')} onClick={() => { newGame(); setTimeout(startGame, 0); }}>{isFreeMode ? 'FREE ROUND AGAIN' : 'BET AGAIN'}</button>
              )}
            </div>

            {/* Bet Amount */}
            <div className="ctrl-group ctrl-bet">
              <label className="ctrl-label">{isFreeMode ? 'Bet Amount (Free Round)' : 'Bet Amount'}</label>
              <div className={'input-row' + (isFreeMode ? ' locked' : '')}>
                <span className="currency">$</span>
                <input
                  type="number"
                  value={isFreeMode ? parseFloat(freeGrant.bet_amount).toFixed(2) : betStr}
                  onChange={(e) => setBetStr(e.target.value)}
                  disabled={phase === 'playing' || isFreeMode}
                  min={minBet} max={maxBet} step="0.01"
                />
                <div className="chips">
                  <button className="chip" onClick={() => setBetStr((getBet() / 2).toFixed(2))} disabled={phase === 'playing' || isFreeMode}>&frac12;</button>
                  <button className="chip" onClick={() => setBetStr(Math.min(getBet() * 2, maxBet).toFixed(2))} disabled={phase === 'playing' || isFreeMode}>2x</button>
                </div>
              </div>
            </div>

            {/* Number of Mines */}
            <div className="ctrl-group ctrl-mines">
              <label className="ctrl-label">Number of Mines</label>
              <div className="mines-slider-wrap">
                <div className="mines-slider-ends">
                  <span className="slider-end gem-end">
                    <span className="slider-end-icon">{'\u2666'}</span>
                    <span>{gemsTotal}</span>
                  </span>
                  <input
                    type="range"
                    className="mines-slider"
                    min={MIN_MINES} max={maxM}
                    value={mineCount}
                    onChange={(e) => setMineCount(parseInt(e.target.value))}
                    disabled={phase === 'playing'}
                    style={{ background: sliderBg }}
                  />
                  <span className="slider-end mine-end">
                    <span>{mineCount}</span>
                    <svg className="slider-end-icon" width="16" height="16" viewBox="0 0 24 24" fill="var(--red)" xmlns="http://www.w3.org/2000/svg">
                      <circle cx="12" cy="14" r="8" />
                      <line x1="12" y1="6" x2="12" y2="2" stroke="var(--red)" strokeWidth="2" />
                      <circle cx="12" cy="1.5" r="1.5" fill="var(--accent)" />
                      <circle cx="9" cy="11" r="1.5" fill="rgba(255,255,255,0.3)" />
                    </svg>
                  </span>
                </div>
              </div>
            </div>

            {/* Grid Size */}
            <div className="ctrl-group ctrl-grid">
              <label className="ctrl-label">Grid Size</label>
              <div className="grid-size-selector">
                {GRID_SIZES.map((size) => (
                  <button
                    key={size}
                    className={`grid-size-btn${gridSize === size ? ' active' : ''}`}
                    onClick={() => handleGridSize(size)}
                    disabled={phase === 'playing'}
                  >
                    {size}
                  </button>
                ))}
              </div>
            </div>

            {/* Multiplier stats (when playing) */}
            {phase === 'playing' && (
              <div className="ctrl-group ctrl-stats">
                <div className="sidebar-stats">
                  <div className="sidebar-stat">
                    <span className="sidebar-stat-label">Gems</span>
                    <span className="sidebar-stat-val">{revealedCount}/{gemsTotal}</span>
                  </div>
                  <div className="sidebar-stat">
                    <span className="sidebar-stat-label">Current</span>
                    <span className="sidebar-stat-val accent">{currentMult != null ? `${currentMult}x` : '---'}</span>
                  </div>
                  <div className="sidebar-stat">
                    <span className="sidebar-stat-label">Next</span>
                    <span className="sidebar-stat-val">{nextMult != null ? `${nextMult}x` : '---'}</span>
                  </div>
                </div>
              </div>
            )}

            {/* Profit box when playing */}
            {phase === 'playing' && revealedCount > 0 && currentPayout != null && (
              <div className="ctrl-group ctrl-profit">
                <div className="profit-box">
                  <span className="profit-label">Total Profit</span>
                  <span className="profit-val">{fmt(currentPayout - betAmount)}</span>
                </div>
              </div>
            )}
          </aside>

          {/* RIGHT SIDE — grid */}
          <main className="board">
            <div className="grid-wrap">
              <div
                className={`grid${boom ? ' grid-boom' : ''}`}
                style={{ gridTemplateColumns: `repeat(${cols}, 1fr)` }}
              >
                {tiles.map((state, i) => (
                  <button
                    key={i}
                    className={getTileClass(state)}
                    onClick={() => pickTile(i)}
                    disabled={phase !== 'playing' || state !== 'hidden'}
                  >
                    {getTileContent(state)}
                  </button>
                ))}
              </div>
            </div>

            {/* Result badge */}
            <div className={`result-badge ${lastResult === 'win' ? 'win' : lastResult === 'lose' ? 'lose' : 'hidden'}`}>
              {lastResult === 'win'
                ? `CASHED OUT ${currentPayout != null ? fmt(currentPayout) : ''}`
                : lastResult === 'lose' ? 'BOOM!' : '\u00A0'}
            </div>
          </main>
        </div>

        {/* FOOTER */}
        <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_mines', 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">{'\u26A0'}</span>
          {alert}
        </div>
      )}

      <GameInfoModal open={gameInfoOpen} onClose={() => setGameInfoOpen(false)} minBet={minBet} maxBet={maxBet} rtp={rtp} mineCount={mineCount} totalTiles={totalTiles} />
      <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(<MinesApp />);
