'use client';

import { useEffect, useRef } from 'react';
import { DEFAULT_TIMINGS } from '@pintrush/engine';
import {
  AnswerButton,
  Board,
  CategoryChip,
  Confetti,
  CountdownBar,
  CountdownRing,
  LeaderboardList,
  cellName,
  ScoreDelta,
  openPositions,
  standingRows,
  teamRows,
  tileForCategory,
  type AnswerState,
} from '@/components/game';
import { Badge } from '@/components/ui/Badge';
import { BoltIcon, CheckIcon, ClockIcon, DoubleDownIcon, LockIcon, SecondChanceIcon, TrophyIcon, UsersIcon, XIcon } from '@/components/ui/icons';
import { cn } from '@/lib/cn';
import { vibrate } from '@/lib/play/device';
import { formatElapsed, formatPoints, formatSigned, ordinal } from '@/lib/play/format';
import { challengeWinnerNames, pointsBreakdown, resultKind, totalPoints, wonTableChallenge, type PointsItem } from '@/lib/play/points';
import { estimateNextRoundAt } from '@/lib/play/screen';
import gameStyles from '@/components/game/game.module.css';
import { ActionDock, Eyebrow, ScreenBody } from '../chrome';
import { Holding, MyBoard, PowerUpDock, StatTile, TeamStatus, hasPowerUpDock, letterFor, rankText, swapSelection } from '../parts';
import type { ActivePublic, PlayCtx } from '../types';

const KIND_BLURB: Record<string, string> = {
  RUSH: '9 categories plus a wildcard. Light three in a row for +250 — fill the board for +500.',
  MUSIC_MAYHEM: 'Nine music categories, same board. Artists, eras, records — no lyrics, no recordings.',
  POWER_ROUND: 'Same board, power-ups in the spotlight. Lines earn tokens — spend them wisely.',
  TABLE_WARS: 'Every question is a team challenge: the table with the best accuracy wins +200 for each player.',
  FINAL_RUSH: 'The top five play five hard questions. Everyone else calls the winner.',
};

function active(ctx: PlayCtx): ActivePublic {
  return ctx.pub as ActivePublic;
}

/** Category chip + question number + difficulty + Double Down marker. */
function QuestionMeta({ ctx, showCategory = true }: { ctx: PlayCtx; showCategory?: boolean }) {
  const pub = active(ctx);
  const q = pub.question;
  if (!q) return null;
  const doubled = ctx.me?.powerUps.doubleDownOn === q.id;
  return (
    <div className="flex flex-wrap items-center gap-2">
      {showCategory &&
        (q.isWildcard ? (
          <CategoryChip category="wildcard" label="Wildcard" size="sm" tone="amber" />
        ) : (
          <CategoryChip category={q.category} label={q.categoryLabel} size="sm" />
        ))}
      <span className="num text-sm font-semibold text-muted">
        Q{q.index + 1} of {pub.round.questionCount}
      </span>
      {q.difficulty === 'DIFFICULT' && (
        <Badge tone="violet" className="px-2 py-0.5 text-[0.6875rem]">
          Hard · 150
        </Badge>
      )}
      {doubled && (
        <Badge tone="amber" className="px-2 py-0.5 text-[0.6875rem]">
          <DoubleDownIcon /> Double Down
        </Badge>
      )}
    </div>
  );
}

// ───────────────────────────── INTRO ─────────────────────────────

