/* ===================================================================
   lobby-presence.jsx — lobby-coordination UI on top of window.Presence

   Three pieces, all server-backed (Cloudflare Worker /presence/*), all
   fail-soft (a dropped poll never throws into the tree):

     • useLobbyStats(pollMs)  — live "{n} DMs running games" counter for
       the startup page.
     • WaitingRoom            — where a player with no room code lands;
       polls for a DM invite and lets them accept/decline/leave.
     • DmWaitingPanel         — a DM's roster of waiting players with an
       Invite button each.

   This is presentation + polling only. All authority (who may list the
   waiting pool, who may invite, single-use bearer tokens) lives in the
   Worker; see lib/presence.ts. Exposed via window globals because the
   no-build Babel setup isolates each script's scope.
   =================================================================== */
const { useState, useEffect, useRef, useCallback } = React;

const PRESENCE = (typeof window !== 'undefined' && window.Presence) || null;

/* Focusable selector for the modal focus trap. */
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';

/* Poll the public lobby counts. Returns { dmCount, waitingCount, ready }.
   `ready` flips true after the first successful (or attempted) fetch so the
   UI can avoid a flash of "0". Always safe to call (rules of hooks): when the
   presence backend is absent it simply stays at zeros. */
function useLobbyStats(pollMs = 5000) {
  const [stats, setStats] = useState({ dmCount: 0, waitingCount: 0, ready: false });
  useEffect(() => {
    if (!PRESENCE) return;
    let live = true;
    const tick = async () => {
      const s = await PRESENCE.stats();
      if (live) setStats({ dmCount: s.dmCount, waitingCount: s.waitingCount, ready: true });
    };
    tick();
    const id = setInterval(tick, pollMs);
    return () => { live = false; clearInterval(id); };
  }, [pollMs]);
  return stats;
}

/* The no-code waiting room. Joins the pool on mount, polls every `pollMs` for
   an invite, and surfaces accept / decline / leave. Modal: focus is trapped in
   the card, Escape leaves, and focus is returned to the page on close. */
function WaitingRoom({ name, onJoin, onCancel, pollMs = 4000 }) {
  const [phase, setPhase] = useState('joining');   // joining | waiting | invited | error
  const [invite, setInvite] = useState(null);      // { roomCode, dmName }
  const [error, setError] = useState('');
  const dismissed = useRef(new Set());             // invite keys the player said "not now" to
  const acceptRef = useRef(null);                  // focus target when an invite arrives
  const cardRef = useRef(null);                    // dialog container (initial focus + trap)
  const returnFocusRef = useRef(null);             // element to restore focus to on close

  const keyOf = (inv) => inv && `${inv.roomCode}:${inv.dmName}`;

  // Remember who opened the dialog, and on unmount return focus to the page so
  // keyboard/SR users aren't stranded on <body>. If the opener was torn down
  // (e.g. the lobby was replaced after accept), fall back to the app heading.
  useEffect(() => {
    returnFocusRef.current = document.activeElement;
    if (cardRef.current) cardRef.current.focus();
    return () => {
      const prev = returnFocusRef.current;
      window.requestAnimationFrame(() => {
        const target = (prev && prev.isConnected) ? prev : document.querySelector('h1, [role="main"], main');
        if (target && typeof target.focus === 'function') target.focus();
      });
    };
  }, []);

  // Join once on mount; leave the pool on unmount. Guard against the unmount
  // racing an in-flight join: if we lose the race, issue a compensating leave
  // once the join resolves so we never leak a KV seat.
  useEffect(() => {
    if (!PRESENCE) { setPhase('error'); setError('Lobby is offline right now.'); return; }
    let live = true;
    PRESENCE.waitJoin(name || 'Guest').then((res) => {
      if (!live) { PRESENCE.waitLeave(); return; }   // unmounted before join resolved
      if (res.ok) setPhase('waiting');
      else { setPhase('error'); setError('Could not enter the waiting room.'); }
    });
    return () => { live = false; PRESENCE.waitLeave(); };
  }, [name]);

  // Poll for an invite while waiting. A lost seat escalates to an error (with a
  // retry) rather than silently re-joining, which previously could double-book
  // a seat against the mount-effect's join.
  useEffect(() => {
    if (phase !== 'waiting' || !PRESENCE) return;
    let live = true;
    const tick = async () => {
      const res = await PRESENCE.waitPoll();
      if (!live) return;
      if (!res.ok) {
        setPhase('error');
        setError('Lost your place in the waiting room — please retry.');
        return;
      }
      if (res.invite && !dismissed.current.has(keyOf(res.invite))) {
        setInvite(res.invite);
        setPhase('invited');
      }
    };
    const id = setInterval(tick, pollMs);
    return () => { live = false; clearInterval(id); };
  }, [phase, pollMs]);

  // Move focus to the Accept button when an invite appears (keyboard + SR).
  useEffect(() => { if (phase === 'invited' && acceptRef.current) acceptRef.current.focus(); }, [phase]);

  const accept = async () => {
    const code = invite && invite.roomCode;
    await PRESENCE.waitLeave();
    onJoin && onJoin(code, invite);
  };
  const decline = () => {
    if (invite) dismissed.current.add(keyOf(invite));
    setInvite(null);
    setPhase('waiting');
    if (cardRef.current) cardRef.current.focus();   // return focus to a stable target
  };
  const leave = async () => { await PRESENCE.waitLeave(); onCancel && onCancel(); };

  // Modal keyboard contract: Escape leaves; Tab is trapped within the card.
  const onKeyDown = (e) => {
    if (e.key === 'Escape') { e.preventDefault(); leave(); return; }
    if (e.key !== 'Tab' || !cardRef.current) return;
    const items = Array.from(cardRef.current.querySelectorAll(FOCUSABLE)).filter((el) => el.offsetParent !== null);
    if (items.length === 0) { e.preventDefault(); cardRef.current.focus(); return; }
    const first = items[0];
    const last = items[items.length - 1];
    const active = document.activeElement;
    if (e.shiftKey && (active === first || active === cardRef.current)) { e.preventDefault(); last.focus(); }
    else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
  };

  return (
    <div className="waiting-overlay" onKeyDown={onKeyDown}>
      <div
        className="waiting-card parchment stitched"
        role="dialog"
        aria-modal="true"
        aria-labelledby="wait-title"
        aria-describedby="wait-status"
        tabIndex={-1}
        ref={cardRef}
      >
        <div className="lobby-crest"><span className="seal big">E</span></div>
        <div className="eyebrow" style={{ textAlign: 'center' }}>Waiting room</div>
        <h2 className="display lobby-title" id="wait-title">A seat by the fire</h2>

        {phase !== 'invited' && (
          <div className="waiting-ember" aria-hidden="true"><span className="we-core" /></div>
        )}

        {/* Single live region so a screen reader announces each state change. */}
        <p className="wait-status" id="wait-status" role="status" aria-live="polite">
          {phase === 'joining' && 'Finding you a place at the table…'}
          {phase === 'waiting' && `Welcome, ${name || 'Guest'}. Hold tight — a DM will invite you in.`}
          {phase === 'invited' && invite && `${invite.dmName} has invited you to their table (${invite.roomCode}).`}
          {phase === 'error' && error}
        </p>

        {phase === 'invited' && invite && (
          <div className="invite-pop">
            <div className="invite-room">Table code <strong>{invite.roomCode}</strong></div>
            <div className="waiting-actions">
              <button ref={acceptRef} className="btn" style={{ flex: 1 }} onClick={accept}>
                Join {invite.dmName}’s table
              </button>
              <button className="btn ghost" onClick={decline}>Not now</button>
            </div>
          </div>
        )}

        {phase === 'error' && (
          <div className="waiting-actions">
            <button className="btn" style={{ flex: 1 }} onClick={onCancel}>Back to the lobby</button>
          </div>
        )}

        {(phase === 'waiting' || phase === 'joining') && (
          <button className="btn ghost waiting-leave" onClick={leave}>Leave the waiting room</button>
        )}
      </div>
    </div>
  );
}

