/**
 * AFL Season + Finals Simulator
 *
 * Monte Carlo simulation of the AFL regular season and finals for 2026.
 *
 * Algorithm:
 *   1. Load all participants for the sports season from DB
 *   2. Load current regular season standings (wins, gamesPlayed) — if available
 *   3. Match participant names to hardcoded team data (Elo ratings)
 *   4. For each simulation:
 *      a. For each team, simulate remaining regular season games (TOTAL_GAMES - gamesPlayed)
 *         using Elo win probability vs. an average opponent (Elo 1500)
 *         → projectedPoints = currentWins*4 + simulatedRemainingWins*4
 *      b. Sort all 18 teams by projected points desc + random tiebreaker → final ladder
 *         → Top 10 advance to the AFL Finals Series
 *      c. Simulate AFL Finals Series (AFL_10 bracket):
 *
 *         Wildcard Round:       #7 vs #10, #8 vs #9         → losers exit (0 pts)
 *         Qualifying Finals:    #1 vs #4,  #2 vs #3         → winners → Prelim Finals (bye)
 *                                                              losers → Semi-Finals (2nd chance)
 *         Elimination Finals:   #5 vs WC2w, #6 vs WC1w     → losers exit (7th/8th)
 *         Semi-Finals:          QF1L vs EF2w, QF2L vs EF1w → losers exit (5th/6th)
 *         Preliminary Finals:   QF1w vs SF2w, QF2w vs SF1w → losers exit (3rd/4th)
 *         Grand Final:          PF1w vs PF2w               → winner 1st, loser 2nd
 *
 *   5. Track placement counts per scoring tier
 *   6. Convert counts to probability distributions
 *
 * Win probability (Elo, PARITY_FACTOR = 450):
 *   P(A beats B) = 1 / (1 + 10^((eloB - eloA) / 450))
 *   A higher parity factor means more randomness per game. AFL uses 450, which is
 *   slightly above the NBA (400) — meaning AFL games are marginally less predictable
 *   than NBA games but far more predictable than NHL (1000).
 *
 * Regular season projection:
 *   Per-game win probability = eloWinProbability(teamElo, 1500) where 1500 = average opponent.
 *   If no standings exist in DB, defaults to 0 wins / TOTAL_GAMES remaining (seeding by Elo only).
 *
 * Elo ratings (see TEAMS_DATA below):
 *   Backsolved from Squiggle's projected season win totals using the inverse formula:
 *     elo = 1500 - 450 × log₁₀((1 − wins/23) / (wins/23))
 *   Projected win counts were read from a screenshot of squiggle.com.au's season
 *   simulation table (as of Round 2, 2026). Update each round as projections shift.
 *   Source: https://squiggle.com.au
 *
 * Placement tiers → SimulationProbabilities mapping:
 *   probFirst          = Grand Final winner (1 per sim)
 *   probSecond         = Grand Final loser (1 per sim)
 *   probThird/Fourth   = Preliminary Finals losers (2 per sim — split evenly)
 *   probFifth/Sixth    = Semi-Finals losers (2 per sim — split evenly)
 *   probSeventh/Eighth = Elimination Finals losers (2 per sim — split evenly)
 *   Wildcard losers    → all 0 (score 0 points, same as 9th/10th)
 *   Missed finals      → all 0
 *
 * NOTE: AFL uses the AFL_10 bracket template which splits the 5–8 tier into two
 * separate pairs (5/6 and 7/8). This is already handled by scoring-rules.ts
 * (SPLIT_5678_TEMPLATE_IDS); this simulator outputs the correct probabilities
 * into the appropriate tiers.
 */

import { database } from "~/database/context";
import { eq } from "drizzle-orm";
import * as schema from "~/database/schema";
import type { Simulator, SimulationResult } from "./types";
import { normalizeTeamName } from "~/lib/normalize-team-name";
import { logger } from "~/lib/logger";
import { getRegularSeasonStandings } from "~/models/regular-season-standings";

