// Live Quiz — REAL realtime transport + the single full-screen entry.
//
// Everything here talks to the LiveGame Durable Object over a WebSocket
// (/api/live/ws). The server is authoritative: it owns the questions, the
// scoring, and the timing, and it broadcasts to the whole room at once — so
// every player advances LIVE together. There are no simulated players and no
// client-side auto-advance.
//
// One entry point: LiveScreen (reached from the top-bar "Live" tab).
//   · desktop → choose Host or Join
//   · phone   → Join only (hosting needs the big screen)
// The host & player VIEWS are unchanged (live-host.jsx / live-player.jsx); they
// render purely from the `game` object this file produces.

const { useState: lgUseState, useEffect: lgUseEffect, useRef: lgUseRef, useMemo: lgUseMemo } = React;

function lvWsUrl(params) {
  const proto = location.protocol === "https:" ? "wss:" : "ws:";
  const qs = new URLSearchParams(params).toString();
  return `${proto}//${location.host}/api/live/ws?${qs}`;
}

function lvShuffle(a) {
  const r = a.slice();
  for (let i = r.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const t = r[i]; r[i] = r[j]; r[j] = t; }
  return r;
}
function lvFamilyLabel(type) {
  return type === "grammar" ? "Grammar" : type === "multiple-choice" ? "Multiple Choice" : "Word Formation";
}
// Build a randomized live question set from the app's REAL bank (window.VOCAB),
// filtered to the host's chosen task types. MC options are shuffled too.
function buildLiveQuestions(typeKeys, count) {
  const VOCAB = window.VOCAB || [];
  const wanted = new Set(typeKeys);
  const pool = VOCAB.filter((v) => wanted.has(v.type) && typeof v.sentence === "string" && v.sentence.includes("___"));
  const out = [];
  for (const v of lvShuffle(pool)) {
    if (out.length >= count) break;
    if (v.type === "multiple-choice") {
      const choices = (v.choices || []).slice(0, 4);
      if (choices.length < 2 || !choices.includes(v.answer)) continue;
      const opts = lvShuffle(choices);
      out.push({ family: "Multiple Choice", mode: "mc", prompt: v.sentence, options: opts, correct: opts.indexOf(v.answer) });
    } else {
      if (!v.answer) continue;
      out.push({ family: lvFamilyLabel(v.type), mode: "typed", base: v.base || null, prompt: v.sentence, answer: v.answer, alts: v.alts || [] });
    }
  }
  return out;
}
function countAvailable(typeKeys) {
  const VOCAB = window.VOCAB || [];
  const wanted = new Set(typeKeys);
  return VOCAB.filter((v) => wanted.has(v.type) && typeof v.sentence === "string" && v.sentence.includes("___")
    && (v.type !== "multiple-choice" || ((v.choices || []).includes(v.answer)))).length;
}