export function IntroScreen({ ctx }: { ctx: PlayCtx }) {
  const pub = active(ctx);
  const { round } = pub;
  const me = ctx.me;
  const plan = pub.plan;
  const swapping = ctx.ctl.swap !== null;
  const awaitingBoard = round.hasBoard && me !== null && !me.inRound && !me.waitingForNextRound;
  return (
    <>
      <ScreenBody>
        <Eyebrow tone="violet">{plan ? `Round ${plan.index + 1} of ${plan.total}` : `Round ${round.index + 1} tonight`}</Eyebrow>
        <h2 className="text-gradient-brand mt-1 font-display text-[clamp(2.25rem,11vw,3.25rem)] leading-[1.02] font-extrabold motion-safe:animate-rise">
          {round.label}
        </h2>
        <p className="mt-2 text-muted">{KIND_BLURB[round.kind]}</p>
        <CountdownBar
          conn={ctx.conn}
          endsAt={round.phaseEndsAt}
          totalMs={DEFAULT_TIMINGS.introMs}
          label="Round starts in"
          alert={false}
          className="mt-4"
        />

        {me?.board && (
          <div className="mt-5">
            <MyBoard ctx={ctx} reveal={!swapping} {...swapSelection(ctx)} />
            <p className="mt-2 text-center text-sm text-subtle">Your board — one square per category. Answer right to light it.</p>
          </div>
        )}
        {awaitingBoard && <Holding title="Dealing your board…" />}
        {round.kind === 'TABLE_WARS' && <TeamStatus ctx={ctx} className="mt-5" />}
        {round.kind === 'FINAL_RUSH' && round.finalists && (
          <div className="mt-5 rounded-2xl border border-amber/50 bg-amber/10 p-4">
            <p className="flex items-center gap-2 font-display text-lg font-bold text-amber-soft">
              <TrophyIcon className="size-5" /> You’re in the Final Rush!
            </p>
            <ul className="mt-2 flex flex-wrap gap-2">
              {round.finalists.map((f) => (
                <li
                  key={f.playerId}
                  className={cn(
                    'rounded-full border px-3 py-1 text-sm font-semibold',
                    f.playerId === me?.playerId ? 'border-amber bg-amber text-on-bright' : 'border-line-strong text-fg',
                  )}
                >
                  {f.nickname}
                  {f.playerId === me?.playerId && ' (you)'}
                </li>
              ))}
            </ul>
          </div>
        )}
      </ScreenBody>
      {hasPowerUpDock(me, swapping) && (
        <ActionDock>
          <PowerUpDock ctx={ctx} />
        </ActionDock>
      )}
    </>
  );
}

// ───────────────────────────── PENDING ─────────────────────────────

export function PendingScreen({ ctx }: { ctx: PlayCtx }) {
  const pub = active(ctx);
  const q = pub.question;
  const me = ctx.me;
  const swapping = ctx.ctl.swap !== null;
  if (!q)
    return (
      <ScreenBody center>
        <Holding title="Next question coming up…" />
      </ScreenBody>
    );
  const board = me?.board ?? null;
  const highlight = board && !q.isWildcard ? tileForCategory(board.tiles, q.category) : null;
  const wildcardSelect = board && q.isWildcard && !swapping;
  const picked = ctx.ctl.wildcardPick;
  return (
    <>
      <ScreenBody>
        <QuestionMeta ctx={ctx} showCategory={false} />
        <div className="mt-4 flex items-center justify-between gap-3">
          <div className="min-w-0">
            <Eyebrow tone={q.isWildcard ? 'amber' : 'cyan'}>Up next</Eyebrow>
            <div className="mt-2 motion-safe:animate-rise">
              {q.isWildcard ? (
                <CategoryChip category="wildcard" label="Wildcard" size="lg" tone="amber" />
              ) : (
                <CategoryChip category={q.category} label={q.categoryLabel} size="lg" />
              )}
            </div>
          </div>
          <CountdownRing
            conn={ctx.conn}
            endsAt={q.opensAt}
            totalMs={DEFAULT_TIMINGS.pendingMs}
            label="Question opens in"
            alert={false}
            className="size-16"
          />
        </div>
        {pub.round.kind === 'TABLE_WARS' && (
          <p className="mt-4 flex items-center gap-2 rounded-xl border border-cyan/40 bg-cyan/10 px-3 py-2 text-sm font-semibold text-cyan-soft">
            <UsersIcon className="size-5 shrink-0" /> Team challenge: best table accuracy wins +200 each.
          </p>
        )}
        {q.isWildcard && board && (
          <p className="mt-4 text-sm text-muted">
            <span className="font-semibold text-amber-soft">Wildcard:</span> get it right and claim any unlit square.{' '}
            {wildcardSelect && 'Pick one now, or let us choose the square that completes the most lines.'}
          </p>
        )}
        {board && (
          <div className="mt-4">
            <MyBoard
              ctx={ctx}
              highlight={highlight}
              {...(swapping
                ? swapSelection(ctx)
                : wildcardSelect
                  ? {
                      selectable: openPositions(board.tiles),
                      selected: picked === null ? [] : [picked],
                      selectionMark: 'check' as const,
                      onSelect: (p: number) => ctx.ctl.chooseWildcard(picked === p ? null : p),
                    }
                  : {})}
            />
          </div>
        )}
        {pub.round.kind === 'TABLE_WARS' && <TeamStatus ctx={ctx} className="mt-4" />}
      </ScreenBody>
      {hasPowerUpDock(me, swapping) && (
        <ActionDock>
          <PowerUpDock ctx={ctx} />
        </ActionDock>
      )}
    </>
  );
}

