/**
 * compute-elo-ratings.ts
 * ======================
 * Batch ELO rating computation for all finished football matches.
 *
 * Processes 109K+ matches in chronological order and computes:
 * - Overall ELO (general strength)
 * - Home ELO (home performance)
 * - Away ELO (away performance)
 * - Form ELO (recent 10-match weighted performance)
 *
 * Usage: npx ts-node --transpile-only -r tsconfig-paths/register src/scripts/compute-elo-ratings.ts
 */

import { PrismaClient } from "@prisma/client";

// ─────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────

interface EloState {
  overallElo: number;
  homeElo: number;
  awayElo: number;
  formElo: number;
  matchesPlayed: number;
  recentResults: string[]; // Last 10 results: W/D/L
}

interface MatchRecord {
  id: string;
  homeTeamId: string | null;
  awayTeamId: string | null;
  scoreHome: number | null;
  scoreAway: number | null;
  mstUtc: bigint;
}

// ─────────────────────────────────────────────────────────────
// ELO Algorithm
// ─────────────────────────────────────────────────────────────

const BASE_ELO = 1500.0;
const K_FACTOR = 32;
const HOME_ADVANTAGE = 50; // Home team gets +50 ELO advantage in expected score calc
const FORM_DECAY = 0.9; // Recent matches weighted more heavily for form ELO
const MAX_RECENT_RESULTS = 10;

function getExpectedScore(ratingA: number, ratingB: number): number {
  return 1 / (1 + Math.pow(10, (ratingB - ratingA) / 400));
}

function getAdaptiveK(matchesPlayed: number): number {
  // New teams get higher K for faster convergence
  if (matchesPlayed < 10) return K_FACTOR * 2;
  if (matchesPlayed < 30) return K_FACTOR * 1.5;
  return K_FACTOR;
}

function getActualScore(
  scoreHome: number,
  scoreAway: number,
  isHomeTeam: boolean,
): number {
  if (scoreHome > scoreAway) return isHomeTeam ? 1.0 : 0.0;
  if (scoreHome < scoreAway) return isHomeTeam ? 0.0 : 1.0;
  return 0.5; // Draw
}

function getResultChar(
  scoreHome: number,
  scoreAway: number,
  isHomeTeam: boolean,
): string {
  if (scoreHome > scoreAway) return isHomeTeam ? "W" : "L";
  if (scoreHome < scoreAway) return isHomeTeam ? "L" : "W";
  return "D";
}

function calculateFormElo(recentResults: string[]): number {
  if (recentResults.length === 0) return BASE_ELO;

  let formScore = 0;
  let totalWeight = 0;

  for (let i = 0; i < recentResults.length; i++) {
    const weight = Math.pow(FORM_DECAY, i); // Most recent = highest weight
    const result = recentResults[i];
    const score = result === "W" ? 3 : result === "D" ? 1 : 0;
    formScore += score * weight;
    totalWeight += 3 * weight; // Max possible per match
  }

  // Normalize to ELO-like scale (1200-1800 range)
  const normalizedForm = totalWeight > 0 ? formScore / totalWeight : 0.5;
  return 1200 + normalizedForm * 600;
}

// ─────────────────────────────────────────────────────────────
// Main Computation
// ─────────────────────────────────────────────────────────────