// OGE live: passage tasks with ONE rotating blank. To save time we don't blank
// every gap like the solo sets — each question hides a single gap and keeps the
// rest of the passage filled in for context. The hidden gap rotates, so the same
// passage can recur with a different word missing.
function buildOgeLiveQuestions(typeKeys, count) {
  const VOCAB = window.VOCAB || [];
  const wanted = new Set(typeKeys);
  const passages = VOCAB.filter((v) => Array.isArray(v.gaps) && v.gaps.length && typeof v.text === "string" && (wanted.size === 0 || wanted.has(v.type)));
  // One combo per (passage, gap). Group by passage so we can round-robin across
  // passages — that keeps the SAME passage from appearing twice in a row.
  const byPassage = new Map();
  for (const p of passages) {
    const bucket = [];
    for (let gi = 0; gi < p.gaps.length; gi++) if (p.gaps[gi] && p.gaps[gi].answer) bucket.push({ p, gi });
    if (bucket.length) byPassage.set(p.id, lvShuffle(bucket));
  }
  const buckets = lvShuffle([...byPassage.values()]);
  const spread = [];
  let added = true;
  while (added) {
    added = false;
    for (const b of buckets) { if (b.length) { spread.push(b.shift()); added = true; } }
  }
  const out = [];
  for (const { p, gi } of spread) {
    if (out.length >= count) break;
    const g = p.gaps[gi];
    // Whole passage for context; current gap is the blank, every other gap is
    // filled with its REAL key answer (never an invented fill). The board/phone
    // show it at a small font + auto-scroll to the highlighted blank.
    const prompt = String(p.text).replace(/⟦(\d+)⟧/g, (m, n) => {
      const idx = parseInt(n, 10) - 1;
      if (idx === gi) return "___";
      const og = p.gaps[idx];
      return og && og.answer ? og.answer : "…";
    });
    out.push({ family: p.type === "word-formation" ? "Word Formation" : "Grammar", mode: "typed", base: g.base || null, prompt, answer: g.answer, alts: g.alts || [], passage: true });
  }
  return out;
}
function lvBlankWindowUNUSED(text, blank) {
  const parts = String(text).match(/[^.!?]+[.!?]*\s*/g) || [text];
  const bi = parts.findIndex((s) => s.indexOf(blank) >= 0);
  if (bi < 0) return text;
  const lo = Math.max(0, bi - 1), hi = Math.min(parts.length, bi + 2);
  const win = parts.slice(lo, hi).join("").trim();
  return (lo > 0 ? "…" : "") + win + (hi < parts.length ? "…" : "");
}
function countOgeAvailable(typeKeys) {
  const VOCAB = window.VOCAB || [];
  const wanted = new Set(typeKeys);
  let n = 0;
  for (const v of VOCAB) if (Array.isArray(v.gaps) && (wanted.size === 0 || wanted.has(v.type))) for (const g of v.gaps) if (g && g.answer) n++;
  return n;
}