// ───────────────────────────── OPEN / LOCKED ─────────────────────────────

/** The square this phone will claim on a correct wildcard answer (server record once answered). */
function wildcardChoice(ctx: PlayCtx): number | null {
  const answered = ctx.ctl.pick !== null;
  const serverChoice = ctx.me?.currentAnswer?.wildcardPosition ?? null;
  return answered ? (serverChoice ?? ctx.ctl.wildcardPick) : ctx.ctl.wildcardPick;
}

const WILDCARD_PICKER_ID = 'wildcard-picker';

/** One line under the prompt, so the choice is visible even when the picker is below the fold. */
function WildcardSummary({ ctx }: { ctx: PlayCtx }) {
  const board = ctx.me?.board;
  if (!board) return null;
  const picked = wildcardChoice(ctx);
  const tile = picked === null ? null : board.tiles[picked];
  const name = tile
    ? `${board.categoryLabels[tile.category] ?? tile.category} (${cellName(tile.position)})`
    : 'Auto — the square that completes the most lines';
  return (
    <button
      type="button"
      onClick={() => document.getElementById(WILDCARD_PICKER_ID)?.scrollIntoView({ behavior: 'smooth', block: 'center' })}
      className="mt-3 flex min-h-12 w-full items-center gap-2 rounded-xl border border-amber/45 bg-amber/10 px-3 text-left text-sm"
    >
      <BoltIcon className="size-4 shrink-0 text-amber" />
      <span className="min-w-0 flex-1">
        <span className="font-semibold text-amber-soft">Wildcard square: </span>
        <span className="text-fg">{name}</span>
      </span>
      <span className="shrink-0 text-xs font-bold tracking-[0.1em] text-amber-soft uppercase">{ctx.ctl.pick === null ? 'Change' : 'Locked'}</span>
    </button>
  );
}

function WildcardPicker({ ctx, locked }: { ctx: PlayCtx; locked: boolean }) {
  const board = ctx.me?.board;
  if (!board) return null;
  const picked = wildcardChoice(ctx);
  const frozen = ctx.ctl.pick !== null || locked;
  return (
    <section
      id={WILDCARD_PICKER_ID}
      aria-labelledby="wildcard-title"
      className="mt-4 scroll-mt-4 rounded-2xl border border-amber/45 bg-amber/[0.07] p-3"
    >
      <h3 id="wildcard-title" className="flex items-center gap-2 text-sm font-bold text-amber-soft">
        <BoltIcon className="size-4" /> Pick the square you’ll claim if you’re right
      </h3>
      <p className="mt-1 text-xs text-muted">
        {picked === null ? 'Auto: we’ll pick the square that completes the most lines.' : 'Tap it again to go back to auto-pick.'}
      </p>
      <div className="mx-auto mt-3 w-full max-w-[15rem]">
        <Board
          tiles={board.tiles}
          categoryLabels={board.categoryLabels}
          lines={board.lines}
          label="Wildcard square picker"
          selectable={frozen ? [] : openPositions(board.tiles)}
          selected={picked === null ? [] : [picked]}
          selectionMark="check"
          onSelect={(p) => ctx.ctl.chooseWildcard(picked === p ? null : p)}
        />
      </div>
    </section>
  );
}

