/**
 * Golf / Qualifying Points Simulator
 *
 * Monte Carlo simulation of the 4 golf majors using a Plackett-Luce ranking model.
 *
 * Algorithm:
 *   1. Load participants and their actual QP from completed majors.
 *   2. For each incomplete major, build a simulated field of FIELD_SIZE players:
 *        - Tracked participants: strength = exp(PL_BETA × SG_Total)
 *        - Synthetic rest-of-field: strength = 1.0 (SG = 0, field average)
 *   3. Draw finishing positions using the Plackett-Luce model:
 *        P(player i placed next) ∝ strength_i / sum(remaining strengths)
 *   4. Award QP for top placements per qualifyingPointConfig.
 *   5. Rank all tracked players by total QP; tally 1st–8th placement counts.
 *   6. Return normalized SimulationResult[].
 *
 * Strength calibration (PL_BETA = 1.5, FIELD_SIZE = 156):
 *   SG +3.0 → win prob ≈ 12%  (elite major contender)
 *   SG +2.0 → win prob ≈ 5.8%
 *   SG  0.0 → win prob ≈ 0.6% (field average)
 *
 * Per-major odds (optional):
 *   If American odds are stored for this major and a player has no SG: Total,
 *   the odds are converted to an SG-equivalent skill for that major.
 *   If SG: Total is available it always takes precedence.
 *
 * Major name → odds column mapping (matched case-insensitively):
 *   "Masters"                 → mastersOdds
 *   "PGA Championship"        → pgaChampionshipOdds
 *   "US Open" / "U.S. Open"  → usOpenOdds
 *   "The Open" / "Open"       → openChampionshipOdds
 */

import { database } from "~/database/context";
import { eq, and, inArray } from "drizzle-orm";
import * as schema from "~/database/schema";
import { getGolfSkillsMap, type GolfSkillsRecord } from "~/models/golf-skills";
import { getQPConfig } from "~/models/qualifying-points";
import type { Simulator, SimulationResult } from "./types";

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

const NUM_SIMULATIONS = 10_000;

/**
 * Simulated field size for each major.
 * Major fields typically have 156 players. Tracked participants fill their slots;
 * the remainder are synthetic "rest of field" players at strength 1.0.
 */
const FIELD_SIZE = 156;

/**
 * Plackett-Luce exponential scaling factor.
 * strength_i = exp(PL_BETA × sgTotal_i)
 *
 * Calibration (typical 50-player tracked field + 106 rest-of-field at SG=0):
 *   SG +3.0 wins a single major ~12%, SG +2.0 ~6%, SG 0.0 ~0.6%
 *
 * Higher beta increases separation between skill levels, concentrating QP
 * accumulation toward the best players across all 4 majors.
 */
const PL_BETA = 1.5;

// ─── Helpers ──────────────────────────────────────────────────────────────────

/** Convert American odds to implied probability. Returns null for invalid input. */
export function americanToImplied(odds: number): number | null {
  if (odds === 0) return null;
  const p = odds > 0 ? 100 / (odds + 100) : -odds / (-odds + 100);
  return p > 0 && p <= 1 ? p : null;
}

/**
 * Determine which per-major odds column to use for a scoring event, based on its name.
 * Returns null if the name doesn't match any known major pattern.
 */
export function getMajorOddsKey(
  eventName: string
): keyof Pick<
  GolfSkillsRecord,
  "mastersOdds" | "usOpenOdds" | "openChampionshipOdds" | "pgaChampionshipOdds"
> | null {
  const n = eventName.toLowerCase();
  if (n.includes("masters")) return "mastersOdds";
  if (n.includes("pga championship") || (n.includes("pga") && !n.includes("tour"))) return "pgaChampionshipOdds";
  if (n.includes("us open") || n.includes("u.s. open")) return "usOpenOdds";
  if (n.includes("open")) return "openChampionshipOdds";
  return null;
}

/**
 * Resolve a player's effective skill score (in SG: Total units) for a specific major.
 *
 * Priority:
 *   1. sgTotal (if set — applies to all majors uniformly)
 *   2. Per-major odds converted to an SG-equivalent
 *   3. 0.0 (field average fallback)
 */
export function resolveSkill(
  skills: GolfSkillsRecord | undefined,
  oddsKey: keyof Pick<GolfSkillsRecord, "mastersOdds" | "usOpenOdds" | "openChampionshipOdds" | "pgaChampionshipOdds"> | null
): number {
  if (skills?.sgTotal !== null && skills?.sgTotal !== undefined) {
    return skills.sgTotal;
  }
  if (oddsKey && skills) {
    const rawOdds = skills[oddsKey];
    if (rawOdds !== null && rawOdds !== undefined) {
      const implied = americanToImplied(rawOdds);
      if (implied !== null) {
        // Convert implied win probability to SG-equivalent:
        //   strength = exp(PL_BETA × sg) ≈ implied × FIELD_SIZE
        //   sg = ln(implied × FIELD_SIZE) / PL_BETA
        const strength = Math.max(implied * FIELD_SIZE, 0.01);
        return Math.log(strength) / PL_BETA;
      }
    }
  }
  return 0;
}