// The one client. role "host" | "player". Connects once on mount, maps every
// server message into the `game` shape the existing views expect.
function useLiveClient({ role, pin, name }) {
  const [status, setStatus] = lgUseState("connecting"); // connecting | open | error | ended
  const [error, setError] = lgUseState(null);           // { reason }
  const [phase, setPhase] = lgUseState("lobby");        // lobby | question | reveal | final
  const [serverPin, setServerPin] = lgUseState(pin || "");
  const [players, setPlayers] = lgUseState([]);         // host roster [{id,name}]
  const [questionCount, setQuestionCount] = lgUseState(0);
  const [timePerQ, setTimePerQ] = lgUseState(20);
  const [qIndex, setQIndex] = lgUseState(-1);
  const [question, setQuestion] = lgUseState(null);     // {family,mode,base,prompt,options,correct,answer}
  const [remaining, setRemaining] = lgUseState(20);
  const [answeredCount, setAnsweredCount] = lgUseState(0);
  const [distribution, setDistribution] = lgUseState([]);
  const [leaderboard, setLeaderboard] = lgUseState([]);
  const [review, setReview] = lgUseState({ perQuestion: [], perStudent: [] });
  // player-local state
  const [youId, setYouId] = lgUseState(null);
  const [youName, setYouName] = lgUseState(name || "You");
  const [youChoice, setYouChoice] = lgUseState(null);
  const [youText, setYouText] = lgUseState("");
  const [youLocked, setYouLocked] = lgUseState(false);
  const [youGain, setYouGain] = lgUseState(0);
  const [youSpeedBonus, setYouSpeedBonus] = lgUseState(0);
  const [youScore, setYouScore] = lgUseState(0);
  const [youCorrect, setYouCorrect] = lgUseState(0);
  const [lastAnswered, setLastAnswered] = lgUseState(false);
  const [lastCorrect, setLastCorrect] = lgUseState(false);

  const wsRef = lgUseRef(null);
  const tickRef = lgUseRef(null);

  const stopTick = () => { if (tickRef.current) { clearInterval(tickRef.current); tickRef.current = null; } };
  const startTick = (dur) => {
    stopTick();
    setRemaining(dur);
    const startAt = Date.now();
    tickRef.current = setInterval(() => {
      const left = Math.max(0, dur - (Date.now() - startAt) / 1000);
      setRemaining(left);
      if (left <= 0) stopTick();
    }, 200);
  };

  lgUseEffect(() => {
    let closedByUs = false;
    const ws = new WebSocket(lvWsUrl(role === "host" ? { role, pin } : { role, pin, name }));
    wsRef.current = ws;

    ws.onopen = () => { if (!closedByUs) setStatus((s) => (s === "error" ? s : "open")); };
    ws.onerror = () => {};
    ws.onclose = () => { if (!closedByUs) { stopTick(); setStatus((s) => (s === "error" ? s : "ended")); } };
    ws.onmessage = (ev) => {
      let m; try { m = JSON.parse(ev.data); } catch (e) { return; }
      switch (m.type) {
        case "welcome":
          setStatus("open");
          if (m.pin) setServerPin(m.pin);
          if (m.questionCount != null) setQuestionCount(m.questionCount);
          if (m.timePerQ != null) setTimePerQ(m.timePerQ);
          if (m.role === "host") setPlayers(m.players || []);
          if (m.role === "player") { setYouId(m.id); if (m.name) setYouName(m.name); }
          setPhase("lobby");
          break;
        case "roster":
          setPlayers(m.players || []);
          break;
        case "question":
          setPhase("question");
          setQIndex(m.index);
          setQuestion({ ...m.question, correct: null, answer: null });
          setAnsweredCount(0);
          setYouChoice(null); setYouText(""); setYouLocked(false);
          setYouGain(0); setYouSpeedBonus(0);
          setLastAnswered(false); setLastCorrect(false);
          if (m.duration != null) startTick(m.duration); else { stopTick(); setRemaining(null); }
          if (role === "player" && window.haptic) window.haptic("light");
          break;
        case "answer_count":
          setAnsweredCount(m.answered || 0);
          break;
        case "answer_ack":
          setYouLocked(true);
          break;
        case "reveal":
          stopTick(); setRemaining(0);
          setPhase("reveal");
          setLeaderboard(m.leaderboard || []);
          setQuestion((q) => (q ? { ...q, correct: m.correctChoice, answer: m.answerLabel } : q));
          if (role === "host") setDistribution(m.distribution || []);
          if (role === "player") {
            setLastAnswered(!!m.answered); setLastCorrect(!!m.correct);
            setYouGain(m.gain || 0); setYouSpeedBonus(m.speedBonus || 0);
            if (m.score != null) setYouScore(m.score);
          }
          break;
        case "final":
          stopTick();
          setPhase("final");
          setLeaderboard(m.leaderboard || []);
          if (role === "host") setReview(m.review || { perQuestion: [], perStudent: [] });
          if (role === "player") {
            if (m.score != null) setYouScore(m.score);
            if (m.correctN != null) setYouCorrect(m.correctN);
          }
          break;
        case "ended":
          setStatus("ended");
          break;
        case "error":
          setError({ reason: m.reason });
          setStatus("error");
          break;
      }
    };

    // Keep-alive: a periodic ping so the WebSocket (and the game) survives long
    // idle stretches — a wide-open lobby, or a "no limit" question where nobody
    // answers for minutes. Without it an idle connection can drop and end the game.
    const pingIv = setInterval(() => { try { if (ws.readyState === 1) ws.send(JSON.stringify({ type: "ping" })); } catch (e) {} }, 25000);

    return () => { closedByUs = true; clearInterval(pingIv); stopTick(); try { ws.close(); } catch (e) {} };
  }, []); // connect exactly once

  const sendMsg = (obj) => { const ws = wsRef.current; if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); };
  const start = () => sendMsg({ type: "start" });
  const next = () => sendMsg({ type: "next" });
  const skip = () => sendMsg({ type: "skip" });
  const advance = () => { if (phase === "lobby") start(); else if (phase === "reveal") next(); };
  const playerAnswer = (i) => {
    if (phase !== "question" || youLocked) return;
    setYouChoice(i);
    if (window.haptic) window.haptic("light");
    sendMsg({ type: "answer", choice: i });
  };
  const playerSubmitText = (text) => {
    if (phase !== "question" || youLocked || !text.trim()) return;
    setYouText(text);
    sendMsg({ type: "answer", text });
  };

  // flag the player's own leaderboard row
  const lb = leaderboard.map((r) => ({ ...r, you: r.id === youId }));

  return {
    status, error, phase, qIndex, question, remaining, timePerQ, questionCount,
    pin: serverPin, joined: players, answeredCount, distribution,
    leaderboard: lb, review,
    youJoined: !!youId, youChoice, youText, youLocked, youName,
    youGain, youSpeedBonus, youScore, youCorrect, lastAnswered, lastCorrect,
    edge: null,
    advance, start, next, skip, playerAnswer, playerSubmitText,
  };
}

