import 'dotenv/config';
import pool from '../db';
import {
  extractMlbDirectFeatures,
  MLB_DIRECT_MODEL_LIVE_CONFIG,
  MLB_DIRECT_MODEL_LIVE_POLICY,
  MLB_F5_MODEL_LIVE_CONFIG,
  type MlbDirectMarketContext,
  type MlbDirectPolicyConfig,
  type MlbF5ModelConfig,
  scoreMlbDirectModel,
  scoreMlbF5Model,
} from '../services/mlb-team-direct-model';
import { getTeamAbbr, normalizeTeamNameKey } from '../lib/team-abbreviations';
import type { SignalResult } from '../services/rie/types';

type BacktestRow = {
  eventId: string;
  homeTeam: string;
  awayTeam: string;
  homeShort: string;
  awayShort: string;
  startsAt: string | Date;
  rieSignals: SignalResult[];
  oddsData: any;
};

type MarketHistoryRow = {
  gameDay: string;
  homeTeam: string | null;
  awayTeam: string | null;
  marketType: string | null;
  openLine: number | null;
  currentLine: number | null;
  closingLine: number | null;
  lineMovement: number | null;
  movementDirection: string | null;
  steamMove: boolean | null;
  reverseLineMove: boolean | null;
  recordedAt: string | Date | null;
};

type BookSnapshotRow = {
  gameDay: string;
  homeTeam: string | null;
  awayTeam: string | null;
  bookmaker: string | null;
  market: string | null;
  lineValue: number | null;
  homeOdds: number | null;
  awayOdds: number | null;
  overOdds: number | null;
  underOdds: number | null;
  openingLineValue: number | null;
  openingHomeOdds: number | null;
  openingAwayOdds: number | null;
  openingOverOdds: number | null;
  openingUnderOdds: number | null;
  fetchedAt: string | Date | null;
};

type EspnF5Result = {
  homeTeam: string;
  awayTeam: string;
  homeF5Runs: number;
  awayF5Runs: number;
  fullHomeRuns: number;
  fullAwayRuns: number;
};

type Summary = {
  picks: number;
  wins: number;
  losses: number;
  pushes: number;
  wr: number;
};

function round3(value: number): number {
  return Math.round(value * 1000) / 1000;
}

function average(values: number[]): number | null {
  const usable = values.filter((value) => Number.isFinite(value));
  if (usable.length === 0) return null;
  return usable.reduce((sum, value) => sum + value, 0) / usable.length;
}

function stddev(values: number[]): number {
  const usable = values.filter((value) => Number.isFinite(value));
  if (usable.length < 2) return 0;
  const mean = usable.reduce((sum, value) => sum + value, 0) / usable.length;
  const variance = usable.reduce((sum, value) => sum + ((value - mean) ** 2), 0) / usable.length;
  return Math.sqrt(variance);
}

function range(values: number[]): number {
  const usable = values.filter((value) => Number.isFinite(value));
  if (usable.length < 2) return 0;
  return Math.max(...usable) - Math.min(...usable);
}

function americanToProbability(odds: number | null | undefined): number | null {
  if (odds == null || !Number.isFinite(odds) || odds === 0) return null;
  return odds > 0 ? 100 / (odds + 100) : (-odds) / ((-odds) + 100);
}

