/**
 * FIFA World Cup Simulator (2026+ 48-team format)
 *
 * Monte Carlo simulation covering both the group stage and knockout bracket.
 *
 * Algorithm:
 *   1. Load participants, futures odds, and bracket/group data from DB
 *   2. Build per-team Elo: from futures odds if available (blended 70/30 Elo+odds),
 *      otherwise from hardcoded 2026 national team Elo ratings, fallback 1500.
 *   3. For each of NUM_SIMULATIONS iterations:
 *      a. GROUP STAGE (12 groups × 6 matches each = 72 matches)
 *         - Each match: compute P(win)/P(draw)/P(loss) from Elo difference
 *         - Track pts / GD / GF per team; sort to determine group positions
 *         - Group winner (pos 1) and runner-up (pos 2) always advance
 *         - 3rd-place teams ranked across all 12 groups; top 8 also advance
 *      b. KNOCKOUT (R32 → R16 → QF → SF)
 *         - Standard single-elimination using Elo win probabilities
 *         - Completed knockout matches use actual results
 *      c. THIRD PLACE GAME
 *         - SF loser 1 vs SF loser 2; winner = 3rd, loser = 4th
 *      d. FINAL
 *         - SF winner 1 vs SF winner 2; winner = 1st, loser = 2nd
 *   4. Accumulate integer placement counts; convert to probabilities.
 *      Placement buckets → SimulationProbabilities mapping:
 *        probFirst   = P(champion)
 *        probSecond  = P(runner-up)
 *        probThird   = P(3rd place game winner)
 *        probFourth  = P(3rd place game loser)
 *        probFifth–probEighth = P(QF elimination) / 4
 *        R32/R16 losers → 0 (scoringStartsAtRound = "Quarterfinals")
 *
 * Group stage W/D/L probabilities:
 *   pDraw = 0.28 × exp(−0.002 × |eloDiff|)   (≈28% when equal, decreases with skill gap)
 *   pWin  = (1 − pDraw) × eloWinProb(eloA, eloB)
 *   pLoss = 1 − pWin − pDraw
 */

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 { logger } from "~/lib/logger";
import type { Simulator, SimulationResult, SimulationProbabilities } from "./types";

// ─── Name normalisation (same logic as elo-ratings bulk-import fuzzy match) ──

function normalizeTeamName(name: string): string {
  return name.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, " ").trim();
}

// ─── Parameters ──────────────────────────────────────────────────────────────

const NUM_SIMULATIONS = 50_000;
const ELO_WEIGHT = 0.7;
const ODDS_WEIGHT = 1 - ELO_WEIGHT;

/** Base draw rate when teams are evenly matched. Decays with Elo difference. */
const BASE_DRAW_RATE = 0.28;
const DRAW_DECAY = 0.002;

// ─── National team Elo ratings (eloratings.net, pre-2026 World Cup) ──────────
// These are used when no futures odds are stored for a team.
// Update before the tournament starts.

const NATIONAL_TEAM_ELO: Record<string, number> = {
  "france": 2083,
  "england": 2047,
  "brazil": 2038,
  "spain": 2031,
  "belgium": 2003,
  "argentina": 1997,
  "portugal": 1993,
  "netherlands": 1988,
  "germany": 1978,
  "italy": 1966,
  "croatia": 1961,
  "uruguay": 1955,
  "colombia": 1950,
  "mexico": 1945,
  "usa": 1935,
  "united states": 1935,
  "senegal": 1930,
  "denmark": 1928,
  "switzerland": 1925,
  "austria": 1918,
  "morocco": 1915,
  "japan": 1912,
  "south korea": 1905,
  "ecuador": 1900,
  "poland": 1898,
  "australia": 1893,
  "nigeria": 1888,
  "iran": 1883,
  "cameroon": 1878,
  "ghana": 1875,
  "canada": 1870,
  "peru": 1865,
  "chile": 1862,
  "venezuela": 1858,
  "costa rica": 1853,
  "cote d'ivoire": 1850,
  "ivory coast": 1850,
  "egypt": 1848,
  "hungary": 1845,
  "turkey": 1842,
  "ukraine": 1838,
  "serbia": 1835,
  "czech republic": 1830,
  "romania": 1825,
  "slovakia": 1820,
  "new zealand": 1790,
  "saudi arabia": 1785,
  "qatar": 1780,
  "honduras": 1778,
  "panama": 1775,
  "el salvador": 1770,
  "guatemala": 1765,
  "cuba": 1760,
};