// force dark for the live screens; restore the user's theme on exit
function useForceDark() {
  lgUseEffect(() => {
    const root = document.documentElement;
    const prev = root.dataset.theme;
    root.dataset.theme = "dark";
    return () => { root.dataset.theme = prev; };
  }, []);
}
function useTooSmall(maxW = 760) {
  const [small, setSmall] = lgUseState(typeof window !== "undefined" && window.innerWidth < maxW);
  lgUseEffect(() => {
    const on = () => setSmall(window.innerWidth < maxW);
    window.addEventListener("resize", on);
    return () => window.removeEventListener("resize", on);
  }, []);
  return small;
}

// ---- small shared status panels ----
function LiveConnecting({ label, pin }) {
  return (
    <div className="lv-p-pad"><div className="lv-p-center">
      <div className="lv-spinner" />
      <div style={{ fontSize: 18, fontWeight: 700 }}>{label || "Connecting…"}</div>
      {pin ? <div className="m-tasktag" style={{ fontSize: 14 }}>PIN {pin}</div> : null}
    </div></div>
  );
}
function LiveError({ message, onExit, onRetry, retryLabel }) {
  return (
    <div className="lv-p-pad"><div className="lv-p-center">
      <div className="lv-verdict no">!</div>
      <div style={{ fontSize: 22, fontWeight: 800 }}>Can't join</div>
      <div style={{ fontSize: 15, color: "var(--ink-3)", textAlign: "center", maxWidth: 320 }}>{message}</div>
      <div style={{ display: "flex", gap: 10, marginTop: 12 }}>
        {onRetry ? <button className="cta-secondary" onClick={onRetry}>{retryLabel || "Try again"}</button> : null}
        <button className="cta-primary" onClick={onExit}><span>Back</span></button>
      </div>
    </div></div>
  );
}
function LivePlayerEnded({ onExit }) {
  return (
    <div className="lv-p-pad"><div className="lv-p-center">
      <div className="lv-verdict ok" style={{ background: "var(--bg-3)", color: "var(--ink)" }}>✓</div>
      <div style={{ fontSize: 24, fontWeight: 800 }}>Game over</div>
      <div style={{ fontSize: 15, color: "var(--ink-3)" }}>The host ended the game — thanks for playing!</div>
      <button className="m-cta" style={{ marginTop: 12 }} onClick={onExit}><span>Back to practice</span><span className="arrow">→</span></button>
    </div></div>
  );
}

// ---- desktop chooser: host or join ----
function LiveChooser({ onHost, onJoin }) {
  const tap = (e) => { if (window.tapFeedback) window.tapFeedback("medium", e); };
  return (
    <LiveFunStage>
      <div className="lf-wrap is-landing lf-screen">
        <div className="lf-landing">
          <div className="lf-landing-head">
            <div className="lf-rise" style={{ "--i": 0 }}><LiveBrand /></div>
            <h1 className="lf-title lf-rise" style={{ "--i": 1 }}>Live <span className="em">quiz</span></h1>
            <p className="lf-lead lf-rise" style={{ "--i": 2 }}>Run a game on the board for the whole room, or join one from this device.</p>
          </div>
          <div className="lf-cards">
            <button className="lf-card lf-rise" style={{ "--i": 3 }} onClick={(e) => { tap(e); onHost(); }}>
              <div className="lf-card-glyph"><div className="lf-glyph-grid"><LfShape i={0} size={26} /><LfShape i={1} size={26} /><LfShape i={2} size={26} /><LfShape i={3} size={26} /></div></div>
              <h2 className="lf-card-title">Host a game</h2>
              <p className="lf-card-sub">Show the questions on the projector. Players join from their phones with a PIN.</p>
              <span className="lf-card-go">Host a game <span className="arr"><LfArrow size={18} /></span></span>
            </button>
            <button className="lf-card lf-rise" style={{ "--i": 4 }} onClick={(e) => { tap(e); onJoin(); }}>
              <div className="lf-card-glyph"><span className="lf-glyph-solo" /></div>
              <h2 className="lf-card-title">Join a game</h2>
              <p className="lf-card-sub">Enter the PIN your teacher put on the board and play along.</p>
              <span className="lf-card-go">Join a game <span className="arr"><LfArrow size={18} /></span></span>
            </button>
          </div>
        </div>
      </div>
    </LiveFunStage>
  );
}

