/**
 * grading-service.ts
 *
 * Responsible for resolving forecasts into graded outcomes.
 * Called by resolve-forecasts worker and can be invoked on-demand.
 *
 * BENCHMARK RULE: Uses the "best_valid_number" grading policy from
 * benchmark-grading.ts. All grades are W/L/P using the most advantageous
 * valid number between the original forecast and closing market.
 *
 * See benchmark-grading.ts for full policy documentation.
 */

import pool from '../db';
import { deriveGrade, deriveScores, ForecastStatus } from './forecast-lifecycle';
import { settleArchivedForecast, refreshBucketStats } from '../services/archive';
import { gradeForecast, gradeFromForecastData, type BenchmarkGrade, type BenchmarkSource } from './benchmark-grading';
import { findFinalResult } from './final-result-service';

const GRADE_TOTALS = process.env.GRADE_TOTALS_ENABLED === 'true';

export interface GradingResult {
  forecastId: string;
  eventId: string;
  league: string;
  forecastType?: 'spread' | 'total';
  predictedWinner: string;
  actualWinner: string;
  /** Benchmark grade: W, L, or P (Push). Uses best_valid_number policy. */
  grade: BenchmarkGrade;
  homeScore: number;
  awayScore: number;
  actualSpread: number;
  actualTotal: number;
  predictedSpread: number | null;
  predictedTotal: number | null;
  accuracyPct: number;
  accuracyBucket: string;
  /** Original model projected spread for the forecast side. */
  originalForecast: number | null;
  /** Benchmark spread used for grading (most advantageous valid number). */
  benchmarkForecast: number | null;
  /** Closing market spread for the forecast side. */
  closingMarket: number | null;
  /** Which source was selected as benchmark ('original', 'closing', 'equal', etc). */
  benchmarkSource: BenchmarkSource;
  /** Grading policy used. */
  gradingPolicy: string;
}

function toNullableNumber(value: any): number | null {
  if (value == null || value === '') return null;
  const parsed = typeof value === 'number' ? value : parseFloat(String(value));
  return Number.isFinite(parsed) ? parsed : null;
}

function normalizeTotalSide(value: any): 'over' | 'under' | null {
  const normalized = String(value ?? '').trim().toLowerCase();
  if (normalized === 'over' || normalized === 'o') return 'over';
  if (normalized === 'under' || normalized === 'u') return 'under';
  return null;
}

export function gradeTotalFromForecastData(forecast: any, homeScore: number, awayScore: number): GradingResult | null {
  if (!forecast?.forecast_data) return null;

  const fdata = typeof forecast.forecast_data === 'string'
    ? JSON.parse(forecast.forecast_data)
    : forecast.forecast_data;
  const oddsData = forecast.odds_data
    ? (typeof forecast.odds_data === 'string' ? JSON.parse(forecast.odds_data) : forecast.odds_data)
    : null;

  const totalSide = normalizeTotalSide(
    fdata.total_side ??
    fdata.total_pick ??
    fdata.over_under ??
    fdata.pick_side
  );
  if (!totalSide) return null;

  const originalTotal = toNullableNumber(
    fdata.projected_lines?.total ??
    fdata.projected_total ??
    fdata.projected_total_points ??
    fdata.total_line
  );
  const closingTotal = toNullableNumber(
    oddsData?.total?.[totalSide]?.line ??
    oddsData?.total?.line ??
    oddsData?.total?.over?.line ??
    oddsData?.total?.under?.line
  );

  if (originalTotal == null && closingTotal == null) return null;

  const benchmarkResult = gradeForecast({
    forecastType: 'total',
    side: totalSide,
    originalForecast: originalTotal,
    closingMarket: closingTotal,
    homeScore,
    awayScore,
  });

  const confidenceScore = forecast.confidence_score ? parseFloat(forecast.confidence_score) : 0;
  const accuracyPct = Math.round(confidenceScore * 100);
  const accuracyBucket =
    confidenceScore >= 0.85 ? 'A+' :
    confidenceScore >= 0.70 ? 'A' :
    confidenceScore >= 0.55 ? 'B+' :
    confidenceScore > 0 ? 'B' : '<50';

  return {
    forecastId: forecast.id,
    eventId: forecast.event_id,
    league: forecast.league,
    forecastType: 'total',
    predictedWinner: fdata.winner_pick ?? totalSide.toUpperCase(),
    actualWinner: homeScore === awayScore ? 'DRAW' : (homeScore > awayScore ? forecast.home_team : forecast.away_team),
    grade: benchmarkResult.grade,
    homeScore,
    awayScore,
    actualSpread: homeScore - awayScore,
    actualTotal: homeScore + awayScore,
    predictedSpread: null,
    predictedTotal: benchmarkResult.benchmarkForecast,
    accuracyPct,
    accuracyBucket,
    originalForecast: benchmarkResult.originalForecast,
    benchmarkForecast: benchmarkResult.benchmarkForecast,
    closingMarket: benchmarkResult.closingMarket,
    benchmarkSource: benchmarkResult.benchmarkSource,
    gradingPolicy: benchmarkResult.gradingPolicy,
  };
}

