import { and, eq, inArray, sql } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import {
  getManifestSimulatorProfile,
  simulatorInputLabel,
  type SimulatorInputKey,
  type SimulatorManifestProfile,
} from "~/services/simulations/manifest";
import {
  ratingRequirementLabel,
  resolveRatings,
  resolveSourceElos,
  sourceEloRequirementLabel,
} from "~/services/simulations/input-policy";
import { SIMULATOR_TYPES, type SimulatorType } from "~/services/simulations/registry";

export interface SimulatorProfile extends SimulatorManifestProfile {
  isActive: boolean;
}

export interface SportsSeasonSimulatorConfig {
  sportsSeasonId: string;
  simulatorType: SimulatorType;
  config: Record<string, unknown>;
  profile: SimulatorProfile;
}

export interface ParticipantSimulatorInput {
  participantId: string;
  sportsSeasonId: string;
  sourceOdds: number | null;
  sourceElo: number | null;
  worldRanking: number | null;
  rating: number | null;
  projectedWins: number | null;
  projectedTablePoints: number | null;
  seed: number | null;
  region: string | null;
  metadata: Record<string, unknown> | null;
}

export interface UpsertParticipantSimulatorInput {
  participantId: string;
  sportsSeasonId: string;
  sourceOdds?: number | null;
  sourceElo?: number | null;
  worldRanking?: number | null;
  rating?: number | null;
  projectedWins?: number | null;
  projectedTablePoints?: number | null;
  seed?: number | null;
  region?: string | null;
  metadata?: Record<string, unknown> | null;
}

export interface SimulatorReadiness {
  status: "ready" | "not_ready";
  canRun: boolean;
  participantCount: number;
  participantInputCount: number;
  derivedInputCount: number;
  fallbackInputCount: number;
  missingInputs: string[];
  warnings: string[];
}

export interface SportsSeasonSimulatorSummary {
  sportsSeasonId: string;
  seasonName: string;
  year: number;
  seasonStatus: string;
  simulationStatus: string;
  fantasySeasonId: string | null;
  fantasySeasonName: string | null;
  leagueName: string | null;
  sportName: string;
  sportSlug: string;
  simulatorType: SimulatorType;
  simulatorName: string;
  participantCount: number;
  participantInputCount: number;
  lastSimulatedDate: string | null;
  readiness: SimulatorReadiness;
}

function parseDecimal(value: string | null | undefined): number | null {
  if (value === null || value === undefined) return null;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : null;
}

function isSimulatorType(value: string | null | undefined): value is SimulatorType {
  return typeof value === "string" && (SIMULATOR_TYPES as readonly string[]).includes(value);
}

function mergeProfile(
  simulatorType: SimulatorType,
  dbProfile?: typeof schema.simulatorProfiles.$inferSelect | null
): SimulatorProfile {
  const manifest = getManifestSimulatorProfile(simulatorType);
  if (!dbProfile) return { ...manifest, isActive: true };

  return {
    ...manifest,
    displayName: dbProfile.displayName,
    description: dbProfile.description ?? manifest.description,
    defaultConfig: {
      ...manifest.defaultConfig,
      ...dbProfile.defaultConfig,
    },
    isActive: dbProfile.isActive,
  };
}

function hasInputValue(input: ParticipantSimulatorInput, key: SimulatorInputKey): boolean {
  const value = input[key];
  if (value === null || value === undefined) return false;
  if (typeof value === "string") return value.trim().length > 0;
  if (typeof value === "object") return Object.keys(value).length > 0;
  return true;
}

function isFallbackMethod(method: string): boolean {
  return method === "fallbackElo" || method === "fallbackRating" || method === "averageKnown" || method === "worstKnownMinus";
}

const GENERATED_RATING_METHODS = ["sourceOdds", "fallbackRating", "averageKnown", "worstKnownMinus"] as const;
const GENERATED_SOURCE_ELO_METHODS = ["projectedWins", "projectedTablePoints", "sourceOdds", "fallbackElo", "averageKnown", "worstKnownMinus"] as const;

function isGeneratedRatingMethod(method: unknown): boolean {
  return typeof method === "string" && GENERATED_RATING_METHODS.includes(method as (typeof GENERATED_RATING_METHODS)[number]);
}

