/**
 * Phase → screen mapping for the phone. Pure so it can be unit-tested: the play app renders
 * exactly one screen from the latest complete views, never from message order.
 */
import { DEFAULT_SESSION_SETTINGS, DEFAULT_TIMINGS, type PrivateState, type PublicState } from '@pintrush/engine';
import type { RoomSnapshot } from '@pintrush/protocol';
import type { ConnectionStatus } from '../game/connection';

export type ScreenKind =
  | 'connecting'
  | 'kicked'
  | 'denied'
  | 'idle'
  | 'invite'
  | 'lobby'
  | 'complete'
  | 'break'
  | 'waiting'
  | 'intro'
  | 'pending'
  | 'open'
  | 'locked'
  | 'revealed'
  | 'round-over'
  | 'predict'
  | 'watch';

export interface ScreenInput {
  status: ConnectionStatus;
  kicked: boolean;
  snapshot: Pick<RoomSnapshot, 'activeEventKind' | 'mySessionActive' | 'mySessionId' | 'activeSessionId'> | null;
  public: PublicState | null;
  me: PrivateState | null;
}

export type ChampionshipInvite = 'none' | 'banner' | 'screen';

export interface ScreenModel {
  kind: ScreenKind;
  /** The session is paused: render the screen underneath with an overlay. */
  paused: boolean;
  /**
   * A championship is live in this room but this phone plays another session. While the phone is
   * mid-round in its own session the invite is a banner (let them finish), otherwise it takes over.
   */
  invite: ChampionshipInvite;
  /**
   * This phone's own session is over (a championship's final standings, last night's game) while
   * tonight's always-on game is the room's active session again: offer "Keep playing".
   */
  backOn: boolean;
}

const PHASE_SCREEN: Record<NonNullable<PublicState['round']>['phase'], ScreenKind> = {
  INTRO: 'intro',
  PENDING: 'pending',
  OPEN: 'open',
  LOCKED: 'locked',
  REVEALED: 'revealed',
  ROUND_OVER: 'round-over',
};

export function championshipInvite(input: ScreenInput): ChampionshipInvite {
  const snap = input.snapshot;
  if (!snap || snap.activeEventKind !== 'CHAMPIONSHIP' || snap.mySessionActive) return 'none';
  const pub = input.public;
  // Only a running round is worth finishing; a paused one (the host paused always-on for the show) is not.
  const midRound = pub !== null && pub.status === 'RUNNING' && pub.round !== null;
  return snap.mySessionId !== null && midRound ? 'banner' : 'screen';
}

/** True when the phone sits on a finished session while tonight's always-on game runs again. */
export function alwaysOnBackOn(input: Pick<ScreenInput, 'snapshot' | 'public'>): boolean {
  const snap = input.snapshot;
  return (
    snap !== null &&
    snap.activeEventKind === 'ALWAYS_ON' &&
    snap.activeSessionId !== null &&
    !snap.mySessionActive &&
    input.public?.status === 'COMPLETED'
  );
}

export type ChampionshipStage = 'lobby' | 'live';

/**
 * Whether the room's active championship is still in its lobby or already running, for phones that
 * play another session (they don't receive the championship's own stream; the server re-sends them a
 * snapshot whenever the active session's status changes).
 */
export function championshipStage(snap: Pick<RoomSnapshot, 'activeSessionStatus'>): ChampionshipStage {
  return snap.activeSessionStatus === 'WAITING' ? 'lobby' : 'live';
}

/** True while predictions are accepted (engine: INTRO, or the first question's PENDING). */
export function predictionWindowOpen(round: NonNullable<PublicState['round']>): boolean {
  return round.phase === 'INTRO' || (round.phase === 'PENDING' && round.qIndex === 0);
}

export function resolveScreen(input: ScreenInput): ScreenModel {
  const invite = championshipInvite(input);
  const base = { paused: false, invite, backOn: false };
  if (input.kicked || input.me?.status === 'KICKED') return { ...base, kind: 'kicked', invite: 'none' };
  if (input.status === 'denied') return { ...base, kind: 'denied', invite: 'none' };
  if (!input.snapshot) return { ...base, kind: 'connecting', invite: 'none' };

  const pub = input.public;
  if (invite === 'screen') return { ...base, kind: 'invite' };
  if (!pub) return { ...base, kind: 'idle' };
  if (pub.status === 'COMPLETED') return { ...base, kind: 'complete', backOn: alwaysOnBackOn(input) };
  if (pub.status === 'WAITING') return { ...base, kind: 'lobby' };

  const paused = pub.status === 'PAUSED';
  const round = pub.round;
  if (!round) return { ...base, paused, kind: 'break' };

  const me = input.me;
  if (round.kind === 'FINAL_RUSH' && !me?.isFinalist) {
    return { ...base, paused, kind: predictionWindowOpen(round) && me !== null ? 'predict' : 'watch' };
  }
  if (me?.waitingForNextRound) return { ...base, paused, kind: 'waiting' };
  return { ...base, paused, kind: PHASE_SCREEN[round.phase] };
}

/**
 * When the next round should start, for a phone that has to sit this one out. Uses the server's
 * nextRoundAt once known; otherwise the round's estimated end plus the default break for the mode
 * (an estimate: venues can tune breaks). Null when no further round is planned.
 */
export function estimateNextRoundAt(pub: PublicState): number | null {
  if (pub.nextRoundAt !== null) return pub.nextRoundAt;
  if (!pub.round) return null;
  if (pub.mode === 'ALWAYS_ON') return pub.round.estimatedEndAt + DEFAULT_TIMINGS.breakMs;
  const plan = pub.plan;
  if (!plan || plan.nextLabel === null) return null;
  const sameSegment = plan.label !== null && plan.nextLabel === plan.label;
  return (
    pub.round.estimatedEndAt + (sameSegment ? DEFAULT_SESSION_SETTINGS.championshipRoundBreakMs : DEFAULT_SESSION_SETTINGS.championshipSegmentBreakMs)
  );
}

/**
 * Server time at which Final Rush predictions close: the first question opens at the end of its
 * PENDING phase (during INTRO that is INTRO's end plus one PENDING phase).
 */
export function predictionsCloseAt(round: NonNullable<PublicState['round']>, question: PublicState['question']): number | null {
  if (round.phase === 'INTRO') return round.phaseEndsAt + DEFAULT_TIMINGS.pendingMs;
  if (round.phase === 'PENDING' && round.qIndex === 0) return question?.opensAt ?? round.phaseEndsAt;
  return null;
}
