'use client';

import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useState, type ReactNode } from 'react';
import type { PrivateState, PublicState } from '@pintrush/engine';
import { RESYNC } from '@pintrush/protocol';
import { cn } from '@/lib/cn';
import { clearTokens, loadTokens, saveToken, useGameConnection, useWakeLock } from '@/lib/game';
import { syncTokenNickname } from '@/lib/play/device';
import { needsRejoin } from '@/lib/play/errors';
import { fairnessHandled, markFairnessHandled, sendFairness } from '@/lib/play/feedback';
import { useSettledKey } from '@/lib/play/hooks';
import { resultAnnouncement, pointsBreakdown, wonTableChallenge } from '@/lib/play/points';
import { championshipStage, resolveScreen, type ChampionshipStage, type ScreenModel } from '@/lib/play/screen';
import { ChampionshipBanner, ConnectionBanner, FairnessPrompt, LiveAnnouncer, NoticeToast, PausedOverlay, TopBar } from './chrome';
import { MenuSheet } from './MenuSheet';
import { PredictScreen, WatchScreen } from './screens/final';
import { IntroScreen, PendingScreen, QuestionScreen, RevealScreen, RoundOverScreen } from './screens/round';
import {
  BreakScreen,
  CompleteScreen,
  ConnectingScreen,
  DeniedScreen,
  IdleScreen,
  InviteScreen,
  LobbyScreen,
  RemovedScreen,
  WaitingScreen,
} from './screens/session';
import { usePlayController } from './usePlayController';
import type { MenuTab, PlayCtx } from './types';

export interface PlayAppProps {
  roomId: string;
  venueName: string;
  roomName: string;
  /** Join page for this room (keeps ?embed=1). */
  joinHref: string;
  embed: boolean;
}

const QUIET_SCREENS = new Set<ScreenModel['kind']>(['connecting', 'kicked', 'denied', 'idle']);

/** What a screen reader hears when the screen changes (derived from views, so it never repeats). */
function announcement(model: ScreenModel, pub: PublicState | null, me: PrivateState | null, stage: ChampionshipStage): string {
  if (model.paused) return 'Game paused. The host will resume shortly.';
  const round = pub?.round;
  const q = pub?.question;
  switch (model.kind) {
    case 'intro':
      return round ? `${round.label} is starting.` : '';
    case 'pending':
      return q && round ? `Up next: ${q.isWildcard ? 'the wildcard' : q.categoryLabel}. Question ${q.index + 1} of ${round.questionCount}.` : '';
    case 'open':
      return q?.prompt ? `Question ${q.index + 1}: ${q.prompt}` : '';
    case 'locked':
      return 'Answers locked.';
    case 'revealed': {
      if (me?.lastResult && q && pub) {
        const won = round?.kind === 'TABLE_WARS' && wonTableChallenge(pub.reveal?.challengeWinners, me);
        return resultAnnouncement(me.lastResult, pointsBreakdown(me.lastResult, { difficulty: q.difficulty, tableChallengeWon: won }));
      }
      const correct = q?.options?.find((o) => o.id === pub?.reveal?.correctOptionId);
      return correct ? `The answer was ${correct.text}.` : '';
    }
    case 'round-over':
      return `Round complete.${me ? ` You scored ${me.roundPoints} points this round.` : ''}`;
    case 'break':
      return 'Break time. The next round starts soon.';
    case 'waiting':
      return 'You’ll join at the start of the next round.';
    case 'lobby':
      return 'You’re in. The host will start the show soon.';
    case 'invite':
      return stage === 'lobby' ? 'The championship lobby is open. Join with one tap.' : 'The championship is live. Join with one tap.';
    case 'predict':
      return 'Final Rush: pick the winner before the first question.';
    case 'watch':
      return q?.prompt ? `Final Rush question ${q.index + 1}: ${q.prompt}` : '';
    case 'complete':
      return model.backOn
        ? 'This game is over, and tonight’s game is back on. Tap Keep playing to rejoin.'
        : 'The game is over. Final standings are on screen.';
    case 'kicked':
      return 'You were removed from this game.';
    default:
      return '';
  }
}

