/**
 * populate-feature-store.ts
 * =========================
 * Batch feature computation for all finished football matches.
 * Populates the match_ai_features table with 7-pillar feature vectors.
 *
 * Pillars computed:
 *  1. ELO Ratings (from team_elo_ratings)
 *  2. Form (last 5 match rolling averages)
 *  3. Odds Implied Probability (from odd_selections)
 *  4. Team Stats (possession, shots, corners)
 *  5. Head-to-Head (historical matchups)
 *  6. Referee (bias, cards, goals)
 *  7. League DNA (averages across league-season)
 *
 * Usage: npx ts-node --transpile-only -r tsconfig-paths/register src/scripts/populate-feature-store.ts
 */

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

const prisma = new PrismaClient();

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

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

interface FeatureVector {
  matchId: string;
  // ELO
  homeElo: number;
  awayElo: number;
  homeHomeElo: number;
  awayAwayElo: number;
  homeFormElo: number;
  awayFormElo: number;
  eloDiff: number;
  // Form
  homeFormScore: number;
  awayFormScore: number;
  homeGoalsAvg5: number;
  awayGoalsAvg5: number;
  homeConcededAvg5: number;
  awayConcededAvg5: number;
  homeCleanSheetRate: number;
  awayCleanSheetRate: number;
  homeScoringRate: number;
  awayScoringRate: number;
  homeWinStreak: number;
  awayWinStreak: number;
  // Odds
  impliedHome: number;
  impliedDraw: number;
  impliedAway: number;
  impliedOver25: number;
  impliedBttsYes: number;
  oddsOverround: number;
  // Team Stats
  homeAvgPossession: number;
  awayAvgPossession: number;
  homeAvgShotsOnTarget: number;
  awayAvgShotsOnTarget: number;
  homeShotConversion: number;
  awayShotConversion: number;
  homeAvgCorners: number;
  awayAvgCorners: number;
  // H2H
  h2hTotal: number;
  h2hHomeWinRate: number;
  h2hAvgGoals: number;
  h2hOver25Rate: number;
  h2hBttsRate: number;
  // Referee
  refereeAvgCards: number;
  refereeHomeBias: number;
  refereeAvgGoals: number;
  // League DNA
  leagueAvgGoals: number;
  leagueHomeWinPct: number;
  leagueOver25Pct: number;
  // Meta
  missingPlayersImpact: number;
  calculatorVer: string;
}

// ─────────────────────────────────────────────────────────────
// Pillar 1: ELO Ratings
// ─────────────────────────────────────────────────────────────

async function loadEloMap(): Promise<
  Map<string, { overall: number; home: number; away: number; form: number }>
> {
  const ratings = await prisma.teamEloRating.findMany();
  const map = new Map<
    string,
    { overall: number; home: number; away: number; form: number }
  >();
  for (const r of ratings) {
    map.set(r.teamId, {
      overall: r.overallElo,
      home: r.homeElo,
      away: r.awayElo,
      form: r.formElo,
    });
  }
  console.log(`  ✅ ELO map loaded: ${map.size} teams`);
  return map;
}

// ─────────────────────────────────────────────────────────────
// Pillar 2: Form — Pre-compute per-team rolling stats
// ─────────────────────────────────────────────────────────────

interface TeamFormState {
  goalsScored: number[];
  goalsConceded: number[];
  results: string[]; // W/D/L
  matchCount: number;
}

