/**
 * NCAA Men's College Hockey Tournament Simulator
 *
 * Supports two modes with admin-editable inputs only:
 *   1. Pre-bracket mode: samples a 16-team tournament field from all season
 *      participants using futures odds, NPI rank, and/or Elo entered in Admin.
 *   2. Bracket-aware mode: when a 16-team playoff bracket exists, simulates the
 *      actual bracket and honors completed match results.
 *
 * No team ratings are hardcoded. Strength comes from season_participant_expected_values:
 *   - sourceElo: optional direct Elo / admin power rating
 *   - worldRanking: optional NPI rank (1 = best)
 *   - sourceOdds: optional American championship futures odds
 */

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

const NUM_SIMULATIONS = 50_000;
const BRACKET_SIZE = 16;
const HOCKEY_PARITY_FACTOR = 850;
const DEFAULT_ELO = 1500;
const RANK_ELO_SCALE = 125;
const RANK_ZIPF_EXPONENT = 1.1;
const ODDS_ELO_WEIGHT = 0.25;
const DIRECT_ELO_WEIGHT = 1 - ODDS_ELO_WEIGHT;
const COLLEGE_HOCKEY_TEMPLATE_ID = "college_hockey_16";

const ROUND_OF_16_NAMES = new Set(["Round of 16", "Regional Semifinals"]);
const QUARTERFINAL_NAMES = new Set(["Quarterfinals", "Regional Finals"]);
const SEMIFINAL_NAMES = new Set(["Semifinals", "Frozen Four Semifinals"]);
const FINAL_NAMES = new Set(["Finals", "National Championship"]);

interface Team {
  participantId: string;
  elo: number;
  seedScore: number;
  oddsProb: number;
  selectionWeight: number;
}

interface EvInputRow {
  participantId: string;
  sourceElo: number | null;
  sourceOdds: number | null;
  worldRanking: number | null;
}

interface PlacementCounts {
  champion: number;
  finalist: number;
  semifinalLoser: number;
  quarterfinalLoser: number;
}

type PlayoffMatch = typeof schema.playoffMatches.$inferSelect;

export function npiRankToElo(rank: number, fieldSize: number): number {
  if (!Number.isFinite(rank) || rank <= 0) return DEFAULT_ELO;
  const safeFieldSize = Math.max(fieldSize, BRACKET_SIZE);
  return Math.round(DEFAULT_ELO + RANK_ELO_SCALE * Math.log(safeFieldSize / rank));
}

export function npiRankSelectionWeight(rank: number | null): number {
  if (!rank || rank <= 0) return 0;
  return 1 / Math.pow(rank, RANK_ZIPF_EXPONENT);
}

function normalizeOdds(evRows: EvInputRow[]): Map<string, number> {
  const raw = new Map<string, number>();
  for (const row of evRows) {
    if (row.sourceOdds !== null) {
      raw.set(row.participantId, convertAmericanOddsToProbability(row.sourceOdds));
    }
  }

  const sum = [...raw.values()].reduce((acc, p) => acc + p, 0);
  const normalized = new Map<string, number>();
  if (sum <= 0) return normalized;

  for (const [id, prob] of raw) normalized.set(id, prob / sum);
  return normalized;
}

function buildOddsEloMap(evRows: EvInputRow[]): Map<string, number> {
  const oddsInput = evRows
    .filter((row) => row.sourceOdds !== null)
    .map((row) => ({ participantId: row.participantId, odds: row.sourceOdds ?? 0 }));

  if (oddsInput.length < 2) return new Map();
  return convertFuturesToElo(oddsInput, "american", {
    exponent: 0.33,
    eloMin: 1325,
    eloMax: 1725,
  });
}

