/**
 * WNBA Playoff Simulator
 *
 * Monte Carlo simulation of the WNBA playoffs including regular season
 * remainder projection and seeding.
 *
 * Algorithm:
 *   1. Load participants, regular season standings, and futures odds in parallel.
 *   2. Choose Elo source automatically based on season progress:
 *        Pre-season  (avg gamesPlayed < SRS_GAMES_THRESHOLD):
 *          → futures odds via convertFuturesToElo() from participantExpectedValues
 *          → falls back to 1500 for teams without odds
 *        In-season   (avg gamesPlayed >= SRS_GAMES_THRESHOLD):
 *          → SRS from regularSeasonStandings: elo = 1500 + srs * SRS_ELO_SCALE
 *          → falls back to futures Elo for teams without SRS, then 1500
 *   3. For each simulation:
 *      a. For each team, simulate remaining regular season games (40 - gamesPlayed)
 *         using Elo win probability vs. an average opponent (Elo 1500)
 *         → projectedWins = currentWins + simulatedRemainingWins
 *      b. Sort all teams by projected wins (desc) + random tiebreaker
 *         → Top 8 qualify for playoffs
 *      c. Simulate WNBA playoff bracket (no byes, no play-in):
 *         Round 1    (best-of-3):  1v8, 4v5, 2v7, 3v6
 *         Semifinals (best-of-5):  winner(1/8) vs winner(4/5), winner(2/7) vs winner(3/6)
 *         Finals     (best-of-7):  Semifinal winners
 *   4. Track placement counts per scoring tier
 *   5. Convert counts to probability distributions
 *
 * Win probability (Elo, PARITY_FACTOR = 400):
 *   P(A beats B) = 1 / (1 + 10^((eloB - eloA) / 400))
 *
 * SRS → Elo conversion (in-season):
 *   elo = 1500 + srs * 20
 *   Calibration: WNBA SRS typically ranges ±8–10. A ±10 SRS gap → ±200 Elo → ~76% win
 *   probability for the stronger team, which is appropriate for single-game WNBA matchups.
 *   Source: basketball-reference.com/wnba/years/YYYY.html (SRS column in team standings)
 *
 * Futures → Elo conversion (pre-season):
 *   Uses convertFuturesToElo() from probability-engine (same as BracketSimulator / UCLSimulator).
 *   Reads sourceOdds (American format) from participantExpectedValues.
 *
 * Placement tiers → SimulationProbabilities mapping:
 *   probFirst         = WNBA champion (1 per sim)
 *   probSecond        = Finals loser (1 per sim)
 *   probThird/Fourth  = Semifinal losers (2 per sim)
 *   probFifth–Eighth  = Round 1 losers (4 per sim)
 *   Missed playoffs   → all 0
 */

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

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

const NUM_SIMULATIONS = 50_000;

/** WNBA regular season games per team. */
const WNBA_REGULAR_SEASON_GAMES = 40;

/**
 * Multiplier converting SRS to Elo offset from 1500.
 *   elo = 1500 + srs * SRS_ELO_SCALE
 *
 * At scale 20:
 *   SRS +10 → Elo 1700, SRS -10 → Elo 1300 → P(+10 vs -10) ≈ 76%
 *   SRS +5  → Elo 1600, SRS  0  → Elo 1500 → P(+5 vs 0)   ≈ 64%
 */
const SRS_ELO_SCALE = 20;

/**
 * Average games played threshold to switch from futures-derived Elo to SRS-derived Elo.
 * Once most teams have played this many games, SRS is meaningful enough to use.
 */
const SRS_GAMES_THRESHOLD = 5;

/** Number of playoff teams. */
const PLAYOFF_TEAMS = 8;

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

export { normalizeTeamName };

/**
 * Convert an SRS rating to an Elo rating.
 * An SRS of 0 maps to 1500 (league average).
 */