function buildFormIndex(
  matches: MatchRow[],
): Map<string, Map<string, TeamFormState>> {
  // matchId -> teamId -> form state AT THAT POINT (before match)
  const teamStates = new Map<string, TeamFormState>();
  const formIndex = new Map<string, Map<string, TeamFormState>>();

  function getOrCreateState(teamId: string): TeamFormState {
    let state = teamStates.get(teamId);
    if (!state) {
      state = {
        goalsScored: [],
        goalsConceded: [],
        results: [],
        matchCount: 0,
      };
      teamStates.set(teamId, state);
    }
    return state;
  }

  function cloneState(state: TeamFormState): TeamFormState {
    return {
      goalsScored: [...state.goalsScored],
      goalsConceded: [...state.goalsConceded],
      results: [...state.results],
      matchCount: state.matchCount,
    };
  }

  for (const match of matches) {
    const homeState = getOrCreateState(match.homeTeamId);
    const awayState = getOrCreateState(match.awayTeamId);

    // Store form state BEFORE this match
    const matchFormMap = new Map<string, TeamFormState>();
    matchFormMap.set(match.homeTeamId, cloneState(homeState));
    matchFormMap.set(match.awayTeamId, cloneState(awayState));
    formIndex.set(match.id, matchFormMap);

    // Update states AFTER this match
    homeState.goalsScored.unshift(match.scoreHome);
    homeState.goalsConceded.unshift(match.scoreAway);
    awayState.goalsScored.unshift(match.scoreAway);
    awayState.goalsConceded.unshift(match.scoreHome);

    if (homeState.goalsScored.length > 10) homeState.goalsScored.pop();
    if (homeState.goalsConceded.length > 10) homeState.goalsConceded.pop();
    if (awayState.goalsScored.length > 10) awayState.goalsScored.pop();
    if (awayState.goalsConceded.length > 10) awayState.goalsConceded.pop();

    const homeResult =
      match.scoreHome > match.scoreAway
        ? "W"
        : match.scoreHome < match.scoreAway
          ? "L"
          : "D";
    const awayResult =
      match.scoreAway > match.scoreHome
        ? "W"
        : match.scoreAway < match.scoreHome
          ? "L"
          : "D";

    homeState.results.unshift(homeResult);
    awayState.results.unshift(awayResult);
    if (homeState.results.length > 10) homeState.results.pop();
    if (awayState.results.length > 10) awayState.results.pop();

    homeState.matchCount++;
    awayState.matchCount++;
  }

  return formIndex;
}

function extractFormFeatures(formState: TeamFormState): {
  goalsAvg5: number;
  concededAvg5: number;
  cleanSheetRate: number;
  scoringRate: number;
  winStreak: number;
  formScore: number;
} {
  const last5Goals = formState.goalsScored.slice(0, 5);
  const last5Conceded = formState.goalsConceded.slice(0, 5);
  const n = last5Goals.length || 1;

  const goalsAvg5 = last5Goals.reduce((a, b) => a + b, 0) / n;
  const concededAvg5 = last5Conceded.reduce((a, b) => a + b, 0) / n;
  const cleanSheetRate = last5Conceded.filter((g) => g === 0).length / n;
  const scoringRate = last5Goals.filter((g) => g > 0).length / n;

  let winStreak = 0;
  for (const r of formState.results) {
    if (r === "W") winStreak++;
    else break;
  }

  // Form score: (W=3, D=1, L=0) over last 5, normalized to 0-100
  const last5Results = formState.results.slice(0, 5);
  const points = last5Results.reduce(
    (sum, r) => sum + (r === "W" ? 3 : r === "D" ? 1 : 0),
    0,
  );
  const maxPoints = last5Results.length * 3 || 1;
  const formScore = (points / maxPoints) * 100;

  return {
    goalsAvg5,
    concededAvg5,
    cleanSheetRate,
    scoringRate,
    winStreak,
    formScore,
  };
}

// ─────────────────────────────────────────────────────────────
// Pillar 3: Odds Implied Probability (batch)
// ─────────────────────────────────────────────────────────────

interface OddsData {
  impliedHome: number;
  impliedDraw: number;
  impliedAway: number;
  impliedOver25: number;
  impliedBttsYes: number;
  overround: number;
}