export function buildCollegeHockeyTeams(
  participantIds: string[],
  evRows: EvInputRow[]
): Team[] {
  const evById = new Map(evRows.map((row) => [row.participantId, row]));
  const normalizedOdds = normalizeOdds(evRows);
  const oddsEloMap = buildOddsEloMap(evRows);
  const hasOdds = normalizedOdds.size > 0;
  const hasAnyRank = evRows.some((row) => row.worldRanking !== null);
  const hasAnyDirectElo = evRows.some((row) => row.sourceElo !== null);

  const teams: Team[] = participantIds.map((participantId) => {
    const ev = evById.get(participantId);
    const rankElo = ev?.worldRanking ? npiRankToElo(ev.worldRanking, participantIds.length) : null;
    const baseElo = ev?.sourceElo ?? rankElo ?? oddsEloMap.get(participantId) ?? DEFAULT_ELO;
    const oddsElo = oddsEloMap.get(participantId);
    const elo = oddsElo !== undefined && ((ev?.sourceElo !== null && ev?.sourceElo !== undefined) || rankElo !== null)
      ? Math.round(DIRECT_ELO_WEIGHT * baseElo + ODDS_ELO_WEIGHT * oddsElo)
      : baseElo;

    return {
      participantId,
      elo,
      seedScore: rankElo ?? ev?.sourceElo ?? elo,
      oddsProb: normalizedOdds.get(participantId) ?? 0,
      selectionWeight: 0,
    };
  });

  const maxElo = Math.max(...teams.map((team) => team.elo));
  const fallbackWeights = new Map<string, number>();
  for (const team of teams) {
    const rank = evById.get(team.participantId)?.worldRanking ?? null;
    const fallbackWeight = hasAnyRank
      ? npiRankSelectionWeight(rank)
      : hasAnyDirectElo
        ? Math.exp((team.elo - maxElo) / 140)
        : 1;
    fallbackWeights.set(team.participantId, fallbackWeight > 0 ? fallbackWeight : 0.01);
  }

  if (hasOdds) {
    const fallbackTotal = [...fallbackWeights.values()].reduce((sum, weight) => sum + weight, 0);
    for (const team of teams) {
      const fallbackShare = (fallbackWeights.get(team.participantId) ?? 0.01) / fallbackTotal;
      team.selectionWeight = team.oddsProb + 0.1 * fallbackShare;
    }
  } else {
    for (const team of teams) team.selectionWeight = fallbackWeights.get(team.participantId) ?? 1;
  }

  return teams;
}

function simGame(team1: Team, team2: Team): { winner: Team; loser: Team } {
  const p1Wins = Math.random() < eloWinProbabilityWithParity(team1.elo, team2.elo, HOCKEY_PARITY_FACTOR);
  return p1Wins ? { winner: team1, loser: team2 } : { winner: team2, loser: team1 };
}

function bump(counts: Map<string, PlacementCounts>, id: string, key: keyof PlacementCounts): void {
  const entry = counts.get(id);
  if (entry) entry[key]++;
}

function sampleField(pool: Team[], n: number): Team[] {
  const remaining = [...pool];
  const selected: Team[] = [];

  for (let i = 0; i < n; i++) {
    const totalWeight = remaining.reduce((sum, team) => sum + team.selectionWeight, 0);
    let r = Math.random() * totalWeight;
    let index = 0;
    for (; index < remaining.length - 1; index++) {
      r -= remaining[index].selectionWeight;
      if (r <= 0) break;
    }
    selected.push(remaining[index]);
    remaining.splice(index, 1);
  }

  return seedField(selected);
}

function seedField(teams: Team[]): Team[] {
  return [...teams].toSorted((a, b) => b.seedScore - a.seedScore || b.elo - a.elo);
}

function simulateSeeded16TeamBracket(teams: Team[], counts: Map<string, PlacementCounts>): void {
  const r16Pairs: Array<[number, number]> = [
    [0, 15], [7, 8], [4, 11], [3, 12], [5, 10], [2, 13], [6, 9], [1, 14],
  ];

  const r16Winners = r16Pairs.map(([a, b]) => simGame(teams[a], teams[b]).winner);

  const qfWinners: Team[] = [];
  for (let i = 0; i < 4; i++) {
    const result = simGame(r16Winners[i * 2], r16Winners[i * 2 + 1]);
    qfWinners.push(result.winner);
    bump(counts, result.loser.participantId, "quarterfinalLoser");
  }

  const sf1 = simGame(qfWinners[0], qfWinners[1]);
  const sf2 = simGame(qfWinners[2], qfWinners[3]);
  bump(counts, sf1.loser.participantId, "semifinalLoser");
  bump(counts, sf2.loser.participantId, "semifinalLoser");

  const final = simGame(sf1.winner, sf2.winner);
  bump(counts, final.winner.participantId, "champion");
  bump(counts, final.loser.participantId, "finalist");
}

