// Host app — orchestrates: room creation, Spotify auth, playlist load,
// device pick, round start/reveal/next via socket + Spotify API.

(() => {
const H = window.HostScreens;
const { Stage, GlobalRisoStyles } = window;
const Sp = window.Spotify;

// Per-host persistence keys
const SAVED_ROOM_KEY = 'hf_host_room';
const SAVED_PLAYLIST_KEY = 'hf_host_recent_playlists';
const SAVED_TRACKS_KEY = 'hf_host_tracks';
const SAVED_USED_KEY = 'hf_host_used';
const SAVED_SONG_KEY = 'hf_host_current_song';
const SAVED_PLAYLIST_NAME_KEY = 'hf_host_playlist_name';
const SAVED_TIMER_KEY = 'hf_host_round_timer';

function loadJson(key, fallback) {
  try { return JSON.parse(localStorage.getItem(key) || '') ?? fallback; }
  catch (_) { return fallback; }
}
function saveJson(key, val) {
  try { localStorage.setItem(key, JSON.stringify(val)); } catch (_) {}
}

function App() {
  const [config, setConfig] = React.useState(null);     // /api/config
  const [configError, setConfigError] = React.useState(null);
  const [view, setView] = React.useState('start');      // start | spotify | playlist | device | room | playing | reveal | final
  const [token, setToken] = React.useState(Sp.loadStoredToken());
  const [authError, setAuthError] = React.useState(null);
  const [authBusy, setAuthBusy] = React.useState(false);
  const [code, setCode] = React.useState(null);
  const [room, setRoom] = React.useState(null);
  const [tracks, setTracks] = React.useState(() => loadJson(SAVED_TRACKS_KEY, []));
  const [usedTrackIds, setUsedTrackIds] = React.useState(() => new Set(loadJson(SAVED_USED_KEY, [])));
  const [currentSong, setCurrentSong] = React.useState(() => loadJson(SAVED_SONG_KEY, null));
  const [playlistError, setPlaylistError] = React.useState(null);
  const [playlistBusy, setPlaylistBusy] = React.useState(false);
  const [recentPlaylists, setRecentPlaylists] = React.useState(loadJson(SAVED_PLAYLIST_KEY, []));
  const [curatedPresets, setCuratedPresets] = React.useState([]);
  const [playlistName, setPlaylistName] = React.useState(() => localStorage.getItem(SAVED_PLAYLIST_NAME_KEY) || '');
  const [roundTimerSec, setRoundTimerSec] = React.useState(() => {
    const v = parseInt(localStorage.getItem(SAVED_TIMER_KEY) || '', 10);
    return Number.isFinite(v) && v > 0 ? v : null;
  });
  const setRoundTimerPersist = (v) => {
    setRoundTimerSec(v);
    if (v) localStorage.setItem(SAVED_TIMER_KEY, String(v));
    else localStorage.removeItem(SAVED_TIMER_KEY);
  };
  const setPlaylistNamePersist = (name) => {
    setPlaylistName(name || '');
    if (name) localStorage.setItem(SAVED_PLAYLIST_NAME_KEY, name);
    else localStorage.removeItem(SAVED_PLAYLIST_NAME_KEY);
  };
  const [myPlaylists, setMyPlaylists] = React.useState([]);
  const [userId, setUserId] = React.useState(null);
  const [loadingMine, setLoadingMine] = React.useState(false);
  const [devices, setDevices] = React.useState([]);
  // 'preview' = free 30s MP3 from Spotify embed (no auth, no Premium, anyone can host).
  // 'spotify' = full songs via Spotify Connect (Premium + dev-app allowlist).
  const [mode, setMode] = React.useState(localStorage.getItem('hf_mode') === 'spotify' ? 'spotify' : 'preview');
  const audioRef = React.useRef(null);
  // Host-as-player state
  const [hostPlays, setHostPlays] = React.useState(localStorage.getItem('hf_host_plays') === 'true');
  const [hostName, setHostName] = React.useState(localStorage.getItem('hf_host_name') || '');
  const [hostGuessYear, setHostGuessYear] = React.useState(1995);
  const [hostLocked, setHostLocked] = React.useState(false);
  const playerSocketRef = React.useRef(null);
  const HOST_PLAYER_ID = React.useMemo(() => {
    let id = localStorage.getItem('hf_host_player_id');
    if (!id) {
      id = 'p_host_' + Math.random().toString(36).slice(2, 8);
      localStorage.setItem('hf_host_player_id', id);
    }
    return id;
  }, []);
  const [pickedDevice, setPickedDevice] = React.useState(localStorage.getItem('hf_device_id') || null);
  const [refreshingDevices, setRefreshingDevices] = React.useState(false);
  const [reveal, setReveal] = React.useState(null);
  const [settingsOpen, setSettingsOpen] = React.useState(false);
  const [showStats, setShowStats] = React.useState(false);
  const [debugOpen, setDebugOpen] = React.useState(false);
  const [leaveConfirmOpen, setLeaveConfirmOpen] = React.useState(false);
  const socketRef = React.useRef(null);
  const tokenRef = React.useRef(token);
  React.useEffect(() => { tokenRef.current = token; }, [token]);

  // Persist game state for mid-game reconnect.
  React.useEffect(() => {
    if (tracks.length) saveJson(SAVED_TRACKS_KEY, tracks);
    else localStorage.removeItem(SAVED_TRACKS_KEY);
  }, [tracks]);
  React.useEffect(() => {
    saveJson(SAVED_USED_KEY, Array.from(usedTrackIds));
  }, [usedTrackIds]);
  React.useEffect(() => {
    if (currentSong) saveJson(SAVED_SONG_KEY, currentSong);
    else localStorage.removeItem(SAVED_SONG_KEY);
  }, [currentSong]);

  // ── Bootstrap: load config + handle auth callback if present ──
  React.useEffect(() => {
    let cancelled = false;
    fetch('/api/config').then(r => r.json()).then(async (cfg) => {
      if (cancelled) return;
      setConfig(cfg);
      if (!cfg.spotifyClientId) {
        setConfigError("Server is missing SPOTIFY_CLIENT_ID. Add it to .env and restart, then retry.");
      }

      const url = new URL(window.location.href);
      const authCode = url.searchParams.get('code');
      const authErr = url.searchParams.get('error');
      if (authErr) {
        setAuthError(authErr);
        url.searchParams.delete('error');
        url.searchParams.delete('state');
        window.history.replaceState({}, '', url.toString());
      }
      if (authCode && cfg.spotifyClientId) {
        try {
          setAuthBusy(true);
          const t = await Sp.exchangeCode(cfg.spotifyClientId, cfg.redirectUri, authCode);
          Sp.storeToken(t);
          setToken(t);
          url.searchParams.delete('code');
          url.searchParams.delete('state');
          window.history.replaceState({}, '', url.toString());
          // If solo (or any other page) initiated the auth, bounce back there.
          const returnTo = localStorage.getItem('hf_post_auth_return');
          if (returnTo) {
            localStorage.removeItem('hf_post_auth_return');
            window.location.href = returnTo;
            return;
          }
          setView('playlist');
        } catch (e) {
          setAuthError(e.message);
        } finally {
          setAuthBusy(false);
        }
      }
    }).catch(e => setConfigError(`Could not load config: ${e.message}`));
    return () => { cancelled = true; };
  }, []);

  // ── Load curated playlist presets (operator-verified years) ──
  React.useEffect(() => {
    fetch('/api/playlists').then(r => r.json()).then((j) => {
      setCuratedPresets(j.presets || []);
    }).catch(() => {});
  }, []);

  // Open a second socket as a "player" when host wants to play too. Joins the same
  // room with a separate playerId so the server scores them like any guest.
  React.useEffect(() => {
    if (!hostPlays || !code || !(hostName && hostName.trim().length >= 2)) {
      if (playerSocketRef.current) {
        playerSocketRef.current.disconnect();
        playerSocketRef.current = null;
      }
      return;
    }
    const sock = io({ transports: ['websocket', 'polling'] });
    playerSocketRef.current = sock;
    sock.on('connect', () => {
      sock.emit('player:join', { code, name: hostName.trim(), playerId: HOST_PLAYER_ID }, () => {});
    });
    return () => {
      sock.disconnect();
      if (playerSocketRef.current === sock) playerSocketRef.current = null;
    };
  }, [hostPlays, code, hostName]);

  // Reset host's per-round guess state on new round.
  React.useEffect(() => {
    if (room?.phase === 'playing') {
      setHostLocked(false);
      setHostGuessYear(1995);
    }
  }, [room?.phase, room?.round]);

  const setHostPlaysPersist = (v) => {
    setHostPlays(v);
    localStorage.setItem('hf_host_plays', String(v));
  };
  const setModePersist = (v) => {
    setMode(v);
    localStorage.setItem('hf_mode', v);
  };
  const setHostNamePersist = (v) => {
    setHostName(v);
    localStorage.setItem('hf_host_name', v);
  };

  const submitHostGuess = (y) => {
    if (!playerSocketRef.current) return;
    setHostLocked(true);
    playerSocketRef.current.emit('player:guess', { year: y }, (resp) => {
      if (!resp?.ok) setHostLocked(false);
    });
  };

  // ── Connect socket ──
  React.useEffect(() => {
    const sock = io({ transports: ['websocket', 'polling'] });
    socketRef.current = sock;

    const savedCode = localStorage.getItem(SAVED_ROOM_KEY);
    sock.on('connect', () => {
      if (savedCode) {
        sock.emit('host:reattach', { code: savedCode }, (resp) => {
          if (resp?.ok) setCode(savedCode);
          else localStorage.removeItem(SAVED_ROOM_KEY);
        });
      }
    });

    sock.on('room:state', (state) => {
      setRoom(state);
    });
    sock.on('round:reveal', (payload) => {
      setReveal(payload);
    });
    return () => sock.disconnect();
  }, []);

  // Drive view from server room phase. After a successful host:reattach the
  // initial view is still 'start' (splash) — drop that into 'room' so the host
  // lands back in the lobby instead of getting stuck on the splash.
  React.useEffect(() => {
    if (!room || !code) return;
    if (room.phase === 'lobby') setView(v => (v === 'playlist' || v === 'device' || v === 'spotify') ? v : 'room');
    else if (room.phase === 'playing') setView('playing');
    else if (room.phase === 'revealed') setView('reveal');
    else if (room.phase === 'ended') setView('final');
    if (room.phase && room.phase !== 'ended') setShowStats(false);
  }, [room?.phase, code]);


  const refresh = async () => {
    if (!tokenRef.current) throw new Error('not_authed');
    return await Sp.refreshToken(config.spotifyClientId, tokenRef.current.refresh).then(t => {
      Sp.storeToken(t); setToken(t); tokenRef.current = t; return t;
    });
  };

  // ── Actions ──
  const createRoom = () => {
    if (mode === 'spotify' && !token) { setView('spotify'); return; }
    socketRef.current.emit('host:create', {}, (resp) => {
      if (resp?.ok) {
        setCode(resp.code);
        localStorage.setItem(SAVED_ROOM_KEY, resp.code);
        if (tracks.length === 0) setView('playlist');
        else if (mode === 'spotify' && !pickedDevice) setView('device');
        else setView('room');
      }
    });
  };
  const resumeRoom = () => {
    const saved = localStorage.getItem(SAVED_ROOM_KEY);
    if (!saved) return;
    socketRef.current.emit('host:reattach', { code: saved }, (resp) => {
      if (resp?.ok) {
        setCode(saved);
        // Navigate based on whatever phase we already know about (room state may
        // already be set from the auto-reattach on connect). Default to 'room' for
        // lobby/missing-state.
        const phase = room?.phase;
        if (phase === 'playing') setView('playing');
        else if (phase === 'revealed') setView('reveal');
        else if (phase === 'ended') setView('final');
        else setView('room');
      } else {
        localStorage.removeItem(SAVED_ROOM_KEY);
        setAuthError('Saved room is gone — create a new one.');
      }
    });
  };

  const connectSpotify = async () => {
    if (!config?.spotifyClientId) {
      setAuthError('Spotify not configured on server.');
      return;
    }
    setAuthBusy(true);
    try {
      await Sp.startAuth(config.spotifyClientId, config.redirectUri);
    } catch (e) {
      setAuthError(e.message);
      setAuthBusy(false);
    }
  };

  // Server pages /v1/playlists/{id}/tracks via the host's Spotify token, then
  // enriches each track with embed-scraped year + preview URL. Multiple IDs are
  // merged + deduped.
  const loadPlaylist = async (idOrIds) => {
    const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
    setPlaylistBusy(true); setPlaylistError(null);
    try {
      let access = token?.access;
      if (token && Date.now() >= token.expiresAt - 5000) {
        const fresh = await refresh();
        access = fresh.access;
      }
      if (!access) throw new Error('Spotify not connected.');
      const responses = await Promise.all(ids.map(async (id) => {
        const r = await fetch(`/api/spotify-playlist?id=${encodeURIComponent(id)}`, {
          headers: { Authorization: `Bearer ${access}` },
        });
        if (!r.ok) {
          const j = await r.json().catch(() => ({}));
          throw new Error(`Couldn't read playlist ${id} (${j.error || r.status}).`);
        }
        return r.json();
      }));

      const seen = new Set();
      const pool = [];
      for (const resp of responses) {
        for (const t of resp.tracks || []) {
          if (seen.has(t.id)) continue;
          seen.add(t.id);
          pool.push(t);
        }
      }
      if (pool.length === 0) throw new Error('No tracks found.');

      const name = ids.length === 1 ? responses[0].name : `${ids.length} playlists · ${pool.length} tracks`;

      setTracks(pool);
      setUsedTrackIds(new Set());
      setPlaylistNamePersist(name);
      const next2 = [
        { ids, name },
        ...recentPlaylists.filter(p => JSON.stringify(p.ids || [p.id]) !== JSON.stringify(ids)),
      ].slice(0, 5);
      setRecentPlaylists(next2);
      saveJson(SAVED_PLAYLIST_KEY, next2);

      if (mode === 'spotify' && !pickedDevice) { setView('device'); refreshDevices(); }
      else if (!code) createRoom();
      else setView('room');
    } catch (e) {
      setPlaylistError(e.message || 'Loading playlist failed.');
    } finally {
      setPlaylistBusy(false);
    }
  };

  // Curated path: tracks with operator-verified years served from
  // data/playlists/<slug>.json — no Spotify Web API hit, year is frozen.
  const loadCuratedPlaylist = async (slug) => {
    setPlaylistBusy(true); setPlaylistError(null);
    try {
      const r = await fetch(`/api/playlists/${encodeURIComponent(slug)}`);
      if (!r.ok) throw new Error(`Couldn't load curated playlist (${r.status}).`);
      const data = await r.json();
      const all = (data.tracks || []).filter(t => Number.isFinite(t.year)).map(t => ({
        id: t.id, uri: t.uri, title: t.title, artist: t.artist, year: t.year,
        previewUrl: t.previewUrl || null,
      }));
      if (!all.length) throw new Error('Curated playlist has no tracks with year metadata.');
      setTracks(all);
      setUsedTrackIds(new Set());
      setPlaylistNamePersist(data.name || slug);
      // Preview mode doesn't need a Spotify device — only Premium/Connect does.
      if (mode === 'spotify' && !pickedDevice) { setView('device'); refreshDevices(); }
      else if (!code) createRoom();
      else setView('room');
    } catch (e) {
      setPlaylistError(e.message);
    } finally {
      setPlaylistBusy(false);
    }
  };

  const reauthSpotify = () => {
    Sp.storeToken(null);
    setToken(null);
    setView('spotify');
  };

  // "Back to home" — drops the host on the splash. Keeps the saved room/tracks
  // in localStorage so they can hit RESUME to come back.
  const goHome = () => {
    stopPreview();
    setSettingsOpen(false);
    setReveal(null);
    setView('start');
  };

  // "New game" — end the room server-side, wipe local game state, return to
  // splash. From there the host can CREATE A ROOM (different playlist /
  // settings) or click "I'M JOINING INSTEAD" to leave the host flow entirely.
  const startFreshGame = () => {
    try { socketRef.current?.emit('host:end', {}, () => {}); } catch {}
    stopPreview();
    setSettingsOpen(false);
    setReveal(null);
    setRoom(null);
    setTracks([]);
    setUsedTrackIds(new Set());
    setCurrentSong(null);
    setPlaylistNamePersist('');
    localStorage.removeItem(SAVED_ROOM_KEY);
    setView('start');
  };

  const refreshDevices = async () => {
    setRefreshingDevices(true);
    try {
      const json = await Sp.spApi(token, refresh, '/me/player/devices');
      setDevices(json.devices || []);
      if (!pickedDevice && json.devices?.length === 1) {
        setPickedDevice(json.devices[0].id);
        localStorage.setItem('hf_device_id', json.devices[0].id);
      } else if (!pickedDevice) {
        const active = json.devices?.find(d => d.is_active);
        if (active) {
          setPickedDevice(active.id);
          localStorage.setItem('hf_device_id', active.id);
        }
      }
    } catch (e) {
      setAuthError(`Devices: ${e.message}`);
    } finally {
      setRefreshingDevices(false);
    }
  };

  const pickDevice = (id) => {
    setPickedDevice(id);
    localStorage.setItem('hf_device_id', id);
  };

  const pickRandomTrack = () => {
    let pool = tracks.filter(t => !usedTrackIds.has(t.id));
    if (mode === 'preview') pool = pool.filter(t => !!t.previewUrl);
    if (pool.length === 0) return null;
    return pool[Math.floor(Math.random() * pool.length)];
  };

  // Preview mode — play a 30s MP3 directly via HTML5 audio on the host's phone.
  // No Spotify auth, no Premium, no device picker.
  const playPreview = async (track) => {
    if (!track?.previewUrl) throw new Error('This track has no 30-second preview from Spotify. Skipping…');
    if (!audioRef.current) audioRef.current = new Audio();
    const a = audioRef.current;
    a.src = track.previewUrl;
    a.volume = 1;
    a.currentTime = 0;
    try { await a.play(); }
    catch (e) {
      throw new Error(`Browser blocked audio: ${e.message}. Tap the screen, then try again.`);
    }
  };

  const stopPreview = () => {
    if (audioRef.current) { try { audioRef.current.pause(); } catch (_) {} }
  };

  // Robust playback — verifies a Spotify device is reachable, transfers to it,
  // then plays. Throws with a clear message if nothing works.
  const playOnSpotify = async (uri) => {
    let deviceId = pickedDevice;

    // Verify the picked device still exists; otherwise fall back to whatever's available.
    let availableDevices;
    try {
      const json = await Sp.spApi(token, refresh, '/me/player/devices');
      availableDevices = json?.devices || [];
    } catch (e) {
      throw new Error(`Couldn't list Spotify devices (${e.status || ''}): ${e.message}`);
    }
    if (availableDevices.length === 0) {
      throw new Error("No Spotify device is reachable. Open Spotify on your phone (or a speaker), tap play on any track for a moment, then try again.");
    }
    if (!deviceId || !availableDevices.find(d => d.id === deviceId)) {
      const fresh = availableDevices.find(d => d.is_active) || availableDevices[0];
      deviceId = fresh.id;
      setPickedDevice(deviceId);
      localStorage.setItem('hf_device_id', deviceId);
    }

    // Transfer playback to the chosen device first — this guarantees it's the active target.
    try {
      await Sp.transferPlayback(token, refresh, deviceId, false);
      await new Promise(r => setTimeout(r, 350));
    } catch (e) {
      // Some devices (e.g. iOS Spotify app in background) don't accept transfer when
      // they've been quiet for a while. Surface the error rather than silently failing.
      throw new Error(`Couldn't take over playback on ${availableDevices.find(d=>d.id===deviceId)?.name || 'device'}. Open Spotify and tap play on any song to wake it, then try again. (${e.status || ''} ${e.message})`);
    }

    // Now play the URI on that device.
    try {
      await Sp.playTrack(token, refresh, uri, deviceId);
    } catch (e) {
      // Premium-required errors look like 403 here.
      const lower = (e.body?.error?.message || e.message || '').toLowerCase();
      if (lower.includes('premium')) {
        throw new Error('Playback requires Spotify Premium on the host account.');
      }
      throw new Error(`Couldn't start track: ${e.message} (${e.status || ''})`);
    }
  };

  const advanceRound = (t) => {
    setUsedTrackIds(s => new Set([...s, t.id]));
    setCurrentSong(t);
    setReveal(null);
    socketRef.current.emit('host:start_round', { song: t, timerSec: roundTimerSec || null }, (resp) => {
      if (!resp?.ok) setAuthError('Failed to start round.');
    });
  };

  const startRound = async () => {
    setAuthError(null);
    const t = pickRandomTrack();
    if (!t) {
      setAuthError(mode === 'preview'
        ? 'Out of tracks with previews. Try a different playlist or switch to Spotify Premium mode.'
        : 'Out of fresh tracks. End game or load a different playlist.');
      return;
    }

    // Preview mode — audio.play() MUST run in the same JS tick as the user tap so
    // iOS Safari treats it as user-initiated. No awaits between tap and play().
    if (mode === 'preview') {
      if (!t.previewUrl) { setAuthError('That track has no preview from Spotify. Try Start again.'); return; }
      if (!audioRef.current) audioRef.current = new Audio();
      const a = audioRef.current;
      a.src = t.previewUrl;
      a.currentTime = 0;
      a.volume = 1;
      const p = a.play();
      Promise.resolve(p).then(() => {
        advanceRound(t);
      }).catch((e) => {
        setAuthError(`Browser blocked audio: ${e.message}. Tap the screen once, then try again.`);
      });
      return;
    }

    // Spotify Connect mode (full songs)
    try { await playOnSpotify(t.uri); }
    catch (e) { setAuthError(e.message || 'Playback failed.'); return; }
    advanceRound(t);
  };

  // Replay the current round's track — useful if audio dropped mid-round.
  const replayCurrent = async () => {
    if (!currentSong) return;
    setAuthError(null);
    try {
      if (mode === 'preview') await playPreview(currentSong);
      else await playOnSpotify(currentSong.uri);
    } catch (e) { setAuthError(e.message); }
  };

  const reveal_ = () => {
    if (mode === 'preview') stopPreview();
    socketRef.current.emit('host:reveal', {}, (resp) => {
      if (!resp?.ok) setAuthError('Failed to reveal.');
    });
  };

  const nextRound = async () => {
    setReveal(null);
    await startRound();
  };
  const endGame = () => {
    socketRef.current.emit('host:end', {}, () => {});
  };
  const playAgain = () => {
    socketRef.current.emit('host:reset', {}, () => {
      setUsedTrackIds(new Set());
      setCurrentSong(null);
      setReveal(null);
      setView('room');
    });
  };

  // ── Render ──
  if (!config) return <Stage><GlobalRisoStyles /><Loading text="Loading…" /></Stage>;

  if (authBusy && !token) return <Stage><GlobalRisoStyles /><Loading text="Connecting Spotify…" /></Stage>;

  // Global error banner — shown above any view when authError is set.
  const errorBanner = authError ? (
    <div style={{
      position: 'fixed', left: 0, right: 0, top: 0, zIndex: 1000,
      padding: '12px 16px',
      background: window.R.pink, color: window.R.paper,
      fontSize: 12, fontWeight: 700, lineHeight: 1.45,
      borderBottom: `2px solid ${window.R.ink}`,
      display: 'flex', gap: 10, alignItems: 'flex-start',
    }}>
      <div style={{ flex: 1 }}>{authError}</div>
      <span onClick={() => setAuthError(null)} style={{
        cursor: 'pointer', fontWeight: 900, fontSize: 14, padding: '0 6px',
      }}>×</span>
    </div>
  ) : null;

  const savedRoom = localStorage.getItem(SAVED_ROOM_KEY);
  const joinUrl = `${config.publicUrl}/?code=${code || ''}`;
  const players = room?.players || [];

  let body = null;
  if (view === 'start') {
    body = <H.HostStart
      onCreate={createRoom}
      onResume={savedRoom ? resumeRoom : null}
      savedRoom={savedRoom}
      configError={configError}
      hostPlays={hostPlays}
      setHostPlays={setHostPlaysPersist}
      hostName={hostName}
      setHostName={setHostNamePersist}
      mode={mode}
      setMode={setModePersist}
    />;
  } else if (view === 'spotify') {
    body = <H.HostSpotify onConnect={connectSpotify} busy={authBusy} error={authError || configError} />;
  } else if (view === 'playlist') {
    body = <H.HostPlaylist
      onPick={loadPlaylist}
      onPickCurated={loadCuratedPlaylist}
      curatedPresets={curatedPresets}
      busy={playlistBusy}
      error={playlistError}
      onReauth={reauthSpotify}
    />;
  } else if (view === 'device') {
    body = <H.HostDevice
      devices={devices}
      picked={pickedDevice}
      onPick={pickDevice}
      onRefresh={refreshDevices}
      refreshing={refreshingDevices}
      onContinue={() => { if (!code) createRoom(); else setView('room'); }}
    />;
  } else if (view === 'room') {
    const previewableTracks = tracks.filter(t => !!t.previewUrl);
    const previewableUsed = previewableTracks.filter(t => usedTrackIds.has(t.id)).length;
    body = <H.HostRoom
      code={code}
      players={players}
      joinUrl={joinUrl}
      trackPoolCount={tracks.length}
      usedCount={usedTrackIds.size}
      previewablePoolCount={previewableTracks.length}
      previewableUsedCount={previewableUsed}
      mode={mode}
      pickedDevice={pickedDevice}
      devices={devices}
      onChangeDevice={() => setView('device')}
      onStart={startRound}
      onEnd={endGame}
      playlistName={playlistName}
      onChangePlaylist={() => setView('playlist')}
      roundTimerSec={roundTimerSec}
      onChangeRoundTimer={setRoundTimerPersist}
    />;
  } else if (view === 'playing') {
    body = hostPlays ? (
      <H.HostPlayingAsPlayer
        round={room?.round || 1}
        year={hostGuessYear}
        setYear={setHostGuessYear}
        locked={hostLocked}
        onLock={submitHostGuess}
        onReveal={reveal_}
        onReplay={replayCurrent}
        players={players}
        roundDeadline={room?.roundDeadline}
      />
    ) : (
      <H.HostPlaying
        round={room?.round || 1}
        song={currentSong}
        players={players}
        onReveal={reveal_}
        onReplay={replayCurrent}
        roundDeadline={room?.roundDeadline}
      />
    );
  } else if (view === 'reveal') {
    const guesses = reveal?.guesses || [];
    const song = reveal?.song || currentSong;
    body = <H.HostRevealed
      song={song}
      guesses={guesses}
      players={players}
      round={room?.round || 1}
      myPid={hostPlays ? HOST_PLAYER_ID : null}
      onNext={nextRound}
      onEnd={endGame}
    />;
  } else if (view === 'final') {
    if (showStats) {
      body = <window.FunStats history={room?.history || []} onBack={() => setShowStats(false)} onNewGame={playAgain} />;
    } else {
      body = <H.HostFinal players={players} onAgain={playAgain} onSeeStats={() => setShowStats(true)} />;
    }
  } else {
    body = <Loading text="Hmm." />;
  }

  // Show the settings (⋯) menu only after we have a token + room.
  const showSettingsBtn = token && code && view !== 'spotify' && view !== 'start';
  const resyncHost = () => {
    const sock = socketRef.current;
    if (!sock || !code) return;
    sock.emit('host:reattach', { code }, () => {});
  };

  const settingsOverlay = showSettingsBtn && settingsOpen ? (
    <H.SettingsSheet
      onClose={() => setSettingsOpen(false)}
      devices={devices}
      pickedDevice={pickedDevice}
      onPickDevice={(id) => { pickDevice(id); }}
      onRefreshDevices={refreshDevices}
      refreshingDevices={refreshingDevices}
      onChangeDevice={() => { setSettingsOpen(false); setView('device'); }}
      onChangePlaylist={() => { setSettingsOpen(false); setView('playlist'); }}
      onReauth={() => { setSettingsOpen(false); reauthSpotify(); }}
      onEndGame={() => { setSettingsOpen(false); setLeaveConfirmOpen(true); }}
      onNewGame={() => { setSettingsOpen(false); startFreshGame(); }}
      onJoinInstead={() => { startFreshGame(); window.location.href = '/'; }}
      onShowState={() => { setSettingsOpen(false); setDebugOpen(true); }}
      onResync={() => { setSettingsOpen(false); resyncHost(); }}
      onResetScores={() => {
        setSettingsOpen(false);
        socketRef.current.emit('host:reset', {}, () => {
          setUsedTrackIds(new Set());
          setReveal(null);
          setView('room');
        });
      }}
    />
  ) : null;

  const leaveConfirm = leaveConfirmOpen ? (
    <div
      style={{
        position: 'fixed', inset: 0, zIndex: 1200,
        background: 'rgba(26,26,26,.55)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}
      onClick={() => setLeaveConfirmOpen(false)}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          width: '90%', maxWidth: 360,
          background: window.R.paper, border: `2px solid ${window.R.ink}`,
          boxShadow: `4px 4px 0 ${window.R.ink}`,
          padding: '18px 18px 16px',
          display: 'flex', flexDirection: 'column', gap: 14,
        }}
      >
        <div style={{ fontSize: 16, fontWeight: 900, letterSpacing: '.04em' }}>End game?</div>
        <div style={{ fontSize: 13, color: window.R.muted, lineHeight: 1.4 }}>
          The room will close for everyone. Sure you want to end?
        </div>
        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <window.Btn big={false} bg={window.R.paper2} onClick={() => setLeaveConfirmOpen(false)}>CANCEL</window.Btn>
          <window.Btn big={false} bg={window.R.pink} fg={window.R.paper} onClick={() => { setLeaveConfirmOpen(false); endGame(); }}>END GAME</window.Btn>
        </div>
      </div>
    </div>
  ) : null;

  const debugOverlay = debugOpen ? (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 1100,
      background: 'rgba(26,26,26,.55)',
      display: 'flex', alignItems: 'flex-end',
    }} onClick={() => setDebugOpen(false)}>
      <div onClick={(e) => e.stopPropagation()} style={{
        width: '100%', maxWidth: 460, margin: '0 auto',
        background: window.R.paper, border: `2px solid ${window.R.ink}`,
        padding: '14px 18px calc(env(safe-area-inset-bottom, 0px) + 18px)',
        display: 'flex', flexDirection: 'column', gap: 10,
        fontFamily: 'ui-monospace, monospace', fontSize: 11, lineHeight: 1.5,
      }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <b style={{ fontFamily: 'inherit' }}>STATE · HOST</b>
          <span onClick={() => setDebugOpen(false)} style={{ cursor: 'pointer', fontSize: 16, padding: '0 8px' }}>×</span>
        </div>
        <div>view: <b>{view}</b></div>
        <div>socket: <b>{socketRef.current?.connected ? 'connected' : 'disconnected'}</b></div>
        <div>code: <b>{code || '—'}</b></div>
        <div>mode: <b>{mode}</b></div>
        <div>spotify token: <b>{token ? 'yes' : 'no'}</b></div>
        <div>device: <b>{pickedDevice || '—'}</b></div>
        <div>tracks: <b>{tracks.length}</b> (used {usedTrackIds.size})</div>
        <div>currentSong: <b>{currentSong ? `${currentSong.title} (${currentSong.year})` : '—'}</b></div>
        <div>room.phase: <b>{room?.phase || '—'}</b></div>
        <div>room.round: <b>{room?.round ?? '—'}</b></div>
        <div>players: <b>{players.length}</b> · locked: <b>{players.filter(p => p.locked).length}</b></div>
        <div style={{ display: 'flex', gap: 8, marginTop: 6 }}>
          <window.Btn big={false} bg={window.R.green} onClick={() => { resyncHost(); setDebugOpen(false); }}>↻ RE-SYNC</window.Btn>
          <window.Btn big={false} bg={window.R.pink} fg={window.R.paper} onClick={() => { goHome(); setDebugOpen(false); }}>HOME</window.Btn>
        </div>
      </div>
    </div>
  ) : null;
  const settingsButton = showSettingsBtn ? (
    <div onClick={() => setSettingsOpen(true)} style={{
      position: 'fixed', top: 'calc(env(safe-area-inset-top, 0px) + 60px)', right: 10,
      width: 38, height: 38, zIndex: 900,
      background: window.R.paper, border: `2px solid ${window.R.ink}`,
      boxShadow: `2px 2px 0 ${window.R.ink}`,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      cursor: 'pointer', fontSize: 16, fontWeight: 900, lineHeight: 1,
    }} title="Settings">⋯</div>
  ) : null;

  return <Stage><GlobalRisoStyles />{errorBanner}{body}{settingsButton}{settingsOverlay}{debugOverlay}{leaveConfirm}</Stage>;
}

function Loading({ text = 'Loading…' }) {
  return (
    <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: window.R.muted, fontWeight: 700 }}>
      {text}
    </div>
  );
}

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