/**
 * benchmark-grading.ts
 *
 * STANDING BENCHMARK RULE — "best_valid_number" grading policy
 * ──────────────────────────────────────────────────────────────
 *
 * This is the centralized, canonical grading utility for all Rainmaker
 * forecast performance measurement. Every graded forecast — Recently
 * Graded, By League, By Confidence Tier, and all derived metrics —
 * MUST use this module as the single source of truth.
 *
 * POLICY: Forecasts are graded using the most advantageous valid number
 * available between the original forecast and the closing market line.
 * Both values are ALWAYS compared — neither source is preferred by default.
 * If the original forecast is better, use the original.
 * If the closing market is better, use the closing.
 * If both are the same, use that shared value.
 * This is intentional. This is not accidental. This is the benchmark
 * rule moving forward unless explicitly changed.
 *
 * WHY benchmark_source is tracked:
 *   - Identifies whether 'original' or 'closing' was selected as the benchmark
 *   - Enables audit of which source provided the grading advantage
 *   - Values: 'original', 'closing', 'equal', 'only_original', 'only_closing'
 *
 * WHY original_forecast is preserved:
 *   - Audit trail: we can always reference what was originally posted
 *   - CLV analysis: comparing original vs close is a separate concern
 *   - Historical integrity: the raw data is never lost
 *
 * WHY benchmark_forecast may differ from original_forecast:
 *   - The benchmark represents the strongest defensible grading position
 *   - If either the original or the close is more favorable, we grade from that number
 *   - The displayed forecast MUST match the benchmark forecast
 *
 * HOW Push is determined:
 *   - If the final result lands exactly on the benchmark forecast number,
 *     the result is a Push (P). Not W, not L.
 *   - Push applies to spreads and totals where integer lines exist.
 *   - Half-point lines eliminate push possibility.
 *
 * DO NOT simplify, remove, or consolidate this logic without explicit
 * authorization. The separation of original/benchmark/closing is load-bearing.
 */

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

export type BenchmarkGrade = 'W' | 'L' | 'P';

export type ForecastType = 'spread' | 'total' | 'moneyline';

/**
 * Identifies which number was selected as the benchmark.
 *   'original' — the model's originally published forecast was more advantageous
 *   'closing'  — the closing market number was more advantageous
 *   'equal'    — both values were identical
 *   'only_original' — closing was missing, original was used by default
 *   'only_closing'  — original was missing, closing was used by default
 */
export type BenchmarkSource = 'original' | 'closing' | 'equal' | 'only_original' | 'only_closing';

export interface BenchmarkGradingInput {
  forecastType: ForecastType;
  /** For spread: the side selected (home or away). For total: 'over' or 'under'. */
  side: 'home' | 'away' | 'over' | 'under';
  /** The model's originally published forecast number (spread line or total). */
  originalForecast: number | null;
  /** The closing market number (spread line or total). */
  closingMarket: number | null;
  /** For spread: actual margin from the forecast side's perspective (positive = side won by that many). */
  actualMargin?: number | null;
  /** For total: actual combined score. */
  actualTotal?: number | null;
  /** Home score */
  homeScore: number;
  /** Away score */
  awayScore: number;
  /** Is the forecast side the home team? (spread only) */
  isHome?: boolean;
  /** Optional league context for market sanitization. */
  league?: string | null;
}

export interface BenchmarkGradingResult {
  /** The benchmark forecast number actually used for grading. */
  benchmarkForecast: number | null;
  /** The original forecast as published. */
  originalForecast: number | null;
  /** The closing market line. */
  closingMarket: number | null;
  /** Which source was selected as the benchmark ('original', 'closing', 'equal', etc). */
  benchmarkSource: BenchmarkSource;
  /** Final grade: W, L, or P. */
  grade: BenchmarkGrade;
  /** Named grading policy used. */
  gradingPolicy: 'best_valid_number';
  /** The forecast type graded. */
  forecastType: ForecastType;
  /** Debug explanation of how grade was determined. */
  explanation: string;
}

// ─── Core Benchmark Selection ────────────────────────────────

/**
 * Select the most advantageous valid number between original and closing.
 *
 * POLICY: Compare both values and use whichever is more favorable for grading.
 *   - If the original forecast is better, use the original forecast.
 *   - If the closing market is better, use the closing market.
 *   - If both are the same, use that shared value.
 *   - Do NOT default to one source just because it is newer.
 *
 * For spreads:
 *   - The LARGER number is always more favorable for the forecast side.
 *   - Favorite -5 vs close -7: max(-5, -7) = -5 (original, less points to lay)
 *   - Favorite -7 vs close -5: max(-7, -5) = -5 (closing, less points to lay)
 *   - Underdog +5 vs close +3: max(5, 3) = +5 (original, more cushion)
 *   - Underdog +3 vs close +5: max(3, 5) = +5 (closing, more cushion)
 *
 * For totals:
 *   - Over: LOWER total is more favorable (easier to go over) -> min(orig, close)
 *   - Under: HIGHER total is more favorable (easier to stay under) -> max(orig, close)
 */