export async function gradeForcast(forecast: any): Promise<GradingResult | null> {
  const finalResult = await findFinalResult({
    league: forecast.league,
    startsAt: forecast.starts_at,
    homeTeam: forecast.home_team,
    awayTeam: forecast.away_team,
  });
  if (!finalResult) return null;

  const homeScore = finalResult.homeScore;
  const awayScore = finalResult.awayScore;

  const fdata = forecast.forecast_data;
  // FIX: Handle draws (especially soccer) — don't assign away team as "winner"
  const isDraw = homeScore === awayScore;
  const actualWinner = isDraw ? 'DRAW' : (homeScore > awayScore ? forecast.home_team : forecast.away_team);
  const predictedWinner = fdata.winner_pick;

  // ── Benchmark grading (best_valid_number policy) ──────────────────
  // Use the centralized grading engine for spread-based grading with push logic.
  const benchmarkResult = gradeFromForecastData(
    fdata,
    forecast.odds_data,
    forecast.home_team,
    forecast.away_team,
    homeScore,
    awayScore,
    forecast.league,
  );

  if (!benchmarkResult && GRADE_TOTALS) {
    const totalResult = gradeTotalFromForecastData(forecast, homeScore, awayScore);
    if (totalResult) return totalResult;
  }

  // Determine the final grade:
  // If benchmark grading is available (has spread data), use it (W/L/P).
  // Otherwise fall back to straight-up winner comparison (W/L only).
  // FIX: Draws without spread data → Push (not a false L for the predicted team).
  let finalGrade: BenchmarkGrade;
  if (benchmarkResult) {
    finalGrade = benchmarkResult.grade;
  } else if (isDraw) {
    // No spread data + draw = Push
    finalGrade = 'P';
  } else {
    const winnerGrade = deriveGrade(predictedWinner, actualWinner);
    if (!winnerGrade) return null;
    finalGrade = winnerGrade;
  }

  // FIX: Use the forecast's confidence_score for accuracy_pct, not a binary W/L flag.
  // This enables meaningful tier breakdowns (A+/A/B+/B) on the performance page.
  const confidenceScore = forecast.confidence_score ? parseFloat(forecast.confidence_score) : 0;
  const accuracyPct = Math.round(confidenceScore * 100);

  const bucket =
    confidenceScore >= 0.85 ? 'A+' :
    confidenceScore >= 0.70 ? 'A' :
    confidenceScore >= 0.55 ? 'B+' :
    confidenceScore > 0 ? 'B' : '<50';

  const actualSpread = homeScore - awayScore;
  const actualTotal = (homeScore || 0) + (awayScore || 0);

  // Extract closing lines for the predicted side (legacy fields)
  const oddsData = forecast.odds_data;
  const isHome = predictedWinner === forecast.home_team;
  const predictedSpread = oddsData?.spread?.[isHome ? 'home' : 'away']?.line ?? null;
  const predictedTotal = oddsData?.total?.over?.line ?? null;

  return {
    forecastId: forecast.id,
    eventId: forecast.event_id,
    league: forecast.league,
    forecastType: 'spread',
    predictedWinner,
    actualWinner,
    grade: finalGrade,
    homeScore,
    awayScore,
    actualSpread,
    actualTotal,
    predictedSpread,
    predictedTotal,
    accuracyPct,
    accuracyBucket: bucket,
    // Benchmark audit fields
    originalForecast: benchmarkResult?.originalForecast ?? null,
    benchmarkForecast: benchmarkResult?.benchmarkForecast ?? null,
    closingMarket: benchmarkResult?.closingMarket ?? null,
    benchmarkSource: benchmarkResult?.benchmarkSource ?? 'equal',
    gradingPolicy: benchmarkResult?.gradingPolicy ?? 'best_valid_number',
  };
}

