/**
 * Tennis Grand Slam Simulator
 *
 * Monte Carlo simulation of the 4 Grand Slam majors using surface-specific Elo
 * ratings. Qualifying points (QP) are accumulated across all majors; the final
 * QP totals determine fantasy placements (1st–8th).
 *
 * Algorithm:
 *   1. Load the 4 Grand Slam scoring events (type = major_tournament).
 *   2. For complete majors, read actual qualifyingPointsAwarded from eventResults.
 *   3. For incomplete majors, simulate the 128-player seeded bracket.
 *   4. Accumulate QP per player across all 4 majors in each simulation.
 *   5. Rank players by total QP; tally 1st–8th placement counts.
 *   6. Return normalized SimulationResult[].
 *
 * QP per round (tie-splitting pre-applied per the rules):
 *   Winner         → 20 QP
 *   Finalist       → 14 QP
 *   SF loser ×2    →  9 QP each  ((10+8)/2)
 *   QF loser ×4    →  4 QP each  ((5+5+3+3)/4)
 *   R16 loser ×8   →  1.5 QP each ((2+2+2+2+1+1+1+1)/8)
 *   Earlier losers →  0 QP
 *
 * Seeded draw (128 players, top 32 seeded by ATP/WTA world ranking):
 *   Seed 1  → slot 0   (top of top half)
 *   Seed 2  → slot 64  (top of bottom half) — can only meet seed 1 in final
 *   Seeds 3–4   → slots 32, 96 (quarter tops), randomly drawn
 *   Seeds 5–8   → slots 16, 48, 80, 112 (eighth tops), randomly drawn
 *   Seeds 9–16  → slots 8, 24, 40, 56, 72, 88, 104, 120 (sixteenth tops), randomly drawn
 *   Seeds 17–32 → slots 4, 12, 20, 28, 36, 44, 52, 60, 68, 76, 84, 92, 100, 108, 116, 124, randomly drawn
 *   Remaining 96 → all remaining slots, randomly placed
 *
 * Surface mapping (matched against scoring event names):
 *   "Australian Open" → hard
 *   "French Open" / "Roland Garros" → clay
 *   "Wimbledon" → grass
 *   "US Open" → hard
 */

import { database } from "~/database/context";
import { eq, and, inArray } from "drizzle-orm";
import * as schema from "~/database/schema";
import { getSurfaceEloMap } from "~/models/surface-elo";
import type { Simulator, SimulationResult } from "./types";

// ─── Simulation parameters ────────────────────────────────────────────────────

const NUM_SIMULATIONS = 10_000;

/**
 * Elo divisor for per-match win probability.
 * Standard chess Elo uses 400. A single tennis match is modelled as one Elo
 * contest (no multi-game Bernoulli model needed), so 400 is appropriate.
 * A 200-point Elo gap → ~76% win probability; a 400-point gap → ~91%.
 */
const ELO_DIVISOR = 400;

/** Fallback Elo for players with no stored surface rating. */
const FALLBACK_ELO = 1500;

// ─── QP constants (tie-splitting pre-applied) ─────────────────────────────────

const QP_WINNER   = 20;
const QP_FINALIST = 14;
const QP_SF_LOSER = 9;    // (10 + 8) / 2
const QP_QF_LOSER = 4;    // (5 + 5 + 3 + 3) / 4
const QP_R16_LOSER = 1.5; // (2 + 2 + 2 + 2 + 1 + 1 + 1 + 1) / 8

// ─── Seeding draw slot positions ──────────────────────────────────────────────

const SEED1_SLOT = 0;
const SEED2_SLOT = 64;
const SEEDS_3_4_SLOTS  = [32, 96]  as const;
const SEEDS_5_8_SLOTS  = [16, 48, 80, 112]  as const;
const SEEDS_9_16_SLOTS = [8, 24, 40, 56, 72, 88, 104, 120] as const;
const SEEDS_17_32_SLOTS = [4, 12, 20, 28, 36, 44, 52, 60, 68, 76, 84, 92, 100, 108, 116, 124] as const;

