/**
 * NFL Season + Playoffs Simulator
 *
 * Monte Carlo simulation of the NFL regular season and playoffs.
 *
 * Algorithm:
 *   1. Load all participants for the sports season from DB
 *   2. Load Elo ratings from participantExpectedValues.sourceElo (user-maintained)
 *      Falls back to sourceOdds → convertFuturesToElo() if no sourceElo set.
 *   3. Validate that all 8 NFL divisions have at least one Elo-rated team.
 *   4. Load current regular season standings (wins, gamesPlayed, conference, division)
 *      If no standings in DB → pre-season mode (all teams start 0-0).
 *   5. For each simulation:
 *      a. Simulate remaining games (17 total) for each team using Elo vs average (1500)
 *         → projectedWins = currentWins + Binomial(remainingGames, winProb)
 *      b. Per conference (AFC, NFC):
 *         - Division winners: best projectedWins in each of 4 divisions (random tiebreak)
 *         - Wildcards: top 3 non-division-winners by projectedWins
 *         - Seed 1: best division winner (gets bye)
 *         - Seeds 2-4: remaining division winners, sorted by projectedWins desc
 *         - Seeds 5-7: wildcards, sorted by projectedWins desc
 *      c. Simulate Wild Card (seed 1 has bye): 2v7, 3v6, 4v5 per conference
 *         Higher seed has home-field advantage (+48 Elo ≈ 57% baseline win rate).
 *      d. Simulate Divisional: seed 1 vs lowest remaining, seed 2 vs next
 *         (higher seed is home)
 *      e. Simulate Conference Championships (higher remaining seed is home)
 *      f. Simulate Super Bowl at a neutral site (no home-field adjustment)
 *   6. Track placements:
 *      - probFirst / probSecond: Super Bowl winner / loser
 *      - probThird / probFourth: Conference Championship losers (1 per conf, split evenly)
 *      - probFifth–probEighth: Divisional losers (4 total, split evenly)
 *      - Wild Card losers and missed playoffs → 0
 *
 * Elo ratings are maintained by the admin via the sourceElo field on
 * participantExpectedValues. Update before each simulation run.
 * Source: nfelo.app (nfelo ratings system)
 *
 * Home-field advantage:
 *   NFL playoff home field is worth ~+48 Elo points (≈57% baseline win rate for
 *   the higher seed). This applies to Wild Card, Divisional, and Conference
 *   Championship rounds. The Super Bowl is played at a neutral site.
 */

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 { eloWinProbability, convertFuturesToElo } from "~/services/probability-engine";
import { getRegularSeasonStandings } from "~/models/regular-season-standings";

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

const NUM_SIMULATIONS = 50_000;

/** NFL regular season games per team. */
const NFL_REGULAR_SEASON_GAMES = 17;

/** Average opponent Elo for regular season game projection. */
const AVG_OPPONENT_ELO = 1500;

/**
 * Home-field Elo advantage for NFL playoff games.
 * Adds to the higher seed's Elo before computing win probability.
 * +48 Elo ≈ 57% baseline win rate for the home team, consistent with
 * NFL regular-season home-field data (~2.5 point spread advantage).
 */
const NFL_HOME_FIELD_ELO_ADVANTAGE = 48;

type Conference = "AFC" | "NFC";
type Division = "East" | "North" | "South" | "West";

const CONFERENCES: Conference[] = ["AFC", "NFC"];
const DIVISIONS: Division[] = ["East", "North", "South", "West"];

// ─── Team data ────────────────────────────────────────────────────────────────

interface NflTeamData {
  names: string[];
  conference: Conference;
  division: Division;
}

