/**
 * MLS Season + Playoffs Simulator
 *
 * Monte Carlo simulation of the MLS regular season and MLS Cup Playoffs.
 *
 * Two modes, auto-detected at runtime:
 *
 * ── Mode 1: Known-Seed ────────────────────────────────────────────────────────
 * Used when all 18 playoff seeds (1–9 East, 1–9 West) are set via the `seed`
 * input and a `region` ("Eastern"/"Western") is present for every playoff team.
 * Simulates the playoff bracket directly from those seeds.
 *
 * ── Mode 2: Regular-Season Projection (default) ───────────────────────────────
 * Simulates remaining regular-season games per team using Elo vs the average
 * opponent (1500), per conference. Top 9 teams per conference qualify.
 * Then simulates the MLS Cup Playoffs bracket.
 *
 * Conference assignment resolution order:
 *   1. regularSeasonStandings.conference (populated by standings sync)
 *   2. seasonParticipantSimulatorInputs.region ("Eastern" or "Western")
 *   3. seasonParticipant.externalId set to "Eastern" or "Western" (pre-sync bootstrap;
 *      note: this prevents externalId-based matching in future syncs for that participant,
 *      which falls back to slower name-matching — acceptable tradeoff for bootstrapping)
 *   4. Error — conference must be known for all teams
 *
 * MLS Cup Playoffs format (18 teams: 9 East + 9 West):
 *   Wild Card     : E8 vs E9, W8 vs W9 — single game (no draws; PKs if tied)
 *   Round 1 (Conf Quarters): best-of-3 — E1 vs WC winner, E2 vs E7, E3 vs E6, E4 vs E5
 *                             (same structure West)
 *   Conference Semifinals   : single game — 4 per conference → 2 per conference
 *   Conference Finals       : single game — 2 per conference → 1 per conference
 *   MLS Cup                 : single game — East champion vs West champion
 *
 * Probability mapping:
 *   probFirst          = MLS Cup champion (1 per sim)
 *   probSecond         = MLS Cup finalist (1 per sim)
 *   probThird/Fourth   = Conference Finals losers (2 per sim — split evenly)
 *   probFifth–Eighth   = Conference Semifinals losers (4 per sim — split evenly)
 *   Wild Card losers, Round 1 losers, non-qualifiers → 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 { eloWinProbabilityWithParity, convertFuturesToElo } from "~/services/probability-engine";
import { getRegularSeasonStandings } from "~/models/regular-season-standings";
import { getParticipantSimulatorInputs, getSportsSeasonSimulatorConfig } from "~/models/simulator";
import { simulateEloSoccerMatch, simulateSimpleSoccerGoals } from "./soccer-helpers";
import type { EloSoccerMatchOptions } from "./soccer-helpers";
import { normalizeSimulationResultColumns } from "./simulation-probabilities";
import { logger } from "~/lib/logger";

// ─── Default constants (overridable via season simulator config) ───────────────

const DEFAULT_NUM_SIMULATIONS = 50_000;
const DEFAULT_SEASON_GAMES = 34;
const DEFAULT_PARITY_FACTOR = 400;
const DEFAULT_AVERAGE_OPPONENT_ELO = 1500;
const DEFAULT_BASE_DRAW_RATE = 0.26;
const DEFAULT_DRAW_DECAY = 0.002;

const MLS_PLAYOFF_TEAMS_PER_CONF = 9;

// ─── Config helpers ────────────────────────────────────────────────────────────

function configNumber(config: Record<string, unknown> | undefined, key: string, fallback: number): number {
  const value = config?.[key];
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
}

/**
 * Infer MLS conference from seasonParticipant.externalId.
 * Admins can set externalId to "Eastern" or "Western" before the first standings
 * sync, mirroring how LLWS uses externalId to encode pool assignment.
 *
 * Side-effect: once externalId holds a conference name instead of an ESPN team ID,
 * the standings-sync write-back won't overwrite it (write-back only fires when
 * externalId is null). That participant will always match by name, never by ID.
 */
export function parseConferenceFromExternalId(externalId: string | null | undefined): MlsConference | null {
  if (!externalId) return null;
  const upper = externalId.toUpperCase();
  if (upper === "EASTERN") return "Eastern";
  if (upper === "WESTERN") return "Western";
  return null;
}

// ─── Public types ─────────────────────────────────────────────────────────────

export type MlsConference = "Eastern" | "Western";

export interface MlsTeamEntry {
  participantId: string;
  elo: number;
  conference: MlsConference;
  currentPoints: number;
  currentGoalsFor: number;
  currentGoalDifference: number;
  remainingGames: number;
}