// ─── Simulation parameters ────────────────────────────────────────────────────

const NUM_SIMULATIONS = 10_000;

/**
 * Elo parity factor for AFL single-game win probability.
 * 450 reflects moderate variance — lower than NHL (1000) to account for
 * AFL's relatively predictable results vs. basketball's coin-flip tendencies.
 */
const PARITY_FACTOR = 450;

/** Approximate total regular season games per AFL team (2026 season). */
const AFL_REGULAR_SEASON_GAMES = 23;

/** Average opponent Elo used for regular season projections. */
const AVERAGE_OPPONENT_ELO = 1500;

// ─── Team data (2026 AFL season, as of end of Round 2) ───────────────────────
//
// Elo ratings are backsolved from Squiggle's projected season win totals.
// Process: we screenshotted squiggle.com.au's season simulation table (Round 2,
// 2026), read each team's projected wins, then applied the inverse formula:
//   elo = 1500 - 450 × log₁₀((1 − wins/23) / (wins/23))
// This calibrates each team so the simulator reproduces Squiggle's ladder
// projection when every game is played against an average opponent (Elo 1500).
// Update each round by re-reading the projected wins from Squiggle and recalculating.
// Source: https://squiggle.com.au

interface AflTeamData {
  elo: number;
}

const TEAMS_DATA: Record<string, AflTeamData> = {
  "Western Bulldogs":      { elo: 1646 }, // 15.6 projected wins
  "Hawthorn":              { elo: 1604 }, // 14.5
  "Gold Coast":            { elo: 1601 }, // 14.5 (3rd by %)
  "Sydney":                { elo: 1579 }, // 13.8
  "Adelaide":              { elo: 1576 }, // 13.7
  "Geelong":               { elo: 1572 }, // 13.6
  "Brisbane Lions":        { elo: 1541 }, // 12.7
  "Fremantle":             { elo: 1524 }, // 12.2
  "Collingwood":           { elo: 1517 }, // 12.0
  "Greater Western Sydney":{ elo: 1500 }, // 11.5
  "GWS Giants":            { elo: 1500 }, // alias
  "Melbourne":             { elo: 1473 }, // 10.7
  "St Kilda":              { elo: 1466 }, // 10.5
  "North Melbourne":       { elo: 1459 }, // 10.3
  "Carlton":               { elo: 1449 }, // 10.0
  "Port Adelaide":         { elo: 1435 }, //  9.6
  "Richmond":              { elo: 1366 }, //  7.7
  "West Coast":            { elo: 1362 }, //  7.6
  "Essendon":              { elo: 1342 }, //  7.1
};

// ─── Public helpers (exported for unit testing) ───────────────────────────────

/**
 * Look up team data by participant name.
 *
 * Uses a two-step match so "Gold Coast Suns" → "Gold Coast", "Hawthorn Hawks" → "Hawthorn", etc.
 * When multiple keys substring-match (e.g. "Adelaide" AND "Port Adelaide" both appear in
 * "Port Adelaide Power"), the longest key wins — giving the more specific match priority.
 * "GWS Giants" is an explicit alias since it won't substring-match "Greater Western Sydney".
 */
export function getTeamData(name: string): AflTeamData | undefined {
  const normalized = normalizeTeamName(name);
  const keys = Object.keys(TEAMS_DATA);

  // 1. Exact match (fast path)
  for (const key of keys) {
    if (normalizeTeamName(key) === normalized) return TEAMS_DATA[key];
  }

  // 2. Substring match — collect all candidates then pick the longest key so that
  //    "Port Adelaide" (13) beats "Adelaide" (8) for "Port Adelaide Power".
  const candidates = keys.filter((key) => {
    const normKey = normalizeTeamName(key);
    return (
      normKey.length >= 4 &&
      normalized.length >= 4 &&
      (normalized.includes(normKey) || normKey.includes(normalized))
    );
  });

  if (candidates.length === 0) return undefined;
  candidates.sort((a, b) => b.length - a.length);
  return TEAMS_DATA[candidates[0]];
}