interface FieldPlayer { id: string | null; strength: number }

/**
 * Simulate one major using the Plackett-Luce model.
 *
 * Draws finishing positions for tracked players and the synthetic rest-of-field,
 * awarding QP to tracked players who land in scoring positions (top N per qpConfig).
 *
 * @returns Map from participantId → QP awarded (0 if outside scoring positions)
 */
export function simulateMajor(
  trackedPlayers: { id: string; strength: number }[],
  restCount: number,
  restStrength: number,
  qpConfig: Map<number, number>
): Map<string, number> {
  const maxScoringPosition = Math.max(...qpConfig.keys());
  const fieldSize = trackedPlayers.length + restCount;
  const positionsToSimulate = Math.min(maxScoringPosition, fieldSize);

  // Pool of remaining players (tracked with real ids, rest-of-field with null ids)
  const remaining: FieldPlayer[] = [
    ...trackedPlayers.map((p) => ({ id: p.id, strength: p.strength })),
    ...Array.from<unknown, FieldPlayer>({ length: restCount }, () => ({ id: null, strength: restStrength })),
  ];

  let totalStrength = remaining.reduce((sum, p) => sum + p.strength, 0);
  const result = new Map<string, number>();

  for (let placement = 1; placement <= positionsToSimulate; placement++) {
    // Sample a winner proportional to strength
    let r = Math.random() * totalStrength;
    let winnerIdx = remaining.length - 1; // fallback to last in case of floating-point drift
    for (let i = 0; i < remaining.length; i++) {
      r -= remaining[i].strength;
      if (r <= 0) { winnerIdx = i; break; }
    }

    const winner = remaining[winnerIdx];
    if (winner.id !== null) {
      result.set(winner.id, qpConfig.get(placement) ?? 0);
    }

    totalStrength -= winner.strength;
    // Swap winner to end and pop — O(1) removal vs O(N) splice
    remaining[winnerIdx] = remaining[remaining.length - 1];
    remaining.pop();
  }

  // Tracked players not drawn in scoring positions get 0 QP
  for (const p of trackedPlayers) {
    if (!result.has(p.id)) result.set(p.id, 0);
  }

  return result;
}

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

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

    // Load participants, skills, QP config, and scoring events in parallel.
    const [allParticipants, skillsMap, qpConfigRows, events] = await Promise.all([
      db
        .select({ id: schema.participants.id })
        .from(schema.participants)
        .where(eq(schema.participants.sportsSeasonId, sportsSeasonId)),
      getGolfSkillsMap(sportsSeasonId),
      getQPConfig(sportsSeasonId),
      db.query.scoringEvents.findMany({
        where: and(
          eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
          eq(schema.scoringEvents.eventType, "major_tournament")
        ),
        orderBy: (e, { asc }) => [asc(e.eventDate)],
      }),
    ]);

    if (allParticipants.length === 0) {
      throw new Error(
        `No participants found for sports season ${sportsSeasonId}. ` +
        `Add participants before running the simulation.`
      );
    }

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

    const qpConfig = new Map<number, number>(
      qpConfigRows.map((row) => [row.placement, Number(row.points)])
    );

    if (events.length === 0) {
      throw new Error(
        `No major_tournament scoring events found for sports season ${sportsSeasonId}. ` +
        `Create the 4 major scoring events first (e.g. "Masters", "US Open", "The Open", "PGA Championship").`
      );
    }

    // For completed majors, read actual QP from eventResults.
    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));
        }
      }
    }

    const incompleteMajors = events.filter((e) => !e.isComplete);

    // Pre-compute per-player strengths per incomplete major (outside the Monte Carlo loop).
    // strength = exp(PL_BETA × effectiveSkill); minimum clamped to 0.01 to avoid division issues.
    const majorConfigs = incompleteMajors.map((event) => {
      const oddsKey = getMajorOddsKey(event.name);
      const players = participantIds.map((id) => ({
        id,
        strength: Math.max(Math.exp(PL_BETA * resolveSkill(skillsMap.get(id), oddsKey)), 0.01),
      }));
      const restCount = Math.max(0, FIELD_SIZE - players.length);
      const restStrength = 1.0; // exp(PL_BETA * 0) = 1, representing SG = 0
      return { players, restCount, restStrength };
    });

    // Monte Carlo loop.
    const counts: number[][] = Array.from({ length: participantIds.length }, () =>
      Array<number>(8).fill(0)
    );
    const idToIndex = new Map<string, number>(participantIds.map((id, i) => [id, i]));

    for (let sim = 0; sim < NUM_SIMULATIONS; sim++) {
      const simQP = new Map<string, number>(actualQPMap);

      for (const { players, restCount, restStrength } of majorConfigs) {
        const majorResult = simulateMajor(players, restCount, restStrength, qpConfig);
        for (const [pid, qp] of majorResult) {
          simQP.set(pid, (simQP.get(pid) ?? 0) + qp);
        }
      }

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

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

    // Normalize counts to probabilities.
    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: "golf_qualifying_points_monte_carlo",
    }));
  }
}
