/**
 * Surface Elo model (canonical).
 *
 * Surface Elos are stored in the canonical `participant_surface_elos` table,
 * keyed by canonical participant id. The admin UI still works in terms of
 * season_participants — we join through season_participants → canonical
 * participant → canonical surface Elo so the UI doesn't need to know about
 * the canonical layer.
 *
 * See `docs/superpowers/specs/2026-05-01-canonical-tournament-layer-design.md`.
 */

import { database } from "~/database/context";
import { participantSurfaceElos, seasonParticipants } from "~/database/schema";
import { eq, inArray, sql } from "drizzle-orm";

export type CourtSurface = "hard" | "clay" | "grass";

export interface SurfaceEloRecord {
  id: string;
  participantId: string;
  sportsSeasonId: string;
  worldRanking: number | null;
  eloHard: number | null;
  eloClay: number | null;
  eloGrass: number | null;
  updatedAt: Date;
}

export interface SurfaceEloWithName extends SurfaceEloRecord {
  participantName: string;
}

export interface SurfaceEloInput {
  participantId: string;
  sportsSeasonId: string;
  worldRanking?: number | null;
  eloHard?: number | null;
  eloClay?: number | null;
  eloGrass?: number | null;
}

/**
 * Load surface Elos for a sports season's roster, joined with the per-window
 * participant's name. Internally joins season_participants → canonical
 * participants → canonical participant_surface_elos.
 *
 * The returned `id` is the canonical `participant_surface_elos.id`;
 * `participantId` is the season_participant id (admin UI keys rows by that).
 */
export async function getSurfaceElosForSeason(
  sportsSeasonId: string,
): Promise<SurfaceEloWithName[]> {
  const db = database();
  return await db
    .select({
      id: participantSurfaceElos.id,
      participantId: seasonParticipants.id,
      sportsSeasonId: seasonParticipants.sportsSeasonId,
      worldRanking: participantSurfaceElos.worldRanking,
      eloHard: participantSurfaceElos.eloHard,
      eloClay: participantSurfaceElos.eloClay,
      eloGrass: participantSurfaceElos.eloGrass,
      updatedAt: participantSurfaceElos.updatedAt,
      participantName: seasonParticipants.name,
    })
    .from(seasonParticipants)
    .innerJoin(
      participantSurfaceElos,
      eq(participantSurfaceElos.participantId, seasonParticipants.participantId),
    )
    .where(eq(seasonParticipants.sportsSeasonId, sportsSeasonId))
    .orderBy(seasonParticipants.name);
}

/**
 * Upsert surface Elos for a batch of season_participants. Writes to the
 * canonical `participant_surface_elos` table; callers pass season_participant
 * ids and we resolve canonical ids internally.
 *
 * season_participants without a canonical link are silently skipped. In
 * practice every qualifying-points roster entry is canonical-linked after
 * Phase 2 + the Phase 3 auto-linking on createParticipant.
 */
export async function batchUpsertSurfaceElos(
  inputs: SurfaceEloInput[],
): Promise<void> {
  if (inputs.length === 0) return;
  const db = database();
  const now = new Date();

  // Resolve canonical ids for every season_participant in the batch.
  const sps = await db
    .select({ id: seasonParticipants.id, canonicalId: seasonParticipants.participantId })
    .from(seasonParticipants)
    .where(inArray(seasonParticipants.id, inputs.map((i) => i.participantId)));
  const canonicalByInputId = new Map(sps.map((sp) => [sp.id, sp.canonicalId]));

  const canonicalRows = inputs
    .map((i) => ({
      canonicalId: canonicalByInputId.get(i.participantId),
      worldRanking: i.worldRanking ?? null,
      eloHard: i.eloHard ?? null,
      eloClay: i.eloClay ?? null,
      eloGrass: i.eloGrass ?? null,
    }))
    .filter((r): r is typeof r & { canonicalId: string } => typeof r.canonicalId === "string");

  if (canonicalRows.length === 0) return;

  await db
    .insert(participantSurfaceElos)
    .values(canonicalRows.map(({ canonicalId, ...rest }) => ({
      participantId: canonicalId,
      ...rest,
      updatedAt: now,
    })))
    .onConflictDoUpdate({
      target: [participantSurfaceElos.participantId],
      set: {
        worldRanking: sql`excluded.world_ranking`,
        eloHard: sql`excluded.elo_hard`,
        eloClay: sql`excluded.elo_clay`,
        eloGrass: sql`excluded.elo_grass`,
        updatedAt: sql`excluded.updated_at`,
      },
    });
}

/**
 * Build a map keyed by season_participant.id → surface Elo values, sourced
 * from the canonical `participant_surface_elos` table. Used by the tennis
 * simulator.
 *
 * Season participants without a linked canonical participant, or with no
 * canonical surface Elo row, are absent from the map — callers handle
 * `undefined` by falling back to 1500 (the simulator's default).
 */
export async function getSurfaceEloMap(
  sportsSeasonId: string,
): Promise<Map<string, { worldRanking: number | null; eloHard: number | null; eloClay: number | null; eloGrass: number | null }>> {
  const db = database();
  const rows = await db
    .select({
      seasonParticipantId: seasonParticipants.id,
      worldRanking: participantSurfaceElos.worldRanking,
      eloHard: participantSurfaceElos.eloHard,
      eloClay: participantSurfaceElos.eloClay,
      eloGrass: participantSurfaceElos.eloGrass,
    })
    .from(seasonParticipants)
    .innerJoin(
      participantSurfaceElos,
      eq(participantSurfaceElos.participantId, seasonParticipants.participantId),
    )
    .where(eq(seasonParticipants.sportsSeasonId, sportsSeasonId));

  return new Map(rows.map((r) => [r.seasonParticipantId, {
    worldRanking: r.worldRanking,
    eloHard: r.eloHard,
    eloClay: r.eloClay,
    eloGrass: r.eloGrass,
  }]));
}
