import type { Fixture, TeamOdds } from '../types';
import { rankGroup, type Result } from '../standings';
import { lambdasFromElo, scoreMatrix, outcomeProbs, type OutcomeProbs } from './poisson';
import { knockoutAdvanceProb, ratingOf, type RatingsModel } from './predict';
import { hostAdvantage } from './hosts';

export type { TeamOdds };

export interface SimResult {
  iterations: number;
  asOf: string;
  teams: TeamOdds[];
}

type Rng = () => number;

// ---- placeholder parsing ----
type Ref =
  | { kind: 'winner'; group: string }
  | { kind: 'runner'; group: string }
  | { kind: 'third'; allowed: Set<string> }
  | { kind: 'matchWinner'; num: number }
  | { kind: 'matchLoser'; num: number }
  | { kind: 'fixed'; team: string };

function parseRef(slot: { team: string | null; placeholder: string | null }): Ref {
  if (slot.team) return { kind: 'fixed', team: slot.team };
  const code = slot.placeholder ?? '';
  const pos = code.match(/^([12])([A-L])$/);
  if (pos) return { kind: pos[1] === '1' ? 'winner' : 'runner', group: pos[2]! };
  const third = code.match(/^3([A-L/]+)$/);
  if (third) return { kind: 'third', allowed: new Set(third[1]!.split('/')) };
  const wl = code.match(/^([WL])(\d{1,3})$/);
  if (wl) return wl[1] === 'W'
    ? { kind: 'matchWinner', num: Number(wl[2]) }
    : { kind: 'matchLoser', num: Number(wl[2]) };
  return { kind: 'fixed', team: code };
}

// ---- precomputed, sim-invariant structures ----
interface RemainingGroupGame {
  group: string;
  home: string;
  away: string;
  /** Cumulative scoreline distribution for sampling. */
  cdf: { h: number; a: number; cum: number }[];
}
interface FinishedResult extends Result { group: string }
interface KnockoutGame {
  num: number;
  stage: Fixture['stage'];
  venue: string;
  home: Ref;
  away: Ref;
  /** Actual winner team name if this fixture is already decided. */
  decidedWinner: string | null;
}

function buildScoreCdf(
  home: string, away: string, venue: string, model: RatingsModel,
): { h: number; a: number; cum: number }[] {
  const homeAdv = hostAdvantage(home, away, venue, model.params.homeAdvElo);
  const { lambdaHome, lambdaAway } = lambdasFromElo(
    ratingOf(model, home), ratingOf(model, away), model.params, homeAdv,
  );
  const m = scoreMatrix(lambdaHome, lambdaAway, model.params.rho);
  const cdf: { h: number; a: number; cum: number }[] = [];
  let cum = 0;
  for (let h = 0; h < m.length; h++) {
    for (let a = 0; a < m[h]!.length; a++) {
      cum += m[h]![a]!;
      cdf.push({ h, a, cum });
    }
  }
  return cdf;
}

/** Bipartite match the third-place slots to the qualified third teams, honoring
 *  each slot's allowed-groups constraint (Kuhn's algorithm). */
function assignThirds(
  slots: { allowed: Set<string> }[],
  thirds: { team: string; group: string }[],
): (string | null)[] {
  const matchSlot: number[] = new Array(slots.length).fill(-1);
  const matchThird: number[] = new Array(thirds.length).fill(-1);
  const tryAssign = (s: number, seen: boolean[]): boolean => {
    for (let t = 0; t < thirds.length; t++) {
      if (!seen[t] && slots[s]!.allowed.has(thirds[t]!.group)) {
        seen[t] = true;
        if (matchThird[t] === -1 || tryAssign(matchThird[t]!, seen)) {
          matchSlot[s] = t;
          matchThird[t] = s;
          return true;
        }
      }
    }
    return false;
  };
  for (let s = 0; s < slots.length; s++) tryAssign(s, new Array(thirds.length).fill(false));
  return matchSlot.map((t) => (t >= 0 ? thirds[t]!.team : null));
}