// ---- host: configure the game ----
const LV_TYPES = [
  { key: "grammar", label: "Grammar" },
  { key: "word-formation", label: "Word Formation" },
  { key: "multiple-choice", label: "Multiple Choice" },
];
// A teacher's custom tasks → Live question shapes (single sentences, not passages).
function buildTaskLiveQuestions(tasks, doShuffle) {
  const list = tasks.slice();
  if (doShuffle) {
    for (let i = list.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const tmp = list[i]; list[i] = list[j]; list[j] = tmp; }
  }
  return list.map((t) => {
    if (t.type === "multiple-choice") {
      const opts = (t.choices || []).slice(0, 4);
      return { family: "Multiple Choice", mode: "mc", prompt: t.sentence, options: opts, correct: Math.max(0, opts.indexOf(t.answer)) };
    }
    return { family: lvFamilyLabel(t.type), mode: "typed", base: t.base || null, prompt: t.sentence, answer: t.answer, alts: t.alts || [] };
  });
}

const LV_TASK_LABEL = { "word-formation": "Word formation", grammar: "Grammar", "multiple-choice": "Multiple choice" };

function LiveHostSetup({ onCreate, onBack, tasks = [], canHostTasks = false }) {
  const [exam, setExam] = lgUseState("ege");
  const [sel, setSel] = lgUseState({ grammar: true, "word-formation": true, "multiple-choice": true });
  const [count, setCount] = lgUseState(10);
  const [time, setTime] = lgUseState(20);  // seconds; 0 = no limit
  const hasTasks = tasks.length > 0;
  // Teachers always see the source toggle (so it's discoverable), even with 0
  // tasks — the "My tasks" panel then shows an empty prompt.
  const showSource = canHostTasks || hasTasks;
  const [source, setSource] = lgUseState("bank");          // bank | tasks
  const [taskSel, setTaskSel] = lgUseState(() => new Set(tasks.map((t) => t.id)));
  const [shuffleTasks, setShuffleTasks] = lgUseState(true);
  const useTasks = showSource && source === "tasks";
  const oge = exam === "oge";
  const keys = LV_TYPES.map((t) => t.key).filter((k) => sel[k]);
  const ogeKeys = ["grammar", "word-formation"].filter((k) => sel[k]);   // OGE = passages only (grammar + WF)
  const selectedTasks = tasks.filter((t) => taskSel.has(t.id));
  const bankAvailable = lgUseMemo(() => oge ? countOgeAvailable(ogeKeys) : countAvailable(keys), [oge, keys.join(","), ogeKeys.join(",")]);
  const available = useTasks ? selectedTasks.length : bankAvailable;
  const realCount = useTasks ? selectedTasks.length : Math.min(count, available);
  const canStart = useTasks ? selectedTasks.length > 0 : (oge ? (ogeKeys.length > 0 && available > 0) : (keys.length > 0 && available > 0));
  const buildQuestions = () => useTasks ? buildTaskLiveQuestions(selectedTasks, shuffleTasks) : (oge ? buildOgeLiveQuestions(ogeKeys, count) : buildLiveQuestions(keys, count));
  const toggleTask = (id) => setTaskSel((s) => { const n = new Set(s); if (n.has(id)) n.delete(id); else n.add(id); return n; });

  return (
    <LiveFunStage>
      <div className="lf-wrap is-setup lf-screen">
        <div className="lf-setup-head">
          {onBack ? (
            <button className="lf-back lf-rise" style={{ "--i": 0 }} onClick={onBack}>
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"><path d="M14 6l-6 6 6 6" /></svg>
              Back
            </button>
          ) : null}
          <div className="lf-rise" style={{ "--i": 1 }}><LiveBrand /></div>
          <h1 className="lf-setup-title lf-rise" style={{ "--i": 2 }}>Set up your game</h1>
        </div>

        {showSource ? (
          <div className="lf-group lf-rise" style={{ "--i": 3 }}>
            <div className="lf-group-label">Question source</div>
            <div className="lf-row">
              <button className={`lf-pill ${!useTasks ? "is-on" : ""}`} onClick={() => setSource("bank")}>From the bank</button>
              <button className={`lf-pill ${useTasks ? "is-on" : ""}`} onClick={() => setSource("tasks")}>My tasks</button>
            </div>
          </div>
        ) : null}

        {!useTasks ? (
          <div className="lf-group lf-rise" style={{ "--i": 4 }}>
            <div className="lf-group-label">Exam</div>
            <div className="lf-row">
              <button className={`lf-pill ${!oge ? "is-on" : ""}`} onClick={() => setExam("ege")}>ЕГЭ</button>
              <button className={`lf-pill ${oge ? "is-on" : ""}`} onClick={() => setExam("oge")}>ОГЭ</button>
            </div>
          </div>
        ) : null}

        {useTasks ? (
          <div className="lf-group lf-rise" style={{ "--i": 5 }}>
            <div className="lf-group-label">Pick tasks</div>
            {hasTasks ? (
              <>
                <div className="lv-tasklist">
                  {tasks.map((t) => {
                    const on = taskSel.has(t.id);
                    return (
                      <button key={t.id} className={`lv-task-row ${on ? "is-on" : ""}`} onClick={() => toggleTask(t.id)}>
                        <span className="lv-task-check" aria-hidden="true">{on ? "✓" : ""}</span>
                        <span className="lv-task-sentence">{(t.sentence || "").replace(/_{2,}|___/g, "______")}</span>
                        <span className="lv-task-fam" data-task={t.type}>{LV_TASK_LABEL[t.type] || t.type}</span>
                      </button>
                    );
                  })}
                </div>
                <div className="lv-task-foot">
                  <span className="lv-task-count"><b>{selectedTasks.length}</b> of {tasks.length} selected</span>
                  <span className="lv-task-actions">
                    <button className="lv-link" onClick={() => setTaskSel(new Set(tasks.map((t) => t.id)))}>Select all</button>
                    <button className="lv-link" onClick={() => setTaskSel(new Set())}>Clear</button>
                  </span>
                </div>
              </>
            ) : (
              <div className="lf-avail" style={{ display: "block", lineHeight: 1.5 }}>You haven't written any tasks yet. Add some on the <b style={{ color: "var(--ink-2)" }}>My&nbsp;Tasks</b> page, then they'll show up here to play live.</div>
            )}
          </div>
        ) : oge ? (
          <div className="lf-group lf-rise" style={{ "--i": 5 }}>
            <div className="lf-group-label">Task types</div>
            <div className="lf-row">
              <button className={`lf-pill ${sel["grammar"] ? "is-on" : ""}`} onClick={() => setSel((s) => ({ ...s, grammar: !s.grammar }))}><LfCheck />Grammar</button>
              <button className={`lf-pill ${sel["word-formation"] ? "is-on" : ""}`} onClick={() => setSel((s) => ({ ...s, "word-formation": !s["word-formation"] }))}><LfCheck />Word Formation</button>
            </div>
            <div className="lf-avail" style={{ display: "block", lineHeight: 1.5 }}>Passage fill — one word is missing each round; the rest stays filled in (with the real answers) for context. The missing word rotates every question. <b>{available}</b> blank{available === 1 ? "" : "s"} available.</div>
          </div>
        ) : (
          <div className="lf-group lf-rise" style={{ "--i": 5 }}>
            <div className="lf-group-label">Task types</div>
            <div className="lf-row">
              {LV_TYPES.map((t) => (
                <button key={t.key} className={`lf-pill ${sel[t.key] ? "is-on" : ""}`} aria-pressed={sel[t.key]}
                  onClick={() => setSel((s) => ({ ...s, [t.key]: !s[t.key] }))}><LfCheck />{t.label}</button>
              ))}
            </div>
            <div className="lf-avail"><LiveCountUp value={available} /> questions available <span className="dot" /> shuffled fresh every game</div>
          </div>
        )}

        {useTasks ? (
          <div className="lf-group lf-rise" style={{ "--i": 6, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16 }}>
            <div>
              <div className="lf-group-label" style={{ margin: 0 }}>Shuffle order</div>
              <div className="lf-avail" style={{ marginTop: 4 }}>Randomize which task comes next</div>
            </div>
            <button role="switch" aria-checked={shuffleTasks} aria-label="Shuffle order" onClick={() => setShuffleTasks((v) => !v)}
              style={{ width: 52, height: 30, borderRadius: 999, border: "none", cursor: "pointer", padding: 3, background: shuffleTasks ? "var(--accent)" : "rgba(128,128,128,.35)", display: "inline-flex", justifyContent: shuffleTasks ? "flex-end" : "flex-start", transition: "background .15s", flexShrink: 0 }}>
              <span style={{ width: 24, height: 24, borderRadius: "50%", background: "#fff", display: "block" }} />
            </button>
          </div>
        ) : (
          <div className="lf-group lf-rise" style={{ "--i": 6 }}>
            <div className="lf-group-label">Questions</div>
            <div className="lf-row">
              {[10, 20, 30].map((n) => (
                <button key={n} className={`lf-circle ${count === n ? "is-on" : ""}`} onClick={() => setCount(n)}>{n}</button>
              ))}
            </div>
          </div>
        )}

        <div className="lf-group lf-rise" style={{ "--i": 7 }}>
          <div className="lf-group-label">Time per question</div>
          <div className="lf-row">
            {[[0, "No limit"], [10, "10s"], [20, "20s"], [30, "30s"], [60, "60s"]].map(([v, l]) => (
              <button key={v} className={`lf-pill ${time === v ? "is-on" : ""}`} onClick={() => setTime(v)}>{l}</button>
            ))}
          </div>
        </div>

        <div className="lf-create-bar lf-rise" style={{ "--i": 8 }}>
          <button className="lf-create" disabled={!canStart}
            onClick={(e) => { if (window.tapFeedback) window.tapFeedback("medium", e); onCreate({ questions: buildQuestions(), timePerQ: time === 0 ? null : time }); }}>
            <span className="lf-create-count">Create game{realCount ? <> · <span className="num" key={realCount}>{realCount}</span> questions</> : ""}</span>
            <span className="lf-create-arr"><LfArrow size={20} /></span>
          </button>
        </div>
      </div>
    </LiveFunStage>
  );
}