/**
 * Look up a national team's Elo rating using fuzzy name matching.
 * Priority:
 *   1. Exact normalized match
 *   2. Substring containment (either direction)
 *   3. Word-overlap (≥50% shared significant words)
 * Logs a warning when no match is found so mismatches are visible in server logs.
 */
function getTeamElo(name: string, fallback = 1500): number {
  const normalized = normalizeTeamName(name);
  const entries = Object.entries(NATIONAL_TEAM_ELO);

  // 1. Exact match
  const exact = entries.find(([k]) => k === normalized);
  if (exact) return exact[1];

  // 2. Substring containment (handles "Ivory Coast" ↔ "Cote d'Ivoire" aliases already in map,
  //    and things like "United States" matching "usa")
  const contains = entries.find(([k]) => k.includes(normalized) || normalized.includes(k));
  if (contains) return contains[1];

  // 3. Word-overlap (≥50% shared words longer than 2 chars)
  const inputWords = normalized.split(" ").filter((w) => w.length > 2);
  if (inputWords.length > 0) {
    const overlap = entries.find(([k]) => {
      const kWords = k.split(" ").filter((w) => w.length > 2);
      const shared = inputWords.filter((w) => kWords.includes(w));
      return shared.length > 0 && shared.length >= Math.min(inputWords.length, kWords.length) * 0.5;
    });
    if (overlap) return overlap[1];
  }

  logger.warn(`[WorldCupSimulator] No Elo found for team "${name}" (normalized: "${normalized}") — using fallback ${fallback}`);
  return fallback;
}

// ─── Group stage helpers ──────────────────────────────────────────────────────

/**
 * Simulate a single group stage match.
 * Returns "win" (team A wins), "draw", or "loss" (team B wins).
 */
export function simGroupMatch(eloA: number, eloB: number): "win" | "draw" | "loss" {
  const eloDiff = eloA - eloB;
  const pDraw = BASE_DRAW_RATE * Math.exp(-DRAW_DECAY * Math.abs(eloDiff));
  const eloWin = eloWinProbability(eloA, eloB);
  const pWin = (1 - pDraw) * eloWin;
  const rand = Math.random();
  if (rand < pWin) return "win";
  if (rand < pWin + pDraw) return "draw";
  return "loss";
}

interface TeamStats {
  id: string;
  pts: number;
  gd: number;
  gf: number;
}

interface GroupMatchResult {
  participant1Id: string;
  participant2Id: string;
  participant1Score: number | null;
  participant2Score: number | null;
  isComplete: boolean;
}

/**
 * Simulate a full round-robin group of 4 teams.
 * Completed matches (isComplete=true with real scores) are replayed with their
 * actual results; remaining matches are simulated via Elo.
 * This correctly handles partial group completion (e.g. 4/6 matches done).
 */