/**
 * Taps in the first moments after a question appears were aimed at the previous screen (a power-up
 * button, the reveal) — the answers render in the same thumb zone. Ignore them briefly.
 */
const TAP_GUARD_MS = 450;

export function QuestionScreen({ ctx, locked }: { ctx: PlayCtx; locked: boolean }) {
  const pub = active(ctx);
  const q = pub.question;
  const { ctl, me } = ctx;
  // A timestamp, not a timer: throttled timers (background tabs, battery saver) must never swallow taps.
  const shownAt = useRef<{ id: string | null; at: number }>({ id: null, at: 0 });
  const questionId = q?.id ?? null;
  useEffect(() => {
    shownAt.current = { id: questionId, at: performance.now() };
  }, [questionId]);
  if (!q || !q.options || q.prompt === null)
    return (
      <ScreenBody center>
        <Holding title="Loading the question…" />
      </ScreenBody>
    );
  const total = q.closesAt !== null && q.openedAt !== null ? q.closesAt - q.openedAt : DEFAULT_TIMINGS.windowMs;
  const pick = ctl.pick;
  const canSecondChance = !locked && pick === null && (me?.powerUps.available.includes('SECOND_CHANCE') ?? false) && !ctl.removedOptionId;
  const pickedIndex = pick ? q.options.findIndex((o) => o.id === pick) : -1;
  const pickedOption = pickedIndex >= 0 ? q.options[pickedIndex] : undefined;

  return (
    <>
      <ScreenBody>
        <QuestionMeta ctx={ctx} />
        {locked ? (
          <p className="mt-3 flex items-center gap-2 font-semibold text-violet-soft">
            <LockIcon className="size-5" /> Answers locked — revealing…
          </p>
        ) : (
          <CountdownBar conn={ctx.conn} endsAt={q.closesAt} totalMs={total} label="Time left to answer" className="mt-3" />
        )}
        <h2 className="mt-4 font-display text-[clamp(1.375rem,6.2vw,1.875rem)] leading-snug font-semibold text-balance">{q.prompt}</h2>
        <p className="num mt-2 text-sm text-subtle">
          {pub.answeredCount} of {pub.participantCount} answered
        </p>
        {locked && (
          <p className="mt-3 rounded-xl border border-line bg-surface/80 px-4 py-3 text-sm">
            {pickedOption ? (
              <>
                Your answer: <span className="font-semibold text-fg">{`${letterFor(pickedIndex)} · ${pickedOption.text}`}</span>
              </>
            ) : (
              <span className="text-muted">No answer this time — catch the next one.</span>
            )}
          </p>
        )}
        {q.isWildcard && <WildcardSummary ctx={ctx} />}
        {q.isWildcard && <WildcardPicker ctx={ctx} locked={locked} />}
      </ScreenBody>
      <ActionDock>
        {canSecondChance && (
          <button
            type="button"
            onClick={() => void ctl.secondChance()}
            disabled={ctl.powerBusy !== null}
            className="mb-2.5 flex min-h-12 w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-cyan/70 bg-cyan/10 px-3 text-sm font-semibold text-cyan-soft hover:bg-cyan/20 disabled:opacity-50"
          >
            <SecondChanceIcon className="size-5" />
            {ctl.powerBusy === 'SECOND_CHANCE' ? 'Removing a wrong answer…' : 'Second Chance: remove a wrong answer'}
          </button>
        )}
        <div role="group" aria-label={locked ? 'Answers (locked)' : 'Answers — one tap locks it in'} className="flex flex-col gap-2">
          {q.options.map((o, i) => {
            const removed = o.id === ctl.removedOptionId && o.id !== pick;
            const state: AnswerState = removed ? 'removed' : pick ? (o.id === pick ? 'picked' : 'dimmed') : locked ? 'dimmed' : 'idle';
            return (
              <AnswerButton
                key={o.id}
                letter={letterFor(i)}
                text={o.text}
                state={state}
                sending={ctl.sending && o.id === pick}
                // The picked answer stays focusable (aria-disabled) so keyboard and screen-reader
                // focus doesn't fall to <body> the moment an answer locks in.
                disabled={locked || removed || (pick !== null && o.id !== pick)}
                aria-disabled={o.id === pick || undefined}
                onClick={() => {
                  if (pick !== null) return;
                  if (shownAt.current.id === q.id && performance.now() - shownAt.current.at < TAP_GUARD_MS) return;
                  void ctl.submitAnswer(o.id);
                }}
              />
            );
          })}
        </div>
      </ActionDock>
    </>
  );
}