// ---- host: configure → allocate a PIN → open the room ----
function LiveHost({ onExit, small, backToChooser, tasks, canHostTasks }) {
  const [config, setConfig] = lgUseState(null);
  const [pin, setPin] = lgUseState(null);
  const [failed, setFailed] = lgUseState(false);
  lgUseEffect(() => {
    if (!config) return;
    let alive = true;
    fetch("/api/live/create", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(config) })
      .then((r) => r.json())
      .then((d) => { if (!alive) return; if (d && d.pin) setPin(d.pin); else setFailed(true); })
      .catch(() => { if (alive) setFailed(true); });
    return () => { alive = false; };
  }, [config]);

  if (!config) return <LiveHostSetup onBack={backToChooser} onCreate={setConfig} tasks={tasks} canHostTasks={canHostTasks} />;
  if (failed) return <LiveError message="Couldn't start a game right now. Check your connection and try again." onExit={onExit} />;
  if (!pin) return <LiveConnecting label="Setting up your game…" />;
  return <LiveHostConnected pin={pin} small={small} onExit={onExit} />;
}
function LiveHostConnected({ pin, small, onExit }) {
  const client = useLiveClient({ role: "host", pin });
  if (client.status === "error") return <LiveError message="This game couldn't start. Please try again." onExit={onExit} />;
  if (client.status === "ended") return <LiveError message="The game closed. Start a new one when you're ready." onExit={onExit} />;
  if (client.status === "connecting") return <LiveConnecting label="Opening the room…" pin={pin} />;
  const game = { ...client, reset: onExit };
  return <HostView game={game} tooSmall={small} />;
}