export function selectBenchmarkForecast(
  forecastType: ForecastType,
  side: string,
  originalForecast: number | null,
  closingMarket: number | null
): { value: number | null; source: BenchmarkSource } {
  // If both are missing, nothing to select
  if (originalForecast == null && closingMarket == null) {
    return { value: null, source: 'equal' };
  }
  // If only one is available, use it with the appropriate source tag
  if (originalForecast == null) {
    return { value: closingMarket, source: 'only_closing' };
  }
  if (closingMarket == null) {
    return { value: originalForecast, source: 'only_original' };
  }
  // Both exist — if identical, tag as equal
  if (originalForecast === closingMarket) {
    return { value: originalForecast, source: 'equal' };
  }

  if (forecastType === 'spread') {
    // For spreads: larger number is always more favorable
    const best = Math.max(originalForecast, closingMarket);
    return { value: best, source: best === originalForecast ? 'original' : 'closing' };
  }

  if (forecastType === 'total') {
    if (side === 'over') {
      // Over: lower total is more favorable
      const best = Math.min(originalForecast, closingMarket);
      return { value: best, source: best === originalForecast ? 'original' : 'closing' };
    } else {
      // Under: higher total is more favorable
      const best = Math.max(originalForecast, closingMarket);
      return { value: best, source: best === originalForecast ? 'original' : 'closing' };
    }
  }

  // Moneyline: no numerical benchmark conversion, use original
  return { value: originalForecast, source: 'only_original' };
}

export function sanitizeClosingSpreadForLeague(
  league: string | null | undefined,
  closingSpread: number | null | undefined,
): number | null {
  if (closingSpread == null) return null;
  if ((league || '').trim().toLowerCase() !== 'mlb') return closingSpread;

  const numeric = Number(closingSpread);
  if (!Number.isFinite(numeric)) return null;

  // Public MLB benchmark grading should not use clearly bogus alt run lines.
  // The feed is polluted with values like +5.5, +6.5, and -14.5.
  if (Math.abs(numeric) > 1.5) return null;

  return numeric;
}

// ─── Spread Grading ──────────────────────────────────────────

/**
 * Grade a spread forecast against the final result.
 *
 * actualMargin = margin from the forecast side's perspective.
 *   - If forecast side is home: homeScore - awayScore
 *   - If forecast side is away: awayScore - homeScore
 *
 * atsResult = actualMargin + benchmarkSpread
 *   - > 0: Win (side covered)
 *   - < 0: Loss (side didn't cover)
 *   - = 0: Push
 */
function gradeSpread(
  benchmarkSpread: number,
  actualMargin: number
): { grade: BenchmarkGrade; explanation: string } {
  const atsResult = actualMargin + benchmarkSpread;

  if (atsResult > 0) {
    return {
      grade: 'W',
      explanation: `Side margin ${actualMargin} + spread ${benchmarkSpread} = ${atsResult} > 0 = WIN`,
    };
  } else if (atsResult < 0) {
    return {
      grade: 'L',
      explanation: `Side margin ${actualMargin} + spread ${benchmarkSpread} = ${atsResult} < 0 = LOSS`,
    };
  } else {
    return {
      grade: 'P',
      explanation: `Side margin ${actualMargin} + spread ${benchmarkSpread} = 0 = PUSH`,
    };
  }
}

// ─── Total Grading ───────────────────────────────────────────

/**
 * Grade a total (over/under) forecast against the final result.
 */
function gradeTotal(
  side: 'over' | 'under',
  benchmarkTotal: number,
  actualTotal: number
): { grade: BenchmarkGrade; explanation: string } {
  if (actualTotal === benchmarkTotal) {
    return {
      grade: 'P',
      explanation: `Actual total ${actualTotal} = benchmark ${benchmarkTotal} = PUSH`,
    };
  }

  if (side === 'over') {
    return actualTotal > benchmarkTotal
      ? { grade: 'W', explanation: `Actual total ${actualTotal} > benchmark ${benchmarkTotal} = OVER WIN` }
      : { grade: 'L', explanation: `Actual total ${actualTotal} < benchmark ${benchmarkTotal} = OVER LOSS` };
  } else {
    return actualTotal < benchmarkTotal
      ? { grade: 'W', explanation: `Actual total ${actualTotal} < benchmark ${benchmarkTotal} = UNDER WIN` }
      : { grade: 'L', explanation: `Actual total ${actualTotal} > benchmark ${benchmarkTotal} = UNDER LOSS` };
  }
}