/* DM-side roster of waiting players with an Invite control each. Render this
   while the local user is the DM and their presence room is open. */
function DmWaitingPanel({ pollMs = 4000 }) {
  const [players, setPlayers] = useState([]);
  const [invited, setInvited] = useState({});   // playerId -> true
  const [open, setOpen] = useState(true);

  useEffect(() => {
    if (!PRESENCE) return;
    let live = true;
    const tick = async () => {
      const list = await PRESENCE.dmPlayers();
      if (live) setPlayers(list);
    };
    tick();
    const id = setInterval(tick, pollMs);
    return () => { live = false; clearInterval(id); };
  }, [pollMs]);

  const invite = useCallback(async (playerId) => {
    const res = await PRESENCE.dmInvite(playerId);
    if (res.ok) setInvited((m) => ({ ...m, [playerId]: true }));
  }, []);

  const count = players.length;
  const noun = count === 1 ? 'player' : 'players';
  return (
    <section className={`dm-wait-panel parchment stitched ${open ? '' : 'collapsed'}`} aria-label="Waiting players">
      {/* Static accessible name so the timer-polled count doesn't rename the
          control under voice-control / SR users. The count is announced once
          per change by the sr-only live region below, not by the button. */}
      <button
        className="dmw-head"
        aria-label={`Waiting room, ${open ? 'collapse' : 'expand'}`}
        aria-expanded={open}
        aria-controls="dmw-body"
        onClick={() => setOpen((o) => !o)}
      >
        <span className="dmw-title">Waiting room</span>
        <span className="dmw-count" aria-hidden="true">{count} {noun}</span>
        <span className="caret" aria-hidden="true">{open ? '▾' : '▸'}</span>
      </button>
      <span className="sr-only" role="status" aria-live="polite">{count} {noun} waiting</span>
      <div id="dmw-body" className="dmw-body" hidden={!open}>
        {count === 0
          ? <p className="dmw-empty">No one is waiting right now.</p>
          : (
            <ul className="dmw-list">
              {players.map((p) => (
                <li key={p.playerId} className="dmw-player">
                  <span className="dmw-name">{p.name}</span>
                  {invited[p.playerId]
                    ? <span className="dmw-sent" role="status">Invited ✓</span>
                    : <button className="btn tiny" onClick={() => invite(p.playerId)}>Invite</button>}
                </li>
              ))}
            </ul>
          )}
      </div>
    </section>
  );
}

Object.assign(window, { useLobbyStats, WaitingRoom, DmWaitingPanel });