function findRound(matches: PlayoffMatch[], names: Set<string>): PlayoffMatch[] {
  return matches
    .filter((match) => names.has(match.round) || (match.templateRound ? names.has(match.templateRound) : false))
    .toSorted((a, b) => a.matchNumber - b.matchNumber);
}

function completedLoser(match: PlayoffMatch): string {
  if (match.loserId) return match.loserId;
  if (match.participant1Id === match.winnerId && match.participant2Id) return match.participant2Id;
  if (match.participant2Id === match.winnerId && match.participant1Id) return match.participant1Id;
  throw new Error(`Completed ${match.round} match ${match.matchNumber} is missing a loser.`);
}

function getTeam(teamById: Map<string, Team>, participantId: string): Team {
  const team = teamById.get(participantId);
  if (!team) throw new Error(`Participant ${participantId} is missing a saved rating row.`);
  return team;
}

function simulateMatchFromIds(
  p1: string,
  p2: string,
  match: PlayoffMatch | undefined,
  teamById: Map<string, Team>
): { winner: Team; loser: Team } {
  if (match?.isComplete && match.winnerId) {
    const loserId = completedLoser(match);
    return { winner: getTeam(teamById, match.winnerId), loser: getTeam(teamById, loserId) };
  }
  return simGame(getTeam(teamById, p1), getTeam(teamById, p2));
}

function simulateExistingBracket(
  rounds: { r16: PlayoffMatch[]; qf: PlayoffMatch[]; sf: PlayoffMatch[]; final: PlayoffMatch },
  teamById: Map<string, Team>,
  counts: Map<string, PlacementCounts>
): void {
  const r16Winners: Team[] = [];
  for (let i = 0; i < 8; i++) {
    const match = rounds.r16[i];
    if (!match.participant1Id || !match.participant2Id) {
      throw new Error(`${match.round} match ${match.matchNumber} is missing participants.`);
    }
    r16Winners.push(simulateMatchFromIds(match.participant1Id, match.participant2Id, match, teamById).winner);
  }

  const qfWinners: Team[] = [];
  for (let i = 0; i < 4; i++) {
    const match = rounds.qf[i];
    const p1 = r16Winners[i * 2]?.participantId;
    const p2 = r16Winners[i * 2 + 1]?.participantId;
    if (!p1 || !p2) throw new Error(`${match.round} match ${match.matchNumber} cannot resolve participants.`);
    const result = simulateMatchFromIds(p1, p2, match, teamById);
    qfWinners.push(result.winner);
    bump(counts, result.loser.participantId, "quarterfinalLoser");
  }

  const sfWinners: Team[] = [];
  for (let i = 0; i < 2; i++) {
    const match = rounds.sf[i];
    const p1 = qfWinners[i * 2]?.participantId;
    const p2 = qfWinners[i * 2 + 1]?.participantId;
    if (!p1 || !p2) throw new Error(`${match.round} match ${match.matchNumber} cannot resolve participants.`);
    const result = simulateMatchFromIds(p1, p2, match, teamById);
    sfWinners.push(result.winner);
    bump(counts, result.loser.participantId, "semifinalLoser");
  }

  const p1 = sfWinners[0]?.participantId;
  const p2 = sfWinners[1]?.participantId;
  if (!p1 || !p2) throw new Error(`${rounds.final.round} match ${rounds.final.matchNumber} cannot resolve participants.`);
  const final = simulateMatchFromIds(p1, p2, rounds.final, teamById);
  bump(counts, final.winner.participantId, "champion");
  bump(counts, final.loser.participantId, "finalist");
}

