/**
 * NCAA Football CFP Simulator
 *
 * Monte Carlo simulation of the College Football Playoff (12-team format, 2024–present).
 *
 * Algorithm:
 *   1. Load all participants for the sports season from DB
 *   2. Load Elo/FPI ratings from participantExpectedValues.sourceElo
 *      (entered via Admin → Elo Ratings page; use FPI, S&P+, or any Elo-scale rating)
 *   3. If sourceOdds (American format) are also stored, build a normalized selection
 *      weight from implied championship probability (used for field selection in step 4)
 *      and blend into per-game win probability (ELO_WEIGHT=0.6 / ODDS_WEIGHT=0.4).
 *   4. Per simulation, select 12 teams for the CFP field:
 *      - If the pool has exactly 12 teams: use all of them (post-bracket mode).
 *      - If the pool has >12 teams: weighted sample without replacement using each
 *        team's selection weight — odds-derived if available, Elo-based otherwise.
 *        Teams with stronger championship odds are sampled more often, naturally
 *        encoding both selection probability and bracket strength into one signal.
 *   5. Seed the 12 selected teams by Elo (best Elo = seed 1).
 *   6. Simulate the CFP bracket:
 *        First Round (not scoring):  5v12, 6v11, 7v10, 8v9
 *        Quarterfinals (scoring):    1 vs 8/9w, 4 vs 5/12w, 3 vs 6/11w, 2 vs 7/10w
 *        Semifinals (scoring):       QF1w vs QF2w, QF3w vs QF4w
 *        National Championship:      SF1w vs SF2w
 *   7. Track placement counts per scoring tier across all simulations.
 *   8. Convert counts to probability distributions.
 *
 * Pre-bracket vs post-bracket mode:
 *   Pre-bracket (>12 participants): probabilities reflect both selection uncertainty
 *     and bracket performance. A bubble team might appear in only 40% of simulated
 *     fields, so its champion probability accounts for that.
 *   Post-bracket (exactly 12 participants): deterministic field, bracket-only sim.
 *
 * Win probability (per game): eloWinProbability() from probability-engine (400-divisor).
 * Blended win probability: ELO_WEIGHT * eloProb + ODDS_WEIGHT * oddsProb (when odds present).
 *
 * Selection weight (pre-bracket mode):
 *   With sourceOdds: normalized implied championship probability (vig removed, sums to 1).
 *   Without sourceOdds: softmax on Elo with temperature SELECTION_TEMP (sharply favors
 *     higher-rated teams — a 200-point Elo gap yields ~7× selection weight difference).
 *
 * Placement tiers → SimulationProbabilities mapping:
 *   probFirst              = National Champion (1 per sim)
 *   probSecond             = Championship game loser (1 per sim)
 *   probThird / probFourth = Semifinal losers (2 per sim — split evenly)
 *   probFifth–probEighth   = Quarterfinal losers (4 per sim — split evenly)
 *   First Round losers     → all 0 (score 0 fantasy points)
 *   Teams not selected     → all 0 (not in field for that sim)
 *
 * Admin setup:
 *   1. Create a Sport with simulatorType = "ncaa_football_bracket"
 *   2. Create a Sports Season and add all contender participants (12 or more)
 *   3. Enter FPI ratings via Admin → Elo Ratings (stored as sourceElo)
 *   4. Optionally enter championship futures odds via Admin → Futures Odds (sourceOdds)
 *      — strongly recommended for pre-bracket mode; drives both selection and bracket strength
 *   5. Run simulation via Admin → Simulate
 */

import { database } from "~/database/context";
import { eq } from "drizzle-orm";
import * as schema from "~/database/schema";
import {
  convertAmericanOddsToProbability,
  convertFuturesToElo,
  eloWinProbability,
} from "~/services/probability-engine";
import type { Simulator, SimulationResult } from "./types";

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

const NUM_SIMULATIONS = 50_000;
const BRACKET_SIZE = 12;

/**
 * Blend weights for per-game win probability when sourceOdds are present.
 * Lower Elo weight than other sports (0.7) gives more influence to Vegas
 * championship odds, which are highly informative in college football.
 */
const ELO_WEIGHT = 0.6;
const ODDS_WEIGHT = 1 - ELO_WEIGHT;

/**
 * Softmax temperature for Elo-based selection weights (pre-bracket mode, no odds).
 * At T=100, a 200-point Elo gap produces ~7× weight difference — enough to strongly
 * favour the top teams while still giving bubble teams meaningful selection probability.
 */
const SELECTION_TEMP = 100;

// ─── Types ────────────────────────────────────────────────────────────────────

interface Team {
  participantId: string;
  elo: number;
  /** Normalized futures win probability (0–1). Used for blending per-game win prob. */
  oddsProb: number;
  /**
   * Weight used for probabilistic CFP field selection (pre-bracket mode only).
   * Derived from oddsProb when available; otherwise softmax on Elo.
   */
  selectionWeight: number;
}

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