// ─── Surface mapping ──────────────────────────────────────────────────────────

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

const SLAM_SURFACES: Array<{ fragments: string[]; surface: CourtSurface }> = [
  { fragments: ["australian open"],       surface: "hard"  },
  { fragments: ["french open", "roland garros"], surface: "clay"  },
  { fragments: ["wimbledon"],             surface: "grass" },
  { fragments: ["us open"],               surface: "hard"  },
];

function getSurfaceForEvent(eventName: string): CourtSurface {
  const lower = eventName.toLowerCase();
  for (const { fragments, surface } of SLAM_SURFACES) {
    if (fragments.some((f) => lower.includes(f))) return surface;
  }
  // Default to hard court if the name doesn't match — admin should use standard names.
  return "hard";
}

// ─── Math helpers ─────────────────────────────────────────────────────────────

/**
 * Per-match win probability for player 1 vs player 2 based on their Elo ratings.
 * Uses the standard logistic function: p = 1 / (1 + 10^((R2 - R1) / ELO_DIVISOR))
 * Exported for unit testing.
 */
export function eloWinProb(elo1: number, elo2: number): number {
  return 1 / (1 + Math.pow(10, (elo2 - elo1) / ELO_DIVISOR));
}

/** Fisher-Yates in-place shuffle. Returns the array for chaining. */
function shuffle<T>(arr: T[]): T[] {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}

// ─── Draw builder ─────────────────────────────────────────────────────────────

/**
 * Build one seeded 128-slot draw for a major.
 *
 * Returns an array of 128 participant IDs in bracket order: pairs [0,1],
 * [2,3], ... are R1 matches. Consecutive R1 winners form R2 matchups, etc.
 * Seeds 1–32 are determined by ATP/WTA world ranking (ascending, 1 = top seed).
 * Players with no world ranking are treated as unseeded (random placement).
 * Remaining 96 unseeded players are placed randomly.
 * Caller must pass exactly 128 participant IDs.
 */
export function buildDraw(
  participantIds: string[],
  eloMap: Map<string, { worldRanking: number | null; eloHard: number | null; eloClay: number | null; eloGrass: number | null } | undefined>
): string[] {
  // Sort by world ranking ascending (lower number = better seed).
  // Players with no ranking sort to the end (unseeded).
  const sorted = [...participantIds].toSorted((a, b) => {
    const ra = eloMap.get(a)?.worldRanking ?? Infinity;
    const rb = eloMap.get(b)?.worldRanking ?? Infinity;
    return ra - rb;
  });
  const seeds    = sorted.slice(0, 32);
  const unseeded = sorted.slice(32);

  const slots: (string | null)[] = Array(128).fill(null);

  // Seed 1 and 2 in opposite halves.
  slots[SEED1_SLOT] = seeds[0];
  slots[SEED2_SLOT] = seeds[1];

  // Seeds 3–4: randomly into the two remaining quarter tops.
  const q34 = shuffle([...SEEDS_3_4_SLOTS]);
  slots[q34[0]] = seeds[2];
  slots[q34[1]] = seeds[3];

  // Seeds 5–8: randomly into the four remaining eighth tops.
  const e58 = shuffle([...SEEDS_5_8_SLOTS]);
  for (let i = 0; i < 4; i++) slots[e58[i]] = seeds[4 + i];

  // Seeds 9–16: randomly into the eight remaining sixteenth tops.
  const s916 = shuffle([...SEEDS_9_16_SLOTS]);
  for (let i = 0; i < 8; i++) slots[s916[i]] = seeds[8 + i];

  // Seeds 17–32: randomly into the 16 remaining 32nd-section tops.
  const s1732 = shuffle([...SEEDS_17_32_SLOTS]);
  for (let i = 0; i < 16; i++) slots[s1732[i]] = seeds[16 + i];

  // Fill remaining 96 slots with the unseeded players in random order.
  const openSlots = slots.reduce<number[]>((acc, v, i) => { if (v === null) acc.push(i); return acc; }, []);
  const shuffledUnseeded = shuffle([...unseeded]);
  shuffledUnseeded.forEach((player, i) => { slots[openSlots[i]] = player; });

  return slots as string[];
}