function toResults(participantIds: string[], counts: Map<string, PlacementCounts>): SimulationResult[] {
  const results = participantIds.map((participantId) => {
    const c = counts.get(participantId) ?? { champion: 0, finalist: 0, semifinalLoser: 0, quarterfinalLoser: 0 };
    const sf = c.semifinalLoser / (2 * NUM_SIMULATIONS);
    const qf = c.quarterfinalLoser / (4 * NUM_SIMULATIONS);
    return {
      participantId,
      probabilities: {
        probFirst: c.champion / NUM_SIMULATIONS,
        probSecond: c.finalist / NUM_SIMULATIONS,
        probThird: sf,
        probFourth: sf,
        probFifth: qf,
        probSixth: qf,
        probSeventh: qf,
        probEighth: qf,
      },
      source: "college_hockey_monte_carlo",
    };
  });

  for (const key of ["probFirst", "probSecond", "probThird", "probFourth", "probFifth", "probSixth", "probSeventh", "probEighth"] as const) {
    const colSum = results.reduce((sum, result) => sum + result.probabilities[key], 0);
    const residual = 1 - colSum;
    if (Math.abs(residual) > 0 && Math.abs(residual) < 1e-9) {
      const maxResult = results.reduce((best, result) =>
        result.probabilities[key] > best.probabilities[key] ? result : best
      );
      maxResult.probabilities[key] += residual;
    }
  }

  return results;
}

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

    const bracketEvent = await db.query.scoringEvents.findFirst({
      where: and(
        eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
        eq(schema.scoringEvents.eventType, "playoff_game")
      ),
    });

    const bracketMatches = bracketEvent
      ? await db.query.playoffMatches.findMany({
          where: eq(schema.playoffMatches.scoringEventId, bracketEvent.id),
          orderBy: (m, { asc }) => [asc(m.matchNumber)],
        })
      : [];

    const existingBracket = this.getExistingBracketRounds(bracketMatches);
    const participantIds = existingBracket
      ? this.getParticipantIdsFromBracket(existingBracket)
      : (await db
          .select({ id: schema.seasonParticipants.id })
          .from(schema.seasonParticipants)
          .where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId))).map((row) => row.id);

    if (participantIds.length < BRACKET_SIZE) {
      throw new Error(
        `College hockey simulator requires at least ${BRACKET_SIZE} participants, found ${participantIds.length}.`
      );
    }

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

    const teams = buildCollegeHockeyTeams(participantIds, evRows);
    const teamById = new Map(teams.map((team) => [team.participantId, team]));
    const counts = new Map<string, PlacementCounts>(
      participantIds.map((id) => [id, { champion: 0, finalist: 0, semifinalLoser: 0, quarterfinalLoser: 0 }])
    );

    if (existingBracket) {
      for (let i = 0; i < NUM_SIMULATIONS; i++) {
        simulateExistingBracket(existingBracket, teamById, counts);
      }
    } else {
      const preBracketMode = participantIds.length > BRACKET_SIZE;
      const deterministicField = preBracketMode ? null : seedField(teams);
      for (let i = 0; i < NUM_SIMULATIONS; i++) {
        const field = preBracketMode ? sampleField(teams, BRACKET_SIZE) : deterministicField ?? [];
        simulateSeeded16TeamBracket(field, counts);
      }
    }

    return toResults(participantIds, counts);
  }

  private getExistingBracketRounds(matches: PlayoffMatch[]): { r16: PlayoffMatch[]; qf: PlayoffMatch[]; sf: PlayoffMatch[]; final: PlayoffMatch } | null {
    if (matches.length === 0) return null;

    const r16 = findRound(matches, ROUND_OF_16_NAMES);
    const qf = findRound(matches, QUARTERFINAL_NAMES);
    const sf = findRound(matches, SEMIFINAL_NAMES);
    const final = findRound(matches, FINAL_NAMES);

    if (r16.length === 0 && qf.length === 0 && sf.length === 0 && final.length === 0) return null;
    if (r16.length !== 8 || qf.length !== 4 || sf.length !== 2 || final.length !== 1) {
      throw new Error(
        `College hockey bracket must have 8 Round of 16, 4 Quarterfinal, 2 Semifinal, and 1 Final match. ` +
        `Found ${r16.length}/${qf.length}/${sf.length}/${final.length}.`
      );
    }

    return { r16, qf, sf, final: final[0] };
  }

  private getParticipantIdsFromBracket(rounds: { r16: PlayoffMatch[]; qf: PlayoffMatch[]; sf: PlayoffMatch[]; final: PlayoffMatch }): string[] {
    const ids = new Set<string>();
    const allMatches = [...rounds.r16, ...rounds.qf, ...rounds.sf, rounds.final];
    for (const match of allMatches) {
      if (match.participant1Id) ids.add(match.participant1Id);
      if (match.participant2Id) ids.add(match.participant2Id);
      if (match.winnerId) ids.add(match.winnerId);
      if (match.loserId) ids.add(match.loserId);
    }
    return [...ids];
  }
}

export { COLLEGE_HOCKEY_TEMPLATE_ID };
