'use client';

import type { Standing } from '@pintrush/engine';
import { useEffect, useRef, useState } from 'react';
import { cn } from '@/lib/cn';
import { formatPoints } from '@/lib/tv/format';
import { rankMoves } from '@/lib/tv/results';
import { useFlip } from '../useFlip';

function prefersReducedMotion(): boolean {
  return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

/** Counts up (or down) to a new score instead of jumping, like a broadcast scoreboard. */
export function PointsTicker({ value, className }: { value: number; className?: string }) {
  const [shown, setShown] = useState(value);
  const fromRef = useRef(value);

  useEffect(() => {
    const from = fromRef.current;
    fromRef.current = value;
    if (from === value) return;
    if (prefersReducedMotion()) {
      setShown(value);
      return;
    }
    const started = performance.now();
    const duration = 900;
    let frame = requestAnimationFrame(function step(now) {
      const t = Math.min(1, (now - started) / duration);
      const eased = 1 - (1 - t) ** 3;
      setShown(Math.round(from + (value - from) * eased));
      if (t < 1) frame = requestAnimationFrame(step);
    });
    // rAF pauses in hidden tabs and off-screen iframes; always land on the real score regardless.
    const settle = setTimeout(() => setShown(value), duration + 50);
    return () => {
      cancelAnimationFrame(frame);
      clearTimeout(settle);
    };
  }, [value]);

  return <span className={cn('num', className)}>{formatPoints(shown)}</span>;
}

const sizes = {
  // Row heights are budgeted against the 1080 px stage: 10 full rows + gaps = 732 px.
  mini: { row: 'h-[60px]', badge: 'size-[44px] text-[22px] rounded-[12px]', name: 'text-[28px]', points: 'text-[28px]', gap: 'gap-[14px]' },
  full: { row: 'h-[66px]', badge: 'size-[50px] text-[26px] rounded-[14px]', name: 'text-[32px]', points: 'text-[32px]', gap: 'gap-[18px]' },
} as const;

function rankBadgeTone(rank: number): string {
  if (rank === 1) return 'bg-amber text-on-bright';
  if (rank <= 3) return 'bg-violet/30 text-violet-soft';
  return 'bg-raised text-muted';
}

/**
 * Leaderboard rows with FLIP rank changes. When `previous` standings are given (the board before
 * this reveal), it first shows them, then shuffles into the new order with the points gained.
 */
export function Leaderboard({
  standings,
  previous,
  limit = 10,
  size = 'full',
  shuffleDelayMs = 1100,
  className,
  label,
}: {
  standings: readonly Standing[];
  previous?: readonly Standing[] | null;
  limit?: number;
  size?: keyof typeof sizes;
  shuffleDelayMs?: number;
  className?: string;
  /** Accessible name for the list. */
  label: string;
}) {
  const [showCurrent, setShowCurrent] = useState(!previous);
  useEffect(() => {
    if (!previous) return;
    const timer = setTimeout(() => setShowCurrent(true), shuffleDelayMs);
    return () => clearTimeout(timer);
  }, [previous, shuffleDelayMs]);

  const rows = (showCurrent || !previous ? standings : previous).slice(0, limit);
  const moves = showCurrent && previous ? rankMoves(previous, standings) : null;
  const listRef = useRef<HTMLOListElement>(null);
  useFlip(listRef, rows.map((r) => r.playerId).join('|'));
  const sz = sizes[size];

  return (
    <ol ref={listRef} aria-label={label} className={cn('relative flex flex-col gap-[8px]', className)}>
      {rows.map((row) => {
        const move = moves?.get(row.playerId);
        return (
          <li
            key={row.playerId}
            data-flip-key={row.playerId}
            className={cn('relative flex items-center rounded-[16px] px-[14px]', sz.row, sz.gap, row.rank === 1 ? 'bg-amber/10 ring-1 ring-amber/40' : 'bg-raised/45')}
          >
            <span className={cn('num grid shrink-0 place-items-center font-display font-bold', sz.badge, rankBadgeTone(row.rank))}>{row.rank}</span>
            <span className={cn('min-w-0 flex-1 truncate font-semibold', sz.name)}>{row.nickname}</span>
            {move && move.places > 0 && (
              <span className="num shrink-0 rounded-full bg-lime/15 px-[12px] py-[2px] text-[22px] font-bold text-lime-soft">
                <span aria-hidden="true">▲</span>
                <span className="sr-only">up </span>
                {move.places}
              </span>
            )}
            {move && move.gained > 0 && (
              <span className="num shrink-0 rounded-full bg-amber px-[12px] py-[2px] font-display text-[20px] font-extrabold text-on-bright motion-safe:animate-score-bump">
                +{formatPoints(move.gained)}
              </span>
            )}
            <PointsTicker value={row.points} className={cn('shrink-0 font-display font-bold', sz.points)} />
          </li>
        );
      })}
    </ol>
  );
}
