import 'dotenv/config';
import pool from '../db';
import {
  extractMlbDirectFeatures,
  MLB_DIRECT_MODEL_LIVE_CONFIG,
  MLB_DIRECT_MODEL_DEFAULTS,
  MLB_DIRECT_MODEL_LIVE_POLICY,
  scoreMlbDirectModel,
  type MlbDirectModelConfig,
  type MlbDirectPolicyConfig,
} from '../services/mlb-team-direct-model';
import type { SignalResult } from '../services/rie/types';

type Row = {
  eventId: string;
  homeTeam: string;
  awayTeam: string;
  predictedWinner: string;
  actualWinner: string;
  resolvedAt: string;
  rieSignals: SignalResult[];
  oddsData: any;
};

type FavoriteOutcome = {
  eventId: string;
  winner: string;
  actual: string;
  odds: number;
  won: boolean;
  confidence: number;
  calibratedConfidence: number;
  metrics: {
    marketEdge: number;
    structuralEdge: number;
    starterQualityEdge: number;
    starterStyleFit: number;
    contactEdge: number;
    decompositionEdge: number;
    bullpenEdge: number;
    favoriteValueEdge: number;
    favoriteLowMarginEdge: number;
    favoriteBullpenFragilityEdge: number;
    favoriteSeparationWeaknessEdge: number;
    favoriteMarketMismatchEdge: number;
  };
};

type BucketSummary = {
  bucket: string;
  picks: number;
  wins: number;
  wr: number;
  units: number;
  roi: number | null;
  avgOdds: number | null;
  avgMetric: number | null;
  avgMarketEdge: number | null;
  avgStructuralEdge: number | null;
  avgConfidence: number | null;
  avgCalibratedConfidence: number | null;
};

const FAVORITE_PRICE_BAND_ORDER = ['-110 to -129', '-130 to -159', '-160 to -199', '-200 to -299', '-300+'];
const FAVORITE_EDGE_BUCKET_ORDER = ['against_strong', 'against_lean', 'neutral', 'support_lean', 'support_strong'];
const FAVORITE_CONFIDENCE_BUCKET_ORDER = ['low', 'mid', 'high', 'top'];

function americanProfitPerUnit(odds: number): number {
  return odds > 0 ? odds / 100 : 100 / Math.abs(odds);
}

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

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

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 metricBucket(value: number): string {
  if (value <= -0.1) return 'against_strong';
  if (value <= -0.03) return 'against_lean';
  if (value < 0.03) return 'neutral';
  if (value < 0.1) return 'support_lean';
  return 'support_strong';
}

function confidenceBucket(value: number): string {
  if (value < 0.6) return 'low';
  if (value < 0.7) return 'mid';
  if (value < 0.8) return 'high';
  return 'top';
}

function priceBand(odds: number): string {
  if (odds <= -300) return '-300+';
  if (odds <= -200) return '-200 to -299';
  if (odds <= -160) return '-160 to -199';
  if (odds <= -130) return '-130 to -159';
  return '-110 to -129';
}

function calculateStructuralEdge(featureVector: ReturnType<typeof extractMlbDirectFeatures>, direction: number): number {
  const config: MlbDirectModelConfig = {
    ...MLB_DIRECT_MODEL_DEFAULTS,
    ...MLB_DIRECT_MODEL_LIVE_CONFIG,
  };
  const starterCompositeEdge = featureVector.starterQualityEdge + (featureVector.starterLeashEdge * 0.6);
  return round3(
    (
      (starterCompositeEdge * config.starterWeight)
      + ((featureVector.bullpenQualityEdge + (featureVector.bullpenUsageEdge * 0.75)) * config.bullpenWeight)
      + (featureVector.favoriteValueEdge * config.favoriteValueWeight)
      + (featureVector.favoriteLowMarginEdge * config.favoriteLowMarginWeight)
      + (featureVector.favoriteBullpenFragilityEdge * config.favoriteBullpenFragilityWeight)
      + (featureVector.favoriteSeparationWeaknessEdge * config.favoriteSeparationWeaknessWeight)
      + (featureVector.favoriteMarketMismatchEdge * config.favoriteMarketMismatchWeight)
      + (featureVector.offenseSplitEdge * config.offenseWeight)
      + (featureVector.offenseContactEdge * config.contactWeight)
      + (featureVector.starterOffenseInteractionEdge * config.interactionWeight)
      + (featureVector.lineupPressureEdge * config.lineupWeight)
      + (featureVector.decompositionEdge * config.decompositionWeight)
      + (featureVector.impliedTotalDisagreementEdge * config.impliedTotalWeight)
      + (featureVector.travelRestEdge * config.travelWeight)
      + (featureVector.parkInteractionEdge * config.parkWeight)
      + (featureVector.platoonPressureEdge * config.platoonWeight)
      + (featureVector.weatherEdge * config.weatherWeight)
    ) * direction,
  );
}

