/**
 * EPL Season Standings Simulator
 *
 * Monte Carlo simulation of a Premier League table. Reads current actual
 * standings when present and simulates only the remaining season; if no current
 * standings exist, simulates a full 38-match season from 0 points.
 *
 * Results persist only placement probabilities (P1–P8) through the existing EV
 * pipeline. Simulated W/D/L/GF/GA/GD rows are internal and discarded.
 */

import { database } from "~/database/context";
import { eq } from "drizzle-orm";
import * as schema from "~/database/schema";
import type { Simulator, SimulationResult } from "./types";
import { getRegularSeasonStandings } from "~/models/regular-season-standings";
import { convertFuturesToElo, eloWinProbabilityWithParity } from "~/services/probability-engine";
import { normalizeSimulationResultColumns } from "./simulation-probabilities";
import { simulateEloSoccerMatch, simulateSimpleSoccerGoals } from "./soccer-helpers";
import { getSportsSeasonSimulatorConfig } from "~/models/simulator";

const DEFAULT_NUM_SIMULATIONS = 10_000;
const DEFAULT_EPL_REGULAR_SEASON_GAMES = 38;
const DEFAULT_AVERAGE_OPPONENT_ELO = 1500;
const DEFAULT_BASE_DRAW_RATE = 0.26;
const DEFAULT_DRAW_DECAY = 0.002;

interface TeamEntry {
  id: string;
  elo: number;
  currentPoints: number;
  currentGoalsFor: number;
  currentGoalsAgainst: number;
  currentGoalDifference: number;
  remainingGames: number;
}

interface SimulatedTableRow {
  team: TeamEntry;
  points: number;
  goalsFor: number;
  goalsAgainst: number;
  goalDifference: number;
  tiebreaker: number;
}

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;
}

function requiredConfigNumber(config: Record<string, unknown> | undefined, key: string): number {
  const value = config?.[key];
  if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
  throw new Error(`EPL simulator config is missing positive numeric ${key}.`);
}

/** EPL single-match win probability using the sport-specific parity factor. */
export function eplWinProbability(
  eloA: number,
  eloB: number,
  parityFactor: number
): number {
  return eloWinProbabilityWithParity(eloA, eloB, parityFactor);
}

