/**
 * Leaderboards (ARCHITECTURE §2.4–2.5). Ranks are always unique: ties fall through
 * points → correct → correctTimeMs → lastCorrectAt (null last) → joinIndex.
 * KICKED players are removed from every board; LEFT players keep their place.
 */
import type { CompletedRoundSummary, GameState, Id, PlayerState, PlayerTotals, RoundState, Standing, TeamStanding } from './types';

export interface StandingKey {
  points: number;
  correct: number;
  correctTimeMs: number;
  lastCorrectAt: number | null;
  joinIndex: number;
}

/** Sort comparator: negative when `a` ranks above `b`. Never returns 0 for different joinIndex. */
export function compareStandings(a: StandingKey, b: StandingKey): number {
  if (a.points !== b.points) return b.points - a.points;
  if (a.correct !== b.correct) return b.correct - a.correct;
  if (a.correctTimeMs !== b.correctTimeMs) return a.correctTimeMs - b.correctTimeMs;
  if (a.lastCorrectAt !== b.lastCorrectAt) {
    if (a.lastCorrectAt === null) return 1;
    if (b.lastCorrectAt === null) return -1;
    return a.lastCorrectAt - b.lastCorrectAt;
  }
  return a.joinIndex - b.joinIndex;
}

export function emptyTotals(): PlayerTotals {
  return { points: 0, correct: 0, answered: 0, correctTimeMs: 0, lastCorrectAt: null, lines: 0, fullBoards: 0 };
}

/** Players that appear on leaderboards, in joinIndex order. */
export function rankedPlayers(state: Pick<GameState, 'players'>): PlayerState[] {
  return Object.values(state.players)
    .filter((p) => p.status !== 'KICKED')
    .sort((a, b) => a.joinIndex - b.joinIndex);
}

/** Session standings for every non-kicked player. */
export function rankPlayers(state: Pick<GameState, 'players' | 'totals'>): Standing[] {
  return rankedPlayers(state)
    .map((p) => ({ p, t: state.totals[p.id] ?? emptyTotals() }))
    .sort((x, y) => compareStandings({ ...x.t, joinIndex: x.p.joinIndex }, { ...y.t, joinIndex: y.p.joinIndex }))
    .map(({ p, t }, i) => ({
      rank: i + 1,
      playerId: p.id,
      nickname: p.nickname,
      teamId: p.teamId,
      points: t.points,
      correct: t.correct,
      correctTimeMs: t.correctTimeMs,
      lastCorrectAt: t.lastCorrectAt,
      lines: t.lines,
    }));
}

export interface TeamGroup {
  /** Team id, or `solo:<playerId>` for a player without a team. */
  id: Id;
  name: string;
  memberIds: Id[];
  /** No team, or a team with exactly one (non-kicked) member. */
  isSolo: boolean;
}

/** Teams and solo players as ranked units. Empty teams are dropped. */
export function teamGroups(state: Pick<GameState, 'players' | 'teams'>): TeamGroup[] {
  const members = new Map<Id, Id[]>();
  const solos: TeamGroup[] = [];
  for (const p of rankedPlayers(state)) {
    if (p.teamId !== null && state.teams[p.teamId]) {
      const list = members.get(p.teamId) ?? [];
      list.push(p.id);
      members.set(p.teamId, list);
    } else {
      solos.push({ id: `solo:${p.id}`, name: p.nickname, memberIds: [p.id], isSolo: true });
    }
  }
  const teams = Object.values(state.teams)
    .filter((t) => members.has(t.id))
    .sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
    .map((t): TeamGroup => {
      const ids = members.get(t.id) ?? [];
      return { id: t.id, name: t.name, memberIds: ids, isSolo: ids.length === 1 };
    });
  return [...teams, ...solos];
}

export interface TeamBoards {
  /** mergeSolos=false: teams of 2+ only. mergeSolos=true: everyone (teams and solos) ranked together. */
  teams: TeamStanding[];
  /** mergeSolos=false: the separate Solo board. mergeSolos=true: null. */
  solos: TeamStanding[] | null;
}

/**
 * Team score = mean points over members with ≥ 1 answer (1 decimal); 0 when nobody answered.
 * Ties: higher unrounded mean, then the unit containing the best-ranked player (unique).
 */
export function teamStandings(state: Pick<GameState, 'players' | 'teams' | 'totals' | 'settings'>): TeamBoards {
  const playerRank = new Map(rankPlayers(state).map((s) => [s.playerId, s.rank]));
  const scored = teamGroups(state).map((g) => {
    const active = g.memberIds.filter((id) => (state.totals[id]?.answered ?? 0) >= 1);
    const sum = active.reduce((acc, id) => acc + (state.totals[id]?.points ?? 0), 0);
    const mean = active.length > 0 ? sum / active.length : 0;
    const bestRank = Math.min(...g.memberIds.map((id) => playerRank.get(id) ?? Number.MAX_SAFE_INTEGER));
    return { g, mean, bestRank, activeMembers: active.length };
  });
  const rank = (list: typeof scored): TeamStanding[] =>
    list
      .sort((a, b) => b.mean - a.mean || a.bestRank - b.bestRank)
      .map((x, i) => ({
        rank: i + 1,
        teamId: x.g.id,
        name: x.g.name,
        score: Math.round(x.mean * 10) / 10,
        members: x.g.memberIds.length,
        activeMembers: x.activeMembers,
        isSolo: x.g.isSolo,
      }));
  if (state.settings.mergeSolos) return { teams: rank(scored), solos: null };
  return { teams: rank(scored.filter((x) => !x.g.isSolo)), solos: rank(scored.filter((x) => x.g.isSolo)) };
}

export interface RoundStanding {
  playerId: Id;
  nickname: string;
  points: number;
  correct: number;
  correctTimeMs: number;
  lastCorrectAt: number | null;
}

/** Round leaderboard over the round's (non-kicked) participants, same tie-break chain. */
export function rankRound(state: Pick<GameState, 'players'>, round: RoundState): RoundStanding[] {
  const rows = round.participants
    .map((id) => state.players[id])
    .filter((p): p is PlayerState => p !== undefined && p.status !== 'KICKED')
    .map((p) => {
      let correct = 0;
      let correctTimeMs = 0;
      let lastCorrectAt: number | null = null;
      for (const byPlayer of Object.values(round.answers)) {
        const a = byPlayer[p.id];
        if (a?.correct) {
          correct++;
          correctTimeMs += a.elapsedMs;
          lastCorrectAt = lastCorrectAt === null ? a.receivedAt : Math.max(lastCorrectAt, a.receivedAt);
        }
      }
      return { p, row: { playerId: p.id, nickname: p.nickname, points: round.roundPoints[p.id] ?? 0, correct, correctTimeMs, lastCorrectAt } };
    });
  return rows
    .sort((x, y) => compareStandings({ ...x.row, joinIndex: x.p.joinIndex }, { ...y.row, joinIndex: y.p.joinIndex }))
    .map((x) => x.row);
}

/** Top 3 of a round for the summary card. */
export function roundPodium(state: Pick<GameState, 'players'>, round: RoundState): CompletedRoundSummary['podium'] {
  return rankRound(state, round)
    .slice(0, 3)
    .map(({ playerId, nickname, points }) => ({ playerId, nickname, points }));
}