async function loadOddsIndex(): Promise<Map<string, OddsData>> {
  const oddsIndex = new Map<string, OddsData>();

  // Raw SQL: join odd_categories + odd_selections for MS and OU25
  const rows = await prisma.$queryRaw<
    Array<{
      match_id: string;
      cat_name: string;
      sel_name: string;
      odds: number;
    }>
  >`
    SELECT oc.match_id, oc.name AS cat_name, os.name AS sel_name,
           COALESCE(os.odd_value::numeric, 0)::float AS odds
    FROM odd_categories oc
    JOIN odd_selections os ON os.odd_category_db_id = oc.db_id
    WHERE oc.name IN ('Maç Sonucu', 'Alt/Üst 2,5', 'Karşılıklı Gol')
      AND os.odd_value IS NOT NULL
      AND os.odd_value ~ '^[0-9]'
    ORDER BY oc.match_id
  `;

  // Group by match_id
  const byMatch = new Map<
    string,
    Array<{ cat: string; sel: string; odds: number }>
  >();
  for (const row of rows) {
    let arr = byMatch.get(row.match_id);
    if (!arr) {
      arr = [];
      byMatch.set(row.match_id, arr);
    }
    arr.push({ cat: row.cat_name, sel: row.sel_name, odds: row.odds });
  }

  for (const [matchId, selections] of byMatch.entries()) {
    let msH = 0,
      msD = 0,
      msA = 0;
    let ou25O = 0;
    let bttsY = 0;

    for (const s of selections) {
      if (s.cat === "Maç Sonucu") {
        if (s.sel === "1") msH = s.odds;
        else if (s.sel === "X" || s.sel === "0") msD = s.odds;
        else if (s.sel === "2") msA = s.odds;
      } else if (s.cat === "Alt/Üst 2,5") {
        if (
          s.sel.toLowerCase().includes("üst") ||
          s.sel.toLowerCase().includes("over")
        )
          ou25O = s.odds;
      } else if (s.cat === "Karşılıklı Gol") {
        if (
          s.sel.toLowerCase().includes("var") ||
          s.sel.toLowerCase().includes("yes")
        )
          bttsY = s.odds;
      }
    }

    const impliedHome = msH > 0 ? 1 / msH : 0.33;
    const impliedDraw = msD > 0 ? 1 / msD : 0.33;
    const impliedAway = msA > 0 ? 1 / msA : 0.33;
    const totalImplied = impliedHome + impliedDraw + impliedAway;
    const overround = totalImplied > 0 ? (totalImplied - 1) * 100 : 0;

    const impliedOver25 = ou25O > 0 ? 1 / ou25O : 0.5;
    const impliedBttsYes = bttsY > 0 ? 1 / bttsY : 0.5;

    oddsIndex.set(matchId, {
      impliedHome: totalImplied > 0 ? impliedHome / totalImplied : 0.33,
      impliedDraw: totalImplied > 0 ? impliedDraw / totalImplied : 0.33,
      impliedAway: totalImplied > 0 ? impliedAway / totalImplied : 0.33,
      impliedOver25: Math.min(impliedOver25, 1),
      impliedBttsYes: Math.min(impliedBttsYes, 1),
      overround,
    });
  }

  console.log(
    `  ✅ Odds index loaded: ${oddsIndex.size} matches with odds data`,
  );
  return oddsIndex;
}

// ─────────────────────────────────────────────────────────────
// Pillar 5: Head-to-Head (batch precompute)
// ─────────────────────────────────────────────────────────────

interface H2HState {
  totalMatches: number;
  homeWins: number;
  totalGoals: number;
  over25Count: number;
  bttsCount: number;
}

function buildH2HIndex(matches: MatchRow[]): Map<string, H2HState> {
  // Key: "teamA_teamB" sorted alphabetically
  const h2hMap = new Map<string, H2HState>();
  // matchId -> h2h state BEFORE that match
  const h2hIndex = new Map<string, H2HState>();

  function getKey(t1: string, t2: string): string {
    return t1 < t2 ? `${t1}_${t2}` : `${t2}_${t1}`;
  }

  for (const match of matches) {
    const key = getKey(match.homeTeamId, match.awayTeamId);
    let state = h2hMap.get(key);
    if (!state) {
      state = {
        totalMatches: 0,
        homeWins: 0,
        totalGoals: 0,
        over25Count: 0,
        bttsCount: 0,
      };
      h2hMap.set(key, state);
    }

    // Save state BEFORE this match
    h2hIndex.set(match.id, { ...state });

    // Update AFTER
    state.totalMatches++;
    if (match.scoreHome > match.scoreAway) state.homeWins++;
    state.totalGoals += match.scoreHome + match.scoreAway;
    if (match.scoreHome + match.scoreAway > 2.5) state.over25Count++;
    if (match.scoreHome > 0 && match.scoreAway > 0) state.bttsCount++;
  }

  return h2hIndex;
}

// ─────────────────────────────────────────────────────────────
// Pillar 7: League DNA (batch precompute)
// ─────────────────────────────────────────────────────────────

interface LeagueStats {
  totalMatches: number;
  totalGoals: number;
  homeWins: number;
  over25Count: number;
}