function simGroup(
  teamIds: string[],
  eloFn: (id: string) => number,
  completedMatches?: GroupMatchResult[]
): TeamStats[] {
  const stats = new Map<string, TeamStats>(
    teamIds.map((id) => [id, { id, pts: 0, gd: 0, gf: 0 }])
  );

  // Build set of completed pairings keyed "minId:maxId" so we can skip them
  const completedPairs = new Set<string>();

  if (completedMatches) {
    for (const m of completedMatches) {
      if (!m.isComplete || m.participant1Score === null || m.participant2Score === null) continue;
      const sa = stats.get(m.participant1Id);
      const sb = stats.get(m.participant2Id);
      if (!sa || !sb) continue;

      const s1 = m.participant1Score;
      const s2 = m.participant2Score;
      sa.gf += s1; sa.gd += s1 - s2;
      sb.gf += s2; sb.gd += s2 - s1;
      if (s1 > s2) { sa.pts += 3; }
      else if (s2 > s1) { sb.pts += 3; }
      else { sa.pts += 1; sb.pts += 1; }

      // Mark this pairing as done so we don't re-simulate it
      const key = [m.participant1Id, m.participant2Id].toSorted().join(":");
      completedPairs.add(key);
    }
  }

  // Simulate remaining (not-yet-played) matches
  for (let i = 0; i < teamIds.length; i++) {
    for (let j = i + 1; j < teamIds.length; j++) {
      const a = teamIds[i];
      const b = teamIds[j];
      const key = [a, b].toSorted().join(":");
      if (completedPairs.has(key)) continue; // already applied above

      const result = simGroupMatch(eloFn(a), eloFn(b));
      const sa = stats.get(a);
      const sb = stats.get(b);
      if (!sa || !sb) continue;

      // Simple goal simulation: winner scores 1-2, loser 0-1, draw both 1
      let ga: number, gb: number;
      if (result === "win") {
        ga = Math.random() < 0.5 ? 2 : 1;
        gb = ga === 2 && Math.random() < 0.3 ? 1 : 0;
        sa.pts += 3;
      } else if (result === "loss") {
        gb = Math.random() < 0.5 ? 2 : 1;
        ga = gb === 2 && Math.random() < 0.3 ? 1 : 0;
        sb.pts += 3;
      } else {
        ga = gb = Math.random() < 0.5 ? 1 : 0;
        sa.pts += 1;
        sb.pts += 1;
      }
      sa.gf += ga; sa.gd += ga - gb;
      sb.gf += gb; sb.gd += gb - ga;
    }
  }

  return [...stats.values()].toSorted(sortTeams);
}

function sortTeams(a: TeamStats, b: TeamStats): number {
  if (b.pts !== a.pts) return b.pts - a.pts;
  if (b.gd !== a.gd) return b.gd - a.gd;
  return b.gf - a.gf;
}

// ─── Knockout helpers ─────────────────────────────────────────────────────────

function simKnockout(
  teamA: string,
  teamB: string,
  eloFn: (id: string) => number,
  normalizedProb: (id: string) => number
): { winner: string; loser: string } {
  const eloProb = eloWinProbability(eloFn(teamA), eloFn(teamB));
  const oddsProb = normalizedProb(teamA);
  const blended = ELO_WEIGHT * eloProb + ODDS_WEIGHT * (0.5 + (oddsProb - 0.5));
  const winner = Math.random() < blended ? teamA : teamB;
  return { winner, loser: winner === teamA ? teamB : teamA };
}

// ─── Probability normalisation ────────────────────────────────────────────────

function normalizeProbabilities(
  results: SimulationResult[],
  positionKeys: Array<keyof SimulationProbabilities>
): void {
  for (const key of positionKeys) {
    const colSum = results.reduce((s, r) => s + r.probabilities[key], 0);
    const residual = 1.0 - colSum;
    if (residual !== 0 && Math.abs(residual) < 1e-9) {
      const maxResult = results.reduce((best, r) =>
        r.probabilities[key] > best.probabilities[key] ? r : best
      );
      maxResult.probabilities[key] += residual;
    }
  }
}

// ─── Main simulator ───────────────────────────────────────────────────────────

export class WorldCupSimulator implements Simulator {
  private readonly numSimulations: number;

  constructor(numSimulations = NUM_SIMULATIONS) {
    this.numSimulations = numSimulations;
  }

