import 'dotenv/config';
import pool from '../db';
import {
  extractMlbDirectFeatures,
  MLB_DIRECT_MODEL_LIVE_CONFIG,
  MLB_DIRECT_MODEL_LIVE_POLICY,
  scoreMlbDirectModel,
  type MlbDirectMarketContext,
  type MlbDirectModelConfig,
  type MlbDirectPolicyConfig,
} from '../services/mlb-team-direct-model';
import { getTeamAbbr, normalizeTeamNameKey } from '../lib/team-abbreviations';
import { fetchMlbLineupSnapshot } from '../services/rie/signals/mlb-phase-signal';
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 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 EnrichedRow = BacktestRow & {
  marketContext: MlbDirectMarketContext;
  featureVector: ReturnType<typeof extractMlbDirectFeatures>;
};

type MetricKey =
  | 'marketEdge'
  | 'offenseContactEdge'
  | 'starterOffenseInteractionEdge'
  | 'platoonPressureEdge'
  | 'lineupPressureEdge'
  | 'decompositionEdge';

type MetricBucketSummary = {
  bucket: string;
  picks: number;
  wins: number;
  wr: number;
  avgMetric: number | null;
  avgConfidence: number | null;
  avgCalibratedConfidence: number | 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 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 getSeasonForDate(startsAt: string | Date): number {
  const dt = new Date(startsAt);
  if (dt.getMonth() < 3 || (dt.getMonth() === 2 && dt.getDate() < 25)) {
    return dt.getFullYear() - 1;
  }
  return dt.getFullYear();
}

function injectLineupSnapshot(signals: SignalResult[], lineupSnapshot: Awaited<ReturnType<typeof fetchMlbLineupSnapshot>> | null): SignalResult[] {
  if (!lineupSnapshot) return signals;
  return signals.map((signal) => {
    if (signal.signalId !== 'mlb_phase') return signal;
    return {
      ...signal,
      rawData: {
        ...(signal.rawData || {}),
        context: {
          ...((signal.rawData as any)?.context || {}),
          lineups: lineupSnapshot,
        },
      },
    };
  });
}

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));
  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 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 {
  return homeKeys.includes(normalizeTeamNameKey(row.homeTeam)) && awayKeys.includes(normalizeTeamNameKey(row.awayTeam));
}

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 metricBucket(value: number): string {
  if (value <= -0.1) return 'away_strong';
  if (value <= -0.03) return 'away_lean';
  if (value < 0.03) return 'neutral';
  if (value < 0.1) return 'home_lean';
  return 'home_strong';
}

function summarizeMetricBuckets(params: {
  rows: EnrichedRow[];
  metric: MetricKey;
  config?: Partial<MlbDirectModelConfig>;
  policy?: Partial<MlbDirectPolicyConfig>;
}): MetricBucketSummary[] {
  const buckets = new Map<string, { picks: number; wins: number; metrics: number[]; confidences: number[]; calibrated: 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 value = Number(row.featureVector[params.metric] ?? 0);
    const bucket = metricBucket(value);
    const entry = buckets.get(bucket) || { picks: 0, wins: 0, metrics: [], confidences: [], calibrated: [] };
    entry.picks += 1;
    if (decision.winnerPick === row.actualWinner) entry.wins += 1;
    entry.metrics.push(value);
    entry.confidences.push(decision.confidence);
    entry.calibrated.push(decision.calibratedConfidence);
    buckets.set(bucket, entry);
  }

  const order = ['away_strong', 'away_lean', 'neutral', 'home_lean', 'home_strong'];
  return order.map((bucket) => {
    const entry = buckets.get(bucket) || { picks: 0, wins: 0, metrics: [], confidences: [], calibrated: [] };
    return {
      bucket,
      picks: entry.picks,
      wins: entry.wins,
      wr: entry.picks > 0 ? entry.wins / entry.picks : 0,
      avgMetric: average(entry.metrics),
      avgConfidence: average(entry.confidences),
      avgCalibratedConfidence: average(entry.calibrated),
    };
  });
}

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: EnrichedRow[] = [];

  for (const row of rows) {
    const lineupSnapshot = await fetchMlbLineupSnapshot({
      homeTeam: row.homeTeam,
      awayTeam: row.awayTeam,
      homeShort: row.homeShort,
      awayShort: row.awayShort,
      startsAt: typeof row.startsAt === 'string' ? row.startsAt : row.startsAt.toISOString(),
    } as any, getSeasonForDate(row.startsAt));
    const patchedSignals = injectLineupSnapshot(row.rieSignals, lineupSnapshot);
    const marketContext = fetchHistoricalMarketContext(row, prefetchedMarketData);
    const featureVector = extractMlbDirectFeatures({
      signals: patchedSignals,
      oddsData: row.oddsData,
      marketContext,
    });
    enriched.push({ ...row, rieSignals: patchedSignals, marketContext, featureVector });
  }

  const { train: development, test } = splitTrainTest(enriched);
  const { test: heldOut } = splitTrainTest(development);
  const targetRows = test;

  const metrics: MetricKey[] = [
    'marketEdge',
    'offenseContactEdge',
    'starterOffenseInteractionEdge',
    'platoonPressureEdge',
    'lineupPressureEdge',
    'decompositionEdge',
  ];

  console.log('=== MLB TEAM METRIC CALIBRATION ===');
  console.log(`rows=${enriched.length} held_out=${heldOut.length} test=${test.length}`);
  const liveDecisions = targetRows
    .map((row) => ({
      row,
      decision: scoreMlbDirectModel({
        homeTeam: row.homeTeam,
        awayTeam: row.awayTeam,
        featureVector: row.featureVector,
        config: MLB_DIRECT_MODEL_LIVE_CONFIG,
        policy: MLB_DIRECT_MODEL_LIVE_POLICY,
      }),
    }))
    .filter((entry) => entry.decision.available && entry.decision.policy.shouldBet && entry.decision.policy.recommendedMarket === 'moneyline');
  console.log(`live_selected_test=${liveDecisions.length}`);

  for (const metric of metrics) {
    console.log(`metric=${metric}`);
    for (const bucket of summarizeMetricBuckets({
      rows: targetRows,
      metric,
      config: MLB_DIRECT_MODEL_LIVE_CONFIG,
      policy: MLB_DIRECT_MODEL_LIVE_POLICY,
    })) {
      console.log(`  ${bucket.bucket} picks=${bucket.picks} wr=${(bucket.wr * 100).toFixed(2)}% wins=${bucket.wins} avg_metric=${bucket.avgMetric != null ? round3(bucket.avgMetric) : 'null'} avg_conf=${bucket.avgConfidence != null ? round3(bucket.avgConfidence) : 'null'} avg_cal=${bucket.avgCalibratedConfidence != null ? round3(bucket.avgCalibratedConfidence) : 'null'}`);
    }
  }

  await pool.end();
}

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