/**
 * MLB Playoff Simulator
 *
 * Monte Carlo simulation of the MLB season and playoffs including seeding
 * projection for the current season (2026).
 *
 * Algorithm:
 *   1. Load all participants for the sports season from DB
 *   2. Match participant names to hardcoded team data (RDif + league/division)
 *   3. For each simulation:
 *      a. For each league (AL/NL), draw 1 division winner per division (3 draws),
 *         then draw 3 wildcard teams from the remaining pool — all weighted by win rate.
 *      b. Seed division winners 1–3 by RDif (best RDif = seed 1); WC teams 4–6 by RDif.
 *      c. Run the playoff bracket per league:
 *         - Wildcard Round (best-of-3): 3 vs 6, 4 vs 5 (seeds 1 & 2 get byes)
 *         - Division Series (best-of-5): 1 vs lowest WC survivor, 2 vs other
 *         - League Championship Series (best-of-7)
 *      d. World Series (best-of-7): AL champ vs NL champ
 *   4. Track placement counts per scoring tier
 *   5. Convert counts to probability distributions
 *
 * Win probability (log5 formula):
 *   Step 1 — convert projected RDif to win rate:
 *     winRate = clamp(0.5 + rdif / RDIF_DIVISOR, 0.01, 0.99)
 *     RDIF_DIVISOR compresses team strengths toward .500 for playoff parity.
 *     See the RDIF_DIVISOR constant below for tuning guidance.
 *   Step 2 — Bill James log5 head-to-head probability:
 *     P(A beats B) = (wA - wA·wB) / (wA + wB - 2·wA·wB)
 *
 * Futures blending:
 *   If sourceOdds are stored in participantExpectedValues for this season,
 *   the per-game win probability is blended:
 *     P(game) = RDIF_WEIGHT * rdifProb + ODDS_WEIGHT * oddsProb
 *   where oddsProb = normalizedOdds(A) / (normalizedOdds(A) + normalizedOdds(B)).
 *   Normalized odds are vig-removed futures win probabilities.
 *   RDIF_WEIGHT = 0.7, ODDS_WEIGHT = 0.3.
 *   Falls back to RDif-only when no odds are stored.
 *
 * Placement tiers → SimulationProbabilities mapping:
 *   probFirst              = World Series champion (1 per sim)
 *   probSecond             = World Series loser (1 per sim)
 *   probThird / probFourth = LCS losers (2 per sim — AL + NL, split evenly)
 *   probFifth–probEighth   = Division Series losers (4 per sim, split evenly)
 *   Wildcard Round losers  → all 0 (score 0 points, same as non-playoff teams)
 *   Missed playoffs        → all 0
 *
 * Team data keys:
 *   rdif:  FanGraphs Depth Charts projected run differential for 2026.
 *          Used for per-game win probability via log5.
 *   p_div: Probability of winning own division (FanGraphs Depth Charts divTitle).
 *          Must sum to ~1.0 within each 5-team division.
 *   p_wc:  Probability of claiming one of 3 wildcard slots (FanGraphs Depth Charts wcTitle).
 *          Used as relative weights among non-division-winners in each league.
 *
 * Sources:
 *   rdif:       https://www.fangraphs.com/standings/projected-standings
 *   p_div/p_wc: https://www.fangraphs.com/standings/playoff-odds/dc/div
 * Update at the start of each season.
 *
 * Divisions:
 *   AL East:    Yankees, Orioles, Red Sox, Rays, Blue Jays
 *   AL Central: Royals, Guardians, Twins, Tigers, White Sox
 *   AL West:    Astros, Mariners, Rangers, Angels, Athletics
 *   NL East:    Phillies, Braves, Mets, Nationals, Marlins
 *   NL Central: Brewers, Cubs, Cardinals, Reds, Pirates
 *   NL West:    Dodgers, Padres, Diamondbacks, Giants, Rockies
 */

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

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

const NUM_SIMULATIONS = 50_000;

/**
 * Controls how much projected run differential spreads teams away from .500.
 *
 * Baseline: 1620 (≈ 10 runs per win × 162 games). This gives the raw
 * Pythagorean-derived win rate, which tends to overstate dominance in
 * playoff matchups where you face elite pitching. Increasing this constant
 * compresses all team strengths toward .500 — reducing every strong team's
 * win probability in each series and yielding more upset potential across
 * the entire bracket. Decrease to amplify differences.
 *
 * Effect on Dodgers (RDif +137):
 *   1620 → win rate 0.585 (raw Pythagorean projection — too dominant)
 *   2000 → win rate 0.568
 *   4000 → win rate 0.534
 *   8000 → win rate 0.517 (current — near coin-flip vs any playoff team)
 */