function isGeneratedSourceEloMethod(method: unknown): boolean {
  return typeof method === "string" && GENERATED_SOURCE_ELO_METHODS.includes(method as (typeof GENERATED_SOURCE_ELO_METHODS)[number]);
}

function isGeneratedRatingInput(input: typeof schema.seasonParticipantSimulatorInputs.$inferSelect | undefined): boolean {
  return isGeneratedRatingMethod(input?.metadata?.ratingMethod);
}

function isGeneratedSourceEloInput(input: typeof schema.seasonParticipantSimulatorInputs.$inferSelect | undefined): boolean {
  return isGeneratedSourceEloMethod(input?.metadata?.sourceEloMethod);
}

function inputRequirementLabel(
  key: SimulatorInputKey,
  profile: SimulatorManifestProfile
): string {
  if (key === "sourceElo" && profile.derivableInputs?.sourceElo?.length) {
    return sourceEloRequirementLabel(profile);
  }
  if (key === "rating" && profile.derivableInputs?.rating?.length) {
    return ratingRequirementLabel(profile);
  }
  return simulatorInputLabel(key);
}

export async function getSimulatorProfile(simulatorType: SimulatorType): Promise<SimulatorProfile> {
  const db = database();
  const dbProfile = await db.query.simulatorProfiles.findFirst({
    where: eq(schema.simulatorProfiles.simulatorType, simulatorType),
  });
  return mergeProfile(simulatorType, dbProfile);
}

export async function upsertSimulatorProfile(
  profile: Pick<SimulatorProfile, "simulatorType" | "displayName" | "description" | "defaultConfig"> & {
    inputSchema?: Record<string, unknown>;
    isActive?: boolean;
  }
): Promise<void> {
  const db = database();
  const now = new Date();
  await db
    .insert(schema.simulatorProfiles)
    .values({
      simulatorType: profile.simulatorType,
      displayName: profile.displayName,
      description: profile.description,
      defaultConfig: profile.defaultConfig,
      inputSchema: profile.inputSchema ?? {},
      isActive: profile.isActive ?? true,
      createdAt: now,
      updatedAt: now,
    })
    .onConflictDoUpdate({
      target: schema.simulatorProfiles.simulatorType,
      set: {
        displayName: sql`excluded.display_name`,
        description: sql`excluded.description`,
        defaultConfig: sql`excluded.default_config`,
        inputSchema: sql`excluded.input_schema`,
        isActive: sql`excluded.is_active`,
        updatedAt: now,
      },
    });
}

export async function getSportsSeasonSimulatorConfig(
  sportsSeasonId: string
): Promise<SportsSeasonSimulatorConfig | null> {
  const db = database();
  const season = await db.query.sportsSeasons.findFirst({
    where: eq(schema.sportsSeasons.id, sportsSeasonId),
    with: {
      sport: true,
      simulatorConfig: true,
    },
  });

  const simulatorType = season?.simulatorConfig?.simulatorType ?? season?.sport?.simulatorType;
  if (!season || !isSimulatorType(simulatorType)) return null;

  const profile = await getSimulatorProfile(simulatorType);
  const config = {
    ...profile.defaultConfig,
    ...season.simulatorConfig?.config,
  };

  return {
    sportsSeasonId,
    simulatorType,
    config,
    profile,
  };
}

export async function upsertSportsSeasonSimulatorConfig(input: {
  sportsSeasonId: string;
  simulatorType: SimulatorType;
  config?: Record<string, unknown>;
}): Promise<void> {
  const db = database();
  const now = new Date();
  await db
    .insert(schema.sportsSeasonSimulatorConfigs)
    .values({
      sportsSeasonId: input.sportsSeasonId,
      simulatorType: input.simulatorType,
      config: input.config ?? {},
      createdAt: now,
      updatedAt: now,
    })
    .onConflictDoUpdate({
      target: schema.sportsSeasonSimulatorConfigs.sportsSeasonId,
      set: {
        simulatorType: sql`excluded.simulator_type`,
        config: sql`excluded.config`,
        updatedAt: now,
      },
    });
}

