/**
 * NBA Playoff Simulator
 *
 * Monte Carlo simulation of the NBA playoffs including seeding projection
 * for the current season (2025-26).
 *
 * Algorithm:
 *   1. Load all participants for the sports season from DB
 *   2. Load current regular season standings (wins, gamesPlayed, conference)
 *   3. Match participant names to hardcoded team data (Elo ratings)
 *   4. For each simulation:
 *      a. For each team, simulate remaining regular season games (82 - gamesPlayed)
 *         using Elo win probability vs. an average opponent (Elo 1500)
 *         → projectedWins = currentWins + simulatedRemainingWins
 *      b. Sort each conference by projected wins (desc) + random tiebreaker → seeds 1–10
 *         → Top 6 lock in directly; seeds 7–10 enter the Play-In tournament
 *      c. Simulate Play-In (single game each):
 *         - Game 1: seed 7 vs seed 8 → winner becomes 7th playoff seed
 *         - Game 2: seed 9 vs seed 10 → winner advances
 *         - Game 3: Game 1 loser vs Game 2 winner → winner becomes 8th playoff seed
 *      d. Simulate NBA playoff bracket (best-of-7 series each round):
 *         Round 1:     1v8, 4v5, 2v7, 3v6  (per conference)
 *         Round 2:     Conference Semis (winners of 1v8/4v5, winners of 2v7/3v6)
 *         Round 3:     Conference Finals
 *         NBA Finals:  East champion vs West champion
 *   5. Track placement counts per scoring tier
 *   6. Convert counts to probability distributions
 *
 * Win probability (Elo, PARITY_FACTOR = 400):
 *   P(A beats B) = 1 / (1 + 10^((eloB - eloA) / 400))
 *
 * Regular season projection:
 *   Per-game win probability = eloWinProbability(teamElo, 1500) where 1500 = average opponent.
 *   If no standings exist in DB, defaults to 0 wins / 82 remaining games (seeding by Elo only).
 *   Conference is read from standings table; falls back to TEAMS_DATA if missing.
 *
 * Placement tiers → SimulationProbabilities mapping:
 *   probFirst         = NBA champion (1 per sim)
 *   probSecond        = NBA Finals loser (1 per sim)
 *   probThird/Fourth  = Conference Finals losers (2 per sim — East + West)
 *   probFifth–Eighth  = Conference Semis losers (4 per sim)
 *   Round 1 losers    → all 0 (score 0 points)
 *   Missed playoffs   → all 0
 *
 * Elo ratings are hardcoded below (March 2026 data).
 * Source: Neil Paine Substack playoff Elo estimates (last 110 games, no regression,
 *         postseason games 3× weight). Update at the start of each season.
 */

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 { getRegularSeasonStandings } from "~/models/regular-season-standings";

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

const NUM_SIMULATIONS = 50_000;

/**
 * Elo parity factor. NBA uses 400 (standard formula).
 * A 400-point Elo difference → ~90.9% win probability per game.
 */
const PARITY_FACTOR = 400;

/** NBA regular season games per team. */
const NBA_REGULAR_SEASON_GAMES = 82;

// ─── Team data (2025-26 season, as of March 2026) ─────────────────────────────
//
// elo: Estimated Elo rating (higher = stronger).
//   Source: Playoff rating (last 110 games, no regression to mean, postseason games 3× weight).
//   This is the appropriate signal for simulating both regular season win probability
//   (vs. average opponent) and playoff matchups.
//
// conference: Used as a fallback when the standings table has no conference data.

interface NbaTeamData {
  conference: "Eastern" | "Western";
  elo: number;
}

const TEAMS_DATA: Record<string, NbaTeamData> = {
  // ── Eastern Conference ──────────────────────────────────────────────────────

  "Detroit Pistons":       { conference: "Eastern", elo: 1558 },
  "Boston Celtics":        { conference: "Eastern", elo: 1699 },
  "New York Knicks":       { conference: "Eastern", elo: 1626 },
  "Cleveland Cavaliers":   { conference: "Eastern", elo: 1628 },
  "Orlando Magic":         { conference: "Eastern", elo: 1508 },
  "Miami Heat":            { conference: "Eastern", elo: 1530 },
  "Toronto Raptors":       { conference: "Eastern", elo: 1467 },
  "Atlanta Hawks":         { conference: "Eastern", elo: 1496 },
  "Philadelphia 76ers":    { conference: "Eastern", elo: 1471 },
  "Charlotte Hornets":     { conference: "Eastern", elo: 1496 },
  "Milwaukee Bucks":       { conference: "Eastern", elo: 1442 },
  "Chicago Bulls":         { conference: "Eastern", elo: 1381 },
  "Brooklyn Nets":         { conference: "Eastern", elo: 1334 },
  "Indiana Pacers":        { conference: "Eastern", elo: 1433 },
  "Washington Wizards":    { conference: "Eastern", elo: 1255 },

  // ── Western Conference ──────────────────────────────────────────────────────

  "Oklahoma City Thunder": { conference: "Western", elo: 1731 },
  "San Antonio Spurs":     { conference: "Western", elo: 1599 },
  "Houston Rockets":       { conference: "Western", elo: 1564 },
  "Denver Nuggets":        { conference: "Western", elo: 1618 },
  "LA Lakers":             { conference: "Western", elo: 1569 },
  "Minnesota Timberwolves":{ conference: "Western", elo: 1603 },
  "Phoenix Suns":          { conference: "Western", elo: 1500 },
  "LA Clippers":           { conference: "Western", elo: 1573 },
  "Golden State Warriors": { conference: "Western", elo: 1530 },
  "Portland Trail Blazers":{ conference: "Western", elo: 1426 },
  "Dallas Mavericks":      { conference: "Western", elo: 1473 },
  "Memphis Grizzlies":     { conference: "Western", elo: 1417 },
  "New Orleans Pelicans":  { conference: "Western", elo: 1380 },
  "Sacramento Kings":      { conference: "Western", elo: 1352 },
  "Utah Jazz":             { conference: "Western", elo: 1334 },
};

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