const RDIF_DIVISOR = 8000;

/**
 * Blend weights for RDif vs. Vegas futures odds when sourceOdds are available.
 */
const RDIF_WEIGHT  = 0.7;
const ODDS_WEIGHT = 1 - RDIF_WEIGHT;

// ─── Team data (2026 pre-season — FanGraphs Depth Charts) ────────────────────
//
// rdif: Projected run differential from FanGraphs Depth Charts.
//       Source: https://www.fangraphs.com/standings/projected-standings
//
// Update all three values at the start of each season from the FanGraphs pages above.

interface MlbTeamData {
  league: "AL" | "NL";
  division: "AL East" | "AL Central" | "AL West" | "NL East" | "NL Central" | "NL West";
  rdif: number;  // 2026 FanGraphs Depth Charts projected run differential
  p_div: number; // 2026 FanGraphs Depth Charts divTitle — probability of winning own division
  p_wc: number;  // 2026 FanGraphs Depth Charts wcTitle — probability of claiming a wild card slot
}

const TEAMS_DATA: Record<string, MlbTeamData> = {
  // ── American League East ───────────────────────────────────────────────────
  "New York Yankees":   { league: "AL", division: "AL East",    rdif:  67, p_div: 0.374, p_wc: 0.358 },
  "Baltimore Orioles":  { league: "AL", division: "AL East",    rdif:  23, p_div: 0.128, p_wc: 0.310 },
  "Boston Red Sox":     { league: "AL", division: "AL East",    rdif:  48, p_div: 0.247, p_wc: 0.373 },
  "Tampa Bay Rays":     { league: "AL", division: "AL East",    rdif:   2, p_div: 0.066, p_wc: 0.229 },
  "Toronto Blue Jays":  { league: "AL", division: "AL East",    rdif:  37, p_div: 0.184, p_wc: 0.349 },

  // ── American League Central ────────────────────────────────────────────────
  "Kansas City Royals":    { league: "AL", division: "AL Central", rdif:   8, p_div: 0.297, p_wc: 0.157 },
  "Cleveland Guardians":   { league: "AL", division: "AL Central", rdif: -38, p_div: 0.084, p_wc: 0.075 },
  "Minnesota Twins":       { league: "AL", division: "AL Central", rdif: -15, p_div: 0.166, p_wc: 0.120 },
  "Detroit Tigers":        { league: "AL", division: "AL Central", rdif:  26, p_div: 0.449, p_wc: 0.155 },
  "Chicago White Sox":     { league: "AL", division: "AL Central", rdif: -112, p_div: 0.005, p_wc: 0.004 },

  // ── American League West ───────────────────────────────────────────────────
  "Houston Astros":      { league: "AL", division: "AL West",    rdif:  -1, p_div: 0.126, p_wc: 0.213 },
  "Seattle Mariners":    { league: "AL", division: "AL West",    rdif:  66, p_div: 0.595, p_wc: 0.205 },
  "Texas Rangers":       { league: "AL", division: "AL West",    rdif:  20, p_div: 0.201, p_wc: 0.270 },
  "Los Angeles Angels":  { league: "AL", division: "AL West",    rdif: -65, p_div: 0.011, p_wc: 0.035 },
  "Athletics":           { league: "AL", division: "AL West",    rdif: -17, p_div: 0.068, p_wc: 0.146 },

  // ── National League East ───────────────────────────────────────────────────
  "Philadelphia Phillies": { league: "NL", division: "NL East",    rdif:  51, p_div: 0.243, p_wc: 0.444 },
  "Atlanta Braves":        { league: "NL", division: "NL East",    rdif:  67, p_div: 0.357, p_wc: 0.425 },
  "New York Mets":         { league: "NL", division: "NL East",    rdif:  71, p_div: 0.389, p_wc: 0.413 },
  "Washington Nationals":  { league: "NL", division: "NL East",    rdif: -113, p_div: 0.001, p_wc: 0.006 },
  "Miami Marlins":         { league: "NL", division: "NL East",    rdif: -48, p_div: 0.011, p_wc: 0.075 },

  // ── National League Central ────────────────────────────────────────────────
  "Milwaukee Brewers":   { league: "NL", division: "NL Central", rdif:   9, p_div: 0.243, p_wc: 0.170 },
  "Chicago Cubs":        { league: "NL", division: "NL Central", rdif:  23, p_div: 0.347, p_wc: 0.183 },
  "St. Louis Cardinals": { league: "NL", division: "NL Central", rdif: -55, p_div: 0.038, p_wc: 0.049 },
  "Cincinnati Reds":     { league: "NL", division: "NL Central", rdif: -31, p_div: 0.086, p_wc: 0.095 },
  "Pittsburgh Pirates":  { league: "NL", division: "NL Central", rdif:  13, p_div: 0.285, p_wc: 0.178 },

  // ── National League West ───────────────────────────────────────────────────
  "Los Angeles Dodgers":    { league: "NL", division: "NL West",    rdif: 137, p_div: 0.900, p_wc: 0.082 },
  "San Diego Padres":       { league: "NL", division: "NL West",    rdif:  -9, p_div: 0.023, p_wc: 0.240 },
  "Arizona Diamondbacks":   { league: "NL", division: "NL West",    rdif:   5, p_div: 0.039, p_wc: 0.322 },
  "San Francisco Giants":   { league: "NL", division: "NL West",    rdif:   3, p_div: 0.038, p_wc: 0.317 },
  "Colorado Rockies":       { league: "NL", division: "NL West",    rdif: -173, p_div: 0.001, p_wc: 0.001 },
};

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

