import { eq } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { DEFAULT_SCORING_RULES } from "~/lib/scoring-types"; import type { ScoringRules } from "~/services/ev-calculator"; import { batchUpsertParticipantEvSnapshots } from "~/models/ev-snapshot"; import { batchUpsertParticipantEVs } from "~/models/participant-expected-value"; import { recalculateStandings } from "~/models/scoring-calculator"; import { findParticipantsBySportsSeasonId } from "~/models/season-participant"; import { findSportsSeasonById, updateSportsSeason } from "~/models/sports-season"; import { calculateEV } from "~/services/ev-calculator"; import { getSimulator, type SimulatorType } from "~/services/simulations/registry"; import { normalizeSimulationResultColumns } from "~/services/simulations/simulation-probabilities"; import { getSportsSeasonSimulatorConfig, prepareSimulatorInputsForRun, validateSimulatorReadiness, } from "~/models/simulator"; const ZERO_PROBS = { probFirst: 0, probSecond: 0, probThird: 0, probFourth: 0, probFifth: 0, probSixth: 0, probSeventh: 0, probEighth: 0, }; async function getPersistenceContext( simulatorType: SimulatorType, sportsSeason: Awaited> ): Promise<{ scoringRules: ScoringRules; source: "elo_simulation" | "performance_model" }> { if (simulatorType !== "brackt") { return { scoringRules: DEFAULT_SCORING_RULES, source: "elo_simulation" }; } if (!sportsSeason?.fantasySeasonId) { throw new Error("Brackt simulations must run against a private per-league sports season, not the global Brackt template."); } const fantasySeason = await database().query.seasons.findFirst({ where: eq(schema.seasons.id, sportsSeason.fantasySeasonId), }); if (!fantasySeason) { throw new Error("Linked fantasy season not found for Brackt simulation."); } return { source: "performance_model", scoringRules: { pointsFor1st: fantasySeason.pointsFor1st, pointsFor2nd: fantasySeason.pointsFor2nd, pointsFor3rd: fantasySeason.pointsFor3rd, pointsFor4th: fantasySeason.pointsFor4th, pointsFor5th: fantasySeason.pointsFor5th, pointsFor6th: fantasySeason.pointsFor6th, pointsFor7th: fantasySeason.pointsFor7th, pointsFor8th: fantasySeason.pointsFor8th, }, }; } export interface RunSportsSeasonSimulationResult { sportsSeasonId: string; simulatorType: SimulatorType; simulatedParticipants: number; zeroedParticipants: number; snapshotDate: string; } export async function runSportsSeasonSimulation( sportsSeasonId: string ): Promise { const sportsSeason = await findSportsSeasonById(sportsSeasonId); if (!sportsSeason) { throw new Error("Sports season not found"); } if (sportsSeason.status === "completed") { throw new Error("Completed sports seasons cannot be simulated."); } const simulatorConfig = await getSportsSeasonSimulatorConfig(sportsSeasonId); if (!simulatorConfig) { throw new Error( "This sport has no simulator type configured. Set a simulator type on the sport in the admin panel before running a simulation." ); } if (sportsSeason.simulationStatus === "running") { throw new Error("A simulation is already running for this sports season. Please wait for it to complete."); } const readiness = await validateSimulatorReadiness(sportsSeasonId); if (!readiness.canRun) { throw new Error( `Simulator is not ready: ${readiness.missingInputs.join(", ") || "missing required setup"}.` ); } await prepareSimulatorInputsForRun(sportsSeasonId); await updateSportsSeason(sportsSeasonId, { simulationStatus: "running" }); try { const simulator = getSimulator(simulatorConfig.simulatorType); const results = await simulator.simulate(sportsSeasonId); if (results.length === 0) { throw new Error("Simulation returned no results. Check that participants have simulator input data."); } normalizeSimulationResultColumns(results); const allSeasonParticipants = await findParticipantsBySportsSeasonId(sportsSeasonId); const simulatedIds = new Set(results.map((r) => r.participantId)); const zeroedParticipants = allSeasonParticipants.filter((p) => !simulatedIds.has(p.id)); const persistence = await getPersistenceContext(simulatorConfig.simulatorType, sportsSeason); await batchUpsertParticipantEVs([ ...results.map((r) => ({ participantId: r.participantId, sportsSeasonId, probabilities: r.probabilities, scoringRules: persistence.scoringRules, source: persistence.source, })), ...zeroedParticipants.map((p) => ({ participantId: p.id, sportsSeasonId, probabilities: ZERO_PROBS, scoringRules: persistence.scoringRules, source: persistence.source, })), ]); const seasonSports = await database().query.seasonSports.findMany({ where: eq(schema.seasonSports.sportsSeasonId, sportsSeasonId), }); await Promise.all(seasonSports.map(({ seasonId }) => recalculateStandings(seasonId))); const snapshotDate = new Date().toISOString().slice(0, 10); await batchUpsertParticipantEvSnapshots( results.map((r) => ({ participantId: r.participantId, sportsSeasonId, snapshotDate, probFirst: r.probabilities.probFirst, probSecond: r.probabilities.probSecond, probThird: r.probabilities.probThird, probFourth: r.probabilities.probFourth, probFifth: r.probabilities.probFifth, probSixth: r.probabilities.probSixth, probSeventh: r.probabilities.probSeventh, probEighth: r.probabilities.probEighth, calculatedEV: calculateEV(r.probabilities, persistence.scoringRules), source: r.source, })) ); await updateSportsSeason(sportsSeasonId, { simulationStatus: "idle" }); return { sportsSeasonId, simulatorType: simulatorConfig.simulatorType, simulatedParticipants: results.length, zeroedParticipants: zeroedParticipants.length, snapshotDate, }; } catch (error) { await updateSportsSeason(sportsSeasonId, { simulationStatus: "failed" }); throw error; } }