// ─── Main Grading Function ───────────────────────────────────

/**
 * Grade a forecast using the benchmark-rule policy.
 *
 * This is the SINGLE SOURCE OF TRUTH for grading outcomes.
 * All performance views, summaries, and metrics must use this function.
 */
export function gradeForecast(input: BenchmarkGradingInput): BenchmarkGradingResult {
  const { forecastType, side, originalForecast, closingMarket, homeScore, awayScore, isHome, league } = input;
  const sanitizedClosingMarket = forecastType === 'spread'
    ? sanitizeClosingSpreadForLeague(league, closingMarket)
    : closingMarket;

  const { value: benchmarkForecast, source: benchmarkSource } = selectBenchmarkForecast(forecastType, side, originalForecast, sanitizedClosingMarket);

  const baseResult: Omit<BenchmarkGradingResult, 'grade' | 'explanation'> = {
    benchmarkForecast,
    originalForecast,
    closingMarket: sanitizedClosingMarket,
    benchmarkSource,
    gradingPolicy: 'best_valid_number',
    forecastType,
  };

  // ─── Spread grading ───
  if (forecastType === 'spread') {
    if (benchmarkForecast == null) {
      return { ...baseResult, grade: 'L', explanation: 'No benchmark spread available, cannot grade' };
    }

    // Compute actual margin from the forecast side's perspective
    const actualMargin = (side === 'home' || isHome)
      ? (homeScore - awayScore)
      : (awayScore - homeScore);

    const { grade, explanation } = gradeSpread(benchmarkForecast, actualMargin);
    return { ...baseResult, grade, explanation };
  }

  // ─── Total grading ───
  if (forecastType === 'total') {
    if (benchmarkForecast == null) {
      return { ...baseResult, grade: 'L', explanation: 'No benchmark total available, cannot grade' };
    }

    const actualTotal = homeScore + awayScore;
    const totalSide = (side === 'over' || side === 'under') ? side : 'over';
    const { grade, explanation } = gradeTotal(totalSide, benchmarkForecast, actualTotal);
    return { ...baseResult, grade, explanation };
  }

  // ─── Moneyline grading (preserve existing winner-based logic) ───
  // Moneyline doesn't have a clean numerical benchmark — use straight-up winner.
  // The grade is determined externally by comparing predictedWinner vs actualWinner.
  // This function returns a placeholder; the caller should override with winner-based logic.
  return {
    ...baseResult,
    grade: 'L',
    explanation: 'Moneyline — grade determined by winner comparison (external)',
  };
}

// ─── Convenience: Grade from raw forecast/odds data ──────────

/**
 * Extract spread grading inputs from forecast_data and odds_data JSON blobs.
 * This is the bridge between the raw DB data and the grading engine.
 */
export function gradeFromForecastData(
  forecastData: any,
  oddsData: any,
  homeTeam: string,
  awayTeam: string,
  homeScore: number,
  awayScore: number,
  league?: string | null,
): BenchmarkGradingResult | null {
  if (!forecastData || homeScore == null || awayScore == null) return null;

  const fd = typeof forecastData === 'string' ? JSON.parse(forecastData) : forecastData;
  const odds = oddsData ? (typeof oddsData === 'string' ? JSON.parse(oddsData) : oddsData) : null;

  const forecastSide = fd.forecast_side;
  if (!forecastSide) return null;

  const isHome = forecastSide === homeTeam;
  const side: 'home' | 'away' = isHome ? 'home' : 'away';

  // Extract original forecast spread (from model projected lines)
  const originalSpread: number | null = fd.projected_lines?.spread?.[side] ?? null;

  // Extract closing market spread
  const closingSpread: number | null = sanitizeClosingSpreadForLeague(
    league,
    odds?.spread?.[side]?.line ?? null,
  );

  // If neither spread is available, fall back to moneyline/winner comparison
  if (originalSpread == null && closingSpread == null) return null;

  return gradeForecast({
    forecastType: 'spread',
    side,
    originalForecast: originalSpread,
    closingMarket: closingSpread,
    homeScore,
    awayScore,
    isHome,
    league,
  });
}

// ─── W-L-P Aggregation Helpers ───────────────────────────────

/**
 * Compute win rate from W-L-P counts.
 * Pushes are excluded from the denominator (W / (W + L)).
 */
export function computeWinRate(wins: number, losses: number): number | null {
  const total = wins + losses;
  if (total === 0) return null;
  return Math.round((wins / total) * 1000) / 10;
}

/**
 * Format W-L-P record string.
 */
export function formatWLP(wins: number, losses: number, pushes: number): string {
  if (pushes > 0) return `${wins}-${losses}-${pushes}`;
  return `${wins}-${losses}`;
}