const TEAMS_DATA: NflTeamData[] = [
  // AFC East
  { names: ["Buffalo Bills", "Bills"], conference: "AFC", division: "East" },
  { names: ["Miami Dolphins", "Dolphins"], conference: "AFC", division: "East" },
  { names: ["New England Patriots", "Patriots"], conference: "AFC", division: "East" },
  { names: ["New York Jets", "Jets"], conference: "AFC", division: "East" },

  // AFC North
  { names: ["Baltimore Ravens", "Ravens"], conference: "AFC", division: "North" },
  { names: ["Cincinnati Bengals", "Bengals"], conference: "AFC", division: "North" },
  { names: ["Cleveland Browns", "Browns"], conference: "AFC", division: "North" },
  { names: ["Pittsburgh Steelers", "Steelers"], conference: "AFC", division: "North" },

  // AFC South
  { names: ["Houston Texans", "Texans"], conference: "AFC", division: "South" },
  { names: ["Indianapolis Colts", "Colts"], conference: "AFC", division: "South" },
  { names: ["Jacksonville Jaguars", "Jaguars"], conference: "AFC", division: "South" },
  { names: ["Tennessee Titans", "Titans"], conference: "AFC", division: "South" },

  // AFC West
  { names: ["Denver Broncos", "Broncos"], conference: "AFC", division: "West" },
  { names: ["Kansas City Chiefs", "Chiefs"], conference: "AFC", division: "West" },
  { names: ["Las Vegas Raiders", "Raiders"], conference: "AFC", division: "West" },
  { names: ["Los Angeles Chargers", "Chargers"], conference: "AFC", division: "West" },

  // NFC East
  { names: ["Dallas Cowboys", "Cowboys"], conference: "NFC", division: "East" },
  { names: ["New York Giants", "Giants"], conference: "NFC", division: "East" },
  { names: ["Philadelphia Eagles", "Eagles"], conference: "NFC", division: "East" },
  { names: ["Washington Commanders", "Commanders"], conference: "NFC", division: "East" },

  // NFC North
  { names: ["Chicago Bears", "Bears"], conference: "NFC", division: "North" },
  { names: ["Detroit Lions", "Lions"], conference: "NFC", division: "North" },
  { names: ["Green Bay Packers", "Packers"], conference: "NFC", division: "North" },
  { names: ["Minnesota Vikings", "Vikings"], conference: "NFC", division: "North" },

  // NFC South
  { names: ["Atlanta Falcons", "Falcons"], conference: "NFC", division: "South" },
  { names: ["Carolina Panthers", "Panthers"], conference: "NFC", division: "South" },
  { names: ["New Orleans Saints", "Saints"], conference: "NFC", division: "South" },
  { names: ["Tampa Bay Buccaneers", "Buccaneers"], conference: "NFC", division: "South" },

  // NFC West
  { names: ["Arizona Cardinals", "Cardinals"], conference: "NFC", division: "West" },
  { names: ["Los Angeles Rams", "Rams"], conference: "NFC", division: "West" },
  { names: ["San Francisco 49ers", "49ers"], conference: "NFC", division: "West" },
  { names: ["Seattle Seahawks", "Seahawks"], conference: "NFC", division: "West" },
];

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

interface TeamState {
  participantId: string;
  conference: Conference;
  division: Division;
  elo: number;
  currentWins: number;
  gamesPlayed: number;
}

interface SeededTeam {
  participantId: string;
  elo: number;
  seed: number;
}

