/**
 * Little League World Series (LLWS) Bracket Simulator
 *
 * Monte Carlo simulation of the LLWS (20-team format, 2022–present).
 *
 * Algorithm:
 *   1. Load all 20 participants for the sports season from DB
 *      (must be exactly 10 US + 10 International, identified by externalId)
 *   2. Load championship futures odds from participantExpectedValues.sourceOdds
 *      (entered via Admin → Futures Odds; American format)
 *   3. Convert odds to normalized championship probabilities (vig removed).
 *      These drive per-game win probability: p1 / (p1 + p2). Falls back to 50/50.
 *   4. Determine pool assignment mode from externalId:
 *      - Fixed pools: externalId is "US:A", "US:B", "Intl:A", or "Intl:B"
 *        → use these exact pool assignments every simulation.
 *      - Randomized pools: externalId is "US" or "Intl" only
 *        → randomly shuffle each side into Pool A / Pool B each simulation.
 *   5. Per simulation:
 *      a. Assign pools (fixed or random)
 *      b. Simulate pool play round-robin within each pool (10 games/pool)
 *         Top 2 by W-L record advance. Ties broken randomly.
 *      c. Simulate 4-team double-elimination bracket per side:
 *           G1: A1 vs B2  (WB)
 *           G2: B1 vs A2  (WB)
 *           G3: G1W vs G2W  (WB Final)
 *           G4: G1L vs G2L  (LB R1 — loser eliminated)
 *           G5: G3L vs G4W  (LB Final — loser eliminated)
 *           G6: G3W vs G5W  (Side Championship — loser eliminated)
 *      d. Consolation game: US loser vs Intl loser → 3rd / 4th
 *      e. World Series: US champion vs Intl champion → 1st / 2nd
 *   6. Track placement counts across all simulations.
 *   7. Convert counts to probability distributions.
 *
 * Pool assignment (externalId format):
 *   "US:A" / "US:B" / "Intl:A" / "Intl:B" → fixed pools (post-draw mode)
 *   "US" / "Intl"                           → randomized pools (pre-draw mode)
 *   Mixed: if ANY US or Intl team has a pool suffix, ALL teams on that side must
 *   have one (throws otherwise). Sides can differ — US fixed while Intl randomized.
 *
 * Placement tiers → SimulationProbabilities mapping:
 *   probFirst              = World Series Champion (1 per sim)
 *   probSecond             = World Series Runner-up (1 per sim)
 *   probThird              = Consolation game winner / 3rd place (1 per sim)
 *   probFourth             = Consolation game loser / 4th place (1 per sim)
 *   probFifth–probEighth   = Double-elim bracket losers before side championships
 *                            (4 per sim — split evenly: 2 US + 2 Intl)
 *   Pool play losers       → all 0 (12 teams, did not advance from pool play)
 *
 * Admin setup:
 *   1. Create a Sport with simulatorType = "llws_bracket"
 *   2. Create a Sports Season and add exactly 20 participants (10 US, 10 International)
 *   3. Set externalId on each participant via Admin → Manage Participants (optional if names follow the convention):
 *      Pre-draw:  "US" or "Intl"  (or leave null — names starting with "US " infer US, all others infer Intl)
 *      Post-draw: "US:A", "US:B", "Intl:A", or "Intl:B"
 *   4. Enter championship futures odds via Admin → Futures Odds (sourceOdds)
 *   5. Run simulation via Admin → Simulate
 */

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

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

const NUM_SIMULATIONS = 50_000;
const US_TEAM_COUNT = 10;
const INTL_TEAM_COUNT = 10;
const POOL_SIZE = 5; // teams per pool within each side

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

type Side = "US" | "Intl";

interface Team {
  participantId: string;
  side: Side;
  /** Explicit pool ("A" or "B") if set in externalId; null if randomized. */
  fixedPool: "A" | "B" | null;
  /** Normalized championship win probability (0–1, vig removed). */
  oddsProb: number;
}

interface PlacementCounts {
  champion: number;
  finalist: number;
  thirdPlace: number;
  fourthPlace: number;
  bracketLoser: number;
}

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