/** Normalize a team name for lookup (lowercase, trimmed, collapsed whitespace). */
export function normalizeTeamName(name: string): string {
  return name.toLowerCase().trim().replace(/\s+/g, " ");
}

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

/**
 * Convert projected run differential to a compressed win rate for matchup probability.
 * Uses RDIF_DIVISOR to control how much team strength spreads away from .500.
 * Clamped to [0.01, 0.99] to avoid degenerate log5 values.
 * Exported for unit testing.
 */
export function winRateFromRDif(rdif: number): number {
  return Math.min(0.99, Math.max(0.01, 0.5 + rdif / RDIF_DIVISOR));
}

/**
 * Convert an Elo rating to an equivalent projected run differential.
 * Uses the standard Elo win probability formula (parity factor 400, average Elo 1500),
 * then inverts the winRateFromRDif formula: rdif = (winRate − 0.5) × RDIF_DIVISOR.
 * Exported for unit testing.
 */
export function eloToRDif(elo: number): number {
  const winRate = 1 / (1 + Math.pow(10, (1500 - elo) / 400));
  return (winRate - 0.5) * RDIF_DIVISOR;
}

/**
 * Bill James log5 head-to-head win probability for team A over team B,
 * given their projected run differentials.
 *   P(A beats B) = (wA - wA·wB) / (wA + wB - 2·wA·wB)
 * Exported for unit testing.
 */
export function rdifWinProbability(rdifA: number, rdifB: number): number {
  const wA = winRateFromRDif(rdifA);
  const wB = winRateFromRDif(rdifB);
  // Denominator is always > 0 when wA and wB are in (0,1).
  return (wA - wA * wB) / (wA + wB - 2 * wA * wB);
}

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

interface TeamEntry {
  id: string;
  name: string;
  data: MlbTeamData | undefined;
  originalSeed?: number;
}

/** Get projected RDif for a team entry. Fallback 0 (league-average) for unknown teams. */
function getEntryRDif(entry: TeamEntry): number {
  return entry.data?.rdif ?? 0;
}

/**
 * Weighted pick without replacement using the given weight function.
 * Returns undefined if no eligible team has positive weight.
 */
function weightedPickByKey(
  pool: TeamEntry[],
  excluded: Set<TeamEntry>,
  getWeight: (t: TeamEntry) => number
): TeamEntry | undefined {
  const eligible = pool.filter((t) => !excluded.has(t));
  if (eligible.length === 0) return undefined;

  const weights = eligible.map(getWeight);
  const total = weights.reduce((s, w) => s + w, 0);
  if (total === 0) return undefined;

  let r = Math.random() * total;
  for (let i = 0; i < eligible.length; i++) {
    r -= weights[i];
    if (r <= 0) return eligible[i];
  }
  return eligible[eligible.length - 1];
}