export function srsToElo(srs: number): number {
  return 1500 + srs * SRS_ELO_SCALE;
}

/**
 * Resolve the Elo to use for a team given available signals and the current mode.
 *
 * Priority: sourceElo (admin-entered projected wins) > SRS (in-season) > futures odds > 1500.
 */
export function resolveElo(
  srs: number | null,
  futuresElo: number | null,
  useSRS: boolean,
  sourceElo?: number | null
): number {
  if (sourceElo !== null && sourceElo !== undefined) return sourceElo;
  if (useSRS) {
    if (srs !== null) return srsToElo(srs);
    return futuresElo ?? 1500;
  }
  return futuresElo ?? 1500;
}

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

interface TeamEntry {
  id: string;
  name: string;
  elo: number;
  currentWins: number;
  remainingGames: number;
  /** Elo win probability vs. average opponent (1500) — constant per team. */
  winProb: number;
}

/** Simulate a best-of-N series.
 *  @param winTarget wins needed to win the series (2 for BoX3, 3 for BoX5, 4 for BoX7) */
export function simSeriesN(
  a: TeamEntry,
  b: TeamEntry,
  winTarget: number
): { winner: TeamEntry; loser: TeamEntry } {
  const winProb = eloWinProbability(a.elo, b.elo);
  let winsA = 0;
  let winsB = 0;
  while (winsA < winTarget && winsB < winTarget) {
    if (Math.random() < winProb) winsA++;
    else winsB++;
  }
  return winsA === winTarget ? { winner: a, loser: b } : { winner: b, loser: a };
}

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

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

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

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

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

    if (participantRows.length < PLAYOFF_TEAMS) {
      throw new Error(
        `WNBA simulation requires at least ${PLAYOFF_TEAMS} participants ` +
          `(got ${participantRows.length}). Add all WNBA teams before running simulation.`
      );
    }

    // 2. Build futures Elo map from championship odds and sourceElo map from projected wins.
    const oddsInput = evRows
      .filter((r) => r.sourceOdds !== null)
      .map((r) => ({ participantId: r.participantId, odds: r.sourceOdds ?? 0 }));
    const futuresEloMap: Map<string, number> =
      oddsInput.length > 0
        ? convertFuturesToElo(oddsInput, "american")
        : new Map();

    const sourceEloMap = new Map<string, number>();
    for (const row of evRows) {
      if (row.sourceElo !== null && row.sourceElo !== undefined) {
        sourceEloMap.set(row.participantId, row.sourceElo);
      }
    }

    // 3. Build standings lookup and determine simulation mode.
    const standingsMap = new Map(standings.map((s) => [s.participantId, s]));
    const participantIds = participantRows.map((r) => r.id);

    const totalGamesPlayed = participantRows.reduce((sum, r) => {
      return sum + (standingsMap.get(r.id)?.gamesPlayed ?? 0);
    }, 0);
    const avgGamesPlayed = totalGamesPlayed / participantRows.length;
    const useSRS = avgGamesPlayed >= SRS_GAMES_THRESHOLD;

    // 4. Construct team entries with resolved Elo.
    const teams: TeamEntry[] = participantRows.map((r) => {
      const standing = standingsMap.get(r.id);
      const srs =
        standing?.srs !== null && standing?.srs !== undefined
          ? parseFloat(standing.srs)
          : null;
      const futuresElo = futuresEloMap.get(r.id) ?? null;
      const elo = resolveElo(srs, futuresElo, useSRS, sourceEloMap.get(r.id) ?? null);
      const gamesPlayed = standing?.gamesPlayed ?? 0;
      return {
        id: r.id,
        name: r.name,
        elo,
        currentWins: standing?.wins ?? 0,
        remainingGames: Math.max(0, WNBA_REGULAR_SEASON_GAMES - gamesPlayed),
        winProb: eloWinProbability(elo, 1500),
      };
    });

    const source = useSRS
      ? "wnba_bracket_monte_carlo_srs"
      : "wnba_bracket_monte_carlo_futures";

    /** Seed teams 1–8 by projected wins for this simulation. */
    const buildSeededBracket = (): TeamEntry[] => {
      const projected = teams.map((t) => ({
        team: t,
        projectedWins: simulateProjectedWins(t),
        tiebreaker: Math.random(),
      }));
      projected.sort((a, b) => b.projectedWins - a.projectedWins || b.tiebreaker - a.tiebreaker);
      return projected.slice(0, PLAYOFF_TEAMS).map((x) => x.team);
    };

    // 5. 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 semiLoserCounts  = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const r1LoserCounts    = new Map<string, number>(participantIds.map((id) => [id, 0]));

    // 6. Monte Carlo simulation loop.
    for (let s = 0; s < NUM_SIMULATIONS; s++) {
      const [s1, s2, s3, s4, s5, s6, s7, s8] = buildSeededBracket();

      // Round 1 (best-of-3): 1v8, 4v5, 2v7, 3v6
      const r1_a = simSeriesN(s1, s8, 2);
      const r1_b = simSeriesN(s4, s5, 2);
      const r1_c = simSeriesN(s2, s7, 2);
      const r1_d = simSeriesN(s3, s6, 2);

      r1LoserCounts.set(r1_a.loser.id, (r1LoserCounts.get(r1_a.loser.id) ?? 0) + 1);
      r1LoserCounts.set(r1_b.loser.id, (r1LoserCounts.get(r1_b.loser.id) ?? 0) + 1);
      r1LoserCounts.set(r1_c.loser.id, (r1LoserCounts.get(r1_c.loser.id) ?? 0) + 1);
      r1LoserCounts.set(r1_d.loser.id, (r1LoserCounts.get(r1_d.loser.id) ?? 0) + 1);

      // Semifinals (best-of-5): winner(1/8) vs winner(4/5), winner(2/7) vs winner(3/6)
      const sf_a = simSeriesN(r1_a.winner, r1_b.winner, 3);
      const sf_b = simSeriesN(r1_c.winner, r1_d.winner, 3);

      semiLoserCounts.set(sf_a.loser.id, (semiLoserCounts.get(sf_a.loser.id) ?? 0) + 1);
      semiLoserCounts.set(sf_b.loser.id, (semiLoserCounts.get(sf_b.loser.id) ?? 0) + 1);

      // Finals (best-of-7)
      const final = simSeriesN(sf_a.winner, sf_b.winner, 4);

      championCounts.set(final.winner.id, (championCounts.get(final.winner.id) ?? 0) + 1);
      finalistCounts.set(final.loser.id,  (finalistCounts.get(final.loser.id) ?? 0)  + 1);
    }

    // 7. Convert integer counts to probability distributions.
    const results: SimulationResult[] = participantIds.map((participantId) => {
      const c  = championCounts.get(participantId)  ?? 0;
      const f  = finalistCounts.get(participantId)  ?? 0;
      const sl = semiLoserCounts.get(participantId) ?? 0;
      const r1 = r1LoserCounts.get(participantId)   ?? 0;
      return {
        participantId,
        probabilities: {
          probFirst:   c  / NUM_SIMULATIONS,
          probSecond:  f  / NUM_SIMULATIONS,
          probThird:   sl / (2 * NUM_SIMULATIONS),
          probFourth:  sl / (2 * NUM_SIMULATIONS),
          probFifth:   r1 / (4 * NUM_SIMULATIONS),
          probSixth:   r1 / (4 * NUM_SIMULATIONS),
          probSeventh: r1 / (4 * NUM_SIMULATIONS),
          probEighth:  r1 / (4 * NUM_SIMULATIONS),
        },
        source,
      };
    });

    // 8. Per-position normalization — belt-and-suspenders guard against floating-point residuals.
    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;
  }
}