export async function getParticipantSimulatorInputs(
  sportsSeasonId: string
): Promise<ParticipantSimulatorInput[]> {
  const db = database();
  const [seasonParticipants, simulatorInputs, legacyEvs] = await Promise.all([
    db.query.seasonParticipants.findMany({
      where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
      columns: { id: true },
    }),
    db.query.seasonParticipantSimulatorInputs.findMany({
      where: eq(schema.seasonParticipantSimulatorInputs.sportsSeasonId, sportsSeasonId),
    }),
    db.query.seasonParticipantExpectedValues.findMany({
      where: eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId),
      columns: {
        participantId: true,
        sourceOdds: true,
        sourceElo: true,
        worldRanking: true,
      },
    }),
  ]);

  const simulatorByParticipant = new Map(simulatorInputs.map((row) => [row.participantId, row]));
  const legacyByParticipant = new Map(legacyEvs.map((row) => [row.participantId, row]));

  return seasonParticipants.map((participant) => {
    const input = simulatorByParticipant.get(participant.id);
    const legacy = legacyByParticipant.get(participant.id);
    return {
      participantId: participant.id,
      sportsSeasonId,
      sourceOdds: input?.sourceOdds ?? legacy?.sourceOdds ?? null,
      sourceElo: input
        ? isGeneratedSourceEloInput(input) ? null : input.sourceElo
        : legacy?.sourceElo ?? null,
      worldRanking: input?.worldRanking ?? legacy?.worldRanking ?? null,
      rating: isGeneratedRatingInput(input) ? null : parseDecimal(input?.rating),
      projectedWins: parseDecimal(input?.projectedWins),
      projectedTablePoints: parseDecimal(input?.projectedTablePoints),
      seed: input?.seed ?? null,
      region: input?.region ?? null,
      metadata: input?.metadata ?? null,
    };
  });
}

function generatedRatingMethodSql() {
  return sql`${schema.seasonParticipantSimulatorInputs.metadata}->>'ratingMethod' in ('sourceOdds', 'fallbackRating', 'averageKnown', 'worstKnownMinus')`;
}

export async function batchSaveParticipantSimulatorSourceOdds(
  inputs: Array<{ participantId: string; sportsSeasonId: string; sourceOdds: number }>
): Promise<void> {
  if (inputs.length === 0) return;
  const db = database();
  const now = new Date();
  const seasonIds = [...new Set(inputs.map((input) => input.sportsSeasonId))];

  await db.transaction(async (tx) => {
    await tx
      .update(schema.seasonParticipantSimulatorInputs)
      .set({
        rating: null,
        metadata: sql`coalesce(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod'`,
        updatedAt: now,
      })
      .where(
        and(
          inArray(schema.seasonParticipantSimulatorInputs.sportsSeasonId, seasonIds),
          generatedRatingMethodSql()
        )
      );

    await tx
      .insert(schema.seasonParticipantSimulatorInputs)
      .values(
        inputs.map((input) => ({
          participantId: input.participantId,
          sportsSeasonId: input.sportsSeasonId,
          sourceOdds: input.sourceOdds,
          createdAt: now,
          updatedAt: now,
        }))
      )
      .onConflictDoUpdate({
        target: [
          schema.seasonParticipantSimulatorInputs.participantId,
          schema.seasonParticipantSimulatorInputs.sportsSeasonId,
        ],
        set: {
          sourceOdds: sql`excluded.source_odds`,
          rating: sql`case when ${generatedRatingMethodSql()} then null else ${schema.seasonParticipantSimulatorInputs.rating} end`,
          metadata: sql`case when ${generatedRatingMethodSql()} then coalesce(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod' else ${schema.seasonParticipantSimulatorInputs.metadata} end`,
          updatedAt: now,
        },
      });
  });
}

export async function batchSaveFuturesOddsForSimulator(
  inputs: Array<{ participantId: string; sportsSeasonId: string; sourceOdds: number }>
): Promise<void> {
  if (inputs.length === 0) return;
  const db = database();
  const now = new Date();
  const participantIds = inputs.map((input) => input.participantId);
  const seasonIds = [...new Set(inputs.map((input) => input.sportsSeasonId))];

  await db.transaction(async (tx) => {

    // Clear ALL ratings (both manual and generated) so the simulation
    // re-derives ratings from the newly saved futures odds.
    await tx
      .update(schema.seasonParticipantSimulatorInputs)
      .set({
        rating: null,
        metadata: sql`coalesce(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod'`,
        updatedAt: now,
      })
      .where(
        and(
          inArray(schema.seasonParticipantSimulatorInputs.sportsSeasonId, seasonIds),
          inArray(schema.seasonParticipantSimulatorInputs.participantId, participantIds)
        )
      );

    await tx
      .insert(schema.seasonParticipantSimulatorInputs)
      .values(
        inputs.map((input) => ({
          participantId: input.participantId,
          sportsSeasonId: input.sportsSeasonId,
          sourceOdds: input.sourceOdds,
          createdAt: now,
          updatedAt: now,
        }))
      )
      .onConflictDoUpdate({
        target: [
          schema.seasonParticipantSimulatorInputs.participantId,
          schema.seasonParticipantSimulatorInputs.sportsSeasonId,
        ],
        set: {
          sourceOdds: sql`excluded.source_odds`,
          rating: null,
          metadata: sql`coalesce(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb) - 'ratingMethod'`,
          updatedAt: now,
        },
      });
  });
}