export interface MlsConferenceSeed {
  participantId: string;
  elo: number;
  conference: MlsConference;
  seed: number; // 1–9 within conference
}

export interface MlsPlayoffResult {
  champion: string;
  finalist: string;
  confFinalsLosers: [string, string];
  confSemiLosers: [string, string, string, string];
}

// ─── Match helpers ────────────────────────────────────────────────────────────

export function mlsWinProbability(eloA: number, eloB: number, parityFactor = DEFAULT_PARITY_FACTOR): number {
  return eloWinProbabilityWithParity(eloA, eloB, parityFactor);
}

const DEFAULT_MATCH_OPTIONS: EloSoccerMatchOptions = {
  baseDrawRate: DEFAULT_BASE_DRAW_RATE,
  drawDecay: DEFAULT_DRAW_DECAY,
  parityFactor: DEFAULT_PARITY_FACTOR,
};

/**
 * Simulate a single MLS knockout game (no draws allowed in result).
 * If the Elo match produces a draw, a penalty shootout is simulated
 * using the same Elo win probability.
 */
export function simulateMlsKnockoutGame(
  teamA: MlsConferenceSeed,
  teamB: MlsConferenceSeed,
  matchOptions: EloSoccerMatchOptions = DEFAULT_MATCH_OPTIONS
): { winner: MlsConferenceSeed; loser: MlsConferenceSeed } {
  const result = simulateEloSoccerMatch(teamA.elo, teamB.elo, matchOptions);
  if (result === "win") return { winner: teamA, loser: teamB };
  if (result === "loss") return { winner: teamB, loser: teamA };
  // Draw → penalty shootout (Elo-biased coin flip)
  const pA = mlsWinProbability(teamA.elo, teamB.elo, matchOptions.parityFactor ?? DEFAULT_PARITY_FACTOR);
  return Math.random() < pA
    ? { winner: teamA, loser: teamB }
    : { winner: teamB, loser: teamA };
}

/**
 * Simulate a best-of-3 MLS Round 1 series.
 * Individual games use knockout rules (no draws; PKs decide ties).
 * Higher seed is home for games 1 and 3.
 */
export function simulateMlsBestOfThree(
  higherSeed: MlsConferenceSeed,
  lowerSeed: MlsConferenceSeed,
  matchOptions: EloSoccerMatchOptions = DEFAULT_MATCH_OPTIONS,
  existing: { winsHigher: number; winsLower: number } = { winsHigher: 0, winsLower: 0 }
): { winner: MlsConferenceSeed; loser: MlsConferenceSeed; gamesPlayed: number } {
  let wH = existing.winsHigher;
  let wL = existing.winsLower;
  let gamesPlayed = 0;
  while (wH < 2 && wL < 2) {
    const { winner } = simulateMlsKnockoutGame(higherSeed, lowerSeed, matchOptions);
    if (winner.participantId === higherSeed.participantId) wH++; else wL++;
    gamesPlayed++;
  }
  return wH === 2
    ? { winner: higherSeed, loser: lowerSeed, gamesPlayed }
    : { winner: lowerSeed, loser: higherSeed, gamesPlayed };
}

// ─── Regular-season simulation ────────────────────────────────────────────────

/**
 * Simulate remaining regular season games per conference and return the
 * top-9 seeds for each conference sorted by points → GD → GF → random tiebreak.
 */
export function simulateMlsRegularSeason(
  teams: MlsTeamEntry[],
  matchOptions: EloSoccerMatchOptions = DEFAULT_MATCH_OPTIONS,
  averageOpponentElo = DEFAULT_AVERAGE_OPPONENT_ELO
): { east: MlsConferenceSeed[]; west: MlsConferenceSeed[] } {
  const jitter = new Map(teams.map((t) => [t.participantId, Math.random()]));

  const rows = teams.map((t) => {
    let points = t.currentPoints;
    let gd = t.currentGoalDifference;
    let gf = t.currentGoalsFor;

    for (let g = 0; g < t.remainingGames; g++) {
      const result = simulateEloSoccerMatch(t.elo, averageOpponentElo, matchOptions);
      const goals = simulateSimpleSoccerGoals(result);
      gf += goals.gf;
      gd += goals.gf - goals.ga;
      if (result === "win") points += 3;
      else if (result === "draw") points += 1;
    }

    return { participantId: t.participantId, elo: t.elo, conference: t.conference, points, gd, gf };
  });

  function seedConference(conf: MlsConference): MlsConferenceSeed[] {
    const confRows = rows.filter((r) => r.conference === conf);
    const sorted = confRows.toSorted(
      (a, b) =>
        b.points - a.points ||
        b.gd - a.gd ||
        b.gf - a.gf ||
        (jitter.get(b.participantId) ?? 0) - (jitter.get(a.participantId) ?? 0)
    );
    return sorted.slice(0, MLS_PLAYOFF_TEAMS_PER_CONF).map((r, i) => ({
      participantId: r.participantId,
      elo: r.elo,
      conference: conf,
      seed: i + 1,
    }));
  }

  return { east: seedConference("Eastern"), west: seedConference("Western") };
}