interface ConferenceBracketResult {
  finalist: SeededTeam;
  confChampLoser: SeededTeam;
  divisionalLosers: [SeededTeam, SeededTeam];
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

/**
 * Look up team data by participant name.
 */
export function getTeamData(name: string): NflTeamData | undefined {
  const normalized = normalizeTeamName(name);
  return TEAMS_DATA.find((t) =>
    t.names.some((n) => normalizeTeamName(n) === normalized)
  );
}

/**
 * Simulate projected wins for a team over its remaining games.
 * Each game is an independent Bernoulli trial.
 */
function simulateProjectedWins(
  currentWins: number,
  gamesPlayed: number,
  winProb: number
): number {
  const remaining = Math.max(0, NFL_REGULAR_SEASON_GAMES - gamesPlayed);
  let additional = 0;
  for (let g = 0; g < remaining; g++) {
    if (Math.random() < winProb) additional++;
  }
  return currentWins + additional;
}

/**
 * Determine the 7-team playoff seeding for one conference.
 *
 * Seeds 1-4 are division winners (sorted by projectedWins desc).
 * Seeds 5-7 are wildcards (top 3 non-division-winners).
 * Seed 1 receives the bye.
 *
 * Ties in projectedWins are broken by a pre-computed random jitter, which
 * avoids the instability of using Math.random() directly inside a sort comparator.
 */
export function seedConference(
  teams: Array<{ participantId: string; elo: number; division: Division; projectedWins: number }>
): SeededTeam[] {
  // Pre-compute per-team jitter so each sort comparison is deterministic
  const jitter = new Map(teams.map((t) => [t.participantId, Math.random()]));
  const cmp = (a: typeof teams[0], b: typeof teams[0]) =>
    b.projectedWins - a.projectedWins ||
    (jitter.get(b.participantId) ?? 0) - (jitter.get(a.participantId) ?? 0);

  const divisionWinners: typeof teams[0][] = [];
  const nonWinners: typeof teams[0][] = [];

  for (const div of DIVISIONS) {
    const divTeams = teams.filter((t) => t.division === div).toSorted(cmp);
    if (divTeams.length === 0) continue;
    divisionWinners.push(divTeams[0]);
    nonWinners.push(...divTeams.slice(1));
  }

  divisionWinners.sort(cmp);
  nonWinners.sort(cmp);
  const wildcards = nonWinners.slice(0, 3);

  const seeded: SeededTeam[] = [];
  divisionWinners.forEach((t, i) =>
    seeded.push({ participantId: t.participantId, elo: t.elo, seed: i + 1 })
  );
  wildcards.forEach((t, i) =>
    seeded.push({ participantId: t.participantId, elo: t.elo, seed: i + 5 })
  );

  return seeded;
}

/**
 * Simulate a playoff game with home-field advantage for the higher seed.
 * The team with the lower seed number (better seed) is the home team.
 */
function playGame(a: SeededTeam, b: SeededTeam): { winner: SeededTeam; loser: SeededTeam } {
  const [home, away] = a.seed < b.seed ? [a, b] : [b, a];
  const winProbHome = eloWinProbability(
    home.elo + NFL_HOME_FIELD_ELO_ADVANTAGE,
    away.elo
  );
  if (Math.random() < winProbHome) return { winner: home, loser: away };
  return { winner: away, loser: home };
}

/**
 * Simulate a game at a neutral site (no home-field adjustment).
 * Used for the Super Bowl.
 */
function playNeutralGame(a: SeededTeam, b: SeededTeam): { winner: SeededTeam; loser: SeededTeam } {
  const winProbA = eloWinProbability(a.elo, b.elo);
  if (Math.random() < winProbA) return { winner: a, loser: b };
  return { winner: b, loser: a };
}

/**
 * Simulate one conference's playoff bracket (seeds 1-7, seed 1 has bye).
 *
 * Wild Card:         2v7, 3v6, 4v5  (seed 1 bye; higher seed is home)
 * Divisional:        1 vs lowest remaining, 2 vs next  (higher seed is home)
 * Conference Champ:  divisional winners  (higher seed is home)
 *
 * Returns the finalist, the conference championship loser, and the two
 * divisional-round losers (for 5th-8th place EV).
 */
export function simulateConferenceBracket(seeds: SeededTeam[]): ConferenceBracketResult {
  const byIndex = new Map(seeds.map((t) => [t.seed, t]));
  const s = (seed: number) => byIndex.get(seed) as SeededTeam;

  // Wild Card (seed 1 bye; lower seed = home)
  const wc27 = playGame(s(2), s(7));
  const wc36 = playGame(s(3), s(6));
  const wc45 = playGame(s(4), s(5));

  // Divisional: sort WC winners by original seed (ascending = better seed = home)
  // Seed 1 hosts the lowest-ranked (highest seed number) WC winner
  const wcWinners = [wc27.winner, wc36.winner, wc45.winner].toSorted(
    (a, b) => a.seed - b.seed
  );

  const div1 = playGame(s(1), wcWinners[wcWinners.length - 1]);
  const div2 = playGame(wcWinners[0], wcWinners[1]);

  // Conference Championship (higher remaining seed is home)
  const conf = playGame(div1.winner, div2.winner);

  return {
    finalist: conf.winner,
    confChampLoser: conf.loser,
    divisionalLosers: [div1.loser, div2.loser],
  };
}

// ─── Simulator class ──────────────────────────────────────────────────────────

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

    // Load participants
    const participants = await db.query.seasonParticipants.findMany({
      where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
    });

    if (participants.length === 0) {
      throw new Error(`No participants found for sports season ${sportsSeasonId}.`);
    }