export async function batchUpsertParticipantSimulatorInputs(
  inputs: UpsertParticipantSimulatorInput[]
): Promise<void> {
  if (inputs.length === 0) return;
  const db = database();
  const now = new Date();

  await db.transaction(async (tx) => {
    await tx
      .insert(schema.seasonParticipantSimulatorInputs)
      .values(
        inputs.map((input) => ({
          participantId: input.participantId,
          sportsSeasonId: input.sportsSeasonId,
          sourceOdds: input.sourceOdds ?? null,
          sourceElo: input.sourceElo ?? null,
          worldRanking: input.worldRanking ?? null,
          rating: input.rating?.toString() ?? null,
          projectedWins: input.projectedWins?.toString() ?? null,
          projectedTablePoints: input.projectedTablePoints?.toString() ?? null,
          seed: input.seed ?? null,
          region: input.region ?? null,
          metadata: input.metadata ?? null,
          createdAt: now,
          updatedAt: now,
        }))
      )
      .onConflictDoUpdate({
        target: [
          schema.seasonParticipantSimulatorInputs.participantId,
          schema.seasonParticipantSimulatorInputs.sportsSeasonId,
        ],
        set: {
          sourceOdds: sql`excluded.source_odds`,
          sourceElo: sql`excluded.source_elo`,
          worldRanking: sql`excluded.world_ranking`,
          rating: sql`excluded.rating`,
          projectedWins: sql`excluded.projected_wins`,
          projectedTablePoints: sql`excluded.projected_table_points`,
          seed: sql`excluded.seed`,
          region: sql`excluded.region`,
          metadata: sql`excluded.metadata`,
          updatedAt: sql`excluded.updated_at`,
        },
      });

    // Compatibility bridge: keep legacy simulator metadata columns populated until
    // every simulator has been migrated to season_participant_simulator_inputs.
    await tx
      .insert(schema.seasonParticipantExpectedValues)
      .values(
        inputs.map((input) => ({
          participantId: input.participantId,
          sportsSeasonId: input.sportsSeasonId,
          probFirst: "0",
          probSecond: "0",
          probThird: "0",
          probFourth: "0",
          probFifth: "0",
          probSixth: "0",
          probSeventh: "0",
          probEighth: "0",
          expectedValue: "0",
          source: input.sourceOdds !== null && input.sourceOdds !== undefined ? "futures_odds" as const : "elo_simulation" as const,
          sourceOdds: input.sourceOdds ?? null,
          sourceElo: input.sourceElo ?? null,
          worldRanking: input.worldRanking ?? null,
          calculatedAt: now,
          updatedAt: now,
        }))
      )
      .onConflictDoUpdate({
        target: [
          schema.seasonParticipantExpectedValues.participantId,
          schema.seasonParticipantExpectedValues.sportsSeasonId,
        ],
        set: {
          sourceOdds: sql`COALESCE(excluded.source_odds, ${schema.seasonParticipantExpectedValues.sourceOdds})`,
          sourceElo: sql`COALESCE(excluded.source_elo, ${schema.seasonParticipantExpectedValues.sourceElo})`,
          worldRanking: sql`COALESCE(excluded.world_ranking, ${schema.seasonParticipantExpectedValues.worldRanking})`,
          updatedAt: now,
        },
      });
  });
}