// ---- join: empty PIN + nickname form, then play ----
function LiveJoin({ onExit, backToChooser, initialPin }) {
  const [pin, setPin] = lgUseState(initialPin || "");
  const [nick, setNick] = lgUseState("");
  const [submitted, setSubmitted] = lgUseState(null);
  const ready = /^\d{6}$/.test(pin.trim()) && nick.trim().length > 0;
  const [readyPulse, setReadyPulse] = lgUseState(false);
  const wasReady = lgUseRef(false);
  lgUseEffect(() => {
    if (ready && !wasReady.current) { setReadyPulse(true); setTimeout(() => setReadyPulse(false), 380); if (window.haptic) window.haptic("light"); }
    wasReady.current = ready;
  }, [ready]);
  if (submitted) {
    return <LivePlayerConnected pin={submitted.pin} nick={submitted.nick} onExit={onExit} onRejoin={() => setSubmitted(null)} />;
  }
  const closeJoin = backToChooser || onExit;
  const submit = (e) => { if (!ready) return; if (window.haptic) window.haptic("success"); else if (window.tapFeedback) window.tapFeedback("medium", e); setSubmitted({ pin: pin.trim(), nick: nick.trim() }); };
  return (
    <LiveFunStage>
      <div className="lf-wrap is-join lf-screen">
        <div className="lf-join">
          <button className="lf-join-close" onClick={closeJoin} aria-label="Close">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
          </button>
          <div className="lf-join-head">
            <div className="lf-join-brand lf-rise" style={{ "--i": 0 }}><LiveBrand /></div>
            <p className="lf-join-lead lf-rise" style={{ "--i": 1 }}>Enter the PIN your teacher put on the board.</p>
          </div>
          <div className={`lf-field lf-rise ${pin ? "is-filled" : ""}`} style={{ "--i": 2 }}>
            <input className="lf-input pin" value={pin} inputMode="numeric" maxLength={6} placeholder="Game PIN" aria-label="Game PIN"
              onChange={(e) => setPin(e.target.value.replace(/\D/g, "").slice(0, 6))} />
          </div>
          <div className={`lf-field lf-rise ${nick ? "is-filled" : ""}`} style={{ "--i": 3 }}>
            <input className="lf-input nick" value={nick} maxLength={14} placeholder="Nickname" aria-label="Nickname" autoComplete="off"
              onChange={(e) => setNick(e.target.value.slice(0, 14))} />
          </div>
          <button className={`lf-join-btn lf-rise ${readyPulse ? "is-ready" : ""}`} style={{ "--i": 4 }} disabled={!ready} onClick={submit}>
            <span>Join game</span>
            <span className="lf-join-arr"><LfArrow size={20} /></span>
          </button>
          <div className="lf-join-hint lf-rise" style={{ "--i": 5 }}>No app needed — you play right here.</div>
        </div>
      </div>
    </LiveFunStage>
  );
}
function LivePlayerConnected({ pin, nick, onExit, onRejoin }) {
  const client = useLiveClient({ role: "player", pin, name: nick });
  if (client.status === "error") {
    const reason = client.error && client.error.reason;
    const message = reason === "already_started" ? "That game has already started — ask your teacher to start a new one."
      : reason === "full" ? "That game is full."
      : "No game found with that PIN. Double-check the number on the board.";
    return <LiveError message={message} onExit={onExit} onRetry={onRejoin} retryLabel="Try another PIN" />;
  }
  if (client.status === "ended") return <LivePlayerEnded onExit={onExit} />;
  if (client.status === "connecting") return <LiveConnecting label="Joining…" />;
  const game = { ...client, reset: onExit };
  return <PlayerView game={game} wide={false} />;
}