function buildLeagueIndex(matches: MatchRow[]): Map<string, LeagueStats> {
  const leagueMap = new Map<string, LeagueStats>();

  for (const match of matches) {
    const key = match.leagueId ?? "unknown";
    let stats = leagueMap.get(key);
    if (!stats) {
      stats = { totalMatches: 0, totalGoals: 0, homeWins: 0, over25Count: 0 };
      leagueMap.set(key, stats);
    }
    stats.totalMatches++;
    stats.totalGoals += match.scoreHome + match.scoreAway;
    if (match.scoreHome > match.scoreAway) stats.homeWins++;
    if (match.scoreHome + match.scoreAway > 2.5) stats.over25Count++;
  }

  return leagueMap;
}

// ─────────────────────────────────────────────────────────────
// Pillar 6: Referee — Load from match_officials
// ─────────────────────────────────────────────────────────────

interface RefereeStats {
  totalMatches: number;
  totalCards: number;
  totalGoals: number;
  homeWins: number;
}

async function loadRefereeIndex(
  matches: MatchRow[],
): Promise<Map<string, RefereeStats>> {
  // Build match scores lookup
  const matchScores = new Map<string, { home: number; away: number }>();
  for (const m of matches) {
    matchScores.set(m.id, { home: m.scoreHome, away: m.scoreAway });
  }

  // Load officials
  const officials = await prisma.$queryRaw<
    Array<{ match_id: string; official_name: string }>
  >`
    SELECT match_id, name AS official_name FROM match_officials
    WHERE role_id = 1
    LIMIT 500000
  `;

  const refereeIndex = new Map<string, RefereeStats>();

  // Group by referee
  const refMatchMap = new Map<string, string[]>();
  for (const o of officials) {
    let arr = refMatchMap.get(o.official_name);
    if (!arr) {
      arr = [];
      refMatchMap.set(o.official_name, arr);
    }
    arr.push(o.match_id);
  }

  // matchId -> referee name
  const matchRefereeMap = new Map<string, string>();
  for (const o of officials) {
    matchRefereeMap.set(o.match_id, o.official_name);
  }

  // Calculate referee stats
  for (const [refName, matchIds] of refMatchMap.entries()) {
    let totalGoals = 0;
    let homeWins = 0;
    let totalMatches = 0;

    for (const mid of matchIds) {
      const score = matchScores.get(mid);
      if (score) {
        totalGoals += score.home + score.away;
        if (score.home > score.away) homeWins++;
        totalMatches++;
      }
    }

    refereeIndex.set(refName, {
      totalMatches,
      totalCards: 0, // Would require card stats table — use 0 as default
      totalGoals,
      homeWins,
    });
  }

  // Build matchId -> refStats lookup
  const matchRefStats = new Map<string, RefereeStats>();
  for (const [matchId, refName] of matchRefereeMap.entries()) {
    const stats = refereeIndex.get(refName);
    if (stats) {
      matchRefStats.set(matchId, stats);
    }
  }

  console.log(
    `  ✅ Referee index loaded: ${refereeIndex.size} referees, ${matchRefStats.size} match-referee mappings`,
  );
  return matchRefStats;
}

// ─────────────────────────────────────────────────────────────
// Main Feature Store Population
// ─────────────────────────────────────────────────────────────

