// app.jsx — Mobile-first prototype with backend, audio, hash-routing, admin auth.

const { useState, useEffect, useReducer, useRef } = React;

const M_W = 402;
const M_H = 874 - 44 - 34;
const SPIN_MS = 4200;
// Admin auth is handled SERVER-SIDE by Caddy Basic Auth on the /admin/ path
// (bcrypt hash in the Caddyfile). No password lives in this bundle anymore.
// The admin UI is served from /admin/index.html which sets window.FDS_ADMIN_PAGE.

// ─────────────── Hash router ───────────────
function useHashRoute() {
  const parse = () => (window.location.hash || '#').slice(1) || 'home';
  const [route, setRoute] = useState(parse());
  useEffect(() => {
    const onHash = () => setRoute(parse());
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);
  const navigate = (path) => {
    if (path === 'home') window.location.hash = '';
    else window.location.hash = path;
  };
  return [route, navigate];
}

// Subscribe to backend so screens re-render when admin changes labels/stock.
function useBackendState() {
  const [, force] = useReducer(x => x + 1, 0);
  useEffect(() => {
    if (!window.FDSBackend) return;
    return window.FDSBackend.subscribe(() => force());
  }, []);
  return window.FDSBackend?.getStats() || null;
}

function ProtoFlow({ forcedOutcome = null, onNavigate }) {
  const [screen, setScreen] = useState('landing');
  const [prize, setPrize] = useState('cafe');
  const [spin, setSpin] = useState({ phase: 'idle', rotation: 0 });
  const [soundOn, setSoundOnState] = useState(false);
  const [errKind, setErrKind] = useState(null);
  const [cooldownMs, setCooldownMs] = useState(0);
  // Second-chance state
  const [quizIdx, setQuizIdx] = useState(0);
  const secondChanceUsedRef = useRef(false);
  const autoSpinPendingRef = useRef(false);
  const stats = useBackendState();
  const prizes = stats?.prizes;
  const mapping = stats?.wheelMapping;

  const setSoundOn = (v) => {
    const next = typeof v === 'function' ? v(soundOn) : v;
    setSoundOnState(next);
    window.FDSAudio?.setEnabled(next);
  };

  const handleSpin = async () => {
    if (spin.phase !== 'idle') return;

    let result;
    if (forcedOutcome) {
      result = { ok: true, outcome: forcedOutcome === 'lose' ? 'lose' : 'win',
                 prize: forcedOutcome === 'lose' ? null : forcedOutcome };
    } else {
      // Tirage autoritaire côté serveur (asynchrone).
      result = await window.FDSBackend.participate();
    }

    if (!result.ok) {
      if (result.reason === 'cooldown') {
        setCooldownMs(result.meta?.remainingMs || 0);
        setErrKind('A');
      } else if (result.reason === 'sold_out') {
        setErrKind('B');
      } else {
        setErrKind('C');
      }
      setScreen('error');
      return;
    }

    // Build current sectors from backend mapping & pick a target index.
    const sectors = window.buildWheelSectors(prizes, mapping);
    let targetIdx;
    if (result.outcome === 'lose') {
      const losers = sectors.map((s, i) => s.win === null ? i : -1).filter(i => i >= 0);
      targetIdx = losers[Math.floor(Math.random() * losers.length)];
    } else {
      const matches = sectors.map((s, i) => s.win === result.prize ? i : -1).filter(i => i >= 0);
      targetIdx = matches[Math.floor(Math.random() * matches.length)] ?? 0;
    }
    const sliceDeg = 360 / sectors.length;
    const sectorCenterAt0 = -90 + targetIdx * sliceDeg + sliceDeg / 2;
    const baseR = (-90 - sectorCenterAt0) % 360;
    const turns = 5;
    const finalRotation = spin.rotation + (turns * 360) + ((baseR + 360) % 360 - (spin.rotation % 360));

    window.FDSAudio?.scheduleSpinTicks({ totalMs: SPIN_MS, count: 38 });
    setSpin({ phase: 'spinning', rotation: finalRotation });

    setTimeout(() => {
      if (result.outcome === 'win') {
        setPrize(result.prize);
        setScreen('win');
        setTimeout(() => window.FDSAudio?.winJingle(), 300);
      } else if (!secondChanceUsedRef.current) {
        // First lose of the round → offer the second-chance quiz instead of "Perdu"
        setQuizIdx(Math.floor(Math.random() * window.SECOND_CHANCE_QUIZ.length));
        setScreen('second-chance');
      } else {
        // Already used second chance — go straight to lose
        setScreen('lose');
        setTimeout(() => window.FDSAudio?.loseThud(), 200);
      }
      setSpin(s => ({ ...s, phase: 'done' }));
    }, SPIN_MS + 100);
  };

  const handleSecondChanceCorrect = () => {
    secondChanceUsedRef.current = true;
    autoSpinPendingRef.current = true;
    setSpin({ phase: 'idle', rotation: 0 });
    setScreen('wheel');
    // Auto-respin is triggered by the useEffect below once the wheel re-renders
    // with phase='idle' (avoids stale closures).
  };

  const handleSecondChanceWrong = () => {
    secondChanceUsedRef.current = true;
    setScreen('lose');
    setTimeout(() => window.FDSAudio?.loseThud(), 200);
  };

  const reset = () => {
    setSpin({ phase: 'idle', rotation: 0 });
    setErrKind(null);
    secondChanceUsedRef.current = false;
    autoSpinPendingRef.current = false;
    setScreen('landing');
  };

  // Auto-trigger a fresh spin after the player won the second-chance quiz.
  useEffect(() => {
    if (screen === 'wheel' && spin.phase === 'idle' && autoSpinPendingRef.current) {
      autoSpinPendingRef.current = false;
      const t = setTimeout(() => handleSpin(), 600);
      return () => clearTimeout(t);
    }
  }, [screen, spin.phase]); // eslint-disable-line

  // Bouton "Jouer" du bandeau (TopBanner) → amène DIRECTEMENT à la roue depuis
  // n'importe quel écran (y compris depuis l'accueil), pour une action visible.
  useEffect(() => {
    const h = () => {
      secondChanceUsedRef.current = false;
      autoSpinPendingRef.current = false;
      setErrKind(null);
      setSpin({ phase: 'idle', rotation: 0 });
      setScreen('wheel');
    };
    window.addEventListener('fds:goto-game', h);
    return () => window.removeEventListener('fds:goto-game', h);
  }, []); // eslint-disable-line

  const w = M_W, h = M_H;
  const nav = onNavigate || (() => {});

  // Build the current screen element (one place, one return -> wraps with transition)
  let screenEl = null;
  let transitionKey = screen;

  if (screen === 'error' && errKind) {
    transitionKey = `error-${errKind}`;
    screenEl = <ErrorScreen width={w} height={h} mobile kind={errKind}
      cooldownMs={cooldownMs} onAction={reset} onNavigate={nav} onBack={reset} />;
  } else if (screen === 'landing') {
    screenEl = <LandingScreen width={w} height={h} mobile prizes={prizes} onStart={() => {
      // Daily lock check before entering the wheel — if same device already played today,
      // route directly to the "Du calme du calme" screen.
      if (window.FDSBackend?.hasPlayedToday?.()) {
        setCooldownMs(window.FDSBackend.msUntilParisMidnight());
        setErrKind('A');
        setScreen('error');
        return;
      }
      setScreen('wheel');
    }} onNavigate={nav} />;
  } else if (screen === 'wheel') {
    screenEl = <WheelScreen width={w} height={h} mobile spinState={spin} onSpin={handleSpin} soundOn={soundOn} onToggleSound={() => setSoundOn(v => !v)} onNavigate={nav} onBack={reset} />;
  } else if (screen === 'win') {
    screenEl = <WinScreen width={w} height={h} mobile prize={prize} prizes={prizes} onClose={reset} onNavigate={nav} onBack={reset} />;
  } else if (screen === 'second-chance') {
    screenEl = <SecondChanceScreen width={w} height={h} mobile quizIdx={quizIdx} onCorrect={handleSecondChanceCorrect} onWrong={handleSecondChanceWrong} onNavigate={nav} onBack={reset} />;
  } else if (screen === 'lose') {
    screenEl = <LoseScreen width={w} height={h} mobile onClose={reset} onNavigate={nav} onBack={reset} />;
  }

  return <ScreenTransition screenKey={transitionKey}>{screenEl}</ScreenTransition>;
}

function StaticScreen({ kind, prize }) {
  const noop = () => {};
  const w = M_W, h = M_H;
  if (kind === 'landing')    return <LandingScreen width={w} height={h} mobile onStart={noop} onNavigate={noop} />;
  if (kind === 'wheel-idle') return <WheelScreen   width={w} height={h} mobile spinState={{ phase: 'idle', rotation: 0 }} onSpin={noop} soundOn={false} onToggleSound={noop} onNavigate={noop} />;
  if (kind === 'wheel-spin') return <WheelScreen   width={w} height={h} mobile spinState={{ phase: 'spinning', rotation: 1147 }} onSpin={noop} soundOn={false} onToggleSound={noop} onNavigate={noop} />;
  if (kind === 'win')        return <WinScreen     width={w} height={h} mobile prize={prize} onClose={noop} onNavigate={noop} />;
  if (kind === 'lose')       return <LoseScreen    width={w} height={h} mobile onClose={noop} onNavigate={noop} />;
  if (kind === 'errA')       return <ErrorScreen   width={w} height={h} mobile kind="A" onAction={noop} onNavigate={noop} />;
  if (kind === 'errB')       return <ErrorScreen   width={w} height={h} mobile kind="B" onAction={noop} onNavigate={noop} />;
  if (kind === 'errC')       return <ErrorScreen   width={w} height={h} mobile kind="C" onAction={noop} onNavigate={noop} />;
  if (kind === 'reglement')  return <ReglementScreen width={w} height={h} mobile onBack={noop} onNavigate={noop} />;
  return null;
}

// Bandeau contextuel : dès qu'un lien "avis Google" est cliqué (il s'ouvre dans
// un autre onglet), on affiche ici un rappel très visible pour revenir jouer.
function ReviewReturnBanner() {
  const [show, setShow] = useState(false);
  useEffect(() => {
    const on = () => setShow(true);
    window.addEventListener('fds:review-clicked', on);
    return () => window.removeEventListener('fds:review-clicked', on);
  }, []);
  if (!show) return null;
  const backToGame = () => {
    setShow(false);
    if (window.location.hash) window.location.hash = '';
    window.dispatchEvent(new CustomEvent('fds:goto-game'));
  };
  return (
    <div style={{
      position: 'absolute', left: 0, right: 0, bottom: 0, zIndex: 80,
      padding: '12px 14px', boxSizing: 'border-box',
      display: 'flex', alignItems: 'center', gap: 10,
      background: 'rgba(250,246,240,0.98)',
      borderTop: '1px solid rgba(92,68,34,0.15)',
      boxShadow: '0 -4px 20px rgba(92,68,34,0.14)',
      backdropFilter: 'blur(8px)', WebkitBackdropFilter: 'blur(8px)',
      animation: 'fdsScreenEnter 320ms cubic-bezier(0.16, 1, 0.3, 1) both',
    }}>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontFamily: '"Lobster", cursive', fontSize: 17, color: '#5C4422', lineHeight: 1.1 }}>
          Merci pour votre avis 🙏
        </div>
        <div style={{ fontSize: 11.5, color: '#6B7480', marginTop: 2 }}>
          L'avis s'ouvre dans un autre onglet — revenez quand vous voulez.
        </div>
      </div>
      <button onClick={backToGame} style={{
        flexShrink: 0, border: 'none', cursor: 'pointer', whiteSpace: 'nowrap',
        padding: '11px 15px', borderRadius: 999,
        fontFamily: '"Lobster", cursive', fontSize: 16, color: '#3A2A16',
        background: 'linear-gradient(135deg, #EFC139, #E8CF8A)',
        boxShadow: '0 2px 8px rgba(201,149,74,0.4)',
      }}>🎡 Revenir jouer</button>
      <button onClick={() => setShow(false)} aria-label="Fermer" style={{
        flexShrink: 0, border: 'none', background: 'transparent', cursor: 'pointer',
        fontSize: 20, lineHeight: 1, color: '#9A8C7A', padding: '2px 4px',
      }}>×</button>
    </div>
  );
}