// ─── Single-major bracket simulation ──────────────────────────────────────────

interface QPResult {
  participantId: string;
  qp: number;
}

/**
 * Simulate one Grand Slam major and return QP earned per participant.
 * The draw is a pre-shuffled 128-slot array from buildDraw().
 * Exported for unit testing.
 */
export function simulateMajor(draw: string[], eloMap: Map<string, { worldRanking: number | null; eloHard: number | null; eloClay: number | null; eloGrass: number | null } | undefined>, surface: CourtSurface): QPResult[] {
  const qp = new Map<string, number>(draw.map((id) => [id, 0]));

  const getElo = (id: string): number => {
    const e = eloMap.get(id);
    if (!e) return FALLBACK_ELO;
    const v = surface === "hard" ? e.eloHard : surface === "clay" ? e.eloClay : e.eloGrass;
    return v ?? FALLBACK_ELO;
  };

  const simMatch = (p1: string, p2: string): { winner: string; loser: string } => {
    const p = eloWinProb(getElo(p1), getElo(p2));
    const winner = Math.random() < p ? p1 : p2;
    return { winner, loser: winner === p1 ? p2 : p1 };
  };

  // 7 rounds: R1(64 matches) R2(32) R3(16) R16(8) QF(4) SF(2) Final(1)
  let current = [...draw]; // 128 players

  // Rounds 1–3 award 0 QP; just advance winners.
  for (let round = 0; round < 3; round++) {
    const next: string[] = [];
    for (let i = 0; i < current.length; i += 2) {
      const { winner } = simMatch(current[i], current[i + 1]);
      next.push(winner);
    }
    current = next;
  }

  // R16: 8 matches, losers get 1.5 QP each.
  {
    const next: string[] = [];
    for (let i = 0; i < current.length; i += 2) {
      const { winner, loser } = simMatch(current[i], current[i + 1]);
      qp.set(loser, (qp.get(loser) ?? 0) + QP_R16_LOSER);
      next.push(winner);
    }
    current = next;
  }

  // QF: 4 matches, losers get 4 QP each.
  {
    const next: string[] = [];
    for (let i = 0; i < current.length; i += 2) {
      const { winner, loser } = simMatch(current[i], current[i + 1]);
      qp.set(loser, (qp.get(loser) ?? 0) + QP_QF_LOSER);
      next.push(winner);
    }
    current = next;
  }

  // SF: 2 matches, losers get 9 QP each.
  {
    const next: string[] = [];
    for (let i = 0; i < current.length; i += 2) {
      const { winner, loser } = simMatch(current[i], current[i + 1]);
      qp.set(loser, (qp.get(loser) ?? 0) + QP_SF_LOSER);
      next.push(winner);
    }
    current = next;
  }

  // Final: 1 match.
  const { winner: champion, loser: finalist } = simMatch(current[0], current[1]);
  qp.set(champion, (qp.get(champion) ?? 0) + QP_WINNER);
  qp.set(finalist, (qp.get(finalist) ?? 0) + QP_FINALIST);

  return Array.from(qp.entries()).map(([participantId, qpVal]) => ({ participantId, qp: qpVal }));
}