function simGame(t1: Team, t2: Team): { winner: Team; loser: Team } {
  // If either team has no odds entered, treat the game as a coin flip.
  // The 50/50 fallback must cover the one-sided case (one team known, one not)
  // because oddsProb=0 would otherwise give the unknown team a 0% win rate.
  let p1Win: number;
  if (t1.oddsProb === 0 || t2.oddsProb === 0) {
    p1Win = 0.5;
  } else {
    p1Win = t1.oddsProb / (t1.oddsProb + t2.oddsProb);
  }
  return Math.random() < p1Win ? { winner: t1, loser: t2 } : { winner: t2, loser: t1 };
}

/**
 * Fisher-Yates shuffle (in-place).
 */
function shuffle<T>(arr: T[]): T[] {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}

/**
 * Assign teams to Pool A / Pool B for one side.
 * In fixed mode, respects the pre-set pool. In randomized mode, shuffles then splits.
 */
function assignPools(teams: Team[], randomized: boolean): [Team[], Team[]] {
  if (!randomized) {
    return [teams.filter((t) => t.fixedPool === "A"), teams.filter((t) => t.fixedPool === "B")];
  }
  const shuffled = shuffle([...teams]);
  return [shuffled.slice(0, 5), shuffled.slice(5)];
}

/**
 * Simulate round-robin pool play among 5 teams.
 * Returns the top 2 teams by win count (ties broken randomly).
 */
function simulatePoolPlay(pool: Team[]): [Team, Team] {
  const wins = new Map<string, number>(pool.map((t) => [t.participantId, 0]));

  // Each pair plays once.
  for (let i = 0; i < pool.length; i++) {
    for (let j = i + 1; j < pool.length; j++) {
      const { winner } = simGame(pool[i], pool[j]);
      wins.set(winner.participantId, (wins.get(winner.participantId) ?? 0) + 1);
    }
  }

  // Sort by wins descending; pre-generate a stable random tiebreaker per team so
  // the comparator is consistent (Math.random() inside a comparator is a bug — the
  // engine may call it multiple times per pair and get contradictory results).
  const tiebreaker = new Map(pool.map((t) => [t.participantId, Math.random()]));
  const ranked = pool.toSorted((a, b) => {
    const diff = (wins.get(b.participantId) ?? 0) - (wins.get(a.participantId) ?? 0);
    return diff !== 0 ? diff : (tiebreaker.get(a.participantId) ?? 0) - (tiebreaker.get(b.participantId) ?? 0);
  });

  return [ranked[0], ranked[1]];
}

/**
 * Simulate a 4-team double-elimination bracket for one side.
 *
 * Seeds (pool results):
 *   poolA1 = Pool A winner, poolA2 = Pool A runner-up
 *   poolB1 = Pool B winner, poolB2 = Pool B runner-up
 *
 * Bracket:
 *   G1 (WB): A1 vs B2
 *   G2 (WB): B1 vs A2
 *   G3 (WB Final): G1W vs G2W
 *   G4 (LB R1):    G1L vs G2L  → loser eliminated (bracketLoser)
 *   G5 (LB Final): G3L vs G4W  → loser eliminated (bracketLoser)
 *   G6 (Side Championship): G3W vs G5W → loser eliminated (sideLoser)
 *
 * Returns: { sideChampion, sideLoser }
 *   bracketLosers (2) are bumped into counts directly.
 */
function simulateSideBracket(
  poolA1: Team,
  poolA2: Team,
  poolB1: Team,
  poolB2: Team,
  bump: (id: string, key: keyof PlacementCounts) => void
): { sideChampion: Team; sideLoser: Team } {

  // Winners bracket
  const g1 = simGame(poolA1, poolB2);
  const g2 = simGame(poolB1, poolA2);
  const g3 = simGame(g1.winner, g2.winner); // WB Final

  // Losers bracket
  const g4 = simGame(g1.loser, g2.loser);   // LB R1 — g4.loser eliminated
  bump(g4.loser.participantId, "bracketLoser");

  const g5 = simGame(g3.loser, g4.winner);   // LB Final — g5.loser eliminated
  bump(g5.loser.participantId, "bracketLoser");

  // Side championship
  const g6 = simGame(g3.winner, g5.winner);

  return { sideChampion: g6.winner, sideLoser: g6.loser };
}

