import 'dotenv/config';
import pool from '../db';
import {
  extractMlbDirectFeatures,
  MLB_DIRECT_MODEL_LIVE_CONFIG,
  MLB_DIRECT_MODEL_LIVE_POLICY,
  MLB_DIRECT_MODEL_DEFAULTS,
  scoreMlbDirectModel,
  type MlbDirectMarketContext,
  type MlbDirectModelConfig,
  type MlbDirectPolicyConfig,
} from '../services/mlb-team-direct-model';
import { getTeamAbbr, normalizeTeamNameKey, teamNameKeySql } 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;
  predictedWinner: string;
  actualWinner: string;
  rieSignals: SignalResult[];
  oddsData: any;
};

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

type PolicyRunSummary = {
  overall: Summary;
  selected: Summary;
  selectedCount: number;
  abstainCount: number;
  f5OnlyCount: number;
};

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;
};

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

function clamp(value: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, value));
}

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 summarize(rows: Array<{ winner: string; actual: string }>): Summary {
  const wins = rows.filter((row) => row.winner === row.actual).length;
  const picks = rows.length;
  return {
    picks,
    wins,
    wr: picks > 0 ? wins / picks : 0,
  };
}

type ConfidenceBucketSummary = {
  label: string;
  picks: number;
  wins: number;
  wr: number;
  avgMarketEdge: number | null;
  avgStructuralEdge: number | null;
};

function confidenceBucketLabel(confidence: number): string {
  if (confidence >= 0.7) return '0.70+';
  if (confidence >= 0.65) return '0.65-0.69';
  if (confidence >= 0.6) return '0.60-0.64';
  if (confidence >= 0.55) return '0.55-0.59';
  return '0.50-0.54';
}

function summarizeConfidenceBuckets(params: {
  rows: Array<BacktestRow & { marketContext: MlbDirectMarketContext; featureVector: ReturnType<typeof extractMlbDirectFeatures> }> ;
  config: Partial<MlbDirectModelConfig>;
  policy: Partial<MlbDirectPolicyConfig>;
  useCalibrated?: boolean;
}): ConfidenceBucketSummary[] {
  const buckets = new Map<string, { picks: number; wins: number; marketEdges: number[]; structuralEdges: number[] }>();

  for (const row of params.rows) {
    const decision = scoreMlbDirectModel({
      homeTeam: row.homeTeam,
      awayTeam: row.awayTeam,
      featureVector: row.featureVector,
      config: params.config,
      policy: params.policy,
    });
    if (!(decision.available && decision.policy.shouldBet && decision.policy.recommendedMarket === 'moneyline')) {
      continue;
    }
    const label = confidenceBucketLabel(params.useCalibrated ? decision.calibratedConfidence : decision.confidence);
    const bucket = buckets.get(label) || { picks: 0, wins: 0, marketEdges: [], structuralEdges: [] };
    bucket.picks += 1;
    if (decision.winnerPick === row.actualWinner) bucket.wins += 1;
    bucket.marketEdges.push(Math.abs(decision.policy.marketEdgeAbs));
    bucket.structuralEdges.push(Math.abs(decision.policy.structuralEdgeAbs));
    buckets.set(label, bucket);
  }

  const order = ['0.50-0.54', '0.55-0.59', '0.60-0.64', '0.65-0.69', '0.70+'];
  return order.map((label) => {
    const bucket = buckets.get(label) || { picks: 0, wins: 0, marketEdges: [], structuralEdges: [] };
    return {
      label,
      picks: bucket.picks,
      wins: bucket.wins,
      wr: bucket.picks > 0 ? bucket.wins / bucket.picks : 0,
      avgMarketEdge: average(bucket.marketEdges),
      avgStructuralEdge: average(bucket.structuralEdges),
    };
  });
}

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

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

function comparePolicyRuns(left: PolicyRunSummary, right: PolicyRunSummary): number {
  const overallCmp = compareSummaries(left.overall, right.overall);
  if (overallCmp !== 0) return overallCmp;
  const selectedCmp = compareSummaries(left.selected, right.selected);
  if (selectedCmp !== 0) return selectedCmp;
  return left.selectedCount - right.selectedCount;
}

function configAggressionScore(config: MlbDirectModelConfig): number {
  return round3(
    (config.signalScale * config.maxSignalAdjustment * 10)
    + config.marketMicroWeight
    + config.starterWeight
    + config.bullpenWeight
    + config.offenseWeight
    + config.contactWeight
    + config.interactionWeight
    + config.decompositionWeight
    + config.impliedTotalWeight
    + config.travelWeight
    + config.parkWeight
    + config.platoonWeight
    + config.weatherWeight,
  );
}