function buildWinnerScore(actualWinner: string | null | undefined, homeScore: number | null | undefined, awayScore: number | null | undefined): string | null {
  if (homeScore == null || awayScore == null) return null;
  if (homeScore === awayScore) return `${homeScore}-${awayScore}`;
  if (!actualWinner || actualWinner === 'DRAW') return `${homeScore}-${awayScore}`;
  return `${actualWinner} ${Math.max(homeScore, awayScore)}`;
}

export async function upsertAccuracyV2FromRow(row: any): Promise<void> {
  if (!row?.event_id || !row?.league) return;

  const fallbackFinalGrade = row.final_grade
    || (row.actual_winner === 'DRAW' ? 'P' : deriveGrade(row.predicted_winner, row.actual_winner));
  const fallbackForecastType = row.forecast_type || 'spread';
  const fallbackGradingPolicy = row.grading_policy || (row.final_grade ? 'best_valid_number' : 'legacy_binary');
  const fallbackBenchmarkSource = row.benchmark_source || (row.benchmark_forecast != null || row.closing_market != null ? 'equal' : 'legacy');
  const winnerScore = buildWinnerScore(row.actual_winner, row.home_score, row.away_score);
  const params = [
    row.forecast_id ?? null,
    row.event_id,
    row.league,
    row.predicted_winner ?? null,
    row.actual_winner ?? null,
    row.predicted_spread ?? null,
    row.actual_spread ?? null,
    row.predicted_total ?? null,
    row.actual_total ?? null,
    row.accuracy_bucket ?? null,
    row.accuracy_pct ?? null,
    row.resolved_at ?? new Date(),
    row.event_date ?? null,
    row.home_score ?? null,
    row.away_score ?? null,
    winnerScore,
    row.original_forecast ?? null,
    row.benchmark_forecast ?? null,
    row.closing_market ?? null,
    fallbackFinalGrade ?? null,
    fallbackGradingPolicy,
    fallbackForecastType,
    fallbackBenchmarkSource,
    row.forecast_id ? `forecast:${row.forecast_id}` : `event:${row.event_id}`,
  ];

  await pool.query(
    `WITH lock_row AS (
       SELECT pg_advisory_xact_lock(hashtext($24)::bigint)
     ),
     updated_by_forecast AS (
       UPDATE rm_forecast_accuracy_v2
       SET forecast_id = $1::uuid,
           event_id = $2,
           league = $3,
           predicted_winner = $4,
           actual_winner = $5,
           predicted_spread = $6,
           actual_spread = $7,
           predicted_total = $8,
           actual_total = $9,
           accuracy_bucket = $10,
           accuracy_pct = $11,
           resolved_at = $12,
           event_date = $13,
           home_score = $14,
           away_score = $15,
           winner_score = $16,
           original_forecast = $17,
           benchmark_forecast = $18,
           closing_market = $19,
           final_grade = $20,
           grading_policy = $21,
           forecast_type = $22,
           benchmark_source = $23
       WHERE $1::uuid IS NOT NULL
         AND forecast_id = $1::uuid
       RETURNING id
     ),
     updated_by_event AS (
       UPDATE rm_forecast_accuracy_v2
       SET forecast_id = $1::uuid,
           event_id = $2,
           league = $3,
           predicted_winner = $4,
           actual_winner = $5,
           predicted_spread = $6,
           actual_spread = $7,
           predicted_total = $8,
           actual_total = $9,
           accuracy_bucket = $10,
           accuracy_pct = $11,
           resolved_at = $12,
           event_date = $13,
           home_score = $14,
           away_score = $15,
           winner_score = $16,
           original_forecast = $17,
           benchmark_forecast = $18,
           closing_market = $19,
           final_grade = $20,
           grading_policy = $21,
           forecast_type = $22,
           benchmark_source = $23
       WHERE NOT EXISTS (SELECT 1 FROM updated_by_forecast)
         AND forecast_id IS NULL
         AND event_id = $2
       RETURNING id
     ),
     inserted AS (
       INSERT INTO rm_forecast_accuracy_v2
       (forecast_id, event_id, league, predicted_winner, actual_winner,
        predicted_spread, actual_spread, predicted_total, actual_total,
        accuracy_bucket, accuracy_pct, resolved_at, event_date, home_score, away_score,
        winner_score, original_forecast, benchmark_forecast, closing_market,
        final_grade, grading_policy, forecast_type, benchmark_source)
       SELECT $1::uuid, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23
       FROM lock_row
       WHERE NOT EXISTS (SELECT 1 FROM updated_by_forecast)
         AND NOT EXISTS (SELECT 1 FROM updated_by_event)
         AND NOT EXISTS (
           SELECT 1
           FROM rm_forecast_accuracy_v2 existing
           WHERE ($1::uuid IS NOT NULL AND existing.forecast_id = $1::uuid)
              OR (existing.forecast_id IS NULL AND existing.event_id = $2)
         )
       RETURNING id
     )
     SELECT id FROM updated_by_forecast
     UNION ALL
     SELECT id FROM updated_by_event
     UNION ALL
     SELECT id FROM inserted`,
    params,
  );
}

