/**
 * Admin: Run EV Simulation for a Sports Season
 *
 * Action-only route. POSTing here:
 *   1. Guards against concurrent runs via simulationStatus
 *   2. Runs the appropriate simulator based on the sport's simulatorType
 *   3. Persists updated EVs to participantExpectedValues (transactional)
 *   4. Refreshes cached projectedPoints in teamStandings for all affected fantasy seasons
 *   5. Takes an EV snapshot from simulation output (upsert for today's date)
 *   6. Redirects back to the sports season admin page
 */

import { redirect } from "react-router";
import type { Route } from "./+types/admin.sports-seasons.$id.simulate";
import { findSportsSeasonById, updateSportsSeason } from "~/models/sports-season";
import { batchUpsertParticipantEVs } from "~/models/participant-expected-value";
import { batchUpsertParticipantEvSnapshots } from "~/models/ev-snapshot";
import { findParticipantsBySportsSeasonId } from "~/models/participant";
import { getSimulator, type SimulatorType } from "~/services/simulations/registry";
import { calculateEV, type ScoringRules } from "~/services/ev-calculator";
import { recalculateStandings } from "~/models/scoring-calculator";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq } from "drizzle-orm";

// Default scoring rules — matches the season table defaults.
// Per-league EV is recalculated from probabilities using league-specific rules in standings.
const DEFAULT_SCORING_RULES: ScoringRules = {
  pointsFor1st: 100,
  pointsFor2nd: 70,
  pointsFor3rd: 45,
  pointsFor4th: 45,
  pointsFor5th: 20,
  pointsFor6th: 20,
  pointsFor7th: 20,
  pointsFor8th: 20,
};

export async function action({ params }: Route.ActionArgs) {
  const sportsSeasonId = params.id;

  const sportsSeason = await findSportsSeasonById(sportsSeasonId);
  if (!sportsSeason) {
    throw new Response("Sports season not found", { status: 404 });
  }

  if (!sportsSeason.sport?.simulatorType) {
    throw new Response(
      "This sport has no simulator type configured. Set a simulator type on the sport in the admin panel before running a simulation.",
      { status: 400 }
    );
  }

  // Guard against concurrent simulation runs
  if (sportsSeason.simulationStatus === "running") {
    throw new Response(
      "A simulation is already running for this sports season. Please wait for it to complete.",
      { status: 409 }
    );
  }

  // Mark as running
  await updateSportsSeason(sportsSeasonId, { simulationStatus: "running" });

  try {
    const simulator = getSimulator(sportsSeason.sport.simulatorType as SimulatorType);

    const results = await simulator.simulate(sportsSeasonId);

    if (results.length === 0) {
      throw new Error("Simulation returned no results. Check that participants have EV data.");
    }

    // Build a complete set of EV inputs covering ALL season participants.
    // Participants not included in simulation results (e.g., teams in the season
    // but not in the bracket) get zeroed out so stale EVs from prior runs don't
    // inflate the total. Without this, extra participants with old non-zero EVs
    // would cause the total EV to exceed the expected sum of scoring values (340).
    const allSeasonParticipants = await findParticipantsBySportsSeasonId(sportsSeasonId);
    const simulatedIds = new Set(results.map((r) => r.participantId));
    const ZERO_PROBS = {
      probFirst: 0, probSecond: 0, probThird: 0, probFourth: 0,
      probFifth: 0, probSixth: 0, probSeventh: 0, probEighth: 0,
    };
    const evInputs = [
      ...results.map((r) => ({
        participantId: r.participantId,
        sportsSeasonId,
        probabilities: r.probabilities,
        scoringRules: DEFAULT_SCORING_RULES,
        source: "elo_simulation" as const,
      })),
      ...allSeasonParticipants
        .filter((p) => !simulatedIds.has(p.id))
        .map((p) => ({
          participantId: p.id,
          sportsSeasonId,
          probabilities: ZERO_PROBS,
          scoringRules: DEFAULT_SCORING_RULES,
          source: "elo_simulation" as const,
        })),
    ];

    // Persist updated EVs (transactional)
    await batchUpsertParticipantEVs(evInputs);

    // Refresh cached projectedPoints in teamStandings for all affected fantasy seasons
    const seasonSports = await database().query.seasonSports.findMany({
      where: eq(schema.seasonSports.sportsSeasonId, sportsSeasonId),
    });
    await Promise.all(seasonSports.map(({ seasonId }) => recalculateStandings(seasonId)));

    // Take EV snapshot from simulation output (not re-read from DB)
    const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
    await batchUpsertParticipantEvSnapshots(
      results.map((r) => ({
        participantId: r.participantId,
        sportsSeasonId,
        snapshotDate: today,
        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, DEFAULT_SCORING_RULES),
        source: r.source,
      }))
    );

    // Mark as idle
    await updateSportsSeason(sportsSeasonId, { simulationStatus: "idle" });
  } catch (error) {
    // Mark as failed so the UI shows the error state
    await updateSportsSeason(sportsSeasonId, { simulationStatus: "failed" });
    throw error;
  }

  return redirect(`/admin/sports-seasons/${sportsSeasonId}/expected-values`);
}