// ───────────────────────────── REVEALED ─────────────────────────────

const toneClass: Record<PointsItem['tone'], string> = {
  base: 'text-fg',
  speed: 'text-cyan-soft',
  bonus: 'text-amber-soft',
  team: 'text-cyan-soft',
  penalty: 'text-danger-soft',
  token: 'text-lime-soft',
};

function PointsList({ items }: { items: PointsItem[] }) {
  return (
    <ul className="mt-4 divide-y divide-line/70 rounded-2xl border border-line bg-surface/80 px-4" aria-label="Points this question">
      {items.map((item) => (
        <li key={item.key} className="flex min-h-11 items-center justify-between gap-3 py-2">
          <span className={cn('font-semibold', toneClass[item.tone])}>{item.label}</span>
          {item.points === null ? (
            <span className="inline-flex items-center gap-1 rounded-full bg-lime px-2.5 py-0.5 text-xs font-extrabold tracking-[0.08em] text-on-bright uppercase">
              <BoltIcon className="size-3.5" /> Token
            </span>
          ) : (
            <span className={cn('num font-display text-lg font-bold', item.points < 0 ? 'text-danger-soft' : 'text-fg')}>
              {formatSigned(item.points)}
            </span>
          )}
        </li>
      ))}
    </ul>
  );
}