// ─── Series simulators ─────────────────────────────────────────────────────────

type SeriesResult = { winner: TeamEntry; loser: TeamEntry };

/** Simulate a series where the winner must reach `winsNeeded` wins. */
function simSeries(
  a: TeamEntry,
  b: TeamEntry,
  winsNeeded: number,
  gameWinProb: (a: TeamEntry, b: TeamEntry) => number
): SeriesResult {
  const prob = gameWinProb(a, b);
  let winsA = 0;
  let winsB = 0;
  while (winsA < winsNeeded && winsB < winsNeeded) {
    if (Math.random() < prob) winsA++; else winsB++;
  }
  return winsA === winsNeeded ? { winner: a, loser: b } : { winner: b, loser: a };
}

/** Wildcard Round: best-of-3 (first to 2 wins). */
export function simBo3(
  a: TeamEntry,
  b: TeamEntry,
  gameWinProb: (a: TeamEntry, b: TeamEntry) => number
): SeriesResult {
  return simSeries(a, b, 2, gameWinProb);
}

/** Division Series: best-of-5 (first to 3 wins). */
export function simBo5(
  a: TeamEntry,
  b: TeamEntry,
  gameWinProb: (a: TeamEntry, b: TeamEntry) => number
): SeriesResult {
  return simSeries(a, b, 3, gameWinProb);
}

/** League Championship + World Series: best-of-7 (first to 4 wins). */
export function simBo7(
  a: TeamEntry,
  b: TeamEntry,
  gameWinProb: (a: TeamEntry, b: TeamEntry) => number
): SeriesResult {
  return simSeries(a, b, 4, gameWinProb);
}

// ─── League bracket builder ───────────────────────────────────────────────────

/**
 * Draws the 6-team playoff field for one league (AL or NL).
 *
 * Steps:
 *   1. For each of the 3 divisions, draw 1 division winner weighted by p_div.
 *   2. From the remaining non-division-winners, draw 3 WC teams weighted by p_wc.
 *   3. Rank division winners 1–3 by RDif descending (best RDif → seed 1).
 *   4. Rank WC teams 4–6 by RDif descending (best RDif → seed 4).
 *
 * Returns an array of 6 TeamEntry objects in seed order [1..6], each annotated
 * with originalSeed, or undefined if any draw is degenerate (no eligible team
 * with positive weight).
 */
function drawLeaguePlayoffField(
  leagueTeams: TeamEntry[],
  getRDif: (t: TeamEntry) => number = getEntryRDif
): TeamEntry[] | undefined {
  // Group by division
  const divMap = new Map<string, TeamEntry[]>();
  for (const t of leagueTeams) {
    const div = t.data?.division ?? "Unknown";
    if (!divMap.has(div)) divMap.set(div, []);
    divMap.get(div)?.push(t);
  }

  const divisionWinners: TeamEntry[] = [];
  const allDivisionWinnerSet = new Set<TeamEntry>();

  for (const divTeams of divMap.values()) {
    const winner = weightedPickByKey(divTeams, new Set(), (t) => t.data?.p_div ?? 0);
    if (!winner) return undefined;
    divisionWinners.push(winner);
    allDivisionWinnerSet.add(winner);
  }

  // Draw 3 WC teams from non-division-winners, weighted by p_wc
  const wcPool = leagueTeams.filter((t) => !allDivisionWinnerSet.has(t));
  const wcTaken = new Set<TeamEntry>();
  const wcTeams: TeamEntry[] = [];

  for (let i = 0; i < 3; i++) {
    const wc = weightedPickByKey(wcPool, wcTaken, (t) => t.data?.p_wc ?? 0);
    if (!wc) return undefined;
    wcTeams.push(wc);
    wcTaken.add(wc);
  }

  // Rank division winners 1–3 by RDif descending (best RDif = seed 1)
  const sortedDivWinners = divisionWinners.toSorted((a, b) => getRDif(b) - getRDif(a));

  // Rank WC teams 4–6 by RDif descending (best RDif = seed 4)
  const sortedWcTeams = wcTeams.toSorted((a, b) => getRDif(b) - getRDif(a));

  const seeds = [...sortedDivWinners, ...sortedWcTeams];
  return seeds.map((t, i) => ({ ...t, originalSeed: i + 1 }));
}

