/**
 * UCL Bracket Simulator
 *
 * Monte Carlo simulation of the UEFA Champions League 16-team knockout bracket.
 *
 * Algorithm:
 *   1. Load the bracket scoring event and all playoff matches from DB
 *   2. Load futures odds (American format) from participantExpectedValues
 *   3. Build two probability signals per team:
 *      a. Elo — derived from futures via convertFuturesToElo() (long-run team strength)
 *      b. Normalized odds — vig-removed implied win probability from the same futures
 *   4. Per-match win probability = ELO_WEIGHT * eloProb + ODDS_WEIGHT * oddsProb
 *   5. Simulate 50,000 tournaments, respecting already-completed matches
 *   6. Track integer placement counts per tier (champion / finalist / SF loser / QF loser).
 *      At conversion, exact denominators guarantee column sums of 1.0 by construction:
 *      - probFirst           = champion / N
 *      - probSecond          = finalist / N
 *      - probThird/probFourth = sfLoserCount / (2*N)  — 2 SF losers per sim
 *      - probFifth–probEighth = qfLoserCount / (4*N)  — 4 QF losers per sim
 *      - R16 losers          → all 0 (score 0 points per scoring rules)
 *
 * Bracket path follows the same matchNumber pairing used by advanceWinnerTemplate():
 *   nextMatchNumber = Math.ceil(matchNumber / 2)
 *   i.e. R16 match 1 + R16 match 2 → QF match 1, R16 match 3 + R16 match 4 → QF match 2, …
 *
 * In-progress handling:
 *   - Completed matches (isComplete + winnerId + loserId set) use the actual result in
 *     every simulation — giving eliminated teams an exact EV equal to their scored points.
 *   - Incomplete matches are simulated using the blended probability.
 *
 * Notes:
 *   - Requires futures odds in sourceOdds (American format) to be imported first.
 *   - Falls back to uniform probability (coin flip) when no odds are stored.
 *   - ELO_WEIGHT and ODDS_WEIGHT can be tuned here; 0.7/0.3 matches the Python calibration.
 */

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

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

const NUM_SIMULATIONS = 50000;

/**
 * Weight given to the Elo-based win probability (derived from futures).
 * Remaining weight (1 - ELO_WEIGHT) goes to the normalized futures odds component.
 */
const ELO_WEIGHT = 0.7;
const ODDS_WEIGHT = 1 - ELO_WEIGHT;

// ─── Odds helper ──────────────────────────────────────────────────────────────