function Phone({ children }) {
  // En production (route #home / pas de hash) : full screen, pas de mockup iOS.
  // La frame iPhone est utilisee uniquement pour le mode design-canvas (#design / #canvas).
  const inDesign = typeof window !== 'undefined'
    && (window.location.hash === '#design' || window.location.hash === '#canvas');
  if (inDesign) {
    return <IOSDevice width={402} height={874}>{children}</IOSDevice>;
  }
  // Mode live: plein ecran, mobile-first, pas de cadre.
  // width: 100% (pas 100vw) sinon la scrollbar verticale fait deborder a droite
  // et casse le centrage par margin: 0 auto.
  // La banniere de marque (TopBanner) coiffe toute l'app ; les ecrans sont
  // rendus dans un conteneur position:relative pour que leurs elements en
  // position:absolute se calent SOUS la banniere, pas par-dessus.
  return (
    <div style={{
      width: '100%',
      minHeight: '100dvh',
      maxWidth: 480,
      marginLeft: 'auto',
      marginRight: 'auto',
      background: '#FAF6F0',
      position: 'relative',
      overflow: 'hidden',
      display: 'flex',
      flexDirection: 'column',
    }}>
      <TopBanner />
      <div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
        {children}
      </div>
      {/* Bandeau "Revenir jouer" — apparaît après un clic sur l'avis Google
          (qui s'ouvre dans un autre onglet), pour un retour au jeu évident. */}
      <ReviewReturnBanner />
      {/* Entrée gestionnaire discrète — mène à la page admin protégée côté serveur (Basic Auth). */}
      <button
        onClick={() => { window.location.href = '/admin/'; }}
        title="Espace gestionnaire"
        aria-label="Espace gestionnaire"
        style={{
          position: 'absolute', right: 6, bottom: 6, zIndex: 60,
          width: 28, height: 28, padding: 0, lineHeight: '28px', textAlign: 'center',
          border: 'none', background: 'transparent', cursor: 'pointer',
          fontSize: 16, opacity: 0.14, transition: 'opacity 160ms ease',
          WebkitTapHighlightColor: 'transparent',
        }}
        onMouseEnter={(e) => { e.currentTarget.style.opacity = '0.85'; }}
        onMouseLeave={(e) => { e.currentTarget.style.opacity = '0.14'; }}
      >🍑</button>
    </div>
  );
}