// ─── Validation helpers ───────────────────────────────────────────────────────

type PoolSuffix = "A" | "B" | null;

function parseExternalId(raw: string | null): { side: Side; pool: PoolSuffix } | null {
  if (!raw) return null;
  const upper = raw.toUpperCase();
  if (upper === "US") return { side: "US", pool: null };
  if (upper === "INTL") return { side: "Intl", pool: null };
  if (upper === "US:A") return { side: "US", pool: "A" };
  if (upper === "US:B") return { side: "US", pool: "B" };
  if (upper === "INTL:A") return { side: "Intl", pool: "A" };
  if (upper === "INTL:B") return { side: "Intl", pool: "B" };
  return null;
}

/**
 * Infer an externalId from a participant name when none is stored.
 * Teams whose name is exactly "US" or starts with "US " (case-insensitive)
 * are assigned to the US side; all others are assigned to Intl.
 * The inferred value never has a pool suffix, so pools will be randomized.
 */
function inferExternalIdFromName(name: string): string {
  const upper = name.trim().toUpperCase();
  return upper === "US" || upper.startsWith("US ") ? "US" : "Intl";
}

/**
 * Determine whether pool assignments should be randomized for one side.
 * - If ALL teams on the side have a pool suffix → fixed pools (returns false).
 * - If NO teams have a pool suffix → randomized (returns true).
 * - Mixed → throws.
 * Also validates that fixed pools are split exactly POOL_SIZE / POOL_SIZE.
 */
