/* ============================================================
   WHO'S FIRST?  —  finger-picker for the lads
   Everyone present presses one square with one finger. When all
   active players are down at once, it randomizes and reveals who
   goes first. Local only (no backend); same four seeded players.
   ============================================================ */

const { useState, useEffect, useRef, useCallback, useMemo } = React;

const PLAYERS = [
  { id: "abe",       name: "Abe",       color: "#e7b53c" },
  { id: "christian", name: "Christian", color: "#3fa46a" },
  { id: "toby",      name: "Toby",      color: "#3f86db" },
  { id: "jeremy",    name: "Jeremy",    color: "#d34b4b" },
];

const ACTIVE_KEY = "wf_active";

/* ---- unbiased randomness: CSPRNG + rejection sampling (no modulo bias) ---- */
function secureRandomInt(n) {
  if (n <= 1) return 0;
  const limit = Math.floor(0x100000000 / n) * n; // largest multiple of n that fits in 2^32
  const buf = new Uint32Array(1);
  let x;
  do { crypto.getRandomValues(buf); x = buf[0]; } while (x >= limit);
  return x % n;
}

/* ---- long-run standings from the shared results log ---- */
function computeStandings(results) {
  const stat = {};
  PLAYERS.forEach((p) => { stat[p.id] = { wins: 0, games: 0 }; });
  results.forEach((r) => {
    (r.participants || []).forEach((pid) => { if (stat[pid]) stat[pid].games++; });
    if (stat[r.winner_id]) stat[r.winner_id].wins++;
  });
  return PLAYERS
    .map((p) => ({ ...p, wins: stat[p.id].wins, games: stat[p.id].games, rate: stat[p.id].games ? stat[p.id].wins / stat[p.id].games : 0 }))
    .sort((a, b) => b.wins - a.wins || b.rate - a.rate);
}

function loadActive() {
  try {
    const raw = JSON.parse(localStorage.getItem(ACTIVE_KEY));
    if (Array.isArray(raw) && raw.length) return raw.filter((id) => PLAYERS.some((p) => p.id === id));
  } catch (e) { /* ignore */ }
  return PLAYERS.map((p) => p.id);
}

/* ---- tiny Web Audio blips (delight; safe to fail) ---- */
let _ac = null;
function _ctx() {
  try { _ac = _ac || new (window.AudioContext || window.webkitAudioContext)(); return _ac; } catch (e) { return null; }
}
function blip(freq, dur = 0.05, gain = 0.04) {
  const ac = _ctx(); if (!ac) return;
  try {
    const o = ac.createOscillator(), g = ac.createGain();
    o.type = "triangle"; o.frequency.value = freq;
    g.gain.value = gain;
    o.connect(g); g.connect(ac.destination);
    o.start();
    g.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime + dur);
    o.stop(ac.currentTime + dur);
  } catch (e) { /* ignore */ }
}
function fanfare() { [523, 659, 784, 1047].forEach((f, i) => setTimeout(() => blip(f, 0.18, 0.05), i * 90)); }

/* ============================================================
   APP
   ============================================================ */