/**
 * Persist a grading result to rm_forecast_accuracy_v2.
 * v1 is legacy-only and should not receive live writes.
 */
export async function persistGradingResult(
  result: GradingResult,
  forecastType: 'spread' | 'total' = 'spread'
): Promise<void> {
  // Derive event_date from forecast starts_at (contest date in ET)
  // Cast to ::text to avoid pg driver Date object serialization issues (UTC shift)
  const { rows: fcRows } = await pool.query(
    `SELECT ((starts_at AT TIME ZONE 'America/New_York')::date)::text as event_date FROM rm_forecast_cache WHERE id = $1`,
    [result.forecastId]
  );
  const eventDate = fcRows[0]?.event_date || new Date().toISOString().slice(0, 10);

  await upsertAccuracyV2FromRow({
    forecast_id: result.forecastId,
    event_id: result.eventId,
    league: result.league,
    predicted_winner: result.predictedWinner,
    actual_winner: result.actualWinner,
    predicted_spread: result.predictedSpread,
    actual_spread: result.actualSpread,
    predicted_total: result.predictedTotal,
    actual_total: result.actualTotal,
    accuracy_bucket: result.accuracyBucket,
    accuracy_pct: result.accuracyPct,
    resolved_at: new Date(),
    event_date: eventDate,
    home_score: result.homeScore,
    away_score: result.awayScore,
    original_forecast: result.originalForecast,
    benchmark_forecast: result.benchmarkForecast,
    closing_market: result.closingMarket,
    final_grade: result.grade,
    grading_policy: result.gradingPolicy,
    forecast_type: forecastType,
    benchmark_source: result.benchmarkSource,
  });
}

/**
 * Settle associated archive entry if one exists.
 * Push is settled as 'push' in the archive.
 */
export async function settleArchive(forecastId: string, grade: BenchmarkGrade, actualWinner: string, homeScore: number, awayScore: number): Promise<void> {
  try {
    const { rows } = await pool.query(
      `SELECT id FROM rm_archived_forecasts WHERE forecast_id = $1 AND outcome = 'pending'`,
      [forecastId]
    );
    if (rows.length > 0) {
      const outcome = grade === 'W' ? 'win' : grade === 'P' ? 'push' : 'loss';
      const score = `${homeScore}-${awayScore}`;
      await settleArchivedForecast(rows[0].id, outcome, actualWinner, score);
    }
  } catch (err) {
    console.error(`Failed to settle archive for forecast ${forecastId}:`, err);
  }
}

/**
 * Batch-resolve all unresolved forecasts.
 * Returns count of resolved forecasts.
 */
export async function resolveAllPending(): Promise<number> {
  // FIX: Added timing guard — only grade games that started 4+ hours ago
  // to ensure SportsGame has the actual final score, not a mid-game snapshot
  const { rows: expired } = await pool.query(
    `SELECT fc.* FROM rm_forecast_cache fc
     LEFT JOIN rm_forecast_accuracy_v2 fa ON fc.id = fa.forecast_id
     WHERE fc.expires_at < NOW()
       AND fc.starts_at < NOW() - INTERVAL '4 hours'
       AND fa.id IS NULL
       AND COALESCE(fc.is_minor_league, false) = false
     ORDER BY fc.expires_at DESC
     LIMIT 500`
  );

  let resolved = 0;
  for (const forecast of expired) {
    try {
      const result = await gradeForcast(forecast);
      if (!result) continue;

      await persistGradingResult(result, result.forecastType ?? 'spread');
      await settleArchive(forecast.id, result.grade, result.actualWinner, result.homeScore, result.awayScore);
      resolved++;

      console.log(`Resolved: ${forecast.away_team} @ ${forecast.home_team} → ${result.grade} (${result.accuracyBucket})`);
    } catch (err) {
      console.error(`Failed to resolve forecast ${forecast.id}:`, err);
    }
  }

  if (resolved > 0) {
    try {
      await refreshBucketStats();
    } catch (err) {
      console.error('Failed to refresh bucket stats:', err);
    }
  }

  return resolved;
}