/** Build a reusable simulator from the current fixtures + ratings. */
export function buildSimulator(fixtures: Fixture[], model: RatingsModel) {
  const groups = new Map<string, Set<string>>();
  const finished: FinishedResult[] = [];
  const remaining: RemainingGroupGame[] = [];

  for (const f of fixtures) {
    if (f.stage !== 'group' || !f.group || !f.home.team || !f.away.team) continue;
    const g = f.group;
    if (!groups.has(g)) groups.set(g, new Set());
    groups.get(g)!.add(f.home.team);
    groups.get(g)!.add(f.away.team);
    if (f.status === 'finished' && f.homeScore !== null && f.awayScore !== null) {
      finished.push({ group: g, home: f.home.team, away: f.away.team, homeScore: f.homeScore, awayScore: f.awayScore });
    } else {
      remaining.push({ group: g, home: f.home.team, away: f.away.team, cdf: buildScoreCdf(f.home.team, f.away.team, f.venue, model) });
    }
  }

  const knockout: KnockoutGame[] = fixtures
    .filter((f) => f.stage !== 'group')
    .sort((a, b) => a.num - b.num)
    .map((f) => {
      const decided =
        f.status === 'finished' && f.home.team && f.away.team && f.homeScore !== null && f.awayScore !== null
          ? f.homeScore > f.awayScore ? f.home.team : f.away.team
          : null;
      return { num: f.num, stage: f.stage, venue: f.venue, home: parseRef(f.home), away: parseRef(f.away), decidedWinner: decided };
    });

  // The 8 third-place slots (with their allowed groups) in a stable order.
  const thirdSlots: { num: number; side: 'home' | 'away'; allowed: Set<string> }[] = [];
  for (const g of knockout) {
    if (g.stage !== 'r32') continue;
    if (g.home.kind === 'third') thirdSlots.push({ num: g.num, side: 'home', allowed: g.home.allowed });
    if (g.away.kind === 'third') thirdSlots.push({ num: g.num, side: 'away', allowed: g.away.allowed });
  }

  const groupList = [...groups.entries()].map(([g, set]) => ({ g, teams: [...set] }));

  // Advance-probability memo: distinct (home,away,venue) pairings are limited.
  const advMemo = new Map<string, number>();
  const advanceProb = (home: string, away: string, venue: string): number => {
    const key = `${home}|${away}|${venue}`;
    let p = advMemo.get(key);
    if (p === undefined) {
      const homeAdv = hostAdvantage(home, away, venue, model.params.homeAdvElo);
      const { lambdaHome, lambdaAway } = lambdasFromElo(ratingOf(model, home), ratingOf(model, away), model.params, homeAdv);
      const probs: OutcomeProbs = outcomeProbs(scoreMatrix(lambdaHome, lambdaAway, model.params.rho));
      p = knockoutAdvanceProb(probs, ratingOf(model, home), ratingOf(model, away));
      advMemo.set(key, p);
    }
    return p;
  };

  function runOnce(rng: Rng, tally: Map<string, TeamOdds>): void {
    const bump = (team: string, key: keyof Omit<TeamOdds, 'team'>) => {
      let o = tally.get(team);
      if (!o) { o = { team, winGroup: 0, qualify: 0, reachR16: 0, reachQF: 0, reachSF: 0, reachFinal: 0, champion: 0 }; tally.set(team, o); }
      o[key]++;
    };

    // 1) group stage
    const resultsByGroup = new Map<string, Result[]>();
    for (const fr of finished) {
      if (!resultsByGroup.has(fr.group)) resultsByGroup.set(fr.group, []);
      resultsByGroup.get(fr.group)!.push(fr);
    }
    for (const rg of remaining) {
      const u = rng();
      let pick = rg.cdf[rg.cdf.length - 1]!;
      for (const cell of rg.cdf) { if (u <= cell.cum) { pick = cell; break; } }
      if (!resultsByGroup.has(rg.group)) resultsByGroup.set(rg.group, []);
      resultsByGroup.get(rg.group)!.push({ home: rg.home, away: rg.away, homeScore: pick.h, awayScore: pick.a });
    }

    const winnerByGroup = new Map<string, string>();
    const runnerByGroup = new Map<string, string>();
    const thirds: { team: string; group: string; points: number; gd: number; gf: number }[] = [];
    for (const { g, teams } of groupList) {
      const rows = rankGroup(teams, resultsByGroup.get(g) ?? []);
      winnerByGroup.set(g, rows[0]!.team);
      runnerByGroup.set(g, rows[1]!.team);
      bump(rows[0]!.team, 'winGroup');
      bump(rows[0]!.team, 'qualify');
      bump(rows[1]!.team, 'qualify');
      const t3 = rows[2]!;
      thirds.push({ team: t3.team, group: g, points: t3.points, gd: t3.gd, gf: t3.gf });
    }

    // 2) eight best third-placed teams
    thirds.sort((a, b) => b.points - a.points || b.gd - a.gd || b.gf - a.gf);
    const qualifiedThirds = thirds.slice(0, 8);
    for (const t of qualifiedThirds) bump(t.team, 'qualify');

    // 3) assign thirds to their R32 slots
    const assignment = assignThirds(thirdSlots.map((s) => ({ allowed: s.allowed })), qualifiedThirds);
    const thirdBySlot = new Map<string, string>(); // `${num}:${side}` → team
    thirdSlots.forEach((s, i) => { const team = assignment[i]; if (team) thirdBySlot.set(`${s.num}:${s.side}`, team); });

    // 4) knockouts
    const winnerByNum = new Map<number, string>();
    const reachKey: Record<string, keyof Omit<TeamOdds, 'team'>> = {
      r32: 'reachR16', r16: 'reachQF', qf: 'reachSF', sf: 'reachFinal', final: 'champion',
    };
    const resolve = (ref: Ref, num: number, side: 'home' | 'away'): string | null => {
      switch (ref.kind) {
        case 'fixed': return ref.team;
        case 'winner': return winnerByGroup.get(ref.group) ?? null;
        case 'runner': return runnerByGroup.get(ref.group) ?? null;
        case 'third': return thirdBySlot.get(`${num}:${side}`) ?? null;
        case 'matchWinner': return winnerByNum.get(ref.num) ?? null;
        case 'matchLoser': return null; // third-place game only; not tallied
      }
    };

    for (const k of knockout) {
      if (k.stage === 'third') continue; // doesn't affect advancement odds
      const home = resolve(k.home, k.num, 'home');
      const away = resolve(k.away, k.num, 'away');
      if (!home || !away) continue;
      let winner: string;
      if (k.decidedWinner) {
        winner = k.decidedWinner;
      } else {
        winner = rng() < advanceProb(home, away, k.venue) ? home : away;
      }
      winnerByNum.set(k.num, winner);
      const key = reachKey[k.stage];
      if (key) bump(winner, key);
    }
  }

  return {
    run(iterations: number, rng: Rng = Math.random): SimResult {
      const tally = new Map<string, TeamOdds>();
      for (let i = 0; i < iterations; i++) runOnce(rng, tally);
      const teams = [...tally.values()]
        .map((o) => ({
          team: o.team,
          winGroup: o.winGroup / iterations,
          qualify: o.qualify / iterations,
          reachR16: o.reachR16 / iterations,
          reachQF: o.reachQF / iterations,
          reachSF: o.reachSF / iterations,
          reachFinal: o.reachFinal / iterations,
          champion: o.champion / iterations,
        }))
        .sort((a, b) => b.champion - a.champion || b.reachFinal - a.reachFinal);
      return { iterations, asOf: model.asOf, teams };
    },
  };
}

/** Convenience: build + run in one call. */
export function simulateTournament(
  fixtures: Fixture[], model: RatingsModel, iterations = 10_000, rng: Rng = Math.random,
): SimResult {
  return buildSimulator(fixtures, model).run(iterations, rng);
}