function determineRandomized(sideTeams: Team[], sideName: string): boolean {
  const withPool = sideTeams.filter((t) => t.fixedPool !== null);
  const withoutPool = sideTeams.filter((t) => t.fixedPool === null);
  if (withPool.length > 0 && withoutPool.length > 0) {
    throw new Error(
      `${sideName} teams have mixed externalId formats: some have pool suffixes (e.g. "US:A") ` +
      `and some don't. Either all ${sideName} teams must have pool suffixes or none should.`
    );
  }
  if (withPool.length === sideTeams.length) {
    const poolA = sideTeams.filter((t) => t.fixedPool === "A");
    const poolB = sideTeams.filter((t) => t.fixedPool === "B");
    if (poolA.length !== POOL_SIZE || poolB.length !== POOL_SIZE) {
      throw new Error(
        `${sideName} fixed pools must have exactly ${POOL_SIZE} teams each. ` +
        `Found Pool A: ${poolA.length}, Pool B: ${poolB.length}.`
      );
    }
    return false; // fixed pools
  }
  return true; // randomized
}

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

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

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

    if (participants.length !== US_TEAM_COUNT + INTL_TEAM_COUNT) {
      throw new Error(
        `LLWS simulator requires exactly ${US_TEAM_COUNT + INTL_TEAM_COUNT} participants, ` +
        `found ${participants.length}.`
      );
    }

    // 2. Load championship futures odds.
    const evRows = await db
      .select({
        participantId: schema.seasonParticipantExpectedValues.participantId,
        sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
      })
      .from(schema.seasonParticipantExpectedValues)
      .where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));

    const rawOddsMap = new Map<string, number>();
    for (const row of evRows) {
      if (row.sourceOdds !== null) {
        rawOddsMap.set(row.participantId, convertAmericanOddsToProbability(row.sourceOdds));
      }
    }

    // 3. Normalize odds (remove vig) to get championship probability per team.
    const normalizedOddsMap = new Map<string, number>();
    if (rawOddsMap.size > 0) {
      const rawSum = [...rawOddsMap.values()].reduce((a, b) => a + b, 0);
      for (const [id, prob] of rawOddsMap) {
        normalizedOddsMap.set(id, rawSum > 0 ? prob / rawSum : 0);
      }
    }

    // 4. Parse externalId for each participant to determine side and fixed pool.
    const teams: Team[] = [];
    for (const p of participants) {
      const raw = p.externalId ?? inferExternalIdFromName(p.name);
      const parsed = parseExternalId(raw);
      if (!parsed) {
        throw new Error(
          `Participant ${p.id} has invalid externalId "${p.externalId}". ` +
          `Expected: "US", "Intl", "US:A", "US:B", "Intl:A", or "Intl:B".`
        );
      }
      teams.push({
        participantId: p.id,
        side: parsed.side,
        fixedPool: parsed.pool,
        oddsProb: normalizedOddsMap.get(p.id) ?? 0,
      });
    }

    // Validate team counts per side.
    const usTeams = teams.filter((t) => t.side === "US");
    const intlTeams = teams.filter((t) => t.side === "Intl");

    if (usTeams.length !== US_TEAM_COUNT) {
      throw new Error(`Expected ${US_TEAM_COUNT} US teams, found ${usTeams.length}.`);
    }
    if (intlTeams.length !== INTL_TEAM_COUNT) {
      throw new Error(`Expected ${INTL_TEAM_COUNT} International teams, found ${intlTeams.length}.`);
    }

    // Determine pool assignment mode for each side.
    const usRandomized = determineRandomized(usTeams, "US");
    const intlRandomized = determineRandomized(intlTeams, "International");

    // 5. Initialise placement count accumulators for all participants.
    const allIds = participants.map((p) => p.id);
    const counts = new Map<string, PlacementCounts>(
      allIds.map((id) => [id, { champion: 0, finalist: 0, thirdPlace: 0, fourthPlace: 0, bracketLoser: 0 }])
    );
    const bump = (id: string, key: keyof PlacementCounts) => {
      const entry = counts.get(id);
      if (entry) entry[key]++;
    };

    // 6. Run Monte Carlo simulations.
    for (let s = 0; s < NUM_SIMULATIONS; s++) {
      // Assign pools for this simulation.
      const [usPoolA, usPoolB] = assignPools(usTeams, usRandomized);
      const [intlPoolA, intlPoolB] = assignPools(intlTeams, intlRandomized);

      // Pool play: top 2 from each pool advance.
      const [usA1, usA2] = simulatePoolPlay(usPoolA);
      const [usB1, usB2] = simulatePoolPlay(usPoolB);
      const [intlA1, intlA2] = simulatePoolPlay(intlPoolA);
      const [intlB1, intlB2] = simulatePoolPlay(intlPoolB);

      // Double-elimination bracket per side.
      const { sideChampion: usChamp, sideLoser: usLose } =
        simulateSideBracket(usA1, usA2, usB1, usB2, bump);
      const { sideChampion: intlChamp, sideLoser: intlLose } =
        simulateSideBracket(intlA1, intlA2, intlB1, intlB2, bump);

      // Consolation game: 3rd / 4th place.
      const consolation = simGame(usLose, intlLose);
      bump(consolation.winner.participantId, "thirdPlace");
      bump(consolation.loser.participantId, "fourthPlace");

      // World Series: 1st / 2nd place.
      const ws = simGame(usChamp, intlChamp);
      bump(ws.winner.participantId, "champion");
      bump(ws.loser.participantId, "finalist");
    }

    // 7. Convert counts to probability distributions.
    //    bracketLosers: 4 per sim (2 US + 2 Intl) → split evenly.
    const bracketLosersPerSim = 4;
    const bracketDivisor = bracketLosersPerSim * NUM_SIMULATIONS;

    const zeroCounts: PlacementCounts = { champion: 0, finalist: 0, thirdPlace: 0, fourthPlace: 0, bracketLoser: 0 };
    return allIds.map((id) => {
      const c = counts.get(id) ?? zeroCounts;
      const bracketProb = c.bracketLoser / bracketDivisor;
      return {
        participantId: id,
        probabilities: {
          probFirst: c.champion / NUM_SIMULATIONS,
          probSecond: c.finalist / NUM_SIMULATIONS,
          probThird: c.thirdPlace / NUM_SIMULATIONS,
          probFourth: c.fourthPlace / NUM_SIMULATIONS,
          probFifth: bracketProb,
          probSixth: bracketProb,
          probSeventh: bracketProb,
          probEighth: bracketProb,
        },
        source: "llws_monte_carlo",
      };
    });
  }
}