  async simulate(sportsSeasonId: string): Promise<SimulationResult[]> {
    const db = database();

    // 1. Load all participants for this season
    const participantRows = await db.query.participants.findMany({
      where: eq(schema.participants.sportsSeasonId, sportsSeasonId),
    });

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

    const participantIds = participantRows.map((p) => p.id);
    const participantNames = new Map(participantRows.map((p) => [p.id, p.name]));

    // 2. Load stored ratings (sourceElo from Elo page, sourceOdds from futures page)
    const evRows = await db
      .select({
        participantId: schema.participantExpectedValues.participantId,
        sourceOdds: schema.participantExpectedValues.sourceOdds,
        sourceElo: schema.participantExpectedValues.sourceElo,
      })
      .from(schema.participantExpectedValues)
      .where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));

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

    // 3. Build Elo map — priority: sourceElo (direct) > sourceOdds (converted) > hardcoded
    const sourceEloMap = new Map<string, number>();
    for (const r of evRows) {
      if (r.sourceElo !== null && r.sourceElo !== undefined && participantSet.has(r.participantId)) {
        sourceEloMap.set(r.participantId, r.sourceElo);
      }
    }

    const hasOdds = evRows.some(
      (r) => r.sourceOdds !== null && participantSet.has(r.participantId)
    );

    let eloFromOdds: 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 }));
      eloFromOdds = convertFuturesToElo(oddsInput, "american");
    } else {
      eloFromOdds = new Map();
    }

    const eloFn = (id: string): number => {
      if (sourceEloMap.has(id)) return sourceEloMap.get(id) ?? 1500;
      if (eloFromOdds.has(id)) return eloFromOdds.get(id) ?? 1500;
      const name = participantNames.get(id) ?? "";
      return getTeamElo(name, 1500);
    };

    // 4. Build normalized futures win-probability map (vig removed)
    const rawProbs = new Map<string, number>();
    for (const id of participantIds) {
      const ev = evMap.get(id);
      if (ev?.sourceOdds !== null && ev?.sourceOdds !== undefined) {
        rawProbs.set(id, Math.abs(ev.sourceOdds) > 0
          ? ev.sourceOdds > 0
            ? 100 / (ev.sourceOdds + 100)
            : Math.abs(ev.sourceOdds) / (Math.abs(ev.sourceOdds) + 100)
          : 1 / participantIds.length);
      } else {
        rawProbs.set(id, 1 / participantIds.length);
      }
    }

    const totalRawProb = [...rawProbs.values()].reduce((s, p) => s + p, 0);
    const normalizedProb = (id: string): number =>
      totalRawProb > 0 ? (rawProbs.get(id) ?? 0) / totalRawProb : 1 / participantIds.length;

    // 5. Load group stage data from DB
    const bracketEvent = await db.query.scoringEvents.findFirst({
      where: and(
        eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
        eq(schema.scoringEvents.eventType, "playoff_game")
      ),
    });

    const tournamentGroups = bracketEvent
      ? await db.query.tournamentGroups.findMany({
          where: eq(schema.tournamentGroups.scoringEventId, bracketEvent.id),
          with: {
            members: true,
            matches: true,
          },
        })
      : [];

    // Build group definitions: list of teams + their completed match data per group.
    // If no groups set up yet, distribute all participants into 12 synthetic groups of 4.
    const groupDefs: Array<{
      groupName: string;
      teamIds: string[];
      completedMatches: GroupMatchResult[];
    }> = [];

    if (tournamentGroups.length > 0) {
      for (const group of tournamentGroups) {
        const teamIds = group.members.map((m) => m.participantId);
        const completedMatches: GroupMatchResult[] = group.matches
          .filter((m) => m.isComplete)
          .map((m) => ({
            participant1Id: m.participant1Id,
            participant2Id: m.participant2Id,
            participant1Score: m.participant1Score,
            participant2Score: m.participant2Score,
            isComplete: m.isComplete,
          }));
        groupDefs.push({ groupName: group.groupName, teamIds, completedMatches });
      }
    } else {
      // No groups set up — distribute participants into 12 synthetic groups of 4
      const chunkSize = 4;
      const labels = ["A","B","C","D","E","F","G","H","I","J","K","L"];
      for (let i = 0; i < Math.min(12, labels.length); i++) {
        const teamIds = participantIds.slice(i * chunkSize, (i + 1) * chunkSize);
        if (teamIds.length > 0) groupDefs.push({ groupName: labels[i], teamIds, completedMatches: [] });
      }
    }

    // 6. Load completed knockout matches (to fix results in simulation)
    const completedKnockoutMatches = bracketEvent
      ? await db.query.playoffMatches.findMany({
          where: and(
            eq(schema.playoffMatches.scoringEventId, bracketEvent.id),
            eq(schema.playoffMatches.isComplete, true)
          ),
        })
      : [];

    const completedByRoundAndNumber = new Map<string, { winnerId: string; loserId: string }>();
    for (const m of completedKnockoutMatches) {
      if (m.winnerId && m.loserId) {
        completedByRoundAndNumber.set(`${m.round}:${m.matchNumber}`, {
          winnerId: m.winnerId,
          loserId: m.loserId,
        });
      }
    }

    // 7. Monte Carlo simulation
    const counts = {
      champion: new Map<string, number>(),
      runnerUp: new Map<string, number>(),
      thirdPlace: new Map<string, number>(),
      fourthPlace: new Map<string, number>(),
      qfLoser: new Map<string, number>(),
    };
    for (const id of participantIds) {
      counts.champion.set(id, 0);
      counts.runnerUp.set(id, 0);
      counts.thirdPlace.set(id, 0);
      counts.fourthPlace.set(id, 0);
      counts.qfLoser.set(id, 0);
    }

    for (let sim = 0; sim < this.numSimulations; sim++) {
      // ── Group stage ──────────────────────────────────────────────
      const advancingFromGroup: string[] = []; // group winners + runners-up (24 teams)
      const thirdPlaceTeams: TeamStats[] = [];  // 12 third-place teams

      for (const group of groupDefs) {
        if (group.teamIds.length < 3) continue;

        const sorted = simGroup(group.teamIds, eloFn, group.completedMatches);

        advancingFromGroup.push(sorted[0].id, sorted[1].id); // 1st and 2nd
        if (sorted[2]) thirdPlaceTeams.push(sorted[2]);     // 3rd place
      }

      // Pick best 8 third-place teams
      const best8Third = thirdPlaceTeams
        .toSorted(sortTeams)
        .slice(0, 8)
        .map((t) => t.id);

      // Build R32 pool: 24 group advancers + 8 best 3rd-place = 32 teams
      const r32Pool = [...advancingFromGroup, ...best8Third];

      // ── Knockout rounds ──────────────────────────────────────────
      // R32 → R16 → QF → SF → 3PG + Final
      //
      // SEEDING NOTE: We pair teams sequentially (1v2, 3v4, …) in arrival order
      // (group A winner, group A runner-up, group B winner, …, best-8 3rd-place teams).
      // Real FIFA uses a pre-determined bracket path (e.g. Group A winner vs Group B
      // runner-up), which varies by edition.  This simplified pairing produces correct
      // aggregate probabilities for fantasy purposes even though simulated bracket paths
      // may not match the actual draw.
      //
      // Completed knockout matches are honoured by round+matchNumber, so as the real
      // bracket plays out the simulation locks in actual results automatically.

      function runKnockoutRound(
        teams: string[],
        roundName: string
      ): { winners: string[]; losers: string[] } {
        const winners: string[] = [];
        const losers: string[] = [];
        for (let i = 0; i < teams.length; i += 2) {
          const a = teams[i];
          const b = teams[i + 1];
          if (!a || !b) continue;

          const matchNum = Math.floor(i / 2) + 1;
          const fixed = completedByRoundAndNumber.get(`${roundName}:${matchNum}`);
          if (fixed) {
            winners.push(fixed.winnerId);
            losers.push(fixed.loserId);
          } else {
            const { winner, loser } = simKnockout(a, b, eloFn, normalizedProb);
            winners.push(winner);
            losers.push(loser);
          }
        }
        return { winners, losers };
      }

      const r32 = runKnockoutRound(r32Pool, "Round of 32");
      const r16 = runKnockoutRound(r32.winners, "Round of 16");
      const qf = runKnockoutRound(r16.winners, "Quarterfinals");
      const sf = runKnockoutRound(qf.winners, "Semifinals");

      // QF losers
      for (const id of qf.losers) {
        counts.qfLoser.set(id, (counts.qfLoser.get(id) ?? 0) + 1);
      }

      // Third place game (SF losers)
      const [sf1Loser, sf2Loser] = sf.losers;
      if (sf1Loser && sf2Loser) {
        const fixed3pg = completedByRoundAndNumber.get("Third Place Game:1");
        if (fixed3pg) {
          counts.thirdPlace.set(fixed3pg.winnerId, (counts.thirdPlace.get(fixed3pg.winnerId) ?? 0) + 1);
          counts.fourthPlace.set(fixed3pg.loserId, (counts.fourthPlace.get(fixed3pg.loserId) ?? 0) + 1);
        } else {
          const { winner: thirdWinner, loser: thirdLoser } = simKnockout(
            sf1Loser, sf2Loser, eloFn, normalizedProb
          );
          counts.thirdPlace.set(thirdWinner, (counts.thirdPlace.get(thirdWinner) ?? 0) + 1);
          counts.fourthPlace.set(thirdLoser, (counts.fourthPlace.get(thirdLoser) ?? 0) + 1);
        }
      }

      // Final (SF winners)
      const [sfWinner1, sfWinner2] = sf.winners;
      if (sfWinner1 && sfWinner2) {
        const fixedFinal = completedByRoundAndNumber.get("Finals:1");
        if (fixedFinal) {
          counts.champion.set(fixedFinal.winnerId, (counts.champion.get(fixedFinal.winnerId) ?? 0) + 1);
          counts.runnerUp.set(fixedFinal.loserId, (counts.runnerUp.get(fixedFinal.loserId) ?? 0) + 1);
        } else {
          const { winner: champion, loser: runnerUp } = simKnockout(
            sfWinner1, sfWinner2, eloFn, normalizedProb
          );
          counts.champion.set(champion, (counts.champion.get(champion) ?? 0) + 1);
          counts.runnerUp.set(runnerUp, (counts.runnerUp.get(runnerUp) ?? 0) + 1);
        }
      }
    }

    // 8. Convert counts to probabilities
    const N = this.numSimulations;
    const numQfLosers = 4; // 4 QF losers per sim

    const results: SimulationResult[] = participantIds.map((id) => {
      const qfProb = (counts.qfLoser.get(id) ?? 0) / (numQfLosers * N);

      return {
        participantId: id,
        probabilities: {
          probFirst:   (counts.champion.get(id)    ?? 0) / N,
          probSecond:  (counts.runnerUp.get(id)    ?? 0) / N,
          probThird:   (counts.thirdPlace.get(id)  ?? 0) / N,
          probFourth:  (counts.fourthPlace.get(id) ?? 0) / N,
          probFifth:   qfProb,
          probSixth:   qfProb,
          probSeventh: qfProb,
          probEighth:  qfProb,
        },
        source: "World Cup Monte Carlo (group stage + knockout)",
      };
    });

    // Normalise floating-point residuals per position
    const positionKeys: Array<keyof SimulationProbabilities> = [
      "probFirst", "probSecond", "probThird", "probFourth",
    ];
    normalizeProbabilities(results, positionKeys);

    return results;
  }
}