// ─── Playoff simulation ────────────────────────────────────────────────────────

function getBySeeds(
  seeds: MlsConferenceSeed[],
  ...seedNums: number[]
): MlsConferenceSeed[] {
  const byNum = new Map(seeds.map((s) => [s.seed, s]));
  return seedNums.map((n) => {
    const entry = byNum.get(n);
    if (!entry) throw new Error(`MLS playoff seed ${n} not found in conference bracket.`);
    return entry;
  });
}

/**
 * Simulate the full MLS Cup Playoffs from 9+9 conference seeds.
 */
export function simulateMlsPlayoffs(
  east: MlsConferenceSeed[],
  west: MlsConferenceSeed[],
  matchOptions: EloSoccerMatchOptions = DEFAULT_MATCH_OPTIONS
): MlsPlayoffResult {
  function simConference(seeds: MlsConferenceSeed[]): {
    champion: MlsConferenceSeed;
    finalist: MlsConferenceSeed;
    confSemiLosers: [string, string];
  } {
    const [s1, s2, s3, s4, s5, s6, s7, s8, s9] = getBySeeds(seeds, 1, 2, 3, 4, 5, 6, 7, 8, 9);

    // Wild Card: 8 vs 9
    const wc = simulateMlsKnockoutGame(s8, s9, matchOptions);

    // Round 1 (best-of-3): 1 vs WC winner, 2 vs 7, 3 vs 6, 4 vs 5
    const r1a = simulateMlsBestOfThree(s1, wc.winner, matchOptions);
    const r1b = simulateMlsBestOfThree(s2, s7, matchOptions);
    const r1c = simulateMlsBestOfThree(s3, s6, matchOptions);
    const r1d = simulateMlsBestOfThree(s4, s5, matchOptions);

    // Conference Semifinals: single game
    // Arm A: r1a winner vs r1d winner; Arm B: r1b winner vs r1c winner
    const sfA = simulateMlsKnockoutGame(r1a.winner, r1d.winner, matchOptions);
    const sfB = simulateMlsKnockoutGame(r1b.winner, r1c.winner, matchOptions);

    // Conference Finals: single game
    const confFinal = simulateMlsKnockoutGame(sfA.winner, sfB.winner, matchOptions);

    return {
      champion: confFinal.winner,
      finalist: confFinal.loser,
      confSemiLosers: [sfA.loser.participantId, sfB.loser.participantId],
    };
  }

  const eastResult = simConference(east);
  const westResult = simConference(west);

  // MLS Cup: East champion vs West champion
  const cup = simulateMlsKnockoutGame(eastResult.champion, westResult.champion, matchOptions);

  return {
    champion: cup.winner.participantId,
    finalist: cup.loser.participantId,
    confFinalsLosers: [eastResult.finalist.participantId, westResult.finalist.participantId],
    confSemiLosers: [
      ...eastResult.confSemiLosers,
      ...westResult.confSemiLosers,
    ] as [string, string, string, string],
  };
}

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

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

    const [participants, evRows, standingsRows, simInputs, simulatorConfig] = await Promise.all([
      db.query.seasonParticipants.findMany({
        where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
      }),
      db
        .select({
          participantId: schema.seasonParticipantExpectedValues.participantId,
          sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
          sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
        })
        .from(schema.seasonParticipantExpectedValues)
        .where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)),
      getRegularSeasonStandings(sportsSeasonId),
      getParticipantSimulatorInputs(sportsSeasonId),
      getSportsSeasonSimulatorConfig(sportsSeasonId),
    ]);

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

    // Extract config values (admin-overridable; fall back to defaults)
    const config = simulatorConfig?.config;
    const numSimulations = Math.round(configNumber(config, "iterations", DEFAULT_NUM_SIMULATIONS));
    const seasonGames = Math.round(configNumber(config, "seasonGames", DEFAULT_SEASON_GAMES));
    const averageOpponentElo = configNumber(config, "averageOpponentElo", DEFAULT_AVERAGE_OPPONENT_ELO);
    const matchOptions: EloSoccerMatchOptions = {
      baseDrawRate: configNumber(config, "baseDrawRate", DEFAULT_BASE_DRAW_RATE),
      drawDecay: configNumber(config, "drawDecay", DEFAULT_DRAW_DECAY),
      parityFactor: configNumber(config, "matchParityFactor", DEFAULT_PARITY_FACTOR),
    };

    // 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, projectedTablePoints, or sourceOdds via Admin → Elo Ratings 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);
    }

    // Build simulator input maps (seed + region)
    const seedMap = new Map<string, number>();
    const regionMap = new Map<string, string>();
    for (const input of simInputs) {
      if (input.seed !== null && input.seed !== undefined) {
        seedMap.set(input.participantId, input.seed);
      }
      if (input.region !== null && input.region !== undefined) {
        regionMap.set(input.participantId, input.region);
      }
    }

    // Resolve conference for every participant.
    // Priority: (1) regularSeasonStandings.conference, (2) simulatorInputs.region,
    // (3) seasonParticipant.externalId set to "Eastern"/"Western" (pre-sync bootstrap).
    const standingsMap = new Map(standingsRows.map((r) => [r.participantId, r]));
    const conferenceMap = new Map<string, MlsConference>();
    for (const p of participants) {
      const standing = standingsMap.get(p.id);
      const raw =
        (standing?.conference as string | null | undefined) ??
        regionMap.get(p.id) ??
        parseConferenceFromExternalId(p.externalId) ??
        null;
      if (raw === "Eastern" || raw === "Western") {
        conferenceMap.set(p.id, raw);
      }
    }

    const missingConf = participants.filter((p) => !conferenceMap.has(p.id));
    if (missingConf.length > 0) {
      throw new Error(
        `MLSSimulator: Conference assignment missing for ${missingConf.length} team(s): ` +
          `${missingConf.map((p) => p.name).join(", ")}. ` +
          `Options: sync standings (populates conference automatically), set region to "Eastern"/"Western" ` +
          `via Admin → Simulator Inputs, or set externalId to "Eastern"/"Western" on the participant.`
      );
    }

    const allIds = participants.map((p) => p.id);

    // ── Mode detection ─────────────────────────────────────────────────────────

    // Mode 1: Known-seed — all 18 playoff seeds (1-9 East, 1-9 West) are explicitly set
    const eastSeeds: MlsConferenceSeed[] = [];
    const westSeeds: MlsConferenceSeed[] = [];

    for (const p of participants) {
      const seed = seedMap.get(p.id);
      const elo = eloMap.get(p.id);
      const conf = conferenceMap.get(p.id);
      if (seed === undefined || elo === undefined || conf === undefined) continue;
      if (seed < 1 || seed > MLS_PLAYOFF_TEAMS_PER_CONF) continue;
      if (conf === "Eastern") eastSeeds.push({ participantId: p.id, elo, conference: conf, seed });
      else westSeeds.push({ participantId: p.id, elo, conference: conf, seed });
    }

    const eastSeedSet = new Set(eastSeeds.map((s) => s.seed));
    const westSeedSet = new Set(westSeeds.map((s) => s.seed));
    const isKnownSeed =
      eastSeeds.length === MLS_PLAYOFF_TEAMS_PER_CONF &&
      westSeeds.length === MLS_PLAYOFF_TEAMS_PER_CONF &&
      eastSeedSet.size === MLS_PLAYOFF_TEAMS_PER_CONF &&
      westSeedSet.size === MLS_PLAYOFF_TEAMS_PER_CONF;

    if (isKnownSeed) {
      return this.simulateKnownSeeds(allIds, eastSeeds, westSeeds, numSimulations, matchOptions);
    }

    // Mode 2: Regular-season projection (default, including preseason)
    const teamsWithElo = participants.flatMap((p): MlsTeamEntry[] => {
      const elo = eloMap.get(p.id);
      const conf = conferenceMap.get(p.id);
      if (elo === undefined || conf === undefined) {
        logger.warn(
          `MLSSimulator: no Elo rating for participant "${p.name}" (${p.id}). ` +
            `They will be excluded from simulation. Enter sourceElo or projectedTablePoints via Admin → Elo Ratings.`
        );
        return [];
      }
      const standing = standingsMap.get(p.id);
      const wins = standing?.wins ?? 0;
      const draws = standing?.ties ?? 0;
      const gamesPlayed = standing?.gamesPlayed ?? wins + draws + (standing?.losses ?? 0);
      return [{
        participantId: p.id,
        elo,
        conference: conf,
        currentPoints: standing?.tablePoints ?? wins * 3 + draws,
        currentGoalsFor: standing?.goalsFor ?? 0,
        currentGoalDifference: standing?.goalDifference ?? 0,
        remainingGames: Math.max(0, seasonGames - gamesPlayed),
      }];
    });

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

    return this.simulateRegularSeason(allIds, teamsWithElo, numSimulations, matchOptions, averageOpponentElo);
  }

  // ── Mode 1: Known-Seed ──────────────────────────────────────────────────────

  private simulateKnownSeeds(
    allIds: string[],
    east: MlsConferenceSeed[],
    west: MlsConferenceSeed[],
    numSimulations: number,
    matchOptions: EloSoccerMatchOptions
  ): SimulationResult[] {
    const counts = this.zeroCounts(allIds);
    for (let s = 0; s < numSimulations; s++) {
      const result = simulateMlsPlayoffs(east, west, matchOptions);
      this.recordResult(counts, result);
    }
    return this.buildResults(allIds, counts, numSimulations);
  }

  // ── Mode 2: Regular-Season Projection ──────────────────────────────────────

  private simulateRegularSeason(
    allIds: string[],
    teams: MlsTeamEntry[],
    numSimulations: number,
    matchOptions: EloSoccerMatchOptions,
    averageOpponentElo: number
  ): SimulationResult[] {
    const counts = this.zeroCounts(allIds);
    for (let s = 0; s < numSimulations; s++) {
      const { east, west } = simulateMlsRegularSeason(teams, matchOptions, averageOpponentElo);
      if (east.length < MLS_PLAYOFF_TEAMS_PER_CONF || west.length < MLS_PLAYOFF_TEAMS_PER_CONF) {
        // Not enough teams in a conference — skip this iteration
        continue;
      }
      const result = simulateMlsPlayoffs(east, west, matchOptions);
      this.recordResult(counts, result);
    }
    return this.buildResults(allIds, counts, numSimulations);
  }

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

  private zeroCounts(allIds: string[]) {
    return {
      champion: new Map(allIds.map((id) => [id, 0])),
      finalist: new Map(allIds.map((id) => [id, 0])),
      confFinalsLoser: new Map(allIds.map((id) => [id, 0])),
      confSemiLoser: new Map(allIds.map((id) => [id, 0])),
    };
  }

  private recordResult(
    counts: ReturnType<MLSSimulator["zeroCounts"]>,
    result: MlsPlayoffResult
  ) {
    counts.champion.set(result.champion, (counts.champion.get(result.champion) ?? 0) + 1);
    counts.finalist.set(result.finalist, (counts.finalist.get(result.finalist) ?? 0) + 1);
    for (const id of result.confFinalsLosers) {
      counts.confFinalsLoser.set(id, (counts.confFinalsLoser.get(id) ?? 0) + 1);
    }
    for (const id of result.confSemiLosers) {
      counts.confSemiLoser.set(id, (counts.confSemiLoser.get(id) ?? 0) + 1);
    }
  }

  private buildResults(
    allIds: string[],
    counts: ReturnType<MLSSimulator["zeroCounts"]>,
    numSimulations: number
  ): SimulationResult[] {
    const N = numSimulations;
    const results: SimulationResult[] = allIds.map((id) => ({
      participantId: id,
      probabilities: {
        probFirst: (counts.champion.get(id) ?? 0) / N,
        probSecond: (counts.finalist.get(id) ?? 0) / N,
        // 2 conf finals losers per sim — split evenly across 3rd/4th
        probThird: (counts.confFinalsLoser.get(id) ?? 0) / N / 2,
        probFourth: (counts.confFinalsLoser.get(id) ?? 0) / N / 2,
        // 4 conf semi losers per sim — split evenly across 5th–8th
        probFifth: (counts.confSemiLoser.get(id) ?? 0) / N / 4,
        probSixth: (counts.confSemiLoser.get(id) ?? 0) / N / 4,
        probSeventh: (counts.confSemiLoser.get(id) ?? 0) / N / 4,
        probEighth: (counts.confSemiLoser.get(id) ?? 0) / N / 4,
      },
      source: "mls_bracket_monte_carlo",
    }));
    normalizeSimulationResultColumns(results);
    return results;
  }
}