/**
 * Blended win probability for team1 vs team2.
 * Falls back to pure Elo when no futures data is present.
 */
function blendedWinProb(team1: Team, team2: Team): number {
  const eloProbValue = eloWinProbability(team1.elo, team2.elo);

  if (team1.oddsProb === 0 && team2.oddsProb === 0) {
    return eloProbValue;
  }

  const oddsSum = team1.oddsProb + team2.oddsProb;
  const oddsProbValue = oddsSum > 0 ? team1.oddsProb / oddsSum : 0.5;

  return ELO_WEIGHT * eloProbValue + ODDS_WEIGHT * oddsProbValue;
}

function simGame(team1: Team, team2: Team): { winner: Team; loser: Team } {
  const p1Wins = Math.random() < blendedWinProb(team1, team2);
  return p1Wins
    ? { winner: team1, loser: team2 }
    : { winner: team2, loser: team1 };
}

/**
 * Weighted sample without replacement — selects `n` teams from `pool` where each
 * team's probability of being drawn is proportional to its selectionWeight.
 * Returns the selected teams sorted by Elo descending (seed 1 = best Elo).
 */
function sampleBracketField(pool: Team[], n: number): Team[] {
  const remaining = [...pool];
  const selected: Team[] = [];

  for (let i = 0; i < n; i++) {
    const totalWeight = remaining.reduce((sum, t) => sum + t.selectionWeight, 0);
    let r = Math.random() * totalWeight;
    let j = 0;
    for (; j < remaining.length - 1; j++) {
      r -= remaining[j].selectionWeight;
      if (r <= 0) break;
    }
    selected.push(remaining[j]);
    remaining.splice(j, 1);
  }

  // Seed by Elo so that the best team in the sampled field is always seed 1.
  return selected.toSorted((a, b) => b.elo - a.elo);
}

// ─── Bracket simulation ───────────────────────────────────────────────────────

interface PlacementCounts {
  champion: number;
  finalist: number;
  sfLoser: number;
  qfLoser: number;
}

/**
 * Simulate one full 12-team CFP bracket.
 *
 * Seeding (teams sorted best→worst Elo, index 0 = seed 1):
 *   First Round:    [4]v[11], [5]v[10], [6]v[9], [7]v[8]
 *   Quarterfinals:  [0] vs fr4w, [3] vs fr1w, [2] vs fr2w, [1] vs fr3w
 *   Semifinals:     qf1w vs qf2w, qf3w vs qf4w
 *   Championship:   sf1w vs sf2w
 */