// ─── Simulator ────────────────────────────────────────────────────────────────

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

    // 1. Load all participants for this sports season.
    const allParticipants = await db
      .select({ id: schema.participants.id })
      .from(schema.participants)
      .where(eq(schema.participants.sportsSeasonId, sportsSeasonId));

    if (allParticipants.length < 128) {
      throw new Error(
        `Tennis simulation requires at least 128 participants (got ${allParticipants.length}). ` +
        `Ensure all 128 draw entries are added as participants before simulating.`
      );
    }

    const participantIds = allParticipants.map((p) => p.id);

    // 2. Load surface Elo ratings.
    const surfaceEloMap = await getSurfaceEloMap(sportsSeasonId);

    // 3. Load Grand Slam scoring events ordered by date.
    const events = await db.query.scoringEvents.findMany({
      where: and(
        eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
        eq(schema.scoringEvents.eventType, "major_tournament")
      ),
      orderBy: (e, { asc }) => [asc(e.eventDate)],
    });

    if (events.length === 0) {
      throw new Error(
        `No major_tournament scoring events found for sports season ${sportsSeasonId}. ` +
        `Create the 4 Grand Slam events first (e.g., "Australian Open", "French Open", ` +
        `"Wimbledon", "US Open").`
      );
    }

    // 4. For complete events, read actual QP from eventResults.
    //    Map: participantId → totalActualQP (across all completed majors).
    const completedEventIds = events
      .filter((e) => e.isComplete)
      .map((e) => e.id);

    const actualQPMap = new Map<string, number>(participantIds.map((id) => [id, 0]));

    if (completedEventIds.length > 0) {
      const actualResults = await db
        .select({
          participantId: schema.eventResults.participantId,
          qualifyingPointsAwarded: schema.eventResults.qualifyingPointsAwarded,
        })
        .from(schema.eventResults)
        .where(inArray(schema.eventResults.scoringEventId, completedEventIds));

      for (const r of actualResults) {
        if (r.qualifyingPointsAwarded !== null) {
          const prev = actualQPMap.get(r.participantId) ?? 0;
          actualQPMap.set(r.participantId, prev + parseFloat(r.qualifyingPointsAwarded));
        }
      }
    }

    // Incomplete majors that need to be simulated.
    const incompleteMajors = events.filter((e) => !e.isComplete);

    // 5. Monte Carlo loop.
    // For each player, count how many times they finish 1st–8th by QP rank.
    const counts: number[][] = Array.from({ length: participantIds.length }, () => Array.from({ length: 8 }, () => 0));
    const idToIndex = new Map<string, number>(participantIds.map((id, i) => [id, i]));

    for (let sim = 0; sim < NUM_SIMULATIONS; sim++) {
      // Start each simulation from the locked actual QP.
      const simQP = new Map<string, number>(actualQPMap);

      // Simulate each incomplete major and accumulate QP.
      for (const event of incompleteMajors) {
        const surface = getSurfaceForEvent(event.name);
        const draw = buildDraw(participantIds, surfaceEloMap);
        const results = simulateMajor(draw, surfaceEloMap, surface);
        for (const { participantId, qp } of results) {
          simQP.set(participantId, (simQP.get(participantId) ?? 0) + qp);
        }
      }

      // Rank all participants by total QP descending.
      const ranked = [...simQP.entries()].toSorted((a, b) => b[1] - a[1]);

      // Award placements 1–8.
      for (let rank = 0; rank < Math.min(8, ranked.length); rank++) {
        const [pid] = ranked[rank];
        const idx = idToIndex.get(pid);
        if (idx !== undefined) {
          counts[idx][rank]++;
        }
      }
    }

    // 6. Convert counts to probabilities.
    // Column sums are naturally 1.0: exactly one player holds each rank per simulation.
    return participantIds.map((participantId, i) => ({
      participantId,
      probabilities: {
        probFirst:   counts[i][0] / NUM_SIMULATIONS,
        probSecond:  counts[i][1] / NUM_SIMULATIONS,
        probThird:   counts[i][2] / NUM_SIMULATIONS,
        probFourth:  counts[i][3] / NUM_SIMULATIONS,
        probFifth:   counts[i][4] / NUM_SIMULATIONS,
        probSixth:   counts[i][5] / NUM_SIMULATIONS,
        probSeventh: counts[i][6] / NUM_SIMULATIONS,
        probEighth:  counts[i][7] / NUM_SIMULATIONS,
      },
      source: "tennis_grand_slam_monte_carlo",
    }));
  }
}