    // Load EV rows (sourceElo + sourceOdds)
    const evRows = await db
      .select({
        participantId: schema.seasonParticipantExpectedValues.participantId,
        sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
        sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
      })
      .from(schema.seasonParticipantExpectedValues)
      .where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));

    // Build Elo map: prefer sourceElo, fall back to converting sourceOdds
    const eloMap = new Map<string, number>();

    const hasSourceElo = evRows.some((r) => r.sourceElo !== null);
    if (hasSourceElo) {
      for (const r of evRows) {
        if (r.sourceElo !== null) eloMap.set(r.participantId, r.sourceElo);
      }
    } else {
      const hasOdds = evRows.some((r) => r.sourceOdds !== null);
      if (!hasOdds) {
        throw new Error(
          `No Elo ratings or futures odds found for sports season ${sportsSeasonId}. ` +
            `Enter sourceElo ratings via Admin → Expected Values before simulating.`
        );
      }
      const oddsInput = evRows
        .filter((r) => r.sourceOdds !== null)
        .map((r) => ({ participantId: r.participantId, odds: r.sourceOdds as number }));
      const converted = convertFuturesToElo(oddsInput);
      for (const [id, elo] of converted) eloMap.set(id, elo);
    }

    // Load standings (empty pre-season)
    const standingsRows = await getRegularSeasonStandings(sportsSeasonId, db);
    const standingsMap = new Map(standingsRows.map((r) => [r.participantId, r]));

    // Build team state list — skip participants with no Elo or unknown team name
    const teams: TeamState[] = [];
    for (const p of participants) {
      const teamData = getTeamData(p.name);
      if (!teamData) continue;
      const elo = eloMap.get(p.id);
      if (elo === undefined) continue;
      const standing = standingsMap.get(p.id);
      teams.push({
        participantId: p.id,
        conference: teamData.conference,
        division: teamData.division,
        elo,
        currentWins: standing?.wins ?? 0,
        gamesPlayed: standing?.gamesPlayed ?? 0,
      });
    }

    if (teams.length === 0) {
      throw new Error(
        `Could not match any participants to NFL team data for season ${sportsSeasonId}. ` +
          `Ensure participant names match (e.g. "Kansas City Chiefs").`
      );
    }

    // Validate that every division in every conference has at least one Elo-rated team.
    // A missing division means seeding is broken for the whole simulation.
    const covered = new Set(teams.map((t) => `${t.conference} ${t.division}`));
    const missing: string[] = [];
    for (const conf of CONFERENCES) {
      for (const div of DIVISIONS) {
        if (!covered.has(`${conf} ${div}`)) missing.push(`${conf} ${div}`);
      }
    }
    if (missing.length > 0) {
      throw new Error(
        `Missing Elo-rated teams in: ${missing.join(", ")}. ` +
          `All 32 NFL teams must have Elo ratings set before simulating.`
      );
    }

    // Pre-compute per-team win probability vs average opponent (constant across sims)
    const winProbMap = new Map<string, number>();
    for (const t of teams) {
      winProbMap.set(t.participantId, eloWinProbability(t.elo, AVG_OPPONENT_ELO));
    }

    // Placement counters
    const counts: Record<string, Record<1 | 2 | 3 | 4 | 5 | 6 | 7 | 8, number>> = {};
    for (const t of teams) {
      counts[t.participantId] = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0 };
    }

    // ─── Monte Carlo ──────────────────────────────────────────────────────────

    for (let sim = 0; sim < NUM_SIMULATIONS; sim++) {
      // Project wins for each team this simulation
      const projected = teams.map((t) => ({
        participantId: t.participantId,
        elo: t.elo,
        conference: t.conference,
        division: t.division,
        projectedWins: simulateProjectedWins(
          t.currentWins,
          t.gamesPlayed,
          winProbMap.get(t.participantId) as number
        ),
      }));

      const afcTeams = projected.filter((t) => t.conference === "AFC");
      const nfcTeams = projected.filter((t) => t.conference === "NFC");
      const afcSeeds = seedConference(afcTeams);
      const nfcSeeds = seedConference(nfcTeams);

      // Simulate conference brackets
      const afcResult = simulateConferenceBracket(afcSeeds);
      const nfcResult = simulateConferenceBracket(nfcSeeds);

      // Super Bowl (neutral site)
      const sb = playNeutralGame(afcResult.finalist, nfcResult.finalist);

      // 1st / 2nd
      counts[sb.winner.participantId][1] += 1;
      counts[sb.loser.participantId][2] += 1;

      // 3rd / 4th — Conference Championship losers (1 per conference, split 50/50)
      counts[afcResult.confChampLoser.participantId][3] += 0.5;
      counts[afcResult.confChampLoser.participantId][4] += 0.5;
      counts[nfcResult.confChampLoser.participantId][3] += 0.5;
      counts[nfcResult.confChampLoser.participantId][4] += 0.5;

      // 5th–8th — Divisional losers (4 total: 2 per conference, split evenly)
      for (const loser of [...afcResult.divisionalLosers, ...nfcResult.divisionalLosers]) {
        counts[loser.participantId][5] += 0.25;
        counts[loser.participantId][6] += 0.25;
        counts[loser.participantId][7] += 0.25;
        counts[loser.participantId][8] += 0.25;
      }
      // Wild Card losers and missed playoffs remain at 0 (already initialized)
    }

    // Convert counts to probabilities
    return teams.map((t) => {
      const c = counts[t.participantId];
      const N = NUM_SIMULATIONS;
      return {
        participantId: t.participantId,
        probabilities: {
          probFirst: c[1] / N,
          probSecond: c[2] / N,
          probThird: c[3] / N,
          probFourth: c[4] / N,
          probFifth: c[5] / N,
          probSixth: c[6] / N,
          probSeventh: c[7] / N,
          probEighth: c[8] / N,
        },
        source: "nfl_elo_simulation",
      };
    });
  }
}