export function PlayApp({ roomId, venueName, roomName, joinHref, embed }: PlayAppProps) {
  const router = useRouter();
  const [ready, setReady] = useState(false);
  const [removed, setRemoved] = useState(false);
  const [menu, setMenu] = useState<{ open: boolean; tab: MenuTab }>({ open: false, tab: 'leaders' });
  const [slow, setSlow] = useState(false);

  // No token for this room on this phone → nothing to resume: go pick a nickname.
  useEffect(() => {
    if (loadTokens(roomId).length === 0) router.replace(joinHref);
    else setReady(true);
  }, [roomId, joinHref, router]);

  const { conn, state } = useGameConnection(
    {
      // Read on every (re)connect so rotated and newly added tokens are always presented.
      auth: () => ({ tokens: loadTokens(roomId).map((t) => t.token) }),
      onToken: (d) => saveToken(roomId, d),
      onAuthError: (e) => {
        if (needsRejoin(e.code)) {
          clearTokens(roomId);
          router.replace(joinHref);
        } else if (e.code === 'FORBIDDEN') {
          clearTokens(roomId);
          setRemoved(true);
        }
      },
    },
    useSettledKey(ready ? roomId : null),
  );
  const ctl = usePlayController(conn, state, roomId);
  const pub = state.public;
  const me = state.me;

  const model = resolveScreen({ status: state.status, kicked: state.kicked || removed, snapshot: state.snapshot, public: pub, me });

  // Lobby vs live, for a championship this phone doesn't play (yet).
  const stage = state.snapshot ? championshipStage(state.snapshot) : 'live';

  // The server may move this phone into another session by itself (back to tonight's game after a
  // show): its token arrives without a nickname, so record it once the snapshot names the player.
  const mySessionId = state.snapshot?.mySessionId ?? null;
  const myNickname = me?.nickname ?? null;
  useEffect(() => {
    if (mySessionId && myNickname) syncTokenNickname(roomId, mySessionId, myNickname);
  }, [roomId, mySessionId, myNickname]);

  // "Was that round fair?": once per round this phone played, from its ROUND_OVER until the next
  // round starts. Not inside the /demo frame (prospects, not pilot guests).
  const [fairness, setFairness] = useState<{ roundId: string; thanked: boolean } | null>(null);
  const finishedRoundId = !embed && model.kind === 'round-over' && me?.inRound && pub?.round ? pub.round.id : null;
  useEffect(() => {
    if (finishedRoundId && !fairnessHandled(finishedRoundId)) {
      setFairness((f) => (f?.roundId === finishedRoundId ? f : { roundId: finishedRoundId, thanked: false }));
    }
  }, [finishedRoundId]);
  useEffect(() => {
    if (!fairness?.thanked) return;
    const timer = setTimeout(() => setFairness((f) => (f === fairness ? null : f)), 2_500);
    return () => clearTimeout(timer);
  }, [fairness]);
  const fairnessRoundOnScreen =
    (model.kind === 'round-over' && pub?.round?.id) || (model.kind === 'break' && pub?.lastRound?.id) || null;
  const showFairness = fairness !== null && !model.paused && (fairness.thanked || fairnessRoundOnScreen === fairness.roundId);
  const voteFairness = useCallback(
    (fair: boolean) => {
      if (!fairness) return;
      markFairnessHandled(fairness.roundId);
      sendFairness({ roomId, roundId: fairness.roundId, fair });
      setFairness({ roundId: fairness.roundId, thanked: true });
    },
    [fairness, roomId],
  );
  const dismissFairness = useCallback(() => {
    if (fairness) markFairnessHandled(fairness.roundId);
    setFairness(null);
  }, [fairness]);

  // Self-healing: a phone waking from sleep, or a tab that missed a message, asks for a full snapshot.
  useEffect(() => {
    if (!conn) return;
    const onVisible = () => {
      if (document.visibilityState === 'visible' && conn.getState().status === 'connected') void conn.requestState();
    };
    document.addEventListener('visibilitychange', onVisible);
    const stale = setInterval(() => {
      const s = conn.getState();
      const deadline = s.public?.round?.phaseEndsAt ?? s.public?.nextRoundAt ?? null;
      if (s.status === 'connected' && deadline !== null && conn.serverNow() > deadline + RESYNC.staleAfterMs) void conn.requestState();
    }, 5_000);
    return () => {
      document.removeEventListener('visibilitychange', onVisible);
      clearInterval(stale);
    };
  }, [conn]);

  useEffect(() => {
    if (state.snapshot) return setSlow(false);
    const timer = setTimeout(() => setSlow(true), 6_000);
    return () => clearTimeout(timer);
  }, [state.snapshot]);

  useWakeLock(pub !== null && pub.status !== 'COMPLETED' && !QUIET_SCREENS.has(model.kind) && model.kind !== 'invite');

  // A screen counts as "live" when the message that brought it was a game event, not a resync
  // (room.state): celebrations and haptics only fire for moments the player actually saw happen.
  const screenKey = `${model.kind}:${pub?.round?.id ?? '-'}:${pub?.round?.qIndex ?? '-'}`;
  const [liveInfo, setLiveInfo] = useState({ key: '', live: false });
  if (liveInfo.key !== screenKey) setLiveInfo({ key: screenKey, live: state.lastType !== null && state.lastType !== 'room.state' });

  const openMenu = useCallback((tab?: MenuTab) => setMenu((m) => ({ open: true, tab: tab ?? m.tab })), []);
  const closeMenu = useCallback(() => setMenu((m) => ({ ...m, open: false })), []);

  const ctx: PlayCtx | null = state.snapshot
    ? { conn, state, snapshot: state.snapshot, pub, me, ctl, embed, joinHref, venueName, live: liveInfo.live, openMenu, championshipStage: stage }
    : null;

  let screen: ReactNode;
  if (!ctx || model.kind === 'connecting') screen = <ConnectingScreen roomLabel={venueName} slow={slow} />;
  else {
    switch (model.kind) {
      case 'kicked':
        screen = <RemovedScreen joinHref={joinHref} />;
        break;
      case 'denied':
        screen = <DeniedScreen joinHref={joinHref} message={state.error?.message ?? 'This game link is no longer valid.'} />;
        break;
      case 'idle':
        screen = <IdleScreen ctx={ctx} />;
        break;
      case 'invite':
        screen = <InviteScreen ctx={ctx} />;
        break;
      case 'lobby':
        screen = <LobbyScreen ctx={ctx} />;
        break;
      case 'complete':
        screen = <CompleteScreen ctx={ctx} backOn={model.backOn} />;
        break;
      case 'break':
        screen = <BreakScreen ctx={ctx} />;
        break;
      case 'waiting':
        screen = <WaitingScreen ctx={ctx} />;
        break;
      case 'intro':
        screen = <IntroScreen ctx={ctx} />;
        break;
      case 'pending':
        screen = <PendingScreen ctx={ctx} />;
        break;
      case 'open':
        screen = <QuestionScreen ctx={ctx} locked={false} />;
        break;
      case 'locked':
        screen = <QuestionScreen ctx={ctx} locked />;
        break;
      case 'revealed':
        screen = <RevealScreen ctx={ctx} />;
        break;
      case 'round-over':
        screen = <RoundOverScreen ctx={ctx} />;
        break;
      case 'predict':
        screen = <PredictScreen ctx={ctx} />;
        break;
      case 'watch':
        screen = <WatchScreen ctx={ctx} />;
        break;
    }
  }

  const gameOver = model.kind === 'kicked' || model.kind === 'denied';
  const contextLabel =
    state.snapshot?.activeEventKind === 'CHAMPIONSHIP' && state.snapshot.mySessionActive ? (state.snapshot.activeEventName ?? roomName) : venueName;

  return (
    // data-* expose the resolved screen for end-to-end tests (Playwright) and support debugging.
    <div
      className="relative isolate flex h-dvh flex-col overflow-hidden bg-night"
      data-screen={model.kind}
      data-invite={model.invite}
      data-back-on={model.backOn || undefined}
      data-paused={model.paused || undefined}
      data-connection={state.status}
    >
      <div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10">
        <div className="absolute inset-x-0 top-0 h-2/3 bg-[radial-gradient(90%_50%_at_50%_0%,rgb(124_92_255/0.22),transparent_70%)]" />
        <div className="absolute inset-x-0 bottom-0 h-1/2 bg-[radial-gradient(70%_60%_at_50%_100%,rgb(34_211_238/0.08),transparent_70%)]" />
      </div>

      {ctx && !gameOver && <TopBar pub={pub} me={me} fallback={contextLabel} onMenu={() => openMenu()} />}
      <ConnectionBanner status={state.status} onRetry={() => conn?.reconnect()} />
      {ctx && model.invite === 'banner' && (
        <ChampionshipBanner name={ctx.snapshot.activeEventName} stage={stage} joining={ctl.joining} onJoin={() => void ctl.joinActive()} />
      )}

      <div className={cn('relative flex min-h-0 flex-1 flex-col', embed && 'text-[0.9375rem]')}>
        <main id="main" tabIndex={-1} inert={model.paused || undefined} className="relative flex min-h-0 flex-1 flex-col outline-none">
          {screen}
        </main>
        {showFairness && <FairnessPrompt thanked={fairness.thanked} onVote={voteFairness} onDismiss={dismissFairness} />}
        {model.paused && <PausedOverlay />}
        <NoticeToast notice={ctl.notice} onDismiss={ctl.dismissNotice} />
      </div>

      <LiveAnnouncer message={announcement(model, pub, me, stage)} />
      {ctx && !gameOver && (
        <MenuSheet ctx={ctx} open={menu.open} tab={menu.tab} onTab={(tab) => setMenu((m) => ({ ...m, tab }))} onClose={closeMenu} />
      )}
    </div>
  );
}