function timestampMs(value: string | Date | null | undefined): number | null {
  if (value instanceof Date) return value.getTime();
  if (typeof value === 'string' && value) {
    const parsed = Date.parse(value);
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
}

function toIsoDateKey(value: string | Date): string {
  if (value instanceof Date) return value.toISOString().slice(0, 10);
  return String(value || '').slice(0, 10);
}

function candidateGameDateKeys(value: string | Date): string[] {
  const rawMs = timestampMs(value);
  if (rawMs == null) {
    const key = toIsoDateKey(value);
    return key ? [key] : [];
  }
  const sameDay = new Date(rawMs);
  const priorDay = new Date(rawMs - (24 * 60 * 60 * 1000));
  return [...new Set([sameDay.toISOString().slice(0, 10), priorDay.toISOString().slice(0, 10)])];
}

function createGameDateMap<T extends { gameDay: string }>(rows: T[]): Map<string, T[]> {
  const map = new Map<string, T[]>();
  for (const row of rows) {
    const bucket = map.get(row.gameDay);
    if (bucket) bucket.push(row);
    else map.set(row.gameDay, [row]);
  }
  return map;
}

function buildExactKeys(params: { teamName: string | null | undefined; teamShort?: string | null | undefined }): string[] {
  const keys = new Set<string>();
  for (const value of [params.teamName, params.teamShort, getTeamAbbr(params.teamName || null) || null]) {
    const key = normalizeTeamNameKey(value);
    if (key.length >= 2) keys.add(key);
  }
  return [...keys];
}

function matchesExactGame(row: { homeTeam: string | null; awayTeam: string | null }, homeKeys: string[], awayKeys: string[]): boolean {
  const homeKey = normalizeTeamNameKey(row.homeTeam);
  const awayKey = normalizeTeamNameKey(row.awayTeam);
  return homeKeys.includes(homeKey) && awayKeys.includes(awayKey);
}

function parseScoreValue(value: unknown): number | null {
  if (typeof value === 'number' && Number.isFinite(value)) return value;
  if (typeof value === 'string' && value.trim()) {
    const parsed = Number(value);
    if (Number.isFinite(parsed)) return parsed;
  }
  return null;
}

function getEspnCompetitorName(competitor: any): string {
  return competitor?.team?.displayName
    || competitor?.team?.name
    || competitor?.athlete?.displayName
    || competitor?.athlete?.fullName
    || competitor?.athlete?.shortName
    || '';
}

function sumFirstFive(linescores: any[]): number | null {
  if (!Array.isArray(linescores) || linescores.length < 5) return null;
  const firstFive = linescores
    .filter((entry) => Number(entry?.period) >= 1 && Number(entry?.period) <= 5)
    .slice(0, 5)
    .map((entry) => parseScoreValue(entry?.value))
    .filter((value): value is number => value != null);
  if (firstFive.length < 5) return null;
  return firstFive.reduce((sum, value) => sum + value, 0);
}

function splitTrainTest<T>(rows: T[]): { train: T[]; validation: T[]; test: T[] } {
  const trainCut = Math.max(1, Math.floor(rows.length * 0.5));
  const validationCut = Math.max(trainCut + 1, Math.floor(rows.length * 0.7));
  return {
    train: rows.slice(0, trainCut),
    validation: rows.slice(trainCut, validationCut),
    test: rows.slice(validationCut),
  };
}

function summarize(rows: Array<'win' | 'loss' | 'push'>): Summary {
  const wins = rows.filter((row) => row === 'win').length;
  const losses = rows.filter((row) => row === 'loss').length;
  const pushes = rows.filter((row) => row === 'push').length;
  const picks = rows.length;
  const graded = wins + losses;
  return {
    picks,
    wins,
    losses,
    pushes,
    wr: graded > 0 ? wins / graded : 0,
  };
}

type F5PolicyCandidate = {
  minConfidence: number;
  f5OnlySeparation: number;
  minF5Edge: number;
  maxConflictCount: number;
};

type DirectF5Candidate = MlbF5ModelConfig & {
  minAbsEdge: number;
};

function compareSummary(left: Summary, right: Summary): number {
  if (left.wr !== right.wr) return left.wr - right.wr;
  return left.wins - right.wins;
}

function compareF5Runs(left: { summary: Summary }, right: { summary: Summary }): number {
  const cmp = compareSummary(left.summary, right.summary);
  if (cmp !== 0) return cmp;
  return left.summary.picks - right.summary.picks;
}

function* buildDirectF5Candidates(): Generator<DirectF5Candidate> {
  for (const starterQualityWeight of [0.8, 1.0, 1.2, 1.4]) {
    for (const starterLeashWeight of [0.25, 0.3, 0.45, 0.6]) {
      for (const offenseWeight of [0.2, 0.35, 0.5]) {
        for (const phaseWeight of [0.25, 0.45, 0.65]) {
          for (const contextWeight of [0, 0.08, 0.16]) {
            for (const marketWeight of [0.1, 0.2, 0.3]) {
              for (const minAbsEdge of [0.12, 0.16, 0.2, 0.24, 0.28]) {
                yield {
                  starterQualityWeight,
                  starterLeashWeight,
                  offenseWeight,
                  phaseWeight,
                  contextWeight,
                  marketWeight,
                  minAbsEdge,
                };
              }
            }
          }
        }
      }
    }
  }
}

function compareCandidateRuns(left: { summary: Summary }, right: { summary: Summary }): number {
  const cmp = compareSummary(left.summary, right.summary);
  if (cmp !== 0) return cmp;
  return left.summary.picks - right.summary.picks;
}

function runDirectF5Model(rows: Array<BacktestRow & { marketContext: MlbDirectMarketContext; featureVector: ReturnType<typeof extractMlbDirectFeatures>; f5Result: EspnF5Result | null }>, candidate: DirectF5Candidate) {
  const outcomes: Array<'win' | 'loss' | 'push'> = [];
  for (const row of rows) {
    if (!row.f5Result) continue;
    const decision = scoreMlbF5Model({
      homeTeam: row.homeTeam,
      awayTeam: row.awayTeam,
      featureVector: row.featureVector,
      config: candidate,
    });
    if (!decision.available || Math.abs(decision.rawEdge) < candidate.minAbsEdge) continue;
    outcomes.push(resultFromWinnerPick(decision.winnerPick, row.f5Result));
  }
  return { summary: summarize(outcomes) };
}

function* buildF5Candidates(): Generator<F5PolicyCandidate> {
  for (const minConfidence of [0.5, 0.52, 0.54, 0.56]) {
    for (const f5OnlySeparation of [0.01, 0.02, 0.03, 0.04, 0.05]) {
      for (const minF5Edge of [0.03, 0.05, 0.08, 0.12]) {
        for (const maxConflictCount of [1, 2, 4]) {
          yield { minConfidence, f5OnlySeparation, minF5Edge, maxConflictCount };
        }
      }
    }
  }
}

function runF5Policy(rows: Array<BacktestRow & { marketContext: MlbDirectMarketContext; featureVector: ReturnType<typeof extractMlbDirectFeatures>; f5Result: EspnF5Result | null }>, candidate: F5PolicyCandidate) {
  const outcomes: Array<'win' | 'loss' | 'push'> = [];
  for (const row of rows) {
    if (!row.f5Result) continue;
    const decision = scoreMlbDirectModel({
      homeTeam: row.homeTeam,
      awayTeam: row.awayTeam,
      featureVector: row.featureVector,
      config: MLB_DIRECT_MODEL_LIVE_CONFIG,
      policy: {
        ...MLB_DIRECT_MODEL_LIVE_POLICY,
        minConfidence: candidate.minConfidence,
        f5OnlySeparation: candidate.f5OnlySeparation,
        minF5Edge: candidate.minF5Edge,
        maxConflictCount: candidate.maxConflictCount,
      },
    });
    if (!(decision.available && decision.policy.recommendedMarket === 'f5_moneyline')) continue;
    outcomes.push(resultFromWinnerPick(decision.winnerPick, row.f5Result));
  }
  return { summary: summarize(outcomes) };
}

async function loadRows(): Promise<BacktestRow[]> {
  const { rows } = await pool.query(
    `SELECT fa.event_id,
            fc.home_team,
            fc.away_team,
            fc.starts_at,
            fc.odds_data,
            ev.home_short,
            ev.away_short,
            fc.model_signals
       FROM rm_forecast_accuracy_v2 fa
       JOIN rm_forecast_cache fc ON fc.event_id = fa.event_id
       LEFT JOIN rm_events ev ON ev.event_id = fa.event_id
      WHERE fa.model_version = 'rm_2.0'
        AND fa.forecast_type = 'spread'
        AND fa.league = 'mlb'
        AND fa.resolved_at >= NOW() - INTERVAL '120 days'
        AND fa.predicted_winner IS NOT NULL
        AND fa.actual_winner IS NOT NULL
        AND fc.model_signals ? 'rieSignals'
      ORDER BY fa.resolved_at ASC`,
  );

  return rows
    .map((row: any) => ({
      eventId: row.event_id,
      homeTeam: row.home_team,
      awayTeam: row.away_team,
      homeShort: row.home_short || '',
      awayShort: row.away_short || '',
      startsAt: row.starts_at,
      rieSignals: Array.isArray(row.model_signals?.rieSignals) ? row.model_signals.rieSignals : [],
      oddsData: row.odds_data || {},
    }))
    .filter((row) => row.homeShort && row.awayShort);
}

async function loadPrefetchedMarketData(dateKeys: string[]): Promise<{
  movementByDate: Map<string, MarketHistoryRow[]>;
  booksByDate: Map<string, BookSnapshotRow[]>;
}> {
  const [movementRes, booksRes] = await Promise.all([
    pool.query(
      `SELECT DATE("gameDate")::text AS "gameDay",
              "homeTeam" AS "homeTeam",
              "awayTeam" AS "awayTeam",
              "marketType", "openLine", "currentLine", "closingLine", "lineMovement",
              "movementDirection", "steamMove", "reverseLineMove", "recordedAt"
         FROM "LineMovement"
        WHERE league = 'mlb'
          AND DATE("gameDate") = ANY($1::date[])`,
      [dateKeys],
    ),
    pool.query(
      `SELECT DATE("gameDate")::text AS "gameDay",
              "homeTeam" AS "homeTeam",
              "awayTeam" AS "awayTeam",
              bookmaker, market, "lineValue", "homeOdds", "awayOdds", "overOdds", "underOdds",
              "openingLineValue", "openingHomeOdds", "openingAwayOdds", "openingOverOdds", "openingUnderOdds", "fetchedAt"
         FROM "GameOdds"
        WHERE league = 'mlb'
          AND DATE("gameDate") = ANY($1::date[])`,
      [dateKeys],
    ),
  ]);

  return {
    movementByDate: createGameDateMap(movementRes.rows as MarketHistoryRow[]),
    booksByDate: createGameDateMap(booksRes.rows as BookSnapshotRow[]),
  };
}

function fetchHistoricalMarketContext(
  row: BacktestRow,
  prefetched: {
    movementByDate: Map<string, MarketHistoryRow[]>;
    booksByDate: Map<string, BookSnapshotRow[]>;
  },
): MlbDirectMarketContext {
  const homeKeys = buildExactKeys({ teamName: row.homeTeam, teamShort: row.homeShort });
  const awayKeys = buildExactKeys({ teamName: row.awayTeam, teamShort: row.awayShort });
  const gameDays = candidateGameDateKeys(row.startsAt);
  const startsAtMs = timestampMs(row.startsAt) ?? Number.POSITIVE_INFINITY;
  const movementRows = gameDays
    .flatMap((gameDay) => prefetched.movementByDate.get(gameDay) || [])
    .filter((entry) => matchesExactGame(entry, homeKeys, awayKeys))
    .sort((left, right) => (timestampMs(right.recordedAt) ?? 0) - (timestampMs(left.recordedAt) ?? 0));
  const exactBookRows = gameDays
    .flatMap((gameDay) => prefetched.booksByDate.get(gameDay) || [])
    .filter((entry) => matchesExactGame(entry, homeKeys, awayKeys));
  const latestBookRows = new Map<string, BookSnapshotRow>();
  const openerFallbackRows = new Map<string, BookSnapshotRow>();
  for (const entry of exactBookRows) {
    const bucketKey = `${String(entry.bookmaker || '')}:${String(entry.market || '')}`;
    const fetchedAtMs = timestampMs(entry.fetchedAt);
    if (fetchedAtMs != null && fetchedAtMs <= startsAtMs) {
      const existing = latestBookRows.get(bucketKey);
      if (!existing || (timestampMs(existing.fetchedAt) ?? 0) < fetchedAtMs) {
        latestBookRows.set(bucketKey, entry);
      }
      continue;
    }
    const hasOpening = [entry.openingLineValue, entry.openingHomeOdds, entry.openingAwayOdds, entry.openingOverOdds, entry.openingUnderOdds]
      .some((value) => value != null && Number.isFinite(Number(value)));
    if (!hasOpening) continue;
    const existingFallback = openerFallbackRows.get(bucketKey);
    if (!existingFallback || (timestampMs(existingFallback.fetchedAt) ?? Number.POSITIVE_INFINITY) > (fetchedAtMs ?? Number.POSITIVE_INFINITY)) {
      openerFallbackRows.set(bucketKey, entry);
    }
  }
  const bookRows = [...latestBookRows.values()];
  for (const [bucketKey, fallback] of openerFallbackRows.entries()) {
    if (latestBookRows.has(bucketKey)) continue;
    bookRows.push({
      ...fallback,
      lineValue: fallback.openingLineValue ?? fallback.lineValue,
      homeOdds: fallback.openingHomeOdds ?? fallback.homeOdds,
      awayOdds: fallback.openingAwayOdds ?? fallback.awayOdds,
      overOdds: fallback.openingOverOdds ?? fallback.overOdds,
      underOdds: fallback.openingUnderOdds ?? fallback.underOdds,
      fetchedAt: null,
    });
  }
  const spreadMovement = movementRows.find((entry) => String(entry.marketType || '').toUpperCase() === 'SPREAD') || null;

  let marketMoveEdge = 0;
  let marketMoveMagnitude = 0;
  if (spreadMovement?.openLine != null && (spreadMovement.closingLine != null || spreadMovement.currentLine != null)) {
    const terminalLine = Number(spreadMovement.closingLine ?? spreadMovement.currentLine);
    const deltaTowardHome = Number(spreadMovement.openLine) - terminalLine;
    marketMoveEdge = Math.max(-0.25, Math.min(0.25, deltaTowardHome / 1.5));
    marketMoveMagnitude = Math.abs(Number(spreadMovement.lineMovement ?? deltaTowardHome));
  }

  const moneylineRows = bookRows.filter((entry) => ['moneyline', 'h2h'].includes(String(entry.market || '').toLowerCase()));
  const homeProbSamples = moneylineRows
    .map((entry) => {
      const homeProb = americanToProbability(entry.homeOdds);
      const awayProb = americanToProbability(entry.awayOdds);
      if (homeProb == null || awayProb == null) return null;
      const vig = homeProb + awayProb;
      return vig > 0 ? homeProb / vig : homeProb;
    })
    .filter((value): value is number => value != null);
  const spreadRows = bookRows.filter((entry) => ['spread', 'spreads'].includes(String(entry.market || '').toLowerCase()));
  const spreadLines = spreadRows
    .map((entry) => Number(entry.lineValue ?? null))
    .filter((value) => Number.isFinite(value));
  const totalRows = bookRows.filter((entry) => ['total', 'totals'].includes(String(entry.market || '').toLowerCase()));
  const totalLines = totalRows
    .map((entry) => Number(entry.lineValue ?? null))
    .filter((value) => Number.isFinite(value));

  return {
    marketHomeProb: average(homeProbSamples),
    marketSpreadHome: average(spreadLines),
    marketTotal: average(totalLines),
    marketMoveEdge: round3(marketMoveEdge),
    marketMoveMagnitude: round3(marketMoveMagnitude),
    reverseLineMove: movementRows.some((entry) => Boolean(entry.reverseLineMove)),
    steamMove: movementRows.some((entry) => Boolean(entry.steamMove)),
    movementRowCount: movementRows.length,
    bookHomeProbStddev: round3(stddev(homeProbSamples)),
    bookHomeProbRange: round3(range(homeProbSamples)),
    bookHomeProbSampleCount: homeProbSamples.length,
    totalLineRange: round3(range(totalLines)),
    totalLineSampleCount: totalLines.length,
  };
}

async function fetchEspnF5Results(dateKeys: string[]): Promise<Map<string, EspnF5Result[]>> {
  const out = new Map<string, EspnF5Result[]>();
  for (const dateKey of dateKeys) {
    const dateStr = dateKey.replace(/-/g, '');
    const url = `https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/scoreboard?dates=${dateStr}&limit=50`;
    const res = await fetch(url);
    if (!res.ok) continue;
    const data = await res.json() as any;
    const rows: EspnF5Result[] = [];
    for (const event of data.events || []) {
      const comp = event.competitions?.[0];
      if (!comp) continue;
      const statusName = comp.status?.type?.name || '';
      if (statusName !== 'STATUS_FINAL') continue;
      const teams = comp.competitors || [];
      const home = teams.find((t: any) => t.homeAway === 'home') || teams[0];
      const away = teams.find((t: any) => t.homeAway === 'away') || teams[1];
      if (!home || !away) continue;
      const homeF5Runs = sumFirstFive(home.linescores || []);
      const awayF5Runs = sumFirstFive(away.linescores || []);
      const fullHomeRuns = parseScoreValue(home.score);
      const fullAwayRuns = parseScoreValue(away.score);
      if (homeF5Runs == null || awayF5Runs == null || fullHomeRuns == null || fullAwayRuns == null) continue;
      rows.push({
        homeTeam: getEspnCompetitorName(home),
        awayTeam: getEspnCompetitorName(away),
        homeF5Runs,
        awayF5Runs,
        fullHomeRuns,
        fullAwayRuns,
      });
    }
    out.set(dateKey, rows);
  }
  return out;
}

function findEspnF5Result(row: BacktestRow, espnByDate: Map<string, EspnF5Result[]>): EspnF5Result | null {
  const homeKeys = buildExactKeys({ teamName: row.homeTeam, teamShort: row.homeShort });
  const awayKeys = buildExactKeys({ teamName: row.awayTeam, teamShort: row.awayShort });
  for (const gameDay of candidateGameDateKeys(row.startsAt)) {
    const matches = (espnByDate.get(gameDay) || []).filter((entry) => matchesExactGame(entry, homeKeys, awayKeys));
    if (matches.length > 0) return matches[0];
  }
  return null;
}

function resultFromWinnerPick(winnerPick: string, result: EspnF5Result): 'win' | 'loss' | 'push' {
  if (result.homeF5Runs === result.awayF5Runs) return 'push';
  const actualWinner = result.homeF5Runs > result.awayF5Runs ? result.homeTeam : result.awayTeam;
  return normalizeTeamNameKey(actualWinner) === normalizeTeamNameKey(winnerPick) ? 'win' : 'loss';
}

async function main(): Promise<void> {
  const rows = await loadRows();
  const split = splitTrainTest(rows);
  const dateKeys = [...new Set(rows.flatMap((row) => candidateGameDateKeys(row.startsAt)))].sort();
  const [prefetched, espnByDate] = await Promise.all([
    loadPrefetchedMarketData(dateKeys),
    fetchEspnF5Results(dateKeys),
  ]);

  const allRows = rows.map((row) => ({
    ...row,
    marketContext: fetchHistoricalMarketContext(row, prefetched),
    featureVector: extractMlbDirectFeatures({
      signals: row.rieSignals,
      oddsData: row.oddsData,
      marketContext: fetchHistoricalMarketContext(row, prefetched),
    }),
    f5Result: findEspnF5Result(row, espnByDate),
  }));

  const testRows = allRows.slice(split.train.length + split.validation.length);
  const livePolicy = MLB_DIRECT_MODEL_LIVE_POLICY;

  const validationRows = allRows.slice(split.train.length, split.train.length + split.validation.length);

  const f5Outcomes: Array<'win' | 'loss' | 'push'> = [];
  const f5DecisionRows: Array<{ eventId: string; confidence: number; result: 'win' | 'loss' | 'push'; winnerPick: string }> = [];

  for (const row of testRows) {
    if (!row.f5Result) continue;
    const decision = scoreMlbDirectModel({
      homeTeam: row.homeTeam,
      awayTeam: row.awayTeam,
      featureVector: row.featureVector,
      config: MLB_DIRECT_MODEL_LIVE_CONFIG,
      policy: livePolicy,
    });
    if (!(decision.available && decision.policy.recommendedMarket === 'f5_moneyline')) continue;
    const outcome = resultFromWinnerPick(decision.winnerPick, row.f5Result);
    f5Outcomes.push(outcome);
    f5DecisionRows.push({ eventId: row.eventId, confidence: decision.confidence, result: outcome, winnerPick: decision.winnerPick });
  }

  let bestCandidate: F5PolicyCandidate | null = null;
  let bestValidation = { summary: summarize([]) };
  let bestTest = { summary: summarize([]) };
  for (const candidate of buildF5Candidates()) {
    const validationRun = runF5Policy(validationRows, candidate);
    if (validationRun.summary.picks < 4) continue;
    if (!bestCandidate || compareF5Runs(validationRun, bestValidation) > 0) {
      bestCandidate = candidate;
      bestValidation = validationRun;
      bestTest = runF5Policy(testRows, candidate);
    }
  }

  let bestDirectCandidate: DirectF5Candidate | null = null;
  let bestDirectValidation = { summary: summarize([]) };
  let bestDirectTest = { summary: summarize([]) };
  for (const candidate of buildDirectF5Candidates()) {
    const validationRun = runDirectF5Model(validationRows, candidate);
    if (validationRun.summary.picks < 4) continue;
    if (!bestDirectCandidate || compareCandidateRuns(validationRun, bestDirectValidation) > 0) {
      bestDirectCandidate = candidate;
      bestDirectValidation = validationRun;
      bestDirectTest = runDirectF5Model(testRows, candidate);
    }
  }

  const summary = summarize(f5Outcomes);
  console.log('=== MLB F5 BACKTEST ===');
  console.log(`rows=${rows.length} train=${split.train.length} validation=${split.validation.length} test=${split.test.length}`);
  console.log(`espn_f5_graded=${allRows.filter((row) => row.f5Result).length}/${allRows.length}`);
  console.log(`live_policy_f5_only_test picks=${summary.picks} wins=${summary.wins} losses=${summary.losses} pushes=${summary.pushes} wr=${(summary.wr * 100).toFixed(2)}%`);
  if (bestCandidate) {
    console.log(`best_f5_policy validation picks=${bestValidation.summary.picks} wins=${bestValidation.summary.wins} losses=${bestValidation.summary.losses} pushes=${bestValidation.summary.pushes} wr=${(bestValidation.summary.wr * 100).toFixed(2)}%`);
    console.log(`best_f5_policy test picks=${bestTest.summary.picks} wins=${bestTest.summary.wins} losses=${bestTest.summary.losses} pushes=${bestTest.summary.pushes} wr=${(bestTest.summary.wr * 100).toFixed(2)}%`);
    console.log(`best_f5_policy config=${JSON.stringify(bestCandidate)}`);
  } else {
    console.log('best_f5_policy none');
  }
  if (bestDirectCandidate) {
    console.log(`best_direct_f5 validation picks=${bestDirectValidation.summary.picks} wins=${bestDirectValidation.summary.wins} losses=${bestDirectValidation.summary.losses} pushes=${bestDirectValidation.summary.pushes} wr=${(bestDirectValidation.summary.wr * 100).toFixed(2)}%`);
    console.log(`best_direct_f5 test picks=${bestDirectTest.summary.picks} wins=${bestDirectTest.summary.wins} losses=${bestDirectTest.summary.losses} pushes=${bestDirectTest.summary.pushes} wr=${(bestDirectTest.summary.wr * 100).toFixed(2)}%`);
    console.log(`best_direct_f5 config=${JSON.stringify(bestDirectCandidate)}`);
  } else {
    console.log('best_direct_f5 none');
  }
  for (const row of f5DecisionRows.slice(0, 20)) {
    console.log(`  ${row.eventId} ${row.winnerPick} ${row.result} conf=${row.confidence.toFixed(3)}`);
  }
}

main().catch(async (error) => {
  console.error(error);
  await pool.end();
  process.exit(1);
}).finally(async () => {
  await pool.end();
});