export async function validateSimulatorReadiness(
  sportsSeasonId: string
): Promise<SimulatorReadiness> {
  const config = await getSportsSeasonSimulatorConfig(sportsSeasonId);
  const inputs = await getParticipantSimulatorInputs(sportsSeasonId);
  const missingInputs: string[] = [];
  const warnings: string[] = [];
  let derivedInputCount = 0;
  let fallbackInputCount = 0;

  if (!config) {
    return {
      status: "not_ready",
      canRun: false,
      participantCount: inputs.length,
      participantInputCount: 0,
      derivedInputCount,
      fallbackInputCount,
      missingInputs: ["simulator type"],
      warnings,
    };
  }

  const participantCount = inputs.length;
  if (participantCount === 0) {
    missingInputs.push("participants");
  }

  const requiredInputs = config.profile.requiredInputs;
  const resolvedSourceElos = requiredInputs.includes("sourceElo")
    ? resolveSourceElos(inputs, config.profile, config.config)
    : new Map();
  const resolvedRatings = requiredInputs.includes("rating")
    ? resolveRatings(inputs, config.profile, config.config)
    : new Map();
  derivedInputCount =
    [...resolvedSourceElos.values()].filter((input) => input.method !== "direct").length +
    [...resolvedRatings.values()].filter((input) => input.method !== "direct").length;
  fallbackInputCount =
    [...resolvedSourceElos.values()].filter((input) => isFallbackMethod(input.method)).length +
    [...resolvedRatings.values()].filter((input) => isFallbackMethod(input.method)).length;

  function hasRequiredInput(input: ParticipantSimulatorInput, key: SimulatorInputKey): boolean {
    if (hasInputValue(input, key)) return true;
    if (key === "sourceElo") return resolvedSourceElos.has(input.participantId);
    if (key === "rating") return resolvedRatings.has(input.participantId);
    return false;
  }

  const participantInputCount = requiredInputs.length === 0
    ? participantCount
    : inputs.filter((input) => requiredInputs.every((key) => hasRequiredInput(input, key))).length;

  if (requiredInputs.length > 0 && participantInputCount < participantCount) {
    for (const key of requiredInputs) {
      const missingCount = inputs.filter((input) => !hasRequiredInput(input, key)).length;
      if (missingCount > 0) {
        missingInputs.push(`${inputRequirementLabel(key, config.profile)} for ${missingCount} participant(s)`);
      }
    }
  }

  if (derivedInputCount > 0) {
    warnings.push(`Derived simulator inputs will be used for ${derivedInputCount} participant(s).`);
  }
  if (fallbackInputCount > 0) {
    warnings.push(`Configured fallback simulator inputs will be used for ${fallbackInputCount} participant(s).`);
  }
  if (config.simulatorType === "cs2_major_qualifying_points") {
    const missingRankCount = inputs.filter((input) => !hasInputValue(input, "worldRanking")).length;
    if (missingRankCount > 0) {
      warnings.push(
        `CS2 will use rank 9999 for ${missingRankCount} participant(s) without world ranking; this is acceptable for deep-tail teams but less precise.`
      );
    }
  }

  if (config.profile.setupSections.includes("regularStandings")) {
    warnings.push("Regular-season standings may be needed for in-season accuracy.");
  }
  if (config.profile.setupSections.includes("bracket")) {
    warnings.push("Bracket-aware simulators use completed bracket results when available.");
  }

  const status = missingInputs.length === 0 ? "ready" : "not_ready";
  return {
    status,
    canRun: status === "ready",
    participantCount,
    participantInputCount,
    derivedInputCount,
    fallbackInputCount,
    missingInputs,
    warnings,
  };
}

export async function prepareSimulatorInputsForRun(sportsSeasonId: string): Promise<void> {
  const config = await getSportsSeasonSimulatorConfig(sportsSeasonId);
  if (!config) return;

  const inputs = await getParticipantSimulatorInputs(sportsSeasonId);
  const resolvedSourceElos = config.profile.requiredInputs.includes("sourceElo")
    ? resolveSourceElos(inputs, config.profile, config.config)
    : new Map();
  const resolvedRatings = config.profile.requiredInputs.includes("rating")
    ? resolveRatings(inputs, config.profile, config.config)
    : new Map();
  const rowsToUpsert: UpsertParticipantSimulatorInput[] = [];
  for (const input of inputs) {
    const resolvedElo = resolvedSourceElos.get(input.participantId);
    const resolvedRating = resolvedRatings.get(input.participantId);
    if (!resolvedElo && !resolvedRating) continue;

    rowsToUpsert.push({
      participantId: input.participantId,
      sportsSeasonId,
      sourceOdds: input.sourceOdds,
      sourceElo: resolvedElo?.sourceElo ?? input.sourceElo,
      worldRanking: input.worldRanking,
      rating: resolvedRating?.rating ?? input.rating,
      projectedWins: input.projectedWins,
      projectedTablePoints: input.projectedTablePoints,
      seed: input.seed,
      region: input.region,
      metadata: {
        ...input.metadata,
        ...(resolvedElo ? { sourceEloMethod: resolvedElo.method } : {}),
        ...(resolvedRating ? { ratingMethod: resolvedRating.method } : {}),
      },
    });
  }

  await batchUpsertParticipantSimulatorInputs(rowsToUpsert);
}