export { normalizeTeamName };

/** Look up team data by participant name (case-insensitive). */
export function getTeamData(name: string): NbaTeamData | undefined {
  const normalized = normalizeTeamName(name);
  for (const [teamName, data] of Object.entries(TEAMS_DATA)) {
    if (normalizeTeamName(teamName) === normalized) return data;
  }
  return undefined;
}

/**
 * Elo win probability for team A over 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: NbaTeamData | undefined;
  conference: "Eastern" | "Western";
  /** Actual wins from the standings table (0 if no standings loaded). */
  currentWins: number;
  /** Remaining regular season games = 82 - gamesPlayed (0 if season is complete). */
  remainingGames: number;
  /** Elo win probability vs. average opponent (1500) — 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.
 *  Uses the pre-computed per-team winProb (Elo vs. average opponent).
 *  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 NBASimulator 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 NBA teams as participants before running simulation.`
      );
    }

    // 2. Build standings lookup and construct team entries.
    //    Conference, currentWins, remainingGames, and per-game winProb are all
    //    resolved once here so nothing is recomputed inside the hot 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);
      const gamesPlayed = standing?.gamesPlayed ?? 0;
      const conf = standing?.conference;
      const conference: "Eastern" | "Western" =
        conf === "Eastern" || conf === "Western"
          ? conf
          : (data?.conference ?? "Eastern");
      return {
        id: r.id,
        name: r.name,
        data,
        conference,
        currentWins: standing?.wins ?? 0,
        remainingGames: Math.max(0, NBA_REGULAR_SEASON_GAMES - gamesPlayed),
        winProb: eloWinProbability(data?.elo ?? 1400, 1500),
      };
    });

    // 3. Separate by conference for simulation.
    const easternTeams = teams.filter((t) => t.conference === "Eastern");
    const westernTeams = teams.filter((t) => t.conference === "Western");

    // Validate: each conference needs at least 10 teams to fill the bracket + play-in.
    if (easternTeams.length < 10 || westernTeams.length < 10) {
      throw new Error(
        `Each conference needs at least 10 participants (got East: ${easternTeams.length}, ` +
          `West: ${westernTeams.length}). Add all 30 NBA teams before running simulation.`
      );
    }

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

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

    /** Simulate a best-of-7 series. Returns winner and loser. */
    const simSeries = (a: TeamEntry, b: TeamEntry): { winner: TeamEntry; loser: TeamEntry } => {
      const winProb = eloWinProbability(elo(a), elo(b));
      let winsA = 0;
      let winsB = 0;
      while (winsA < 4 && winsB < 4) {
        if (Math.random() < winProb) winsA++; else winsB++;
      }
      return winsA === 4 ? { winner: a, loser: b } : { winner: b, loser: a };
    };

    /** Simulate the Play-In tournament.
     *  @param candidates 4 teams sorted by seeding position [7th, 8th, 9th, 10th]
     *  @returns [7th playoff seed, 8th playoff seed] */
    const simPlayIn = ([s7, s8, s9, s10]: [TeamEntry, TeamEntry, TeamEntry, TeamEntry]): [TeamEntry, TeamEntry] => {
      // Game 1: 7 vs 8 — winner locks up the 7th seed
      const game1Winner = simGame(s7, s8);
      const game1Loser  = game1Winner === s7 ? s8 : s7;
      // Game 2: 9 vs 10 — winner advances to the final play-in game
      const game2Winner = simGame(s9, s10);
      // Game 3: loser of Game 1 vs winner of Game 2 — winner gets the 8th seed
      return [game1Winner, simGame(game1Loser, game2Winner)];
    };

    /** Build an 8-team conference bracket [s1..s8] for one simulation iteration.
     *  Seeds are determined by simulated projected wins; positions 7–10 go through the Play-In. */
    const buildConferenceBracket = (confTeams: TeamEntry[]): TeamEntry[] => {
      const projected = confTeams.map((t) => ({
        team: t,
        projectedWins: simulateProjectedWins(t),
        tiebreaker: Math.random(),
      }));
      // Higher projected wins = better seed (sort descending; random tiebreaker for ties).
      projected.sort((a, b) => b.projectedWins - a.projectedWins || b.tiebreaker - a.tiebreaker);

      const top6 = projected.slice(0, 6).map((x) => x.team);
      const playIn = projected.slice(6, 10).map((x) => x.team) as
        [TeamEntry, TeamEntry, TeamEntry, TeamEntry];
      const [seed7, seed8] = simPlayIn(playIn);
      return [...top6, seed7, seed8];
    };

    /** Round 1: 1v8, 4v5, 2v7, 3v6. Returns 4 winners. */
    const simR1 = ([s1, s2, s3, s4, s5, s6, s7, s8]: TeamEntry[]): TeamEntry[] => [
      simSeries(s1, s8).winner,
      simSeries(s4, s5).winner,
      simSeries(s2, s7).winner,
      simSeries(s3, s6).winner,
    ];

    /** Conference Semis: winner(1v8) vs winner(4v5), winner(2v7) vs winner(3v6). */
    const simR2 = ([w0, w1, w2, w3]: TeamEntry[]): { winners: TeamEntry[]; losers: TeamEntry[] } => {
      const m1 = simSeries(w0, w1);
      const m2 = simSeries(w2, w3);
      return { winners: [m1.winner, m2.winner], losers: [m1.loser, m2.loser] };
    };

    // 4. Integer placement count maps — initialized to 0 for all participants.
    const championCounts       = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const finalistCounts       = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const confFinalLoserCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const confSemiLoserCounts  = new Map<string, number>(participantIds.map((id) => [id, 0]));

    // 5. Monte Carlo simulation loop.
    for (let s = 0; s < NUM_SIMULATIONS; s++) {
      // ── Build brackets ───────────────────────────────────────────────────────
      const eastBracket = buildConferenceBracket(easternTeams);
      const westBracket = buildConferenceBracket(westernTeams);

      // ── Simulate rounds ──────────────────────────────────────────────────────
      const { winners: eastR2Winners, losers: eastR2Losers } = simR2(simR1(eastBracket));
      const { winner: eastChamp, loser: eastCFLoser } = simSeries(eastR2Winners[0], eastR2Winners[1]);

      const { winners: westR2Winners, losers: westR2Losers } = simR2(simR1(westBracket));
      const { winner: westChamp, loser: westCFLoser } = simSeries(westR2Winners[0], westR2Winners[1]);

      const { winner: champion, loser: finalist } = simSeries(eastChamp, westChamp);

      // ── Record counts (maps are pre-populated so .get() always returns a number) ───
      championCounts.set(champion.id,          (championCounts.get(champion.id) ?? 0)          + 1);
      finalistCounts.set(finalist.id,          (finalistCounts.get(finalist.id) ?? 0)          + 1);
      confFinalLoserCounts.set(eastCFLoser.id, (confFinalLoserCounts.get(eastCFLoser.id) ?? 0) + 1);
      confFinalLoserCounts.set(westCFLoser.id, (confFinalLoserCounts.get(westCFLoser.id) ?? 0) + 1);
      for (const loser of [...eastR2Losers, ...westR2Losers]) {
        confSemiLoserCounts.set(loser.id, (confSemiLoserCounts.get(loser.id) ?? 0) + 1);
      }
      // Round 1 losers are not counted (0 points per scoring rules).
    }

    // 6. Convert integer counts to probability distributions.
    //    Exact denominators guarantee column sums of 1.0 by construction:
    //      probFirst/Second  → NUM_SIMULATIONS total (1 per sim)
    //      probThird/Fourth  → confFinalLoserCounts / (2*N)  — 2 conf final losers per sim
    //      probFifth–Eighth  → confSemiLoserCounts  / (4*N)  — 4 conf semi losers per sim
    const results: SimulationResult[] = participantIds.map((participantId) => {
      const c  = championCounts.get(participantId) ?? 0;
      const f  = finalistCounts.get(participantId) ?? 0;
      const cf = confFinalLoserCounts.get(participantId) ?? 0;
      const cs = confSemiLoserCounts.get(participantId) ?? 0;
      return {
        participantId,
        probabilities: {
          probFirst:   c  / NUM_SIMULATIONS,
          probSecond:  f  / NUM_SIMULATIONS,
          probThird:   cf / (2 * NUM_SIMULATIONS),
          probFourth:  cf / (2 * NUM_SIMULATIONS),
          probFifth:   cs / (4 * NUM_SIMULATIONS),
          probSixth:   cs / (4 * NUM_SIMULATIONS),
          probSeventh: cs / (4 * NUM_SIMULATIONS),
          probEighth:  cs / (4 * NUM_SIMULATIONS),
        },
        source: "nba_bracket_monte_carlo",
      };
    });

    // 7. Per-position normalization — belt-and-suspenders guard against floating-point
    //    division residuals. Columns are already near-exactly 1.0 after step 6.
    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;
  }
}