function simulateBracket(teams: Team[], counts: Map<string, PlacementCounts>): void {
  // ── First Round (seeds 5–12) ───────────────────────────────────────────────
  const fr1 = simGame(teams[4], teams[11]); // 5 vs 12
  const fr2 = simGame(teams[5], teams[10]); // 6 vs 11
  const fr3 = simGame(teams[6], teams[9]);  // 7 vs 10
  const fr4 = simGame(teams[7], teams[8]);  // 8 vs 9

  // ── Quarterfinals (seeds 1–4 get byes) ────────────────────────────────────
  const qf1 = simGame(teams[0], fr4.winner); // 1 vs 8/9 winner
  const qf2 = simGame(teams[3], fr1.winner); // 4 vs 5/12 winner
  const qf3 = simGame(teams[2], fr2.winner); // 3 vs 6/11 winner
  const qf4 = simGame(teams[1], fr3.winner); // 2 vs 7/10 winner

  const bump = (id: string, key: keyof PlacementCounts) => {
    const entry = counts.get(id);
    if (entry) entry[key]++;
  };

  bump(qf1.loser.participantId, "qfLoser");
  bump(qf2.loser.participantId, "qfLoser");
  bump(qf3.loser.participantId, "qfLoser");
  bump(qf4.loser.participantId, "qfLoser");

  // ── Semifinals ────────────────────────────────────────────────────────────
  const sf1 = simGame(qf1.winner, qf2.winner);
  const sf2 = simGame(qf3.winner, qf4.winner);

  bump(sf1.loser.participantId, "sfLoser");
  bump(sf2.loser.participantId, "sfLoser");

  // ── National Championship ─────────────────────────────────────────────────
  const final = simGame(sf1.winner, sf2.winner);

  bump(final.winner.participantId, "champion");
  bump(final.loser.participantId, "finalist");
}

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

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

    // 1. Load all participants for this sports season.
    const participants = await db
      .select({ id: schema.seasonParticipants.id })
      .from(schema.seasonParticipants)
      .where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId));

    if (participants.length < BRACKET_SIZE) {
      throw new Error(
        `CFP simulator requires at least ${BRACKET_SIZE} participants, ` +
        `found ${participants.length}. Add all contender teams to the sports season.`
      );
    }

    // 2. Load Elo/FPI ratings and optional futures odds in a single query.
    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 and raw odds maps in a single pass.
    const eloFromDb = new Map<string, number>();
    const rawOddsProbs = new Map<string, number>();

    for (const row of evRows) {
      if (row.sourceElo !== null) {
        eloFromDb.set(row.participantId, row.sourceElo);
      }
      if (row.sourceOdds !== null) {
        rawOddsProbs.set(row.participantId, convertAmericanOddsToProbability(row.sourceOdds));
      }
    }

    // 3. Build normalized odds probability map (vig removed).
    const normalizedOddsMap = new Map<string, number>();

    if (rawOddsProbs.size > 0) {
      const rawSum = [...rawOddsProbs.values()].reduce((a, b) => a + b, 0);
      for (const [id, prob] of rawOddsProbs) {
        normalizedOddsMap.set(id, rawSum > 0 ? prob / rawSum : 0);
      }

      // Backfill Elo from futures for any team missing sourceElo.
      if (eloFromDb.size < participants.length) {
        const oddsInput = evRows
          .filter((r) => r.sourceOdds !== null && !eloFromDb.has(r.participantId))
          .map((r) => ({ participantId: r.participantId, odds: r.sourceOdds ?? 0 }));

        if (oddsInput.length > 0) {
          const oddsEloMap = convertFuturesToElo(oddsInput, "american");
          for (const [id, elo] of oddsEloMap) {
            eloFromDb.set(id, elo);
          }
        }
      }
    }

    // 4. Build team list with Elo, oddsProb, and selectionWeight.
    const hasOdds = normalizedOddsMap.size > 0;

    const allTeams: Team[] = participants.map((p) => ({
      participantId: p.id,
      elo: eloFromDb.get(p.id) ?? 1500,
      oddsProb: normalizedOddsMap.get(p.id) ?? 0,
      selectionWeight: 0, // computed below
    }));

    if (hasOdds) {
      // Selection weight = normalized championship implied probability.
      // This encodes both "probability of making the field" and "strength once there."
      for (const team of allTeams) {
        team.selectionWeight = normalizedOddsMap.get(team.participantId) ?? 0;
      }
    } else {
      // No odds: softmax on Elo so top-rated teams are strongly favoured.
      const eloValues = allTeams.map((t) => t.elo);
      const maxElo = Math.max(...eloValues);
      // Subtract max for numerical stability before exp().
      const expWeights = allTeams.map((t) => Math.exp((t.elo - maxElo) / SELECTION_TEMP));
      const expSum = expWeights.reduce((a, b) => a + b, 0);
      for (let i = 0; i < allTeams.length; i++) {
        allTeams[i].selectionWeight = expWeights[i] / expSum;
      }
    }

    const preBracketMode = participants.length > BRACKET_SIZE;

    // In post-bracket mode (exactly 12), sort once and reuse the same field every sim.
    const deterministicField = preBracketMode
      ? null
      : [...allTeams].toSorted((a, b) => b.elo - a.elo);

    // 5. Initialise placement count accumulators for all participants.
    const allParticipantIds = participants.map((p) => p.id);
    const counts = new Map<string, PlacementCounts>(
      allParticipantIds.map((id) => [id, { champion: 0, finalist: 0, sfLoser: 0, qfLoser: 0 }])
    );

    // 6. Run Monte Carlo simulations.
    for (let s = 0; s < NUM_SIMULATIONS; s++) {
      const field = preBracketMode
        ? sampleBracketField(allTeams, BRACKET_SIZE)
        : (deterministicField ?? []);
      simulateBracket(field, counts);
    }

    // 7. Convert counts to probability distributions.
    //    SF losers: 2 per sim → each team's share = sfLoser / (2 * N).
    //    QF losers: 4 per sim → each team's share = qfLoser / (4 * N).
    const sfDivisor = 2 * NUM_SIMULATIONS;
    const qfDivisor = 4 * NUM_SIMULATIONS;

    return allParticipantIds.map((id) => {
      const c = counts.get(id) ?? { champion: 0, finalist: 0, sfLoser: 0, qfLoser: 0 };
      const sfProb = c.sfLoser / sfDivisor;
      const qfProb = c.qfLoser / qfDivisor;
      return {
        participantId: id,
        probabilities: {
          probFirst: c.champion / NUM_SIMULATIONS,
          probSecond: c.finalist / NUM_SIMULATIONS,
          probThird: sfProb,
          probFourth: sfProb,
          probFifth: qfProb,
          probSixth: qfProb,
          probSeventh: qfProb,
          probEighth: qfProb,
        },
        source: "cfp_monte_carlo",
      };
    });
  }
}