export function RevealScreen({ ctx }: { ctx: PlayCtx }) {
  const pub = active(ctx);
  const { me } = ctx;
  const q = pub.question;
  const reveal = pub.reveal;
  const result = me?.lastResult ?? null;
  const correctId = result?.correctOptionId ?? reveal?.correctOptionId ?? null;
  const options = q?.options ?? [];
  const correctIndex = options.findIndex((o) => o.id === correctId);
  const correctOption = correctIndex >= 0 ? options[correctIndex] : undefined;
  const isTableWars = pub.round.kind === 'TABLE_WARS';
  const tableWon = isTableWars && me ? wonTableChallenge(reveal?.challengeWinners, me) : false;
  const items = result && q ? pointsBreakdown(result, { difficulty: q.difficulty, tableChallengeWon: tableWon }) : [];
  const total = totalPoints(items);
  const kind = result ? resultKind(result) : null;
  const celebrate = ctx.live && result !== null && (result.fullBoard || result.newLines.length > 0);
  const swapping = ctx.ctl.swap !== null;

  // Haptics once per live reveal (not on reload/resync of an old reveal).
  const buzzed = useRef(false);
  useEffect(() => {
    if (!ctx.live || !result || buzzed.current) return;
    buzzed.current = true;
    if (result.fullBoard || result.newLines.length > 0) vibrate([30, 60, 30, 60, 90]);
    else if (result.correct) vibrate(35);
    else vibrate([60, 50, 60]);
  }, [ctx.live, result]);

  const answeredCount = reveal?.answeredCount ?? 0;
  const winnerNames = isTableWars ? challengeWinnerNames(reveal?.challengeWinners, pub) : [];

  return (
    <>
      {/* Celebration layer over the whole game area (the parent <main> is the positioning box). */}
      {celebrate && (
        <div aria-hidden="true" className="pointer-events-none absolute inset-0 z-40 overflow-hidden">
          <Confetti tone={result.fullBoard ? 'gold' : 'line'} pieces={result.fullBoard ? 56 : 36} />
          <div className="absolute inset-x-0 top-[30%] flex justify-center motion-reduce:hidden">
            <p
              className={cn(
                'rounded-2xl px-6 py-3 font-display text-3xl font-extrabold whitespace-nowrap text-on-bright opacity-0',
                result.fullBoard
                  ? 'bg-[linear-gradient(100deg,var(--color-amber),var(--color-lime))] shadow-glow-lime'
                  : 'bg-amber shadow-glow-amber',
                gameStyles.toast,
              )}
            >
              {result.fullBoard
                ? 'FULL BOARD! +500'
                : result.newLines.length > 1
                  ? `${result.newLines.length} LINES! +${result.newLines.length * 250}`
                  : 'LINE! +250'}
            </p>
          </div>
        </div>
      )}
      <ScreenBody>
        <QuestionMeta ctx={ctx} />
        {kind ? (
          <div className="mt-4 flex items-center gap-4">
            <span
              aria-hidden="true"
              className={cn(
                'grid size-16 shrink-0 place-items-center rounded-2xl motion-safe:animate-tile-pop',
                kind === 'correct' && 'bg-lime text-on-bright shadow-glow-lime',
                kind === 'incorrect' && 'bg-danger text-on-bright shadow-glow-danger',
                kind === 'missed' && 'border border-line-strong bg-raised text-muted',
              )}
            >
              {kind === 'correct' ? (
                <CheckIcon className="size-9" strokeWidth={3} />
              ) : kind === 'incorrect' ? (
                <XIcon className="size-9" strokeWidth={3} />
              ) : (
                <ClockIcon className="size-9" />
              )}
            </span>
            <div className="min-w-0 flex-1">
              <h2
                className={cn(
                  'font-display text-[clamp(1.625rem,7.5vw,2rem)] leading-tight font-extrabold',
                  kind === 'correct' ? 'text-lime-soft' : kind === 'incorrect' ? 'text-danger-soft' : 'text-fg',
                )}
              >
                {kind === 'correct' ? 'Correct!' : kind === 'incorrect' ? 'Not this time' : 'Time’s up'}
              </h2>
              {/* The total sits under the title so long titles and big totals never collide on narrow phones. */}
              <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1">
                {total !== 0 && <ScoreDelta value={total} size="md" />}
                <p className="text-sm text-muted">
                  {kind === 'correct' ? 'Nice one.' : kind === 'incorrect' ? 'Shake it off — next one’s yours.' : 'No answer reached us in time.'}
                </p>
              </div>
            </div>
          </div>
        ) : (
          <h2 className="mt-4 font-display text-2xl font-bold">The answer</h2>
        )}

        {correctOption && (
          <p className="mt-4 flex items-center gap-3 rounded-2xl border-2 border-lime/70 bg-lime/10 px-4 py-3">
            <CheckIcon className="size-6 shrink-0 text-lime" strokeWidth={3} />
            <span className="min-w-0">
              <span className="block text-xs font-bold tracking-[0.14em] text-lime-soft uppercase">Correct answer</span>
              <span className="block font-semibold">{`${letterFor(correctIndex)} · ${correctOption.text}`}</span>
            </span>
          </p>
        )}

        {items.length > 0 && <PointsList items={items} />}

        {isTableWars && reveal && (
          <p
            className={cn(
              'mt-4 flex items-start gap-3 rounded-2xl border px-4 py-3 text-sm',
              tableWon ? 'border-lime/60 bg-lime/10' : 'border-line bg-surface/80',
            )}
          >
            <UsersIcon className={cn('mt-0.5 size-5 shrink-0', tableWon ? 'text-lime' : 'text-muted')} />
            <span>
              {tableWon ? (
                <span className="font-semibold text-lime-soft">Your table won this challenge! +200 each.</span>
              ) : winnerNames.length > 0 ? (
                <>
                  Challenge won by <span className="font-semibold text-fg">{winnerNames.join(', ')}</span>.
                </>
              ) : (
                'No table got this one.'
              )}
            </span>
          </p>
        )}

        {me?.board && (
          <div className="mt-5">
            <MyBoard
              ctx={ctx}
              popPosition={ctx.live ? (result?.tileActivated ?? null) : null}
              animateLines={ctx.live ? (result?.newLines ?? []) : []}
              {...swapSelection(ctx)}
            />
          </div>
        )}

        {options.length > 0 && reveal && (
          <div className="mt-5">
            <p className="mb-2 text-xs font-bold tracking-[0.14em] text-subtle uppercase">How the room answered</p>
            <div className="flex flex-col gap-2">
              {options.map((o, i) => {
                const mine = result?.answered && me?.currentAnswer?.optionId === o.id;
                const state: AnswerState = o.id === correctId ? 'correct' : mine ? 'incorrect' : 'faded';
                return (
                  <AnswerButton
                    key={o.id}
                    static
                    letter={letterFor(i)}
                    text={o.text}
                    state={state}
                    share={answeredCount > 0 ? (reveal.distribution[o.id] ?? 0) / answeredCount : 0}
                  />
                );
              })}
            </div>
            <p className="num mt-3 text-sm text-subtle">
              {reveal.correctCount} of {reveal.answeredCount} got it
              {reveal.fastest[0] && ` · Fastest: ${reveal.fastest[0].nickname} (${formatElapsed(reveal.fastest[0].elapsedMs)})`}
            </p>
          </div>
        )}
      </ScreenBody>
      {hasPowerUpDock(me, swapping) && (
        <ActionDock>
          <PowerUpDock ctx={ctx} />
        </ActionDock>
      )}
    </>
  );
}