function WhosFirst() {
  const [activeIds, setActiveIds] = useState(loadActive);
  const [phase, setPhase] = useState("waiting"); // waiting | arming | spinning | result
  const [heldIds, setHeldIds] = useState([]);    // player ids currently pressed
  const [highlight, setHighlight] = useState(null);
  const [winner, setWinner] = useState(null);
  const [results, setResults] = useState(() => (window.WhosFirstDB ? window.WhosFirstDB.getState().results : []));
  const [showStandings, setShowStandings] = useState(false);

  useEffect(() => {
    if (!window.WhosFirstDB) return;
    return window.WhosFirstDB.subscribe((s) => setResults(s.results.slice()));
  }, []);
  const standings = useMemo(() => computeStandings(results), [results]);

  const heldRef = useRef(new Map());  // id -> Set(pointerId)
  const activeRef = useRef(activeIds);
  activeRef.current = activeIds;
  const phaseRef = useRef(phase);
  phaseRef.current = phase;
  const spinTimer = useRef(null);

  const activePlayers = PLAYERS.filter((p) => activeIds.includes(p.id));
  const isActive = (id) => activeIds.includes(id);

  /* ---- presence toggle ---- */
  const toggleActive = useCallback((id) => {
    if (phaseRef.current === "arming" || phaseRef.current === "spinning") return;
    setActiveIds((prev) => {
      const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id];
      const ordered = PLAYERS.map((p) => p.id).filter((x) => next.includes(x));
      localStorage.setItem(ACTIVE_KEY, JSON.stringify(ordered));
      // releasing a player who was holding clears their press
      heldRef.current.delete(id);
      return ordered;
    });
    setWinner(null);
    if (phaseRef.current === "result") setPhase("waiting");
  }, []);

  /* ---- press tracking ---- */
  const syncHeld = useCallback(() => {
    const ids = [];
    for (const [pid, set] of heldRef.current) if (set.size > 0) ids.push(pid);
    setHeldIds(ids);
  }, []);

  const press = useCallback((id, pointerId, down) => {
    const m = heldRef.current;
    if (!m.has(id)) m.set(id, new Set());
    const s = m.get(id);
    if (down) s.add(pointerId); else s.delete(pointerId);
    syncHeld();
  }, [syncHeld]);

  const onPadDown = (id) => (e) => {
    if (phaseRef.current === "spinning" || phaseRef.current === "result") return;
    if (!isActive(id)) return;
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { /* ignore */ }
    blip(180 + Math.random() * 60, 0.04, 0.03);
    press(id, e.pointerId, true);
  };
  const onPadUp = (id) => (e) => press(id, e.pointerId, false);

  /* ---- trigger: all active down (>=2) -> arming ---- */
  useEffect(() => {
    if (phase !== "waiting" && phase !== "arming") return;
    const active = activeIds;
    const allDown = active.length >= 2 && active.every((id) => heldIds.includes(id));
    if (allDown && phase === "waiting") setPhase("arming");
    else if (!allDown && phase === "arming") setPhase("waiting");
  }, [heldIds, phase, activeIds]);

  /* ---- arming: brief hold, then spin (cancels if anyone lifts) ---- */
  useEffect(() => {
    if (phase !== "arming") return;
    const t = setTimeout(() => setPhase("spinning"), 750);
    return () => clearTimeout(t);
  }, [phase]);

  /* ---- spinning: roulette that lands on a random active player ---- */
  useEffect(() => {
    if (phase !== "spinning") return;
    const order = activeRef.current.slice();
    const n = order.length;
    const winnerIdx = secureRandomInt(n); // provably uniform pick
    const totalSteps = n * 3 + winnerIdx; // a few loops, then land on the winner
    const base = 55, max = 330;
    let step = 0;
    const run = () => {
      if (step > totalSteps) {
        setWinner(order[winnerIdx]);
        setPhase("result");
        fanfare();
        if (window.WhosFirstDB) window.WhosFirstDB.record(order[winnerIdx], order);
        return;
      }
      setHighlight(order[step % n]);
      blip(360 + (step % n) * 40, 0.04, 0.035);
      step++;
      const progress = step / (totalSteps + 1);
      const delay = base + (max - base) * Math.pow(progress, 2.4);
      spinTimer.current = setTimeout(run, delay);
    };
    run();
    return () => clearTimeout(spinTimer.current);
  }, [phase]);

  /* ---- reset ---- */
  const goAgain = useCallback(() => {
    heldRef.current = new Map();
    setHeldIds([]);
    setHighlight(null);
    setWinner(null);
    setPhase("waiting");
  }, []);

  const winnerPlayer = PLAYERS.find((p) => p.id === winner);
  const heldCount = activeIds.filter((id) => heldIds.includes(id)).length;
  const activeCount = activeIds.length;
  const spinning = phase === "spinning";
  const setupLocked = phase === "arming" || phase === "spinning";

  return (
    <div className="app">
      <header className="topbar">
        <a className="topbar__home" href="../" aria-label="back to hub">‹</a>
        <div className="topbar__titles">
          <div className="topbar__kicker">there's always an *</div>
          <h1 className="topbar__title">Who's First?</h1>
        </div>
        <button className="standings-btn" onClick={() => setShowStandings(true)}>Standings</button>
      </header>

      {phase === "result" ? (
        <Result player={winnerPlayer} onAgain={goAgain} standings={standings} />
      ) : (
        <>
          <PresenceRow
            activeIds={activeIds}
            onToggle={toggleActive}
            locked={setupLocked}
          />

          <p className="instruction">
            {activeCount < 2
              ? "Tap at least two players in."
              : phase === "arming"
              ? "Hold it…"
              : spinning
              ? "Picking…"
              : heldCount === 0
              ? "Everyone press and hold your square."
              : `${heldCount} of ${activeCount} down — hold till everyone's in.`}
          </p>

          <div className={"pads pads--" + activeCount}>
            {activePlayers.map((p) => {
              const down = heldIds.includes(p.id);
              const lit = spinning && highlight === p.id;
              const cls = ["pad"];
              if (down) cls.push("is-down");
              if (lit) cls.push("is-lit");
              if (spinning) cls.push("is-spinning");
              return (
                <button
                  key={p.id}
                  className={cls.join(" ")}
                  style={{ "--c": p.color }}
                  onPointerDown={onPadDown(p.id)}
                  onPointerUp={onPadUp(p.id)}
                  onPointerCancel={onPadUp(p.id)}
                  onContextMenu={(e) => e.preventDefault()}
                >
                  <span className="pad__name">{p.name}</span>
                  <span className="pad__hint">{down ? "✓ in" : "hold"}</span>
                </button>
              );
            })}
          </div>

          {activeCount >= 2 && (
            <div className="holdmeter" aria-hidden="true">
              {activeIds.map((id) => {
                const p = PLAYERS.find((x) => x.id === id);
                return (
                  <span
                    key={id}
                    className={"holddot" + (heldIds.includes(id) ? " on" : "")}
                    style={{ "--c": p.color }}
                  />
                );
              })}
            </div>
          )}
        </>
      )}

      {showStandings && (
        <StandingsSheet standings={standings} onClose={() => setShowStandings(false)} />
      )}
    </div>
  );
}