/** Simulate an EPL match result for team A vs team B. Exported for tests. */
export function simEplMatch(
  eloA: number,
  eloB: number,
  options: { baseDrawRate?: number; drawDecay?: number; matchParityFactor: number }
): "win" | "draw" | "loss" {
  return simulateEloSoccerMatch(eloA, eloB, {
    baseDrawRate: options?.baseDrawRate ?? DEFAULT_BASE_DRAW_RATE,
    drawDecay: options?.drawDecay ?? DEFAULT_DRAW_DECAY,
    parityFactor: options.matchParityFactor,
  });
}

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

    const [participantRows, evRows, standings, simulatorConfig] = await Promise.all([
      db
        .select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
        .from(schema.seasonParticipants)
        .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),
      getSportsSeasonSimulatorConfig(sportsSeasonId),
    ]);

    const config = simulatorConfig?.config;
    const numSimulations = Math.round(configNumber(config, "iterations", DEFAULT_NUM_SIMULATIONS));
    const seasonGames = Math.round(configNumber(config, "seasonGames", DEFAULT_EPL_REGULAR_SEASON_GAMES));
    const averageOpponentElo = configNumber(config, "averageOpponentElo", DEFAULT_AVERAGE_OPPONENT_ELO);
    const matchOptions = {
      baseDrawRate: configNumber(config, "baseDrawRate", DEFAULT_BASE_DRAW_RATE),
      drawDecay: configNumber(config, "drawDecay", DEFAULT_DRAW_DECAY),
      matchParityFactor: requiredConfigNumber(config, "matchParityFactor"),
    };

    if (participantRows.length === 0) {
      throw new Error(
        `No participants found for sports season ${sportsSeasonId}. Add all 20 EPL clubs before running simulation.`
      );
    }

    if (participantRows.length < 8) {
      throw new Error(
        `EPL simulation requires at least 8 participants to fill top-eight probabilities (got ${participantRows.length}).`
      );
    }

    const dbEloMap = new Map<string, number>();
    for (const row of evRows) {
      if (row.sourceElo !== null && row.sourceElo !== undefined) {
        dbEloMap.set(row.participantId, row.sourceElo);
      }
    }

    if (dbEloMap.size === 0) {
      const oddsInput = evRows
        .filter((row) => row.sourceOdds !== null && row.sourceOdds !== undefined)
        .map((row) => ({ participantId: row.participantId, odds: row.sourceOdds as number }));
      const converted = convertFuturesToElo(oddsInput);
      for (const [participantId, elo] of converted) dbEloMap.set(participantId, elo);
    }

    const missingElo = participantRows.filter((row) => !dbEloMap.has(row.id));
    if (missingElo.length > 0) {
      throw new Error(
        `Missing Elo/projected points for ${missingElo.length} EPL participants: ${missingElo.map((p) => p.name).join(", ")}. ` +
          `Enter Elo ratings or projected points via Admin → Elo Ratings before simulating.`
      );
    }

    const standingsMap = new Map(standings.map((standing) => [standing.participantId, standing]));
    const participantIds = participantRows.map((row) => row.id);

    const teams: TeamEntry[] = participantRows.map((row) => {
      const standing = standingsMap.get(row.id);
      const wins = standing?.wins ?? 0;
      const draws = standing?.ties ?? 0;
      const losses = standing?.losses ?? 0;
      const gamesPlayed = standing?.gamesPlayed ?? wins + draws + losses;
      const goalsFor = standing?.goalsFor ?? 0;
      const goalsAgainst = standing?.goalsAgainst ?? 0;
      const goalDifference = standing?.goalDifference ?? goalsFor - goalsAgainst;

      return {
        id: row.id,
        elo: dbEloMap.get(row.id) as number,
        currentPoints: standing?.tablePoints ?? wins * 3 + draws,
        currentGoalsFor: goalsFor,
        currentGoalsAgainst: goalsAgainst,
        currentGoalDifference: goalDifference,
        remainingGames: Math.max(0, seasonGames - gamesPlayed),
      };
    });

    const rankCounts = new Map<string, number[]>(
      participantIds.map((id) => [id, Array.from({ length: 8 }, () => 0)])
    );

    for (let sim = 0; sim < numSimulations; sim++) {
      const tableRows: SimulatedTableRow[] = teams.map((team) => ({
        team,
        points: team.currentPoints,
        goalsFor: team.currentGoalsFor,
        goalsAgainst: team.currentGoalsAgainst,
        goalDifference: team.currentGoalDifference,
        tiebreaker: Math.random(),
      }));

      for (const row of tableRows) {
        for (let game = 0; game < row.team.remainingGames; game++) {
          const result = simEplMatch(row.team.elo, averageOpponentElo, matchOptions);
          const goals = simulateSimpleSoccerGoals(result);
          row.goalsFor += goals.gf;
          row.goalsAgainst += goals.ga;
          row.goalDifference = row.goalsFor - row.goalsAgainst;

          if (result === "win") row.points += 3;
          else if (result === "draw") row.points += 1;
        }
      }

      const finalTable = tableRows.toSorted((a, b) =>
        b.points - a.points ||
        b.goalDifference - a.goalDifference ||
        b.goalsFor - a.goalsFor ||
        b.tiebreaker - a.tiebreaker
      );

      for (let rank = 0; rank < Math.min(8, finalTable.length); rank++) {
        const counts = rankCounts.get(finalTable[rank].team.id);
        if (counts) counts[rank]++;
      }
    }

    const results: SimulationResult[] = participantIds.map((participantId) => {
      const counts = rankCounts.get(participantId) ?? Array.from({ length: 8 }, () => 0);
      return {
        participantId,
        probabilities: {
          probFirst: counts[0] / numSimulations,
          probSecond: counts[1] / numSimulations,
          probThird: counts[2] / numSimulations,
          probFourth: counts[3] / numSimulations,
          probFifth: counts[4] / numSimulations,
          probSixth: counts[5] / numSimulations,
          probSeventh: counts[6] / numSimulations,
          probEighth: counts[7] / numSimulations,
        },
        source: "epl_standings_monte_carlo",
      };
    });

    normalizeSimulationResultColumns(results);
    return results;
  }
}