// ───────────────────────────── ROUND_OVER ─────────────────────────────

export function RoundOverScreen({ ctx }: { ctx: PlayCtx }) {
  const pub = active(ctx);
  const { me } = ctx;
  const board = me?.board ?? null;
  const lit = board ? board.tiles.filter((t) => t.activated).length : 0;
  const top = standingRows(pub.leaderboard, { highlightId: me?.playerId, limit: 3 });
  const meInTop = top.some((r) => r.highlight);
  const nextAt = estimateNextRoundAt(pub);
  return (
    <ScreenBody>
      <Eyebrow tone="lime">Round complete</Eyebrow>
      <h2 className="mt-1 font-display text-[clamp(2rem,9vw,2.75rem)] leading-tight font-extrabold">{pub.round.label}</h2>
      {me && (
        <div className="mt-4 grid grid-cols-2 gap-3">
          <StatTile label="This round" value={formatSigned(me.roundPoints)} tone={me.roundPoints > 0 ? 'lime' : 'default'} />
          <StatTile
            label="Tonight"
            value={me.totals.rank ? ordinal(me.totals.rank) : '—'}
            sub={`${formatPoints(me.totals.points)} pts`}
            tone="violet"
          />
        </div>
      )}
      {board && (
        <div className="mt-5">
          <MyBoard ctx={ctx} />
          <p className="mt-2 text-center text-sm text-muted">
            {board.fullBoard ? 'Full board — legendary.' : `${lit} of 9 lit · ${board.lines.length} ${board.lines.length === 1 ? 'line' : 'lines'}`}
          </p>
        </div>
      )}
      {pub.round.kind === 'TABLE_WARS' && pub.teamLeaderboard.length > 0 && (
        <section className="mt-6" aria-labelledby="ro-teams">
          <h3 id="ro-teams" className="mb-2 text-xs font-bold tracking-[0.14em] text-subtle uppercase">
            Top tables
          </h3>
          <LeaderboardList rows={teamRows(pub.teamLeaderboard, { highlightId: me?.teamId, limit: 3 })} label="Top tables" />
        </section>
      )}
      <section className="mt-6" aria-labelledby="ro-top">
        <h3 id="ro-top" className="mb-2 text-xs font-bold tracking-[0.14em] text-subtle uppercase">
          Top of the leaderboard
        </h3>
        <LeaderboardList rows={top} label="Top three players" />
        {me && !meInTop && <p className="mt-2 text-center text-sm text-muted">You: {rankText(me)}</p>}
      </section>
      {nextAt !== null && (
        <p className="mt-6 text-center text-sm text-subtle">
          {pub.plan?.nextLabel ? `Up next: ${pub.plan.nextLabel}` : 'Next round after a short break'} — stay close.
        </p>
      )}
    </ScreenBody>
  );
}