// ---- the single entry, mounted by the "Live" top-bar tab ----
function LiveScreen({ onExit, initialPin, tasks, canHostTasks }) {
  useForceDark();
  const small = useTooSmall(760);
  // Arrived via a QR / ?live= deep-link, or on a phone → straight to Join (no host).
  const [mode, setMode] = lgUseState(initialPin ? "join" : (small ? "join" : null));
  // Opening Live is a moment — a soft haptic on entry (mobile).
  lgUseEffect(() => { if (window.haptic) window.haptic("light"); }, []);
  return (
    <div className={"lv-screen" + (mode === "join" ? " lv-screen-player" : "")}>
      {/* The Join screen carries its own close (lf-join-close); elsewhere show the global ✕. */}
      {mode !== "join" && <button className="lv-exit" onClick={onExit} aria-label="Close live quiz">✕</button>}
      {mode === null && <LiveChooser onHost={() => setMode("host")} onJoin={() => setMode("join")} />}
      {mode === "host" && <LiveHost onExit={onExit} small={small} backToChooser={() => setMode(null)} tasks={tasks} canHostTasks={canHostTasks} />}
      {mode === "join" && <LiveJoin onExit={onExit} initialPin={initialPin} backToChooser={(initialPin || small) ? null : () => setMode(null)} />}
    </div>
  );
}

Object.assign(window, { LiveScreen, useLiveClient, useForceDark, useTooSmall });