/**
 * Simulate the full playoff bracket for one league.
 *
 * Bracket structure:
 *   Wildcard Round (best-of-3): seeds 3v6, 4v5 — seeds 1 & 2 get byes
 *   Division Series (best-of-5): 1 vs lowest-seeded WC survivor; 2 vs other
 *   League Championship Series (best-of-7)
 *
 * Returns { lcWinner, lcLoser, dsLosers[2], wcLosers[2] }
 */
function simLeagueBracket(
  seeds: TeamEntry[],
  gameWinProb: (a: TeamEntry, b: TeamEntry) => number
): {
  lcWinner: TeamEntry;
  lcLoser: TeamEntry;
  dsLosers: [TeamEntry, TeamEntry];
  wcLosers: [TeamEntry, TeamEntry];
} {
  const [s1, s2, s3, s4, s5, s6] = seeds;

  // Wildcard Round (best-of-3)
  const wc1 = simBo3(s3, s6, gameWinProb);
  const wc2 = simBo3(s4, s5, gameWinProb);

  // Division Series: re-seed — seed 1 plays the worse WC survivor, seed 2 plays the better one.
  // Sort by originalSeed ascending: [0] = lower seed number = better team, [1] = worse team.
  const survivors = [wc1.winner, wc2.winner].toSorted(
    (a, b) => (a.originalSeed ?? 0) - (b.originalSeed ?? 0)
  );
  const ds1 = simBo5(s1, survivors[1], gameWinProb); // 1 vs worse remaining (higher seed number)
  const ds2 = simBo5(s2, survivors[0], gameWinProb); // 2 vs better remaining (lower seed number)

  // League Championship Series (best-of-7)
  const lcs = simBo7(ds1.winner, ds2.winner, gameWinProb);

  return {
    lcWinner: lcs.winner,
    lcLoser: lcs.loser,
    dsLosers: [ds1.loser, ds2.loser],
    wcLosers: [wc1.loser, wc2.loser],
  };
}

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

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

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

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

    const participantIds = participantRows.map((r) => r.id);

    const teams: TeamEntry[] = participantRows.map((r) => ({
      id: r.id,
      name: r.name,
      data: getTeamData(r.name),
    }));

    // Warn about participants that don't match any hardcoded team.
    const unrecognized = teams.filter((t) => !t.data);
    if (unrecognized.length > 0) {
      logger.warn(
        `[MLBSimulator] ${unrecognized.length} participant(s) not found in TEAMS_DATA and will be excluded: ` +
          unrecognized.map((t) => t.name).join(", ")
      );
    }

    const alTeams = teams.filter((t) => t.data?.league === "AL");
    const nlTeams = teams.filter((t) => t.data?.league === "NL");

    if (alTeams.length < 6 || nlTeams.length < 6) {
      throw new Error(
        `Each league needs at least 6 recognized participants (3 division winners + 3 wild cards) ` +
          `(got AL: ${alTeams.length}, NL: ${nlTeams.length}). ` +
          `Add MLB teams before running simulation.`
      );
    }

    // ─── Futures odds blending ─────────────────────────────────────────────────

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

    const participantIdSet = new Set(participantIds);
    const oddsRows = evRows.filter(
      (r) => r.sourceOdds !== null && participantIdSet.has(r.participantId)
    );
    const hasOdds = oddsRows.length > 0;

    const normalizedOddsMap = new Map<string, number>();
    if (hasOdds) {
      const rawProbs = oddsRows.map((r) =>
        convertAmericanOddsToProbability(r.sourceOdds ?? 0)
      );
      const normalized = normalizeProbabilities(rawProbs);
      oddsRows.forEach(({ participantId }, i) => {
        normalizedOddsMap.set(participantId, normalized[i]);
      });
    }

    // Build a map of sourceElo-derived RDif values (overrides hardcoded TEAMS_DATA.rdif).
    const sourceEloRDifMap = new Map<string, number>();
    for (const r of evRows) {
      if (r.sourceElo !== null && r.sourceElo !== undefined && participantIdSet.has(r.participantId)) {
        sourceEloRDifMap.set(r.participantId, eloToRDif(r.sourceElo));
      }
    }

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

    /** Effective RDif: prefer sourceElo-derived value over hardcoded TEAMS_DATA.rdif. */
    const effectiveRDif = (entry: TeamEntry): number =>
      sourceEloRDifMap.get(entry.id) ?? getEntryRDif(entry);

    /**
     * Blended per-game win probability for team A over team B.
     * When odds are available: 70% RDif log5 + 30% vig-removed futures head-to-head.
     */
    const gameWinProb = (a: TeamEntry, b: TeamEntry): number => {
      const rdifProb = rdifWinProbability(effectiveRDif(a), effectiveRDif(b));
      if (!hasOdds) return rdifProb;

      const o1 = normalizedOddsMap.get(a.id);
      const o2 = normalizedOddsMap.get(b.id);
      // Fall back to RDif if either team lacks odds — avoids inflating one side to 100%.
      if (o1 === null || o1 === undefined || o2 === null || o2 === undefined) return rdifProb;
      const oddsProb = o1 + o2 > 0 ? o1 / (o1 + o2) : 0.5;
      return RDIF_WEIGHT * rdifProb + ODDS_WEIGHT * oddsProb;
    };

    // ─── Placement 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 lcsLoserCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const dsLoserCounts  = new Map<string, number>(participantIds.map((id) => [id, 0]));
    // WC losers are not tracked — they score 0 points

    // ─── Monte Carlo simulation loop ───────────────────────────────────────────

    let effectiveN = 0;

    for (let s = 0; s < NUM_SIMULATIONS; s++) {
      const alField = drawLeaguePlayoffField(alTeams, effectiveRDif);
      const nlField = drawLeaguePlayoffField(nlTeams, effectiveRDif);
      if (!alField || !nlField) continue; // degenerate draw — skip

      effectiveN++;

      // Simulate both league brackets
      const alResult = simLeagueBracket(alField, gameWinProb);
      const nlResult = simLeagueBracket(nlField, gameWinProb);

      // LCS losers (3rd/4th tier)
      lcsLoserCounts.set(alResult.lcLoser.id, (lcsLoserCounts.get(alResult.lcLoser.id) ?? 0) + 1);
      lcsLoserCounts.set(nlResult.lcLoser.id, (lcsLoserCounts.get(nlResult.lcLoser.id) ?? 0) + 1);

      // DS losers (5th–8th tier)
      for (const loser of [...alResult.dsLosers, ...nlResult.dsLosers]) {
        dsLoserCounts.set(loser.id, (dsLoserCounts.get(loser.id) ?? 0) + 1);
      }

      // World Series (best-of-7)
      const ws = simBo7(alResult.lcWinner, nlResult.lcWinner, gameWinProb);
      championCounts.set(ws.winner.id, (championCounts.get(ws.winner.id) ?? 0) + 1);
      finalistCounts.set(ws.loser.id,  (finalistCounts.get(ws.loser.id) ?? 0)  + 1);
    }

    if (effectiveN === 0) {
      throw new Error(
        "All simulations produced degenerate brackets. " +
          "Check that each division has teams with non-degenerate RDif values."
      );
    }

    // ─── Convert counts to probability distributions ───────────────────────────
    //
    //   probFirst/Second  → count / N           (1 team per sim)
    //   probThird/Fourth  → count / (2 * N)     (2 LCS losers per sim: AL + NL)
    //   probFifth–Eighth  → count / (4 * N)     (4 DS losers per sim: 2 per league)
    //   WC losers         → 0 (all probs zero)

    const N = effectiveN;
    const results: SimulationResult[] = participantIds.map((participantId) => {
      const c   = championCounts.get(participantId) ?? 0;
      const f   = finalistCounts.get(participantId) ?? 0;
      const lcs = lcsLoserCounts.get(participantId) ?? 0;
      const ds  = dsLoserCounts.get(participantId) ?? 0;
      return {
        participantId,
        probabilities: {
          probFirst:   c   / N,
          probSecond:  f   / N,
          probThird:   lcs / (2 * N),
          probFourth:  lcs / (2 * N),
          probFifth:   ds  / (4 * N),
          probSixth:   ds  / (4 * N),
          probSeventh: ds  / (4 * N),
          probEighth:  ds  / (4 * N),
        },
        source: "mlb_bracket_monte_carlo",
      };
    });

    // ─── Per-position normalization ────────────────────────────────────────────

    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;
  }
}