/**
 * Elo win probability for team A in a single game against team B.
 *   P(A) = 1 / (1 + 10^((eloB - eloA) / PARITY_FACTOR))
 * Exported for unit testing.
 */
export function eloWinProbability(eloA: number, eloB: number): number {
  return 1 / (1 + Math.pow(10, (eloB - eloA) / PARITY_FACTOR));
}

// ─── Internal types ───────────────────────────────────────────────────────────

interface TeamEntry {
  id: string;
  name: string;
  data: AflTeamData | undefined;
  /** Actual wins from the standings table (0 if no standings loaded). */
  currentWins: number;
  /** Remaining regular season games = TOTAL_GAMES - gamesPlayed (0 if season is complete). */
  remainingGames: number;
  /** Elo win probability vs. average opponent — constant per team. */
  winProb: number;
}

/** Get Elo for a team entry.
 *  Fallback 1400 = conservative below-average estimate for unknown/unrecognized teams. */
function elo(entry: TeamEntry): number {
  return entry.data?.elo ?? 1400;
}

/** Simulate remaining regular season games for a team.
 *  Returns projected total wins for the season. */
function simulateProjectedWins(entry: TeamEntry): number {
  let extra = 0;
  for (let g = 0; g < entry.remainingGames; g++) {
    if (Math.random() < entry.winProb) extra++;
  }
  return entry.currentWins + extra;
}

// ─── Simulator ────────────────────────────────────────────────────────────────