async function computeEloRatings(): Promise<void> {
  const prisma = new PrismaClient();
  const startTime = Date.now();

  try {
    console.log("🏟️  ELO Rating Computation — Starting...");
    console.log("─".repeat(60));

    // 1. Fetch all finished football matches in chronological order
    const matches: MatchRecord[] = await prisma.match.findMany({
      where: {
        sport: "football",
        status: "FT",
        scoreHome: { not: null },
        scoreAway: { not: null },
        homeTeamId: { not: null },
        awayTeamId: { not: null },
      },
      select: {
        id: true,
        homeTeamId: true,
        awayTeamId: true,
        scoreHome: true,
        scoreAway: true,
        mstUtc: true,
      },
      orderBy: { mstUtc: "asc" },
    });

    console.log(
      `📊 Total matches to process: ${matches.length.toLocaleString()}`,
    );

    // 2. Initialize ELO state map
    const eloMap = new Map<string, EloState>();

    function getOrCreateElo(teamId: string): EloState {
      let state = eloMap.get(teamId);
      if (!state) {
        state = {
          overallElo: BASE_ELO,
          homeElo: BASE_ELO,
          awayElo: BASE_ELO,
          formElo: BASE_ELO,
          matchesPlayed: 0,
          recentResults: [],
        };
        eloMap.set(teamId, state);
      }
      return state;
    }

    // 3. Process each match chronologically
    let processed = 0;
    const logInterval = 10000;

    for (const match of matches) {
      const homeTeamId = match.homeTeamId!;
      const awayTeamId = match.awayTeamId!;
      const scoreHome = match.scoreHome!;
      const scoreAway = match.scoreAway!;

      const homeState = getOrCreateElo(homeTeamId);
      const awayState = getOrCreateElo(awayTeamId);

      // Actual scores
      const homeActual = getActualScore(scoreHome, scoreAway, true);
      const awayActual = getActualScore(scoreHome, scoreAway, false);

      // K-factors (adaptive)
      const homeK = getAdaptiveK(homeState.matchesPlayed);
      const awayK = getAdaptiveK(awayState.matchesPlayed);

      // --- Overall ELO ---
      const expectedHome = getExpectedScore(
        homeState.overallElo + HOME_ADVANTAGE,
        awayState.overallElo,
      );
      const expectedAway = 1 - expectedHome;

      homeState.overallElo += homeK * (homeActual - expectedHome);
      awayState.overallElo += awayK * (awayActual - expectedAway);

      // --- Home/Away specific ELO ---
      const expectedHomeSpec = getExpectedScore(
        homeState.homeElo,
        awayState.awayElo,
      );
      const expectedAwaySpec = 1 - expectedHomeSpec;

      homeState.homeElo += homeK * (homeActual - expectedHomeSpec);
      awayState.awayElo += awayK * (awayActual - expectedAwaySpec);

      // --- Recent results & Form ELO ---
      const homeResult = getResultChar(scoreHome, scoreAway, true);
      const awayResult = getResultChar(scoreHome, scoreAway, false);

      homeState.recentResults.unshift(homeResult);
      awayState.recentResults.unshift(awayResult);

      if (homeState.recentResults.length > MAX_RECENT_RESULTS) {
        homeState.recentResults.pop();
      }
      if (awayState.recentResults.length > MAX_RECENT_RESULTS) {
        awayState.recentResults.pop();
      }

      homeState.formElo = calculateFormElo(homeState.recentResults);
      awayState.formElo = calculateFormElo(awayState.recentResults);

      // --- Increment match count ---
      homeState.matchesPlayed++;
      awayState.matchesPlayed++;

      processed++;
      if (processed % logInterval === 0) {
        const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
        console.log(
          `  ⏳ Processed ${processed.toLocaleString()} / ${matches.length.toLocaleString()} matches (${elapsed}s)`,
        );
      }
    }

    console.log(
      `✅ ELO computation complete — ${eloMap.size.toLocaleString()} teams rated`,
    );

    // 4. Bulk upsert to team_elo_ratings
    console.log("💾 Writing to team_elo_ratings...");

    const BATCH_SIZE = 500;
    const teams = Array.from(eloMap.entries());

    for (let i = 0; i < teams.length; i += BATCH_SIZE) {
      const batch = teams.slice(i, i + BATCH_SIZE);

      await prisma.$transaction(
        batch.map(([teamId, state]) =>
          prisma.teamEloRating.upsert({
            where: { teamId },
            update: {
              overallElo: Math.round(state.overallElo * 10) / 10,
              homeElo: Math.round(state.homeElo * 10) / 10,
              awayElo: Math.round(state.awayElo * 10) / 10,
              formElo: Math.round(state.formElo * 10) / 10,
              matchesPlayed: state.matchesPlayed,
              recentForm: state.recentResults.join(""),
            },
            create: {
              teamId,
              overallElo: Math.round(state.overallElo * 10) / 10,
              homeElo: Math.round(state.homeElo * 10) / 10,
              awayElo: Math.round(state.awayElo * 10) / 10,
              formElo: Math.round(state.formElo * 10) / 10,
              matchesPlayed: state.matchesPlayed,
              recentForm: state.recentResults.join(""),
            },
          }),
        ),
      );

      if ((i + BATCH_SIZE) % 2000 === 0 || i + BATCH_SIZE >= teams.length) {
        console.log(
          `  💾 Saved ${Math.min(i + BATCH_SIZE, teams.length).toLocaleString()} / ${teams.length.toLocaleString()} teams`,
        );
      }
    }

    // 5. Print summary stats
    const elapsedTotal = ((Date.now() - startTime) / 1000).toFixed(1);

    const eloValues = Array.from(eloMap.values());
    const overallElos = eloValues
      .map((s) => s.overallElo)
      .sort((a, b) => b - a);

    console.log("─".repeat(60));
    console.log("📊 ELO Rating Summary:");
    console.log(`   Teams rated:  ${eloMap.size.toLocaleString()}`);
    console.log(`   Matches used: ${processed.toLocaleString()}`);
    console.log(`   Highest ELO:  ${overallElos[0]?.toFixed(1) ?? "N/A"}`);
    console.log(
      `   Lowest ELO:   ${overallElos[overallElos.length - 1]?.toFixed(1) ?? "N/A"}`,
    );
    console.log(
      `   Median ELO:   ${overallElos[Math.floor(overallElos.length / 2)]?.toFixed(1) ?? "N/A"}`,
    );
    console.log(`   Duration:     ${elapsedTotal}s`);
    console.log("─".repeat(60));

    // Top 20 teams
    const topTeams = await prisma.teamEloRating.findMany({
      orderBy: { overallElo: "desc" },
      take: 20,
      include: { team: { select: { name: true } } },
    });

    console.log("\n🏆 Top 20 Teams by ELO:");
    topTeams.forEach((t, i) => {
      const form = t.recentForm.split("").join("-");
      console.log(
        `   ${String(i + 1).padStart(2)}. ${t.team.name.padEnd(25)} Overall: ${t.overallElo.toFixed(1).padStart(7)}  Home: ${t.homeElo.toFixed(1).padStart(7)}  Away: ${t.awayElo.toFixed(1).padStart(7)}  Form: ${form}`,
      );
    });

    console.log("\n✅ Done!");
  } catch (error) {
    console.error("❌ ELO computation failed:", error);
    process.exit(1);
  } finally {
    await prisma.$disconnect();
  }
}

// Run
computeEloRatings().catch(console.error);