// Wrapper qui applique une transition d'entree a chaque changement d'ecran.
// Le `key` force React a demonter/remonter l'enfant, declenchant l'animation CSS.
function ScreenTransition({ screenKey, children }) {
  return (
    <div
      key={screenKey}
      style={{
        width: '100%',
        height: '100%',
        animation: 'fdsScreenEnter 320ms cubic-bezier(0.16, 1, 0.3, 1) both',
      }}
    >
      {children}
    </div>
  );
}

// ─────────────── Admin dashboard with editable lots ───────────────
function AdminPanel({ onLogout, onNavigate }) {
  const stats = useBackendState() || window.FDSBackend.getStats();
  const { prizes, participations, wins, losses, lastPlay, cooldownMs, deviceId, stockTotal } = stats;
  const cooldownRemaining = lastPlay ? Math.max(0, cooldownMs - (Date.now() - lastPlay)) : 0;

  // Local edit state for each lot's label so the input stays smooth.
  const [drafts, setDrafts] = useState(() => ({
    cafe: prizes.cafe.label,
    digestif: prizes.digestif.label,
    entree: prizes.entree.label,
  }));
  // sync when backend state changes externally
  useEffect(() => {
    setDrafts(d => ({
      cafe: d.cafe === undefined ? prizes.cafe.label : (d._dirtyCafe ? d.cafe : prizes.cafe.label),
      digestif: d._dirtyDig ? d.digestif : prizes.digestif.label,
      entree: d._dirtyEnt ? d.entree : prizes.entree.label,
    }));
    // simple: just keep drafts in sync on first mount, then user edits override
  }, []); // eslint-disable-line

  const card = {
    background: '#fff', border: '1px solid #E8DCCC', borderRadius: 10,
    padding: 14,
  };
  const num = { fontFamily: '"Lobster", cursive', fontSize: 24, color: '#2D3540', lineHeight: 1 };
  const lab = { fontSize: 9, color: '#6B7480', textTransform: 'uppercase', letterSpacing: '0.08em', marginTop: 6 };
  const btn = (bg) => ({
    background: bg, color: '#fff', border: 'none', padding: '10px 12px',
    borderRadius: 8, fontSize: 12, fontWeight: 500, cursor: 'pointer', flex: 1,
  });
  const inputStyle = {
    width: '100%', boxSizing: 'border-box',
    padding: '8px 10px', border: '1px solid #E8DCCC', borderRadius: 6,
    fontSize: 13, fontFamily: 'inherit', color: '#2D3540', background: '#fff',
    outline: 'none',
  };
  const stepBtn = {
    width: 26, height: 26, borderRadius: 6, border: '1px solid #E8DCCC',
    background: '#fff', cursor: 'pointer', fontSize: 14, fontWeight: 600,
    color: '#2D3540', display: 'flex', alignItems: 'center', justifyContent: 'center',
    flexShrink: 0,
  };

  const setLabel = (key, value) => {
    setDrafts(d => ({ ...d, [key]: value }));
  };
  const commitLabel = (key) => {
    const v = drafts[key].trim();
    if (v) window.FDSBackend.setPrize(key, { label: v });
    else setDrafts(d => ({ ...d, [key]: prizes[key].label }));
  };
  const adjustStock = (key, delta) => {
    const next = Math.max(0, (prizes[key].stock || 0) + delta);
    window.FDSBackend.setPrize(key, { stock: next });
  };
  const setStockDirect = (key, raw) => {
    const n = parseInt(raw, 10);
    window.FDSBackend.setPrize(key, { stock: isNaN(n) ? 0 : Math.max(0, n) });
  };

  const lots = [
    { key: 'cafe',     short: 'Lot 1' },
    { key: 'digestif', short: 'Lot 2' },
    { key: 'entree',   short: 'Lot 3' },
  ];

  const doFullReset = async () => {
    if (!confirm('Réinitialiser : remettre les stocks par défaut, vider les compteurs, et tirer un nouveau plan de roue aléatoire ?')) return;
    await window.FDSBackend.resetAll();
    // refresh local drafts
    const fresh = window.FDSBackend.getPrizes();
    setDrafts({ cafe: fresh.cafe.label, digestif: fresh.digestif.label, entree: fresh.entree.label });
  };

  // Snapshot at entry, used to compute the diff shown before exit.
  const initialSnapshot = React.useRef(null);
  useEffect(() => {
    if (!initialSnapshot.current) {
      const s = window.FDSBackend.getStats();
      initialSnapshot.current = {
        prizes: JSON.parse(JSON.stringify(s.prizes)),
        wheelMapping: JSON.parse(JSON.stringify(s.wheelMapping)),
        whitelisted: s.whitelisted,
      };
    }
  }, []); // eslint-disable-line

  // Modal state: null | { changes: [...] }
  const [exitState, setExitState] = useState(null);

  const handleExit = () => {
    // 1) Force-commit any focused label input by blurring it
    //    (triggers onBlur → commitLabel for label inputs, no-op for stock).
    const a = document.activeElement;
    if (a && a.tagName === 'INPUT') a.blur();
    // 2) Tick to let backend state flush, then compute diff.
    setTimeout(() => {
      const initial = initialSnapshot.current || { prizes, wheelMapping: stats.wheelMapping, whitelisted: stats.whitelisted };
      const fresh = window.FDSBackend.getStats();
      const changes = [];
      ['cafe', 'digestif', 'entree'].forEach((k) => {
        const b = initial.prizes[k]; const a = fresh.prizes[k];
        if (!b || !a) return;
        if (b.label !== a.label) changes.push(`Libellé ${k} : « ${b.label} » → « ${a.label} »`);
        if (b.stock !== a.stock) {
          const d = a.stock - b.stock;
          changes.push(`Stock ${a.label} : ${b.stock} → ${a.stock} (${d > 0 ? '+' : ''}${d})`);
        }
      });
      if (JSON.stringify(initial.wheelMapping) !== JSON.stringify(fresh.wheelMapping)) {
        changes.push('Plan de la roue redistribué');
      }
      if (initial.whitelisted !== fresh.whitelisted) {
        changes.push(fresh.whitelisted ? 'Mode test (whitelist) activé' : 'Mode test (whitelist) désactivé');
      }
      setExitState({ changes });
    }, 60);
  };

  const confirmExit = () => onNavigate('home');
  const cancelExit  = () => setExitState(null);

  return (
    <div style={{
      width: '100%', height: '100%', padding: '18px 16px', boxSizing: 'border-box',
      background: '#FAF7F0', fontFamily: 'Inter, sans-serif',
      color: '#2D3540', overflow: 'auto', position: 'relative',
    }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 14 }}>
        <div>
          <h2 style={{ fontFamily: '"Lobster", cursive', fontSize: 26, margin: 0 }}>Admin</h2>
          <div style={{ fontSize: 11, color: '#6B7480', marginTop: 4 }}>
            Device {deviceId.slice(0, 8)}…
          </div>
        </div>
        <button onClick={onLogout} style={{
          background: 'none', border: '1px solid #E8DCCC', color: '#6B7480',
          padding: '6px 10px', borderRadius: 6, fontSize: 11, cursor: 'pointer',
          fontFamily: 'inherit',
        }}>Déconnexion</button>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8, marginBottom: 12 }}>
        <div style={card}><div style={num}>{participations}</div><div style={lab}>Tentatives</div></div>
        <div style={card}><div style={num}>{wins}</div><div style={lab}>Gains</div></div>
        <div style={card}><div style={num}>{losses}</div><div style={lab}>Pertes</div></div>
      </div>

      <div style={{ ...card, marginBottom: 12 }}>
        <div style={{ fontWeight: 600, fontSize: 13, marginBottom: 10 }}>
          Lots — {stockTotal} en stock
        </div>
        {lots.map(({ key, short }) => (
          <div key={key} style={{
            padding: '10px 0',
            borderTop: key !== 'cafe' ? '1px solid #F1E8D8' : 'none',
          }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
              <span style={{
                fontSize: 10, color: '#A47339', fontWeight: 600,
                letterSpacing: '0.1em', textTransform: 'uppercase',
                background: '#FAF1E5', padding: '2px 6px', borderRadius: 4,
              }}>{short}</span>
              <span style={{ fontSize: 10, color: '#6B7480' }}>clé : {key}</span>
            </div>
            <div style={{ fontSize: 10, color: '#6B7480', marginBottom: 4 }}>Libellé</div>
            <input
              value={drafts[key]}
              onChange={(e) => setLabel(key, e.target.value)}
              onBlur={() => commitLabel(key)}
              onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); }}
              style={inputStyle}
              placeholder="Nom du lot"
            />
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 }}>
              <div style={{ fontSize: 10, color: '#6B7480', flex: 1 }}>Stock</div>
              <button style={stepBtn} onClick={() => adjustStock(key, -1)}>−</button>
              <input
                type="number"
                min="0"
                value={prizes[key].stock}
                onChange={(e) => setStockDirect(key, e.target.value)}
                style={{
                  ...inputStyle, width: 64, textAlign: 'center',
                  fontVariantNumeric: 'tabular-nums', padding: '6px 4px',
                }}
              />
              <button style={stepBtn} onClick={() => adjustStock(key, +1)}>+</button>
              <button style={{ ...stepBtn, width: 38, fontSize: 10, fontWeight: 500 }}
                onClick={() => adjustStock(key, +10)}>+10</button>
            </div>
          </div>
        ))}
      </div>

      <div style={{ ...card, marginBottom: 12, borderColor: stats.whitelisted ? '#A47339' : '#E8DCCC' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 }}>
          <div style={{ fontWeight: 600, fontSize: 13 }}>Mode test (whitelist)</div>
          <span style={{
            fontSize: 10, padding: '2px 8px', borderRadius: 4,
            background: stats.whitelisted ? '#A47339' : '#E8DCCC',
            color: stats.whitelisted ? '#fff' : '#6B7480',
            fontWeight: 600, letterSpacing: '0.05em', textTransform: 'uppercase',
          }}>{stats.whitelisted ? 'Actif' : 'Inactif'}</span>
        </div>
        <div style={{ fontSize: 11, color: '#6B7480', lineHeight: 1.5, marginBottom: 8 }}>
          Bypass le cooldown 30s + le verrou daily Paris. Permet de jouer
          sans limite depuis cet appareil pour les tests.
        </div>
        <button
          onClick={() => stats.whitelisted
            ? window.FDSBackend.removeFromWhitelist()
            : window.FDSBackend.addToWhitelist()}
          style={{
            width: '100%', padding: '10px 12px', borderRadius: 8,
            border: 'none', cursor: 'pointer', fontSize: 12, fontWeight: 500,
            color: '#fff',
            background: stats.whitelisted ? '#9CABB7' : '#A47339',
          }}
        >
          {stats.whitelisted ? 'Retirer cet appareil de la whitelist' : 'Whitelister cet appareil'}
        </button>
        {stats.whitelist?.length > 0 && (
          <div style={{ fontSize: 10, color: '#6B7480', marginTop: 8, lineHeight: 1.5 }}>
            {stats.whitelist.length} appareil(s) whitelisté(s) · le tien : <code>{deviceId.slice(0, 8)}…</code>
          </div>
        )}
      </div>

      <div style={{ ...card, marginBottom: 12 }}>
        <div style={{ fontWeight: 600, fontSize: 13, marginBottom: 6 }}>Anti-rejeu</div>
        <div style={{ fontSize: 12, color: '#6B7480', lineHeight: 1.5 }}>
          Cooldown {Math.round(cooldownMs / 1000)}s par appareil.
          {cooldownRemaining > 0
            ? <strong style={{ color: '#A47339', display: 'block', marginTop: 4 }}>Reste {Math.ceil(cooldownRemaining / 1000)}s</strong>
            : <span style={{ color: '#2a8a4a', display: 'block', marginTop: 4 }}>Prêt à jouer ✓</span>}
        </div>
      </div>

      <div style={{ ...card, marginBottom: 12 }}>
        <div style={{ fontWeight: 600, fontSize: 13, marginBottom: 6 }}>Plan de la roue</div>
        <div style={{ fontSize: 11, color: '#6B7480', lineHeight: 1.5, marginBottom: 8 }}>
          Mapping aléatoire actuel des 4 secteurs gagnants :
        </div>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 8 }}>
          {[0,2,4,6].map(i => (
            <span key={i} style={{
              fontSize: 10, padding: '3px 7px', borderRadius: 4,
              background: '#FAF1E5', color: '#2D3540',
              fontVariantNumeric: 'tabular-nums',
            }}>S{i+1} → {prizes[stats.wheelMapping[i]]?.label || stats.wheelMapping[i]}</span>
          ))}
        </div>
        <button style={{ ...btn('#9CABB7'), width: '100%', flex: 'none' }}
          onClick={() => window.FDSBackend.regenerateWheel()}>
          Re-tirer le plan de la roue
        </button>
      </div>

      <div style={{ display: 'flex', gap: 6, marginBottom: 8 }}>
        <button style={btn('#A47339')} onClick={() => window.FDSBackend.resetCooldown()}>Reset cooldown</button>
        <button style={btn('#9CABB7')} onClick={() => {
          window.FDSBackend.setPrize('cafe', { stock: 0 });
          window.FDSBackend.setPrize('digestif', { stock: 0 });
          window.FDSBackend.setPrize('entree', { stock: 0 });
        }}>Vider stock</button>
      </div>
      <button style={{ ...btn('#6B7480'), width: '100%', flex: 'none' }}
        onClick={doFullReset}>
        Reset complet (recalcule la roue)
      </button>

      <div style={{ marginTop: 14, fontSize: 10, color: '#6B7480', lineHeight: 1.5 }}>
        Toute modification du libellé est immédiatement reflétée dans le règlement, l'écran 1 et sur la roue.
        Le reset complet remet les stocks par défaut et retire au sort un nouveau plan de roue.
      </div>

      <button onClick={handleExit} style={{
        marginTop: 18, width: '100%', padding: '14px 16px', border: 'none',
        borderRadius: 10, background: '#A47339', color: '#FAF7F0',
        fontFamily: '"Lobster", cursive', fontSize: 20, cursor: 'pointer',
      }}>
        Enregistrer et quitter
      </button>

      {exitState && (
        <div style={{
          position: 'absolute', inset: 0, background: 'rgba(45,53,64,0.55)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          padding: 20, zIndex: 50,
        }} onClick={cancelExit}>
          <div onClick={(e) => e.stopPropagation()} style={{
            width: '100%', maxWidth: 340, background: '#FAF7F0',
            borderRadius: 14, padding: 20, boxShadow: '0 20px 50px rgba(0,0,0,0.25)',
            border: '1px solid #E8DCCC',
          }}>
            <div style={{
              fontFamily: '"Lobster", cursive', fontSize: 22, color: '#2D3540',
              marginBottom: 6,
            }}>
              {exitState.changes.length === 0 ? 'Aucune modification' : 'Récapitulatif'}
            </div>
            <div style={{ fontSize: 12, color: '#6B7480', lineHeight: 1.5, marginBottom: 12 }}>
              {exitState.changes.length === 0
                ? 'Tu n\'as rien changé pendant cette session. Tout est déjà à jour.'
                : <>
                    {exitState.changes.length} modification(s) <strong style={{ color: '#2a8a4a' }}>enregistrée(s)</strong> en local pendant cette session :
                  </>}
            </div>
            {exitState.changes.length > 0 && (
              <ul style={{
                margin: 0, padding: '10px 12px', listStyle: 'none',
                background: '#fff', border: '1px solid #E8DCCC', borderRadius: 8,
                fontSize: 12, color: '#2D3540', lineHeight: 1.7,
                marginBottom: 12,
              }}>
                {exitState.changes.map((c, i) => (
                  <li key={i} style={{
                    paddingLeft: 14, position: 'relative',
                    borderTop: i === 0 ? 'none' : '1px solid #F1E8D8',
                    paddingTop: i === 0 ? 0 : 6, paddingBottom: i === exitState.changes.length - 1 ? 0 : 6,
                  }}>
                    <span style={{
                      position: 'absolute', left: 0, top: i === 0 ? 7 : 13,
                      width: 6, height: 6, borderRadius: 3, background: '#A47339',
                    }} />
                    {c}
                  </li>
                ))}
              </ul>
            )}
            <div style={{
              fontSize: 11, color: '#6B7480', lineHeight: 1.4, marginBottom: 14,
              padding: '8px 10px', background: '#FAF1E5', borderRadius: 6,
            }}>
              Toutes les données sont persistées dans le <code>localStorage</code> du navigateur (clé <code>fds:state</code>).
            </div>
            <div style={{ display: 'flex', gap: 8 }}>
              <button onClick={cancelExit} style={{
                flex: 1, padding: '12px', border: '1px solid #E8DCCC',
                borderRadius: 8, background: '#fff', color: '#6B7480',
                fontSize: 13, cursor: 'pointer', fontFamily: 'inherit',
              }}>Annuler</button>
              <button onClick={confirmExit} style={{
                flex: 1, padding: '12px', border: 'none', borderRadius: 8,
                background: '#A47339', color: '#FAF7F0', fontSize: 13,
                cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit',
              }}>Sortir</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// AdminApp — access is gated SERVER-SIDE by Caddy Basic Auth on /admin/, so no
// client-side password is needed. "Logout" just leaves the protected area (the
// browser drops Basic Auth creds on tab close).
function AdminApp({ onNavigate }) {
  return (
    <AdminPanel
      onNavigate={onNavigate}
      onLogout={() => { window.location.href = '/'; }}
    />
  );
}

// ─────────────── Reglement live wrapper ───────────────
function ReglementLive({ onNavigate }) {
  const stats = useBackendState() || window.FDSBackend.getStats();
  return <ReglementScreen
    width={M_W} height={M_H} mobile
    prizes={stats.prizes}
    onBack={() => onNavigate('home')}
    onNavigate={onNavigate}
  />;
}

const ART_W = 402;
const ART_H = 874;

// ─────────────── App with hash routing ───────────────
function App() {
  const [route, navigate] = useHashRoute();

  // Admin page (/admin/index.html sets this flag). Access is protected
  // server-side by Caddy Basic Auth — there is no public #admin hash route.
  if (typeof window !== 'undefined' && window.FDS_ADMIN_PAGE) {
    return <Phone><AdminApp onNavigate={navigate} /></Phone>;
  }

  // Single-screen routes (full-frame, what a real client sees).
  if (route === 'reglement') {
    return <Phone><ReglementLive onNavigate={navigate} /></Phone>;
  }

  // Default (#home or no hash): the actual game — landing → wheel → win/lose.
  // This is what a customer sees when they land on the site.
  if (route !== 'design' && route !== 'canvas') {
    return <Phone><ProtoFlow onNavigate={navigate} /></Phone>;
  }

  // #design or #canvas: designer view with all artboards (used during dev/review).
  return (
    <DesignCanvas>
      <DCSection id="full-flow" title="Prototype interactif — backend live"
        subtitle="Active le son via 🔊. Backend localStorage : stock, anti-rejeu, libellés, mapping aléatoire de la roue.">
        <DCArtboard id="flow" label="Flux complet · iPhone" width={ART_W} height={ART_H}>
          <Phone><ProtoFlow onNavigate={navigate} /></Phone>
        </DCArtboard>
        <DCArtboard id="reglement" label="Règlement · iPhone" width={ART_W} height={ART_H}>
          <Phone><ReglementLive onNavigate={navigate} /></Phone>
        </DCArtboard>
      </DCSection>

      <DCSection id="landing" title="Écran 1 — Landing">
        <DCArtboard id="landing" label="Landing" width={ART_W} height={ART_H}>
          <Phone><StaticScreen kind="landing" /></Phone>
        </DCArtboard>
      </DCSection>

      <DCSection id="wheel" title="Écran 2 — La Roue"
        subtitle="Ticks mécaniques pendant la rotation, jingle à la victoire, thud à la défaite.">
        <DCArtboard id="wheel-idle" label="Idle" width={ART_W} height={ART_H}>
          <Phone><StaticScreen kind="wheel-idle" /></Phone>
        </DCArtboard>
        <DCArtboard id="wheel-spin" label="Spinning" width={ART_W} height={ART_H}>
          <Phone><StaticScreen kind="wheel-spin" /></Phone>
        </DCArtboard>
      </DCSection>

      <DCSection id="win" title="Écran 3 — Win (3 lots configurables)">
        <DCArtboard id="win-cafe" label="Lot 1 (cafe)" width={ART_W} height={ART_H}>
          <Phone><StaticScreen kind="win" prize="cafe" /></Phone>
        </DCArtboard>
        <DCArtboard id="win-dig" label="Lot 2 (digestif)" width={ART_W} height={ART_H}>
          <Phone><StaticScreen kind="win" prize="digestif" /></Phone>
        </DCArtboard>
        <DCArtboard id="win-ent" label="Lot 3 (entree)" width={ART_W} height={ART_H}>
          <Phone><StaticScreen kind="win" prize="entree" /></Phone>
        </DCArtboard>
      </DCSection>

      <DCSection id="lose" title="Écran 4 — Lose">
        <DCArtboard id="lose" label="Perdu" width={ART_W} height={ART_H}>
          <Phone><StaticScreen kind="lose" /></Phone>
        </DCArtboard>
      </DCSection>

      <DCSection id="errors" title="États d'erreur (déclenchés par le backend)">
        <DCArtboard id="errA" label="A · Cooldown" width={ART_W} height={ART_H}>
          <Phone><StaticScreen kind="errA" /></Phone>
        </DCArtboard>
        <DCArtboard id="errB" label="B · Stock épuisé" width={ART_W} height={ART_H}>
          <Phone><StaticScreen kind="errB" /></Phone>
        </DCArtboard>
        <DCArtboard id="errC" label="C · Erreur technique" width={ART_W} height={ART_H}>
          <Phone><StaticScreen kind="errC" /></Phone>
        </DCArtboard>
      </DCSection>
    </DesignCanvas>
  );
}

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