export class AFLSimulator implements Simulator {
  async simulate(sportsSeasonId: string): Promise<SimulationResult[]> {
    const db = database();

    // 1. Load participants and standings in parallel.
    const [participantRows, standings] = await Promise.all([
      db
        .select({ id: schema.participants.id, name: schema.participants.name })
        .from(schema.participants)
        .where(eq(schema.participants.sportsSeasonId, sportsSeasonId)),
      getRegularSeasonStandings(sportsSeasonId),
    ]);

    if (participantRows.length === 0) {
      throw new Error(
        `No participants found for sports season ${sportsSeasonId}. ` +
          `Add all 18 AFL clubs as participants before running simulation.`
      );
    }

    if (participantRows.length < 10) {
      throw new Error(
        `AFL simulation requires at least 10 participants to fill the finals bracket ` +
          `(got ${participantRows.length}). Add all 18 AFL clubs before running simulation.`
      );
    }

    // 2. Build standings lookup and construct team entries.
    //    currentWins, remainingGames, and per-game winProb are all resolved once
    //    here so nothing is recomputed inside the hot simulation loop.
    const standingsMap = new Map(standings.map((s) => [s.participantId, s]));
    const participantIds = participantRows.map((r) => r.id);

    const teams: TeamEntry[] = participantRows.map((r) => {
      const standing = standingsMap.get(r.id);
      const data = getTeamData(r.name);
      if (!data) {
        logger.warn(
          { participantName: r.name, sportsSeasonId },
          `AFL simulator: no Elo found for participant "${r.name}" — falling back to 1400. ` +
            `Add an entry to TEAMS_DATA or rename the participant to match an existing key.`
        );
      }
      const gamesPlayed = standing?.gamesPlayed ?? 0;
      return {
        id: r.id,
        name: r.name,
        data,
        currentWins: standing?.wins ?? 0,
        remainingGames: Math.max(0, AFL_REGULAR_SEASON_GAMES - gamesPlayed),
        winProb: eloWinProbability(data?.elo ?? 1400, AVERAGE_OPPONENT_ELO),
      };
    });

    // ─── Helpers (defined once, outside the hot loop) ─────────────────────────

    /** Simulate a single AFL game. Returns the winner. */
    const simGame = (a: TeamEntry, b: TeamEntry): TeamEntry =>
      Math.random() < eloWinProbability(elo(a), elo(b)) ? a : b;

    /**
     * Project end-of-season ladder and return the top 10 finalists seeded 1–10.
     *
     * Teams are sorted by projected ladder points (4 per win) descending.
     * A small random tiebreaker simulates the percentage-based AFL tiebreaker
     * without requiring actual scores.
     */
    const buildFinalsList = (): TeamEntry[] => {
      const projected = teams.map((t) => ({
        team: t,
        points: simulateProjectedWins(t) * 4,
        tiebreaker: Math.random(),
      }));
      projected.sort((a, b) => b.points - a.points || b.tiebreaker - a.tiebreaker);
      return projected.slice(0, 10).map((x) => x.team);
    };

    /**
     * Simulate the AFL Finals Series from a seeded list of 10 teams.
     *
     * Returns the placement for each team:
     *   "gf_winner"  → 1st
     *   "gf_loser"   → 2nd
     *   "pf_loser"   → 3rd/4th (two teams per sim)
     *   "sf_loser"   → 5th/6th (two teams per sim)
     *   "ef_loser"   → 7th/8th (two teams per sim)
     *   "wc_loser"   → 9th/10th (zero scoring points)
     */
    const simAFLFinals = (
      finalists: TeamEntry[]
    ): {
      gfWinner: TeamEntry;
      gfLoser: TeamEntry;
      pfLosers: [TeamEntry, TeamEntry];
      sfLosers: [TeamEntry, TeamEntry];
      efLosers: [TeamEntry, TeamEntry];
    } => {
      const [s1, s2, s3, s4, s5, s6, s7, s8, s9, s10] = finalists;

      // Wildcard Round: #7 vs #10, #8 vs #9
      const wc1Winner = simGame(s7, s10);
      const wc2Winner = simGame(s8, s9);

      // Qualifying Finals: #1 vs #4, #2 vs #3 (double-chance: winners get bye to PF)
      const qf1Winner = simGame(s1, s4);
      const qf1Loser  = qf1Winner === s1 ? s4 : s1;
      const qf2Winner = simGame(s2, s3);
      const qf2Loser  = qf2Winner === s2 ? s3 : s2;

      // Elimination Finals: #5 vs WC2 winner, #6 vs WC1 winner
      const ef1Winner = simGame(s5, wc2Winner);
      const ef1Loser  = ef1Winner === s5 ? wc2Winner : s5;
      const ef2Winner = simGame(s6, wc1Winner);
      const ef2Loser  = ef2Winner === s6 ? wc1Winner : s6;

      // Semi-Finals: QF losers (2nd chance) vs EF winners
      const sf1Winner = simGame(qf1Loser, ef2Winner);
      const sf1Loser  = sf1Winner === qf1Loser ? ef2Winner : qf1Loser;
      const sf2Winner = simGame(qf2Loser, ef1Winner);
      const sf2Loser  = sf2Winner === qf2Loser ? ef1Winner : qf2Loser;

      // Preliminary Finals: QF winners vs SF winners
      const pf1Winner = simGame(qf1Winner, sf2Winner);
      const pf1Loser  = pf1Winner === qf1Winner ? sf2Winner : qf1Winner;
      const pf2Winner = simGame(qf2Winner, sf1Winner);
      const pf2Loser  = pf2Winner === qf2Winner ? sf1Winner : qf2Winner;

      // Grand Final
      const gfWinner = simGame(pf1Winner, pf2Winner);
      const gfLoser  = gfWinner === pf1Winner ? pf2Winner : pf1Winner;

      return {
        gfWinner,
        gfLoser,
        pfLosers:  [pf1Loser,  pf2Loser ],
        sfLosers:  [sf1Loser,  sf2Loser ],
        efLosers:  [ef1Loser,  ef2Loser ],
      };
    };

    // 3. Integer placement count maps — initialized to 0 for all participants.
    //
    //    AFL scoring uses the AFL_10 bracket template which splits 5–8 into two
    //    separate pairs: Semi-Finals losers share 5th/6th (higher value), and
    //    Elimination Finals losers share 7th/8th (lower value). Both pairs get
    //    distinct point values so we track them in separate count maps.
    const championCounts  = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const finalistCounts  = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const pfLoserCounts   = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const sfLoserCounts   = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const efLoserCounts   = new Map<string, number>(participantIds.map((id) => [id, 0]));

    // 4. Monte Carlo simulation loop.
    for (let s = 0; s < NUM_SIMULATIONS; s++) {
      const finalists = buildFinalsList();
      const { gfWinner, gfLoser, pfLosers, sfLosers, efLosers } = simAFLFinals(finalists);

      championCounts.set(gfWinner.id, (championCounts.get(gfWinner.id) ?? 0) + 1);
      finalistCounts.set(gfLoser.id,  (finalistCounts.get(gfLoser.id)  ?? 0) + 1);

      for (const loser of pfLosers) {
        pfLoserCounts.set(loser.id, (pfLoserCounts.get(loser.id) ?? 0) + 1);
      }
      for (const loser of sfLosers) {
        sfLoserCounts.set(loser.id, (sfLoserCounts.get(loser.id) ?? 0) + 1);
      }
      for (const loser of efLosers) {
        efLoserCounts.set(loser.id, (efLoserCounts.get(loser.id) ?? 0) + 1);
      }
      // Wildcard losers and non-finalists are not counted (0 points per scoring rules).
    }

    // 5. Convert integer counts to probability distributions.
    //
    //    Exact denominators guarantee column sums of 1.0 by construction:
    //      probFirst/Second      → / NUM_SIMULATIONS      (1 per sim)
    //      probThird/Fourth      → / (2 * NUM_SIMULATIONS) (2 PF losers per sim)
    //      probFifth/Sixth       → / (2 * NUM_SIMULATIONS) (2 SF losers per sim)
    //      probSeventh/Eighth    → / (2 * NUM_SIMULATIONS) (2 EF losers per sim)
    //
    //    Within each pair (3rd/4th, 5th/6th, 7th/8th), both positions receive the
    //    same probability — matching the AFL_10 bracket's averaged point values.
    const N = NUM_SIMULATIONS;
    const results: SimulationResult[] = participantIds.map((participantId) => {
      const c  = championCounts.get(participantId) ?? 0;
      const f  = finalistCounts.get(participantId) ?? 0;
      const pf = pfLoserCounts.get(participantId)  ?? 0;
      const sf = sfLoserCounts.get(participantId)  ?? 0;
      const ef = efLoserCounts.get(participantId)  ?? 0;
      return {
        participantId,
        probabilities: {
          probFirst:   c  / N,
          probSecond:  f  / N,
          probThird:   pf / (2 * N),
          probFourth:  pf / (2 * N),
          probFifth:   sf / (2 * N),
          probSixth:   sf / (2 * N),
          probSeventh: ef / (2 * N),
          probEighth:  ef / (2 * N),
        },
        source: "afl_bracket_monte_carlo",
      };
    });

    // 6. Per-position normalization — belt-and-suspenders guard against floating-point
    //    division residuals. Columns are already near-exactly 1.0 after step 5.
    const positionKeys: Array<keyof (typeof results)[0]["probabilities"]> = [
      "probFirst", "probSecond", "probThird", "probFourth",
      "probFifth", "probSixth", "probSeventh", "probEighth",
    ];
    for (const key of positionKeys) {
      const colSum = results.reduce((s, r) => s + r.probabilities[key], 0);
      const residual = 1.0 - colSum;
      if (residual !== 0) {
        const maxResult = results.reduce((best, r) =>
          r.probabilities[key] > best.probabilities[key] ? r : best
        );
        maxResult.probabilities[key] += residual;
      }
    }

    return results;
  }
}