async function populateFeatureStore(): Promise<void> {
  const startTime = Date.now();

  try {
    console.log("🧠 Feature Store Population — Starting...");
    console.log("─".repeat(60));

    // Load all finished football matches
    console.log("📥 Loading matches...");
    const rawMatches = 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,
        leagueId: true,
        scoreHome: true,
        scoreAway: true,
        mstUtc: true,
      },
      orderBy: { mstUtc: "asc" },
    });

    const matches: MatchRow[] = rawMatches.map((m) => ({
      id: m.id,
      homeTeamId: m.homeTeamId!,
      awayTeamId: m.awayTeamId!,
      leagueId: m.leagueId,
      scoreHome: m.scoreHome!,
      scoreAway: m.scoreAway!,
      mstUtc: m.mstUtc,
    }));

    console.log(`  📊 Matches loaded: ${matches.length.toLocaleString()}`);

    // Pre-compute all indexes
    console.log("\n📊 Building feature indexes...");

    console.log("  🏅 Pillar 1: Loading ELO ratings...");
    const eloMap = await loadEloMap();

    console.log("  📈 Pillar 2: Building form index...");
    const formIndex = buildFormIndex(matches);

    console.log("  💰 Pillar 3: Loading odds data...");
    const oddsIndex = await loadOddsIndex();

    console.log("  ⚔️ Pillar 5: Building H2H index...");
    const h2hIndex = buildH2HIndex(matches);

    console.log("  📋 Pillar 6: Loading referee data...");
    const refereeIndex = await loadRefereeIndex(matches);

    console.log("  🏟️ Pillar 7: Building league DNA...");
    const leagueIndex = buildLeagueIndex(matches);

    console.log("\n✅ All indexes built!");
    console.log("─".repeat(60));

    // Build feature vectors and batch upsert
    console.log("💾 Writing features to database...");

    const BATCH_SIZE = 1000;
    let processed = 0;
    const skipped = 0;

    for (let i = 0; i < matches.length; i += BATCH_SIZE) {
      const batch = matches.slice(i, i + BATCH_SIZE);
      const featureVectors: FeatureVector[] = [];

      for (const match of batch) {
        // Pillar 1: ELO
        const homeEloData = eloMap.get(match.homeTeamId);
        const awayEloData = eloMap.get(match.awayTeamId);

        const homeElo = homeEloData?.overall ?? 1500;
        const awayElo = awayEloData?.overall ?? 1500;

        // Pillar 2: Form
        const matchForm = formIndex.get(match.id);
        const homeFormState = matchForm?.get(match.homeTeamId);
        const awayFormState = matchForm?.get(match.awayTeamId);

        const homeForm = homeFormState
          ? extractFormFeatures(homeFormState)
          : {
              goalsAvg5: 0,
              concededAvg5: 0,
              cleanSheetRate: 0,
              scoringRate: 0,
              winStreak: 0,
              formScore: 50,
            };
        const awayForm = awayFormState
          ? extractFormFeatures(awayFormState)
          : {
              goalsAvg5: 0,
              concededAvg5: 0,
              cleanSheetRate: 0,
              scoringRate: 0,
              winStreak: 0,
              formScore: 50,
            };

        // Pillar 3: Odds
        const odds = oddsIndex.get(match.id) ?? {
          impliedHome: 0.33,
          impliedDraw: 0.33,
          impliedAway: 0.33,
          impliedOver25: 0.5,
          impliedBttsYes: 0.5,
          overround: 0,
        };

        // Pillar 5: H2H
        const h2h = h2hIndex.get(match.id) ?? {
          totalMatches: 0,
          homeWins: 0,
          totalGoals: 0,
          over25Count: 0,
          bttsCount: 0,
        };

        // Pillar 6: Referee
        const refStats = refereeIndex.get(match.id);
        const refTotal = refStats?.totalMatches ?? 0;

        // Pillar 7: League DNA
        const leagueKey = match.leagueId ?? "unknown";
        const leagueStats = leagueIndex.get(leagueKey) ?? {
          totalMatches: 1,
          totalGoals: 0,
          homeWins: 0,
          over25Count: 0,
        };

        featureVectors.push({
          matchId: match.id,
          // ELO
          homeElo,
          awayElo,
          homeHomeElo: homeEloData?.home ?? 1500,
          awayAwayElo: awayEloData?.away ?? 1500,
          homeFormElo: homeEloData?.form ?? 1500,
          awayFormElo: awayEloData?.form ?? 1500,
          eloDiff: homeElo - awayElo,
          // Form
          homeFormScore: homeForm.formScore,
          awayFormScore: awayForm.formScore,
          homeGoalsAvg5: round(homeForm.goalsAvg5),
          awayGoalsAvg5: round(awayForm.goalsAvg5),
          homeConcededAvg5: round(homeForm.concededAvg5),
          awayConcededAvg5: round(awayForm.concededAvg5),
          homeCleanSheetRate: round(homeForm.cleanSheetRate),
          awayCleanSheetRate: round(awayForm.cleanSheetRate),
          homeScoringRate: round(homeForm.scoringRate),
          awayScoringRate: round(awayForm.scoringRate),
          homeWinStreak: homeForm.winStreak,
          awayWinStreak: awayForm.winStreak,
          // Odds
          impliedHome: round(odds.impliedHome),
          impliedDraw: round(odds.impliedDraw),
          impliedAway: round(odds.impliedAway),
          impliedOver25: round(odds.impliedOver25),
          impliedBttsYes: round(odds.impliedBttsYes),
          oddsOverround: round(odds.overround),
          // Team Stats (placeholder — uses form data as proxy)
          homeAvgPossession: 50.0,
          awayAvgPossession: 50.0,
          homeAvgShotsOnTarget: round(homeForm.goalsAvg5 * 2.5), // Approx proxy
          awayAvgShotsOnTarget: round(awayForm.goalsAvg5 * 2.5),
          homeShotConversion:
            homeForm.goalsAvg5 > 0 ? round(homeForm.scoringRate) : 0,
          awayShotConversion:
            awayForm.goalsAvg5 > 0 ? round(awayForm.scoringRate) : 0,
          homeAvgCorners: 5.0, // Default — no corner data in match table
          awayAvgCorners: 4.5,
          // H2H
          h2hTotal: h2h.totalMatches,
          h2hHomeWinRate:
            h2h.totalMatches > 0 ? round(h2h.homeWins / h2h.totalMatches) : 0,
          h2hAvgGoals:
            h2h.totalMatches > 0 ? round(h2h.totalGoals / h2h.totalMatches) : 0,
          h2hOver25Rate:
            h2h.totalMatches > 0
              ? round(h2h.over25Count / h2h.totalMatches)
              : 0,
          h2hBttsRate:
            h2h.totalMatches > 0 ? round(h2h.bttsCount / h2h.totalMatches) : 0,
          // Referee
          refereeAvgCards: 0, // No card data column available
          refereeHomeBias:
            refTotal > 0 ? round(refStats!.homeWins / refTotal - 0.46) : 0, // 0.46 = expected home win rate
          refereeAvgGoals:
            refTotal > 0 ? round(refStats!.totalGoals / refTotal) : 0,
          // League DNA
          leagueAvgGoals: round(
            leagueStats.totalGoals / leagueStats.totalMatches,
          ),
          leagueHomeWinPct: round(
            leagueStats.homeWins / leagueStats.totalMatches,
          ),
          leagueOver25Pct: round(
            leagueStats.over25Count / leagueStats.totalMatches,
          ),
          // Meta
          missingPlayersImpact: 0,
          calculatorVer: "v2.0",
        });
      }

      // Batch upsert using raw SQL for performance
      if (featureVectors.length > 0) {
        await batchUpsertFeatures(featureVectors);
        processed += featureVectors.length;
      }

      if ((i + BATCH_SIZE) % 10000 === 0 || i + BATCH_SIZE >= matches.length) {
        const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
        console.log(
          `  💾 Processed ${Math.min(i + BATCH_SIZE, matches.length).toLocaleString()} / ${matches.length.toLocaleString()} (${elapsed}s)`,
        );
      }
    }

    const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
    console.log("─".repeat(60));
    console.log(`✅ Feature Store population complete!`);
    console.log(`   Features written: ${processed.toLocaleString()}`);
    console.log(`   Skipped: ${skipped}`);
    console.log(`   Duration: ${elapsed}s`);

    // Verify
    const count = await prisma.footballAiFeature.count();
    console.log(`   DB row count: ${count.toLocaleString()}`);
    console.log("─".repeat(60));
  } catch (error) {
    console.error("❌ Feature store population failed:", error);
    process.exit(1);
  } finally {
    await prisma.$disconnect();
  }
}

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