/** Convert American odds to implied probability (with vig). Exported for testing. */
export function americanToImpliedProb(odds: number): number {
  if (odds > 0) return 100 / (odds + 100);
  return Math.abs(odds) / (Math.abs(odds) + 100);
}

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

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

    // 1. Find the bracket scoring event for this sports season.
    //    UCL has exactly one playoff_game event per season.
    const bracketEvent = await db.query.scoringEvents.findFirst({
      where: and(
        eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
        eq(schema.scoringEvents.eventType, "playoff_game")
      ),
    });

    if (!bracketEvent) {
      throw new Error(
        `No bracket event found for sports season ${sportsSeasonId}. ` +
          `Create a playoff_game scoring event and set up the bracket first.`
      );
    }

    // 2. Load all playoff matches for this bracket event.
    const allMatches = await db.query.playoffMatches.findMany({
      where: eq(schema.playoffMatches.scoringEventId, bracketEvent.id),
      orderBy: (m, { asc }) => [asc(m.matchNumber)],
    });

    if (allMatches.length === 0) {
      throw new Error(
        `No playoff matches found for the bracket event. ` +
          `Generate the bracket from the admin panel first.`
      );
    }

    // 3. Group matches by round, ordered by number of matches descending.
    //    Round of 16 (8) → Quarterfinals (4) → Semifinals (2) → Finals (1)
    const byRound = new Map<string, typeof allMatches>();
    for (const m of allMatches) {
      if (!byRound.has(m.round)) byRound.set(m.round, []);
      byRound.get(m.round)?.push(m);
    }

    const sortedRoundMatches = [...byRound.values()]
      .toSorted((a, b) => b.length - a.length)
      .map((matches) => matches.sort((a, b) => a.matchNumber - b.matchNumber));

    if (sortedRoundMatches.length < 4) {
      throw new Error(
        `Expected 4 rounds (R16, Quarterfinals, Semifinals, Finals), ` +
          `found ${sortedRoundMatches.length}. Check the bracket structure.`
      );
    }

    const r16Matches = sortedRoundMatches[0]; // 8 matches
    const qfMatches = sortedRoundMatches[1];  // 4 matches
    const sfMatches = sortedRoundMatches[2];  // 2 matches
    const finalMatches = sortedRoundMatches[3]; // 1 match

    if (r16Matches.length !== 8) {
      throw new Error(
        `Expected 8 Round of 16 matches, found ${r16Matches.length}. ` +
          `This simulator only supports the standard UCL 16-team format.`
      );
    }

    // Validate all R16 matches have participants (the draw must be entered).
    for (const m of r16Matches) {
      if (!m.participant1Id || !m.participant2Id) {
        throw new Error(
          `Round of 16 match ${m.matchNumber} is missing participants. ` +
            `Assign all 16 teams to the bracket before running simulation.`
        );
      }
    }

    // 4. Collect all 16 participant IDs from the R16 draw (order matters for pairings).
    //    r16Matches is sorted by matchNumber, so participantIds[0..1] = match 1, [2..3] = match 2, …
    const participantIds: string[] = [];
    for (const m of r16Matches) {
      participantIds.push(m.participant1Id ?? "", m.participant2Id ?? "");
    }
    const participantSet = new Set(participantIds);
    const fallbackProb = 1 / participantIds.length;

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

    const evMap = new Map(evRows.map((r) => [r.participantId, r]));

    // 6. Build Elo map via the futures → Elo pipeline.
    const hasOdds = evRows.some(
      (r) => r.sourceOdds !== null && participantSet.has(r.participantId)
    );

    let eloMap: Map<string, number>;
    if (hasOdds) {
      const oddsInput = evRows
        .filter((r) => r.sourceOdds !== null && participantSet.has(r.participantId))
        .map((r) => ({ participantId: r.participantId, odds: r.sourceOdds ?? 0 }));
      eloMap = convertFuturesToElo(oddsInput, "american");
    } else {
      // No odds stored — all teams get equal Elo (coin-flip bracket)
      eloMap = new Map(participantIds.map((id) => [id, 1500]));
    }

    // 7. Build normalized futures win-probability map (vig removed).
    //    Used as the second signal in the blended per-match probability.
    const rawProbs = new Map<string, number>();
    for (const id of participantIds) {
      const ev = evMap.get(id);
      rawProbs.set(
        id,
        ev !== undefined && ev.sourceOdds !== null && ev.sourceOdds !== undefined ? americanToImpliedProb(ev.sourceOdds) : fallbackProb
      );
    }
    const rawSum = [...rawProbs.values()].reduce((a, b) => a + b, 0);
    const normalizedOddsMap = new Map<string, number>();
    for (const [id, prob] of rawProbs) {
      normalizedOddsMap.set(id, prob / rawSum);
    }

    // 8. Build per-round lookup maps keyed by matchNumber for O(1) access in the hot loop.
    const r16ByNum = new Map(r16Matches.map((m) => [m.matchNumber, m]));
    const qfByNum = new Map(qfMatches.map((m) => [m.matchNumber, m]));
    const sfByNum = new Map(sfMatches.map((m) => [m.matchNumber, m]));
    const finalMatch = finalMatches[0];

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

    /** Blended Elo + normalized-odds win probability for p1 vs p2. */
    const blendedWinProb = (p1: string, p2: string): number => {
      const elo1 = eloMap.get(p1) ?? 1500;
      const elo2 = eloMap.get(p2) ?? 1500;
      const eloProb = eloWinProbability(elo1, elo2);

      const o1 = normalizedOddsMap.get(p1) ?? fallbackProb;
      const o2 = normalizedOddsMap.get(p2) ?? fallbackProb;
      const oddsProb = o1 + o2 > 0 ? o1 / (o1 + o2) : 0.5;

      return ELO_WEIGHT * eloProb + ODDS_WEIGHT * oddsProb;
    };

    const simMatch = (p1: string, p2: string): { winner: string; loser: string } => {
      const w = Math.random() < blendedWinProb(p1, p2) ? p1 : p2;
      return { winner: w, loser: w === p1 ? p2 : p1 };
    };

    // 9. Integer placement counts per tier.
    //    Using separate integer maps avoids fractional accumulation error (e.g. += 0.25 × 50k).
    //    R16 losers are never counted → all probs stay 0 → EV = 0.
    const championCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const finalistCounts = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const sfLoserCounts  = new Map<string, number>(participantIds.map((id) => [id, 0]));
    const qfLoserCounts  = new Map<string, number>(participantIds.map((id) => [id, 0]));

    // 10. Run Monte Carlo simulations.
    for (let s = 0; s < NUM_SIMULATIONS; s++) {
      // ── Round of 16 ──────────────────────────────────────────────────────
      // R16 losers: no count added (0 points per scoring rules)
      const r16Winners: string[] = [];
      for (let i = 1; i <= 8; i++) {
        const m = r16ByNum.get(i);
        if (!m) continue;
        if (m.isComplete && m.winnerId) {
          r16Winners.push(m.winnerId);
        } else {
          const { winner } = simMatch(m.participant1Id ?? "", m.participant2Id ?? "");
          r16Winners.push(winner);
        }
      }

      // ── Quarterfinals ─────────────────────────────────────────────────────
      // Bracket path: QF match N gets winner of R16 match (2N-1) and (2N).
      // r16Winners is 0-indexed: [0,1] = R16 matches 1,2 → QF match 1, etc.
      const qfWinners: string[] = [];
      for (let i = 1; i <= 4; i++) {
        const dbMatch = qfByNum.get(i);
        let winner: string;
        let loser: string;

        if (dbMatch?.isComplete && dbMatch.winnerId && dbMatch.loserId) {
          winner = dbMatch.winnerId;
          loser = dbMatch.loserId;
        } else {
          const p1 = r16Winners[(i - 1) * 2];
          const p2 = r16Winners[(i - 1) * 2 + 1];
          ({ winner, loser } = simMatch(p1, p2));
        }

        qfWinners.push(winner);
        qfLoserCounts.set(loser, (qfLoserCounts.get(loser) ?? 0) + 1);
      }

      // ── Semifinals ───────────────────────────────────────────────────────
      // SF match N gets winner of QF match (2N-1) and (2N).
      const sfWinners: string[] = [];
      for (let i = 1; i <= 2; i++) {
        const dbMatch = sfByNum.get(i);
        let winner: string;
        let loser: string;

        if (dbMatch?.isComplete && dbMatch.winnerId && dbMatch.loserId) {
          winner = dbMatch.winnerId;
          loser = dbMatch.loserId;
        } else {
          const p1 = qfWinners[(i - 1) * 2];
          const p2 = qfWinners[(i - 1) * 2 + 1];
          ({ winner, loser } = simMatch(p1, p2));
        }

        sfWinners.push(winner);
        sfLoserCounts.set(loser, (sfLoserCounts.get(loser) ?? 0) + 1);
      }

      // ── Final ─────────────────────────────────────────────────────────────
      let champion: string;
      let finalist: string;

      if (finalMatch?.isComplete && finalMatch.winnerId && finalMatch.loserId) {
        champion = finalMatch.winnerId;
        finalist = finalMatch.loserId;
      } else {
        ({ winner: champion, loser: finalist } = simMatch(sfWinners[0], sfWinners[1]));
      }

      championCounts.set(champion, (championCounts.get(champion) ?? 0) + 1);
      finalistCounts.set(finalist, (finalistCounts.get(finalist) ?? 0) + 1);
    }

    // 11. Convert counts to probability distributions.
    //     Exact denominators guarantee each paired column group sums to 1.0 by construction:
    //       probFirst/Second  → N total (1 per sim)
    //       probThird/Fourth  → sfLoserCounts / (2*N)  — 2 SF losers per sim
    //       probFifth–Eighth  → qfLoserCounts / (4*N)  — 4 QF losers per sim
    const N = NUM_SIMULATIONS;
    const results: SimulationResult[] = participantIds.map((participantId) => {
      const c = championCounts.get(participantId) ?? 0;
      const f = finalistCounts.get(participantId) ?? 0;
      const sf = sfLoserCounts.get(participantId) ?? 0;
      const qf = qfLoserCounts.get(participantId) ?? 0;
      return {
        participantId,
        probabilities: {
          probFirst:   c  / N,
          probSecond:  f  / N,
          probThird:   sf / (2 * N),
          probFourth:  sf / (2 * N),
          probFifth:   qf / (4 * N),
          probSixth:   qf / (4 * N),
          probSeventh: qf / (4 * N),
          probEighth:  qf / (4 * N),
        },
        source: "ucl_bracket_monte_carlo",
      };
    });

    // 12. Per-position normalization — belt-and-suspenders safety net for floating-point
    //     division residuals. Columns are already near-exactly 1.0 after step 11, but this
    //     guarantees the invariant before probabilities are persisted.
    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;
  }
}