async function loadRows(): Promise<Row[]> {
  const { rows } = await pool.query(
    `SELECT fa.event_id,
            fa.predicted_winner,
            fa.actual_winner,
            fa.resolved_at,
            fc.home_team,
            fc.away_team,
            fc.odds_data,
            fc.model_signals
       FROM rm_forecast_accuracy_v2 fa
       JOIN rm_forecast_cache fc ON fc.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,
    predictedWinner: row.predicted_winner,
    actualWinner: row.actual_winner,
    resolvedAt: row.resolved_at,
    rieSignals: Array.isArray(row.model_signals?.rieSignals) ? row.model_signals.rieSignals : [],
    oddsData: row.odds_data || {},
  }));
}

function resolveMoneylineOdds(row: Row, winnerTeam: string): number | null {
  const moneyline = row.oddsData?.moneyline || {};
  if (winnerTeam === row.homeTeam) {
    const odds = Number(moneyline.home ?? null);
    return Number.isFinite(odds) ? odds : null;
  }
  if (winnerTeam === row.awayTeam) {
    const odds = Number(moneyline.away ?? null);
    return Number.isFinite(odds) ? odds : null;
  }
  return null;
}

function collectFavoriteOutcomes(rows: Row[], policy: MlbDirectPolicyConfig, config: MlbDirectModelConfig = MLB_DIRECT_MODEL_LIVE_CONFIG as MlbDirectModelConfig): FavoriteOutcome[] {
  const outcomes: FavoriteOutcome[] = [];
  for (const row of rows) {
    const featureVector = extractMlbDirectFeatures({
      signals: row.rieSignals,
      oddsData: row.oddsData,
    });
    const decision = scoreMlbDirectModel({
      homeTeam: row.homeTeam,
      awayTeam: row.awayTeam,
      featureVector,
      config,
      policy,
    });
    if (!(decision.available && decision.policy.shouldBet && decision.policy.recommendedMarket === 'moneyline')) {
      continue;
    }
    const odds = resolveMoneylineOdds(row, decision.winnerPick);
    if (odds == null || odds >= 0) continue;
    const direction = decision.winnerPick === row.homeTeam ? 1 : -1;
    outcomes.push({
      eventId: row.eventId,
      winner: decision.winnerPick,
      actual: row.actualWinner,
      odds,
      won: decision.winnerPick === row.actualWinner,
      confidence: decision.confidence,
      calibratedConfidence: decision.calibratedConfidence,
      metrics: {
        marketEdge: round3(featureVector.marketEdge * direction),
        structuralEdge: calculateStructuralEdge(featureVector, direction),
        starterQualityEdge: round3(featureVector.starterQualityEdge * direction),
        starterStyleFit: round3(featureVector.starterOffenseInteractionEdge * direction),
        contactEdge: round3(featureVector.offenseContactEdge * direction),
        decompositionEdge: round3(featureVector.decompositionEdge * direction),
        bullpenEdge: round3((featureVector.bullpenQualityEdge + (featureVector.bullpenUsageEdge * 0.75)) * direction),
        favoriteValueEdge: round3(featureVector.favoriteValueEdge * direction),
        favoriteLowMarginEdge: round3(featureVector.favoriteLowMarginEdge * direction),
        favoriteBullpenFragilityEdge: round3(featureVector.favoriteBullpenFragilityEdge * direction),
        favoriteSeparationWeaknessEdge: round3(featureVector.favoriteSeparationWeaknessEdge * direction),
        favoriteMarketMismatchEdge: round3(featureVector.favoriteMarketMismatchEdge * direction),
      },
    });
  }
  return outcomes;
}

function summarizeFavoriteOutcomes(outcomes: FavoriteOutcome[]): BucketSummary {
  const wins = outcomes.filter((row) => row.won).length;
  const units = outcomes.reduce((sum, row) => sum + (row.won ? americanProfitPerUnit(row.odds) : -1), 0);
  return {
    bucket: 'all',
    picks: outcomes.length,
    wins,
    wr: outcomes.length ? wins / outcomes.length : 0,
    units,
    roi: outcomes.length ? units / outcomes.length : null,
    avgOdds: average(outcomes.map((row) => row.odds)),
    avgMetric: null,
    avgMarketEdge: average(outcomes.map((row) => row.metrics.marketEdge)),
    avgStructuralEdge: average(outcomes.map((row) => row.metrics.structuralEdge)),
    avgConfidence: average(outcomes.map((row) => row.confidence)),
    avgCalibratedConfidence: average(outcomes.map((row) => row.calibratedConfidence)),
  };
}

function summarizeMetricBuckets(outcomes: FavoriteOutcome[], metricKey: keyof FavoriteOutcome['metrics']): BucketSummary[] {
  const buckets = new Map<string, FavoriteOutcome[]>();
  for (const outcome of outcomes) {
    const label = metricBucket(outcome.metrics[metricKey]);
    const bucket = buckets.get(label) || [];
    bucket.push(outcome);
    buckets.set(label, bucket);
  }
  const order = ['against_strong', 'against_lean', 'neutral', 'support_lean', 'support_strong'];
  return order.map((bucket) => {
    const rows = buckets.get(bucket) || [];
    const wins = rows.filter((row) => row.won).length;
    const units = rows.reduce((sum, row) => sum + (row.won ? americanProfitPerUnit(row.odds) : -1), 0);
    return {
      bucket,
      picks: rows.length,
      wins,
      wr: rows.length ? wins / rows.length : 0,
      units,
      roi: rows.length ? units / rows.length : null,
      avgOdds: average(rows.map((row) => row.odds)),
      avgMetric: average(rows.map((row) => row.metrics[metricKey])),
      avgMarketEdge: average(rows.map((row) => row.metrics.marketEdge)),
      avgStructuralEdge: average(rows.map((row) => row.metrics.structuralEdge)),
      avgConfidence: average(rows.map((row) => row.confidence)),
      avgCalibratedConfidence: average(rows.map((row) => row.calibratedConfidence)),
    };
  });
}

function summarizePriceBands(outcomes: FavoriteOutcome[]): BucketSummary[] {
  const buckets = new Map<string, FavoriteOutcome[]>();
  for (const outcome of outcomes) {
    const label = priceBand(outcome.odds);
    const bucket = buckets.get(label) || [];
    bucket.push(outcome);
    buckets.set(label, bucket);
  }
  return FAVORITE_PRICE_BAND_ORDER.map((bucket) => {
    const rows = buckets.get(bucket) || [];
    const wins = rows.filter((row) => row.won).length;
    const units = rows.reduce((sum, row) => sum + (row.won ? americanProfitPerUnit(row.odds) : -1), 0);
    return {
      bucket,
      picks: rows.length,
      wins,
      wr: rows.length ? wins / rows.length : 0,
      units,
      roi: rows.length ? units / rows.length : null,
      avgOdds: average(rows.map((row) => row.odds)),
      avgMetric: null,
      avgMarketEdge: average(rows.map((row) => row.metrics.marketEdge)),
      avgStructuralEdge: average(rows.map((row) => row.metrics.structuralEdge)),
      avgConfidence: average(rows.map((row) => row.confidence)),
      avgCalibratedConfidence: average(rows.map((row) => row.calibratedConfidence)),
    };
  });
}

function summarizeConfidenceBuckets(outcomes: FavoriteOutcome[], useCalibrated = true): BucketSummary[] {
  const buckets = new Map<string, FavoriteOutcome[]>();
  for (const outcome of outcomes) {
    const value = useCalibrated ? outcome.calibratedConfidence : outcome.confidence;
    const label = confidenceBucket(value);
    const bucket = buckets.get(label) || [];
    bucket.push(outcome);
    buckets.set(label, bucket);
  }
  return FAVORITE_CONFIDENCE_BUCKET_ORDER.map((bucket) => {
    const rows = buckets.get(bucket) || [];
    const wins = rows.filter((row) => row.won).length;
    const units = rows.reduce((sum, row) => sum + (row.won ? americanProfitPerUnit(row.odds) : -1), 0);
    return {
      bucket,
      picks: rows.length,
      wins,
      wr: rows.length ? wins / rows.length : 0,
      units,
      roi: rows.length ? units / rows.length : null,
      avgOdds: average(rows.map((row) => row.odds)),
      avgMetric: average(rows.map((row) => (useCalibrated ? row.calibratedConfidence : row.confidence))),
      avgMarketEdge: average(rows.map((row) => row.metrics.marketEdge)),
      avgStructuralEdge: average(rows.map((row) => row.metrics.structuralEdge)),
      avgConfidence: average(rows.map((row) => row.confidence)),
      avgCalibratedConfidence: average(rows.map((row) => row.calibratedConfidence)),
    };
  });
}

function summarizeCombinedBuckets(
  outcomes: FavoriteOutcome[],
  primaryLabel: (outcome: FavoriteOutcome) => string,
  primaryOrder: string[],
  secondaryLabel: (outcome: FavoriteOutcome) => string,
  secondaryOrder: string[],
  secondaryMetric?: (outcome: FavoriteOutcome) => number,
): BucketSummary[] {
  const buckets = new Map<string, FavoriteOutcome[]>();
  for (const outcome of outcomes) {
    const label = `${primaryLabel(outcome)} x ${secondaryLabel(outcome)}`;
    const bucket = buckets.get(label) || [];
    bucket.push(outcome);
    buckets.set(label, bucket);
  }
  const rows: BucketSummary[] = [];
  for (const primary of primaryOrder) {
    for (const secondary of secondaryOrder) {
      const label = `${primary} x ${secondary}`;
      const bucketRows = buckets.get(label) || [];
      const wins = bucketRows.filter((row) => row.won).length;
      const units = bucketRows.reduce((sum, row) => sum + (row.won ? americanProfitPerUnit(row.odds) : -1), 0);
      rows.push({
        bucket: label,
        picks: bucketRows.length,
        wins,
        wr: bucketRows.length ? wins / bucketRows.length : 0,
        units,
        roi: bucketRows.length ? units / bucketRows.length : null,
        avgOdds: average(bucketRows.map((row) => row.odds)),
        avgMetric: secondaryMetric ? average(bucketRows.map((row) => secondaryMetric(row))) : null,
        avgMarketEdge: average(bucketRows.map((row) => row.metrics.marketEdge)),
        avgStructuralEdge: average(bucketRows.map((row) => row.metrics.structuralEdge)),
        avgConfidence: average(bucketRows.map((row) => row.confidence)),
        avgCalibratedConfidence: average(bucketRows.map((row) => row.calibratedConfidence)),
      });
    }
  }
  return rows;
}

function printBucketTable(label: string, buckets: BucketSummary[]): void {
  console.log(label);
  for (const bucket of buckets) {
    const parts = [
      `  ${bucket.bucket}: picks=${bucket.picks}`,
      `wr=${(bucket.wr * 100).toFixed(2)}%`,
      `units=${bucket.units.toFixed(2)}`,
      `roi=${bucket.roi != null ? (bucket.roi * 100).toFixed(2) + '%' : 'na'}`,
      `avg_odds=${bucket.avgOdds != null ? round3(bucket.avgOdds) : 'na'}`,
    ];
    if (bucket.avgMetric != null) parts.push(`avg_metric=${round3(bucket.avgMetric)}`);
    if (bucket.avgMarketEdge != null) parts.push(`avg_market_edge=${round3(bucket.avgMarketEdge)}`);
    if (bucket.avgStructuralEdge != null) parts.push(`avg_structural_edge=${round3(bucket.avgStructuralEdge)}`);
    if (bucket.avgConfidence != null) parts.push(`avg_confidence=${round3(bucket.avgConfidence)}`);
    if (bucket.avgCalibratedConfidence != null) parts.push(`avg_calibrated_confidence=${round3(bucket.avgCalibratedConfidence)}`);
    console.log(parts.join(' '));
  }
}

async function main(): Promise<void> {
  const rows = await loadRows();
  const { train: development, test } = splitTrainTest(rows);
  const candidateConfig: MlbDirectModelConfig = {
    ...MLB_DIRECT_MODEL_DEFAULTS,
    ...MLB_DIRECT_MODEL_LIVE_CONFIG,
    favoriteSeparationWeaknessWeight: Number(process.env.MLB_FAVORITE_DIAG_SEPARATION_WEIGHT ?? (MLB_DIRECT_MODEL_LIVE_CONFIG.favoriteSeparationWeaknessWeight ?? 0)),
    favoriteMarketMismatchWeight: Number(process.env.MLB_FAVORITE_DIAG_MARKET_MISMATCH_WEIGHT ?? (MLB_DIRECT_MODEL_LIVE_CONFIG.favoriteMarketMismatchWeight ?? 0)),
  };

  const favoriteAwarePolicy: MlbDirectPolicyConfig = {
    ...MLB_DIRECT_MODEL_LIVE_POLICY,
    strongMarketOverrideMin: 0.08,
    favoritePriceBlockMax: -160,
    favoritePriceRequireMarketEdgeMin: null,
    starterInteractionWeakPositiveMin: null,
    starterInteractionWeakPositiveMax: null,
    decompositionPositiveBlockMin: null,
    maxConflictCount: 3,
  };

  const liveFavorites = collectFavoriteOutcomes(test, MLB_DIRECT_MODEL_LIVE_POLICY);
  const candidateFavorites = collectFavoriteOutcomes(test, favoriteAwarePolicy, candidateConfig);

  console.log('=== MLB FAVORITE DIAGNOSTICS ===');
  console.log(`rows=${rows.length} development=${development.length} test=${test.length}`);
  const liveSummary = summarizeFavoriteOutcomes(liveFavorites);
  const candidateSummary = summarizeFavoriteOutcomes(candidateFavorites);
  console.log(`live favorites: picks=${liveSummary.picks} wr=${(liveSummary.wr * 100).toFixed(2)}% units=${liveSummary.units.toFixed(2)} roi=${liveSummary.roi != null ? (liveSummary.roi * 100).toFixed(2) + '%' : 'na'} avg_odds=${liveSummary.avgOdds != null ? round3(liveSummary.avgOdds) : 'na'}`);
  console.log(`candidate favorites: picks=${candidateSummary.picks} wr=${(candidateSummary.wr * 100).toFixed(2)}% units=${candidateSummary.units.toFixed(2)} roi=${candidateSummary.roi != null ? (candidateSummary.roi * 100).toFixed(2) + '%' : 'na'} avg_odds=${candidateSummary.avgOdds != null ? round3(candidateSummary.avgOdds) : 'na'}`);

  printBucketTable('live favorite price bands=', summarizePriceBands(liveFavorites));
  printBucketTable('live favorite confidence buckets=', summarizeConfidenceBuckets(liveFavorites));
  printBucketTable('live favorite market edge buckets=', summarizeMetricBuckets(liveFavorites, 'marketEdge'));
  printBucketTable('live favorite structural edge buckets=', summarizeMetricBuckets(liveFavorites, 'structuralEdge'));
  printBucketTable('live favorite starter quality buckets=', summarizeMetricBuckets(liveFavorites, 'starterQualityEdge'));
  printBucketTable('live favorite starter style fit buckets=', summarizeMetricBuckets(liveFavorites, 'starterStyleFit'));
  printBucketTable('live favorite contact edge buckets=', summarizeMetricBuckets(liveFavorites, 'contactEdge'));
  printBucketTable('live favorite decomposition edge buckets=', summarizeMetricBuckets(liveFavorites, 'decompositionEdge'));
  printBucketTable('live favorite bullpen edge buckets=', summarizeMetricBuckets(liveFavorites, 'bullpenEdge'));
  printBucketTable('live favorite value buckets=', summarizeMetricBuckets(liveFavorites, 'favoriteValueEdge'));
  printBucketTable('live favorite low margin buckets=', summarizeMetricBuckets(liveFavorites, 'favoriteLowMarginEdge'));
  printBucketTable('live favorite bullpen fragility buckets=', summarizeMetricBuckets(liveFavorites, 'favoriteBullpenFragilityEdge'));
  printBucketTable('live favorite separation weakness buckets=', summarizeMetricBuckets(liveFavorites, 'favoriteSeparationWeaknessEdge'));
  printBucketTable('live favorite market mismatch buckets=', summarizeMetricBuckets(liveFavorites, 'favoriteMarketMismatchEdge'));

  printBucketTable('live favorite price band x market edge=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.marketEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.marketEdge,
  ));
  printBucketTable('live favorite price band x structural edge=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.structuralEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.structuralEdge,
  ));
  printBucketTable('live favorite price band x confidence=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => confidenceBucket(outcome.calibratedConfidence),
    FAVORITE_CONFIDENCE_BUCKET_ORDER,
    (outcome) => outcome.calibratedConfidence,
  ));
  printBucketTable('live favorite price band x starter style fit=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.starterStyleFit),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.starterStyleFit,
  ));
  printBucketTable('live favorite price band x contact edge=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.contactEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.contactEdge,
  ));
  printBucketTable('live favorite price band x decomposition edge=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.decompositionEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.decompositionEdge,
  ));
  printBucketTable('live favorite price band x low margin edge=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.favoriteLowMarginEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.favoriteLowMarginEdge,
  ));
  printBucketTable('live favorite price band x bullpen fragility=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.favoriteBullpenFragilityEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.favoriteBullpenFragilityEdge,
  ));
  printBucketTable('live favorite price band x separation weakness=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.favoriteSeparationWeaknessEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.favoriteSeparationWeaknessEdge,
  ));
  printBucketTable('live favorite price band x market mismatch=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.favoriteMarketMismatchEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.favoriteMarketMismatchEdge,
  ));
  printBucketTable('live favorite price band x bullpen edge=', summarizeCombinedBuckets(
    liveFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.bullpenEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.bullpenEdge,
  ));

  printBucketTable('candidate favorite price bands=', summarizePriceBands(candidateFavorites));
  printBucketTable('candidate favorite confidence buckets=', summarizeConfidenceBuckets(candidateFavorites));
  printBucketTable('candidate favorite market edge buckets=', summarizeMetricBuckets(candidateFavorites, 'marketEdge'));
  printBucketTable('candidate favorite structural edge buckets=', summarizeMetricBuckets(candidateFavorites, 'structuralEdge'));
  printBucketTable('candidate favorite starter quality buckets=', summarizeMetricBuckets(candidateFavorites, 'starterQualityEdge'));
  printBucketTable('candidate favorite starter style fit buckets=', summarizeMetricBuckets(candidateFavorites, 'starterStyleFit'));
  printBucketTable('candidate favorite contact edge buckets=', summarizeMetricBuckets(candidateFavorites, 'contactEdge'));
  printBucketTable('candidate favorite decomposition edge buckets=', summarizeMetricBuckets(candidateFavorites, 'decompositionEdge'));
  printBucketTable('candidate favorite bullpen edge buckets=', summarizeMetricBuckets(candidateFavorites, 'bullpenEdge'));
  printBucketTable('candidate favorite value buckets=', summarizeMetricBuckets(candidateFavorites, 'favoriteValueEdge'));
  printBucketTable('candidate favorite low margin buckets=', summarizeMetricBuckets(candidateFavorites, 'favoriteLowMarginEdge'));
  printBucketTable('candidate favorite bullpen fragility buckets=', summarizeMetricBuckets(candidateFavorites, 'favoriteBullpenFragilityEdge'));
  printBucketTable('candidate favorite separation weakness buckets=', summarizeMetricBuckets(candidateFavorites, 'favoriteSeparationWeaknessEdge'));
  printBucketTable('candidate favorite market mismatch buckets=', summarizeMetricBuckets(candidateFavorites, 'favoriteMarketMismatchEdge'));

  printBucketTable('candidate favorite price band x market edge=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.marketEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.marketEdge,
  ));
  printBucketTable('candidate favorite price band x structural edge=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.structuralEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.structuralEdge,
  ));
  printBucketTable('candidate favorite price band x confidence=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => confidenceBucket(outcome.calibratedConfidence),
    FAVORITE_CONFIDENCE_BUCKET_ORDER,
    (outcome) => outcome.calibratedConfidence,
  ));
  printBucketTable('candidate favorite price band x starter style fit=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.starterStyleFit),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.starterStyleFit,
  ));
  printBucketTable('candidate favorite price band x contact edge=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.contactEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.contactEdge,
  ));
  printBucketTable('candidate favorite price band x decomposition edge=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.decompositionEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.decompositionEdge,
  ));
  printBucketTable('candidate favorite price band x low margin edge=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.favoriteLowMarginEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.favoriteLowMarginEdge,
  ));
  printBucketTable('candidate favorite price band x bullpen fragility=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.favoriteBullpenFragilityEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.favoriteBullpenFragilityEdge,
  ));
  printBucketTable('candidate favorite price band x separation weakness=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.favoriteSeparationWeaknessEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.favoriteSeparationWeaknessEdge,
  ));
  printBucketTable('candidate favorite price band x market mismatch=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.favoriteMarketMismatchEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.favoriteMarketMismatchEdge,
  ));
  printBucketTable('candidate favorite price band x bullpen edge=', summarizeCombinedBuckets(
    candidateFavorites,
    (outcome) => priceBand(outcome.odds),
    FAVORITE_PRICE_BAND_ORDER,
    (outcome) => metricBucket(outcome.metrics.bullpenEdge),
    FAVORITE_EDGE_BUCKET_ORDER,
    (outcome) => outcome.metrics.bullpenEdge,
  ));
}

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