function* buildPolicyCandidates(): Generator<MlbDirectPolicyConfig> {
  for (const minConfidence of [0.54, 0.56, 0.58, 0.6]) {
    for (const minMarketEdge of [0.015, 0.02, 0.025, 0.03]) {
      for (const minFeatureCoverageScore of [0.5, 0.625, 0.75]) {
        for (const minStructuralEdge of [0.015, 0.02, 0.025, 0.03]) {
          for (const minSupportCount of [2, 3, 4]) {
            for (const maxConflictCount of [1, 2]) {
              for (const f5OnlySeparation of [0.015, 0.018, 0.022]) {
                for (const minF5Edge of [0.035, 0.045, 0.055]) {
                  yield {
                    minConfidence,
                    minMarketEdge,
                    minFeatureCoverageScore,
                    minStructuralEdge,
                    minSupportCount,
                    maxConflictCount,
                    f5OnlySeparation,
                    minF5Edge,
                  };
                }
              }
            }
          }
        }
      }
    }
  }
}

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 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 >= 3) keys.add(key);
  }
  return [...keys];
}

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

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));
  const keys = [sameDay.toISOString().slice(0, 10), priorDay.toISOString().slice(0, 10)];
  return [...new Set(keys)];
}

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 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 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);
}

async function loadRows(): Promise<BacktestRow[]> {
  const { rows } = await pool.query(
    `SELECT fa.event_id,
            fa.predicted_winner,
            fa.actual_winner,
            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,
      predictedWinner: row.predicted_winner,
      actualWinner: row.actual_winner,
      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 = clamp(deltaTowardHome / 1.5, -0.25, 0.25);
    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,
  };
}

function* buildConfigCandidates(): Generator<MlbDirectModelConfig> {
  for (const starterScale of [0.75, 1, 1.25]) {
    for (const offenseScale of [0.75, 1, 1.25]) {
      for (const decompositionScale of [0.75, 1, 1.25]) {
        for (const shapeScale of [0.75, 1, 1.25]) {
          for (const marketMicroScale of [0, 0.5, 1]) {
            for (const travelWeatherScale of [0.75, 1]) {
              for (const impliedScale of [0.5, 1]) {
                for (const parkScale of [0.5, 1]) {
                  for (const signalScale of [0, 0.2, 0.35, 0.5, 0.7]) {
                    for (const maxSignalAdjustment of [0.03, 0.05, 0.08, 0.12]) {
                      yield {
                        starterWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.starterWeight * starterScale),
                        bullpenWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.bullpenWeight * starterScale),
                        parkWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.parkWeight * parkScale),
                        marketMicroWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.marketMicroWeight * marketMicroScale),
                        offenseWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.offenseWeight * offenseScale),
                        contactWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.contactWeight * shapeScale),
                        interactionWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.interactionWeight * shapeScale),
                        travelWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.travelWeight * travelWeatherScale),
                        decompositionWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.decompositionWeight * decompositionScale),
                        impliedTotalWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.impliedTotalWeight * impliedScale),
                        platoonWeight: MLB_DIRECT_MODEL_DEFAULTS.platoonWeight,
                        weatherWeight: round3(MLB_DIRECT_MODEL_DEFAULTS.weatherWeight * travelWeatherScale),
                        signalScale: round3(signalScale),
                        maxSignalAdjustment: round3(maxSignalAdjustment),
                      };
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

function summarizePolicyRun(params: {
  rows: Array<BacktestRow & { marketContext: MlbDirectMarketContext; featureVector: ReturnType<typeof extractMlbDirectFeatures> }>;
  config: Partial<MlbDirectModelConfig>;
  policy: Partial<MlbDirectPolicyConfig>;
}): PolicyRunSummary {
  const overallRows: Array<{ winner: string; actual: string }> = [];
  const selectedRows: Array<{ winner: string; actual: string }> = [];
  let abstainCount = 0;
  let f5OnlyCount = 0;

  for (const row of params.rows) {
    const decision = scoreMlbDirectModel({
      homeTeam: row.homeTeam,
      awayTeam: row.awayTeam,
      featureVector: row.featureVector,
      config: params.config,
      policy: params.policy,
    });

    if (decision.available && decision.policy.shouldBet && decision.policy.recommendedMarket === 'moneyline') {
      overallRows.push({ winner: decision.winnerPick, actual: row.actualWinner });
      selectedRows.push({ winner: decision.winnerPick, actual: row.actualWinner });
    } else {
      overallRows.push({ winner: row.predictedWinner, actual: row.actualWinner });
      if (decision.policy.recommendedMarket === 'f5_moneyline') {
        f5OnlyCount += 1;
      } else {
        abstainCount += 1;
      }
    }
  }

  return {
    overall: summarize(overallRows),
    selected: summarize(selectedRows),
    selectedCount: selectedRows.length,
    abstainCount,
    f5OnlyCount,
  };
}

async function main(): Promise<void> {
  const rows = await loadRows();
  const dateKeys = [...new Set(rows.flatMap((row) => candidateGameDateKeys(row.startsAt)).filter(Boolean))];
  const prefetchedMarketData = await loadPrefetchedMarketData(dateKeys);
  const enriched = [];

  for (const row of rows) {
    const marketContext = fetchHistoricalMarketContext(row, prefetchedMarketData);
    const featureVector = extractMlbDirectFeatures({
      signals: row.rieSignals,
      oddsData: row.oddsData,
      marketContext,
    });
    enriched.push({ ...row, marketContext, featureVector });
  }

  const { train: development, test } = splitTrainTest(enriched);
  const { train, test: validation } = splitTrainTest(development);
  const baselineTrain = summarize(train.map((row) => ({ winner: row.predictedWinner, actual: row.actualWinner })));
  const baselineValidation = summarize(validation.map((row) => ({ winner: row.predictedWinner, actual: row.actualWinner })));
  const baselineTest = summarize(test.map((row) => ({ winner: row.predictedWinner, actual: row.actualWinner })));
  const marketTrain = summarize(train.map((row) => {
    const homeProb = row.marketContext.marketHomeProb ?? row.featureVector.raw.marketHomeProb ?? 0.5;
    return { winner: homeProb >= 0.5 ? row.homeTeam : row.awayTeam, actual: row.actualWinner };
  }));
  const marketValidation = summarize(validation.map((row) => {
    const homeProb = row.marketContext.marketHomeProb ?? row.featureVector.raw.marketHomeProb ?? 0.5;
    return { winner: homeProb >= 0.5 ? row.homeTeam : row.awayTeam, actual: row.actualWinner };
  }));
  const marketTest = summarize(test.map((row) => {
    const homeProb = row.marketContext.marketHomeProb ?? row.featureVector.raw.marketHomeProb ?? 0.5;
    return { winner: homeProb >= 0.5 ? row.homeTeam : row.awayTeam, actual: row.actualWinner };
  }));

  let bestConfig: MlbDirectModelConfig | null = null;
  let bestTrain: Summary | null = null;
  let bestValidation: Summary | null = null;
  let bestTest: Summary | null = null;
  let bestLiveComboConfig: MlbDirectModelConfig | null = null;
  let bestLiveComboTrain: PolicyRunSummary | null = null;
  let bestLiveComboValidation: PolicyRunSummary | null = null;
  let bestLiveComboTest: PolicyRunSummary | null = null;
  const liveTrain = summarizePolicyRun({
    rows: train,
    config: MLB_DIRECT_MODEL_LIVE_CONFIG,
    policy: MLB_DIRECT_MODEL_LIVE_POLICY,
  });
  const liveValidation = summarizePolicyRun({
    rows: validation,
    config: MLB_DIRECT_MODEL_LIVE_CONFIG,
    policy: MLB_DIRECT_MODEL_LIVE_POLICY,
  });
  const liveTest = summarizePolicyRun({
    rows: test,
    config: MLB_DIRECT_MODEL_LIVE_CONFIG,
    policy: MLB_DIRECT_MODEL_LIVE_POLICY,
  });
  const liveConfidenceBuckets = summarizeConfidenceBuckets({
    rows: test,
    config: MLB_DIRECT_MODEL_LIVE_CONFIG,
    policy: MLB_DIRECT_MODEL_LIVE_POLICY,
  });
  const liveCalibratedConfidenceBuckets = summarizeConfidenceBuckets({
    rows: test,
    config: MLB_DIRECT_MODEL_LIVE_CONFIG,
    policy: MLB_DIRECT_MODEL_LIVE_POLICY,
    useCalibrated: true,
  });

  let bestPolicy: MlbDirectPolicyConfig | null = null;
  let bestPolicyTrain: PolicyRunSummary | null = null;
  let bestPolicyValidation: PolicyRunSummary | null = null;
  let bestPolicyTest: PolicyRunSummary | null = null;
  const minValidationFires = Math.max(8, Math.floor(validation.length * 0.18));

  for (const policy of buildPolicyCandidates()) {
    const trainSummary = summarizePolicyRun({
      rows: train,
      config: MLB_DIRECT_MODEL_LIVE_CONFIG,
      policy,
    });
    const validationSummary = summarizePolicyRun({
      rows: validation,
      config: MLB_DIRECT_MODEL_LIVE_CONFIG,
      policy,
    });
    if (validationSummary.selectedCount < minValidationFires) continue;

    const shouldReplace =
      !bestPolicy
      || !bestPolicyValidation
      || comparePolicyRuns(validationSummary, bestPolicyValidation) > 0
      || (
        comparePolicyRuns(validationSummary, bestPolicyValidation) === 0
        && bestPolicyTrain
        && (
          Math.abs(trainSummary.overall.wr - validationSummary.overall.wr)
            < Math.abs(bestPolicyTrain.overall.wr - bestPolicyValidation.overall.wr)
          || (
            Math.abs(trainSummary.overall.wr - validationSummary.overall.wr)
              === Math.abs(bestPolicyTrain.overall.wr - bestPolicyValidation.overall.wr)
            && trainSummary.selectedCount > bestPolicyTrain.selectedCount
          )
        )
      );

    if (shouldReplace) {
      bestPolicy = policy;
      bestPolicyTrain = trainSummary;
      bestPolicyValidation = validationSummary;
      bestPolicyTest = summarizePolicyRun({
        rows: test,
        config: MLB_DIRECT_MODEL_LIVE_CONFIG,
        policy,
      });
    }
  }

  for (const config of buildConfigCandidates()) {
    const trainSummary = summarize(train.map((row) => {
      const decision = scoreMlbDirectModel({
        homeTeam: row.homeTeam,
        awayTeam: row.awayTeam,
        featureVector: row.featureVector,
        config,
      });
      return { winner: decision.winnerPick, actual: row.actualWinner };
    }));
    const validationSummary = summarize(validation.map((row) => {
      const decision = scoreMlbDirectModel({
        homeTeam: row.homeTeam,
        awayTeam: row.awayTeam,
        featureVector: row.featureVector,
        config,
      });
      return { winner: decision.winnerPick, actual: row.actualWinner };
    }));

    const shouldReplace =
      !bestConfig
      || !bestValidation
      || compareSummaries(validationSummary, bestValidation) > 0
      || (
        compareSummaries(validationSummary, bestValidation) === 0
        && bestConfig
        && (
          configAggressionScore(config) < configAggressionScore(bestConfig)
          || (
            configAggressionScore(config) === configAggressionScore(bestConfig)
            && bestTrain
            && (
              Math.abs(trainSummary.wr - validationSummary.wr) < Math.abs(bestTrain.wr - bestValidation.wr)
              || (
                Math.abs(trainSummary.wr - validationSummary.wr) === Math.abs(bestTrain.wr - bestValidation.wr)
                && compareSummaries(trainSummary, bestTrain) > 0
              )
            )
          )
        )
      );

    if (shouldReplace) {
      bestConfig = config;
      bestTrain = trainSummary;
      bestValidation = validationSummary;
      bestTest = summarize(test.map((row) => {
        const decision = scoreMlbDirectModel({
          homeTeam: row.homeTeam,
          awayTeam: row.awayTeam,
          featureVector: row.featureVector,
          config,
        });
        return { winner: decision.winnerPick, actual: row.actualWinner };
      }));
    }

    const comboTrain = summarizePolicyRun({
      rows: train,
      config,
      policy: MLB_DIRECT_MODEL_LIVE_POLICY,
    });
    const comboValidation = summarizePolicyRun({
      rows: validation,
      config,
      policy: MLB_DIRECT_MODEL_LIVE_POLICY,
    });
    if (comboValidation.selectedCount < minValidationFires) {
      continue;
    }
    const shouldReplaceLiveCombo =
      !bestLiveComboConfig
      || !bestLiveComboValidation
      || comparePolicyRuns(comboValidation, bestLiveComboValidation) > 0
      || (
        comparePolicyRuns(comboValidation, bestLiveComboValidation) === 0
        && bestLiveComboTrain
        && (
          Math.abs(comboTrain.overall.wr - comboValidation.overall.wr)
            < Math.abs(bestLiveComboTrain.overall.wr - bestLiveComboValidation.overall.wr)
          || (
            Math.abs(comboTrain.overall.wr - comboValidation.overall.wr)
              === Math.abs(bestLiveComboTrain.overall.wr - bestLiveComboValidation.overall.wr)
            && configAggressionScore(config) < configAggressionScore(bestLiveComboConfig)
          )
        )
      );
    if (shouldReplaceLiveCombo) {
      bestLiveComboConfig = config;
      bestLiveComboTrain = comboTrain;
      bestLiveComboValidation = comboValidation;
      bestLiveComboTest = summarizePolicyRun({
        rows: test,
        config,
        policy: MLB_DIRECT_MODEL_LIVE_POLICY,
      });
    }
  }

  const coverage = enriched.reduce((acc, row) => {
    const cov = row.featureVector.featureCoverage;
    Object.keys(cov).forEach((key) => {
      if ((cov as any)[key]) (acc as any)[key] += 1;
    });
    return acc;
  }, {
    starterData: 0,
    bullpenData: 0,
    parkData: 0,
    marketData: 0,
    movementData: 0,
    bookDisagreementData: 0,
    platoonData: 0,
    weatherData: 0,
  });

  console.log('\n=== MLB TEAM DIRECT BACKTEST ===');
  console.log(`rows=${enriched.length} train=${train.length} validation=${validation.length} test=${test.length}`);
  console.log(`baseline train=${(baselineTrain.wr * 100).toFixed(2)}% (${baselineTrain.wins}/${baselineTrain.picks})`);
  console.log(`baseline validation=${(baselineValidation.wr * 100).toFixed(2)}% (${baselineValidation.wins}/${baselineValidation.picks})`);
  console.log(`baseline test=${(baselineTest.wr * 100).toFixed(2)}% (${baselineTest.wins}/${baselineTest.picks})`);
  console.log(`market_only train=${(marketTrain.wr * 100).toFixed(2)}% (${marketTrain.wins}/${marketTrain.picks})`);
  console.log(`market_only validation=${(marketValidation.wr * 100).toFixed(2)}% (${marketValidation.wins}/${marketValidation.picks})`);
  console.log(`market_only test=${(marketTest.wr * 100).toFixed(2)}% (${marketTest.wins}/${marketTest.picks})`);
  console.log(`live_policy train=${(liveTrain.overall.wr * 100).toFixed(2)}% (${liveTrain.overall.wins}/${liveTrain.overall.picks}) fires=${liveTrain.selectedCount} f5_only=${liveTrain.f5OnlyCount} abstain=${liveTrain.abstainCount}`);
  console.log(`live_policy validation=${(liveValidation.overall.wr * 100).toFixed(2)}% (${liveValidation.overall.wins}/${liveValidation.overall.picks}) fires=${liveValidation.selectedCount} f5_only=${liveValidation.f5OnlyCount} abstain=${liveValidation.abstainCount}`);
  console.log(`live_policy test=${(liveTest.overall.wr * 100).toFixed(2)}% (${liveTest.overall.wins}/${liveTest.overall.picks}) fires=${liveTest.selectedCount} f5_only=${liveTest.f5OnlyCount} abstain=${liveTest.abstainCount}`);
  if (liveTest.selectedCount > 0) {
    console.log(`live_policy_selected test=${(liveTest.selected.wr * 100).toFixed(2)}% (${liveTest.selected.wins}/${liveTest.selected.picks})`);
  }
  if (bestPolicy && bestPolicyTrain && bestPolicyValidation && bestPolicyTest) {
    console.log(`best_policy train=${(bestPolicyTrain.overall.wr * 100).toFixed(2)}% (${bestPolicyTrain.overall.wins}/${bestPolicyTrain.overall.picks}) fires=${bestPolicyTrain.selectedCount}`);
    console.log(`best_policy validation=${(bestPolicyValidation.overall.wr * 100).toFixed(2)}% (${bestPolicyValidation.overall.wins}/${bestPolicyValidation.overall.picks}) fires=${bestPolicyValidation.selectedCount}`);
    console.log(`best_policy test=${(bestPolicyTest.overall.wr * 100).toFixed(2)}% (${bestPolicyTest.overall.wins}/${bestPolicyTest.overall.picks}) fires=${bestPolicyTest.selectedCount} f5_only=${bestPolicyTest.f5OnlyCount} abstain=${bestPolicyTest.abstainCount}`);
    if (bestPolicyTest.selectedCount > 0) {
      console.log(`best_policy_selected test=${(bestPolicyTest.selected.wr * 100).toFixed(2)}% (${bestPolicyTest.selected.wins}/${bestPolicyTest.selected.picks})`);
    }
    console.log(`policy=${JSON.stringify(bestPolicy)}`);
  }
  if (bestLiveComboConfig && bestLiveComboTrain && bestLiveComboValidation && bestLiveComboTest) {
    console.log(`best_live_combo train=${(bestLiveComboTrain.overall.wr * 100).toFixed(2)}% (${bestLiveComboTrain.overall.wins}/${bestLiveComboTrain.overall.picks}) fires=${bestLiveComboTrain.selectedCount}`);
    console.log(`best_live_combo validation=${(bestLiveComboValidation.overall.wr * 100).toFixed(2)}% (${bestLiveComboValidation.overall.wins}/${bestLiveComboValidation.overall.picks}) fires=${bestLiveComboValidation.selectedCount}`);
    console.log(`best_live_combo test=${(bestLiveComboTest.overall.wr * 100).toFixed(2)}% (${bestLiveComboTest.overall.wins}/${bestLiveComboTest.overall.picks}) fires=${bestLiveComboTest.selectedCount} f5_only=${bestLiveComboTest.f5OnlyCount} abstain=${bestLiveComboTest.abstainCount}`);
    if (bestLiveComboTest.selectedCount > 0) {
      console.log(`best_live_combo_selected test=${(bestLiveComboTest.selected.wr * 100).toFixed(2)}% (${bestLiveComboTest.selected.wins}/${bestLiveComboTest.selected.picks})`);
    }
    console.log(`live_combo_config=${JSON.stringify(bestLiveComboConfig)}`);
  }
  if (bestTrain && bestValidation && bestTest && bestConfig) {
    console.log(`direct train=${(bestTrain.wr * 100).toFixed(2)}% (${bestTrain.wins}/${bestTrain.picks})`);
    console.log(`direct validation=${(bestValidation.wr * 100).toFixed(2)}% (${bestValidation.wins}/${bestValidation.picks})`);
    console.log(`direct test=${(bestTest.wr * 100).toFixed(2)}% (${bestTest.wins}/${bestTest.picks})`);
    console.log(`config=${JSON.stringify(bestConfig)}`);
  }
  console.log(`coverage=${JSON.stringify({
    starterData: `${coverage.starterData}/${enriched.length}`,
    bullpenData: `${coverage.bullpenData}/${enriched.length}`,
    parkData: `${coverage.parkData}/${enriched.length}`,
    marketData: `${coverage.marketData}/${enriched.length}`,
    movementData: `${coverage.movementData}/${enriched.length}`,
    bookDisagreementData: `${coverage.bookDisagreementData}/${enriched.length}`,
    platoonData: `${coverage.platoonData}/${enriched.length}`,
    weatherData: `${coverage.weatherData}/${enriched.length}`,
  })}`);
  console.log('live_confidence_buckets_test=');
  for (const bucket of liveConfidenceBuckets) {
    console.log(`  ${bucket.label} picks=${bucket.picks} wr=${(bucket.wr * 100).toFixed(2)}% wins=${bucket.wins} avg_market_edge=${bucket.avgMarketEdge != null ? round3(bucket.avgMarketEdge) : 'null'} avg_structural_edge=${bucket.avgStructuralEdge != null ? round3(bucket.avgStructuralEdge) : 'null'}`);
  }
  console.log('live_calibrated_confidence_buckets_test=');
  for (const bucket of liveCalibratedConfidenceBuckets) {
    console.log(`  ${bucket.label} picks=${bucket.picks} wr=${(bucket.wr * 100).toFixed(2)}% wins=${bucket.wins} avg_market_edge=${bucket.avgMarketEdge != null ? round3(bucket.avgMarketEdge) : 'null'} avg_structural_edge=${bucket.avgStructuralEdge != null ? round3(bucket.avgStructuralEdge) : 'null'}`);
  }
}

main()
  .catch((error) => {
    console.error('[mlb-team-direct-backtest] failed', error);
    process.exit(1);
  })
  .finally(async () => {
    await pool.end().catch(() => undefined);
  });