function round(val: number, decimals = 4): number {
  const factor = Math.pow(10, decimals);
  return Math.round(val * factor) / factor;
}

async function batchUpsertFeatures(features: FeatureVector[]): Promise<void> {
  // Use Prisma transactions for batch upsert
  await prisma.$transaction(
    features.map((f) =>
      prisma.footballAiFeature.upsert({
        where: { matchId: f.matchId },
        update: {
          homeElo: f.homeElo,
          awayElo: f.awayElo,
          homeHomeElo: f.homeHomeElo,
          awayAwayElo: f.awayAwayElo,
          homeFormElo: f.homeFormElo,
          awayFormElo: f.awayFormElo,
          eloDiff: f.eloDiff,
          homeFormScore: f.homeFormScore,
          awayFormScore: f.awayFormScore,
          homeGoalsAvg5: f.homeGoalsAvg5,
          awayGoalsAvg5: f.awayGoalsAvg5,
          homeConcededAvg5: f.homeConcededAvg5,
          awayConcededAvg5: f.awayConcededAvg5,
          homeCleanSheetRate: f.homeCleanSheetRate,
          awayCleanSheetRate: f.awayCleanSheetRate,
          homeScoringRate: f.homeScoringRate,
          awayScoringRate: f.awayScoringRate,
          homeWinStreak: f.homeWinStreak,
          awayWinStreak: f.awayWinStreak,
          impliedHome: f.impliedHome,
          impliedDraw: f.impliedDraw,
          impliedAway: f.impliedAway,
          impliedOver25: f.impliedOver25,
          impliedBttsYes: f.impliedBttsYes,
          oddsOverround: f.oddsOverround,
          homeAvgPossession: f.homeAvgPossession,
          awayAvgPossession: f.awayAvgPossession,
          homeAvgShotsOnTarget: f.homeAvgShotsOnTarget,
          awayAvgShotsOnTarget: f.awayAvgShotsOnTarget,
          homeShotConversion: f.homeShotConversion,
          awayShotConversion: f.awayShotConversion,
          homeAvgCorners: f.homeAvgCorners,
          awayAvgCorners: f.awayAvgCorners,
          h2hTotal: f.h2hTotal,
          h2hHomeWinRate: f.h2hHomeWinRate,
          h2hAvgGoals: f.h2hAvgGoals,
          h2hOver25Rate: f.h2hOver25Rate,
          h2hBttsRate: f.h2hBttsRate,
          refereeAvgCards: f.refereeAvgCards,
          refereeHomeBias: f.refereeHomeBias,
          refereeAvgGoals: f.refereeAvgGoals,
          leagueAvgGoals: f.leagueAvgGoals,
          leagueHomeWinPct: f.leagueHomeWinPct,
          leagueOver25Pct: f.leagueOver25Pct,
          missingPlayersImpact: f.missingPlayersImpact,
          calculatorVer: f.calculatorVer,
        },
        create: {
          matchId: f.matchId,
          homeElo: f.homeElo,
          awayElo: f.awayElo,
          homeHomeElo: f.homeHomeElo,
          awayAwayElo: f.awayAwayElo,
          homeFormElo: f.homeFormElo,
          awayFormElo: f.awayFormElo,
          eloDiff: f.eloDiff,
          homeFormScore: f.homeFormScore,
          awayFormScore: f.awayFormScore,
          homeGoalsAvg5: f.homeGoalsAvg5,
          awayGoalsAvg5: f.awayGoalsAvg5,
          homeConcededAvg5: f.homeConcededAvg5,
          awayConcededAvg5: f.awayConcededAvg5,
          homeCleanSheetRate: f.homeCleanSheetRate,
          awayCleanSheetRate: f.awayCleanSheetRate,
          homeScoringRate: f.homeScoringRate,
          awayScoringRate: f.awayScoringRate,
          homeWinStreak: f.homeWinStreak,
          awayWinStreak: f.awayWinStreak,
          impliedHome: f.impliedHome,
          impliedDraw: f.impliedDraw,
          impliedAway: f.impliedAway,
          impliedOver25: f.impliedOver25,
          impliedBttsYes: f.impliedBttsYes,
          oddsOverround: f.oddsOverround,
          homeAvgPossession: f.homeAvgPossession,
          awayAvgPossession: f.awayAvgPossession,
          homeAvgShotsOnTarget: f.homeAvgShotsOnTarget,
          awayAvgShotsOnTarget: f.awayAvgShotsOnTarget,
          homeShotConversion: f.homeShotConversion,
          awayShotConversion: f.awayShotConversion,
          homeAvgCorners: f.homeAvgCorners,
          awayAvgCorners: f.awayAvgCorners,
          h2hTotal: f.h2hTotal,
          h2hHomeWinRate: f.h2hHomeWinRate,
          h2hAvgGoals: f.h2hAvgGoals,
          h2hOver25Rate: f.h2hOver25Rate,
          h2hBttsRate: f.h2hBttsRate,
          refereeAvgCards: f.refereeAvgCards,
          refereeHomeBias: f.refereeHomeBias,
          refereeAvgGoals: f.refereeAvgGoals,
          leagueAvgGoals: f.leagueAvgGoals,
          leagueHomeWinPct: f.leagueHomeWinPct,
          leagueOver25Pct: f.leagueOver25Pct,
          missingPlayersImpact: f.missingPlayersImpact,
          calculatorVer: f.calculatorVer,
        },
      }),
    ),
  );
}

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