/* ============================================================
   PRESENCE ROW — who's here
   ============================================================ */
function PresenceRow({ activeIds, onToggle, locked }) {
  return (
    <div className="presence">
      <span className="presence__label">who's here?</span>
      <div className="presence__chips">
        {PLAYERS.map((p) => {
          const on = activeIds.includes(p.id);
          return (
            <button
              key={p.id}
              className={"pchip" + (on ? " on" : " off")}
              style={{ "--c": p.color }}
              onClick={() => onToggle(p.id)}
              disabled={locked}
              aria-pressed={on}
            >
              <span className="pchip__dot" />
              <span className="pchip__name">{p.name}</span>
            </button>
          );
        })}
      </div>
    </div>
  );
}

/* ============================================================
   RESULT
   ============================================================ */
function Result({ player, onAgain, standings }) {
  if (!player) return null;
  return (
    <div className="result" style={{ "--c": player.color }}>
      <div className="result__eyebrow">✱ first up</div>
      <div className="result__name">{player.name}</div>
      <div className="result__sub">goes first.</div>
      <button className="result__again" onClick={onAgain}>Go again</button>
      <Standings standings={standings} highlightId={player.id} compact />
    </div>
  );
}

/* ============================================================
   STANDINGS — who's had the advantage over the long run
   ============================================================ */
function Standings({ standings, highlightId, compact }) {
  const totalGames = standings.reduce((n, p) => Math.max(n, p.games), 0);
  const played = standings.some((p) => p.games > 0);
  return (
    <div className={"standings" + (compact ? " standings--compact" : "")}>
      <div className="standings__head">
        <span className="standings__title">Standings</span>
        <span className="standings__total">{played ? `${totalGames} game${totalGames === 1 ? "" : "s"}` : "no games yet"}</span>
      </div>
      {!played ? (
        <p className="standings__empty">Play a round to start the tally.</p>
      ) : (
        <ul className="standings__list">
          {standings.map((p) => (
            <li key={p.id} className={"srow" + (p.id === highlightId ? " is-win" : "")} style={{ "--c": p.color }}>
              <span className="srow__dot" />
              <span className="srow__name">{p.name}</span>
              <span className="srow__rate">{p.games ? Math.round(p.rate * 100) + "%" : "—"}</span>
              <span className="srow__wins">{p.wins}<span className="srow__of"> / {p.games}</span></span>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

function StandingsSheet({ standings, onClose }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);
  return (
    <div className="sheet-backdrop" onClick={onClose}>
      <div className="sheet" role="dialog" aria-modal="true" aria-label="Standings" onClick={(e) => e.stopPropagation()}>
        <Standings standings={standings} />
        <button className="sheet__close" onClick={onClose}>Done</button>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<WhosFirst />);