export async function listSportsSeasonSimulatorSummaries(): Promise<SportsSeasonSimulatorSummary[]> {
  const db = database();
  const seasons = await db.query.sportsSeasons.findMany({
    with: {
      sport: true,
      fantasySeason: { with: { league: true } },
      participants: { columns: { id: true } },
      simulatorConfig: true,
    },
  });

  const simulatorSeasons = seasons.filter((season) => {
    const simulatorType = season.simulatorConfig?.simulatorType ?? season.sport.simulatorType;
    if (!isSimulatorType(simulatorType)) return false;

    // Brackt's public sports season is a draftable template. The runnable
    // simulations are the per-league private Brackt seasons.
    if (season.sport.slug === "brackt") return season.fantasySeasonId !== null;

    return season.fantasySeasonId === null;
  });

  const seasonIds = simulatorSeasons.map((season) => season.id);
  const snapshotRows = seasonIds.length > 0
    ? await db
        .select({
          sportsSeasonId: schema.participantEvSnapshots.sportsSeasonId,
          lastSimulatedDate: sql<string>`max(${schema.participantEvSnapshots.snapshotDate})`,
        })
        .from(schema.participantEvSnapshots)
        .where(inArray(schema.participantEvSnapshots.sportsSeasonId, seasonIds))
        .groupBy(schema.participantEvSnapshots.sportsSeasonId)
    : [];
  const lastSnapshotBySeason = new Map(snapshotRows.map((row) => [row.sportsSeasonId, row.lastSimulatedDate]));

  // Each season calls getSimulatorProfile + validateSimulatorReadiness (~4 queries each).
  // Acceptable for an admin-only page; revisit if season counts grow past ~50.
  const summaries = await Promise.all(
    simulatorSeasons.map(async (season) => {
      const simulatorType = (season.simulatorConfig?.simulatorType ?? season.sport.simulatorType) as SimulatorType;
      const profile = await getSimulatorProfile(simulatorType);
      const readiness = await validateSimulatorReadiness(season.id);
      return {
        sportsSeasonId: season.id,
        seasonName: season.name,
        year: season.year,
        seasonStatus: season.status,
        simulationStatus: season.simulationStatus,
        fantasySeasonId: season.fantasySeasonId,
        fantasySeasonName: season.fantasySeason ? `${season.fantasySeason.year} Season` : null,
        leagueName: season.fantasySeason?.league?.name ?? null,
        sportName: season.sport.name,
        sportSlug: season.sport.slug,
        simulatorType,
        simulatorName: profile.displayName,
        participantCount: season.participants.length,
        participantInputCount: readiness.participantInputCount,
        lastSimulatedDate: lastSnapshotBySeason.get(season.id) ?? null,
        readiness,
      };
    })
  );

  return summaries.toSorted((a, b) =>
    a.sportName.localeCompare(b.sportName) ||
    (a.leagueName ?? "").localeCompare(b.leagueName ?? "") ||
    b.year - a.year ||
    a.seasonName.localeCompare(b.seasonName)
  );
}

export async function assertRegistrySchemaDriftFree(): Promise<void> {
  const schemaValues = schema.simulatorTypeEnum.enumValues;
  const missingFromSchema = SIMULATOR_TYPES.filter((type) => !schemaValues.includes(type));
  const missingFromRegistry = schemaValues.filter((type) => !(SIMULATOR_TYPES as readonly string[]).includes(type));
  if (missingFromSchema.length > 0 || missingFromRegistry.length > 0) {
    throw new Error(
      `Simulator registry/schema drift detected. ` +
      `Missing from schema: ${missingFromSchema.join(", ") || "none"}. ` +
      `Missing from registry: ${missingFromRegistry.join(", ") || "none"}.`
    );
  }
}
