/**
 * nightly-score-resolver.ts
 *
 * Nightly cron worker that:
 * 1. Fetches final scores from ESPN for all leagues
 * 2. Backfills home_score/away_score into rm_forecast_accuracy_v2
 * 3. Resolves any ungraded forecasts that now have final scores
 * 4. Ensures every graded forecast has actual final scores for display
 *
 * Run at 2:00 AM ET nightly — after all US games have finished.
 *
 * Rule: Final score = Winner + Score (e.g. "Lakers 121-98")
 * Rule: Date = contest date (event_date), never grading date
 */

import dotenv from 'dotenv';
import path from 'path';
dotenv.config({ path: path.join(__dirname, '../../.env') });

import pool from '../db';
import { upsertAccuracyV2FromRow } from '../services/grading-service';

// ─── ESPN League Mappings ───────────────────────────────────

const ESPN_LEAGUES: Record<string, { sport: string; league: string }> = {
  nba:        { sport: 'basketball', league: 'nba' },
  nhl:        { sport: 'hockey',     league: 'nhl' },
  mlb:        { sport: 'baseball',   league: 'mlb' },
  ncaab:      { sport: 'basketball', league: 'mens-college-basketball' },
  ncaaf:      { sport: 'football',   league: 'college-football' },
  nfl:        { sport: 'football',   league: 'nfl' },
  wnba:       { sport: 'basketball', league: 'wnba' },
  epl:        { sport: 'soccer',     league: 'eng.1' },
  la_liga:    { sport: 'soccer',     league: 'esp.1' },
  serie_a:    { sport: 'soccer',     league: 'ita.1' },
  bundesliga: { sport: 'soccer',     league: 'ger.1' },
  ligue_1:    { sport: 'soccer',     league: 'fra.1' },
  mls:        { sport: 'soccer',     league: 'usa.1' },
  champions_league: { sport: 'soccer', league: 'uefa.champions' },
  mma:        { sport: 'mma',       league: 'ufc' },
};

interface ESPNScore {
  homeTeam: string;
  awayTeam: string;
  homeScore: number;
  awayScore: number;
  status: string;
}

function parseEspnScoreValue(value: unknown): number | null {
  if (typeof value === 'number' && Number.isFinite(value)) return value;
  if (typeof value === 'string' && value.trim()) {
    const parsed = Number(value);
    if (Number.isFinite(parsed)) return parsed;
  }
  return null;
}

function getEspnCompetitorName(competitor: any): string {
  return competitor?.team?.displayName
    || competitor?.team?.name
    || competitor?.athlete?.displayName
    || competitor?.athlete?.fullName
    || competitor?.athlete?.shortName
    || '';
}

function getEspnCompetitorScore(competitor: any): number | null {
  const direct = parseEspnScoreValue(competitor?.score);
  if (direct != null) return direct;
  return parseEspnScoreValue(competitor?.linescores?.[0]?.value);
}

// ─── ESPN Score Fetcher ─────────────────────────────────────

async function fetchESPNScores(sport: string, league: string, dateStr: string, groups?: string): Promise<ESPNScore[]> {
  let url = `https://site.api.espn.com/apis/site/v2/sports/${sport}/${league}/scoreboard?dates=${dateStr}&limit=500`;
  if (groups) url += `&groups=${groups}`;
  try {
    const res = await fetch(url);
    if (!res.ok) return [];
    const data = await res.json() as any;
    const scores: ESPNScore[] = [];

    for (const event of data.events || []) {
      const comp = event.competitions?.[0];
      if (!comp) continue;
      const statusName = comp.status?.type?.name || '';
      // Soccer uses STATUS_FULL_TIME; US sports use STATUS_FINAL
      if (statusName !== 'STATUS_FINAL' && statusName !== 'STATUS_FULL_TIME') continue;

      const teams = comp.competitors || [];
      const home = teams.find((t: any) => t.homeAway === 'home') || teams[0];
      const away = teams.find((t: any) => t.homeAway === 'away') || teams[1];
      if (!home || !away) continue;

      const homeTeam = getEspnCompetitorName(home);
      const awayTeam = getEspnCompetitorName(away);
      const homeScore = getEspnCompetitorScore(home);
      const awayScore = getEspnCompetitorScore(away);
      if (!homeTeam || !awayTeam || homeScore == null || awayScore == null) continue;

      scores.push({
        homeTeam,
        awayTeam,
        homeScore,
        awayScore,
        status: 'final',
      });
    }
    return scores;
  } catch (err) {
    console.error(`ESPN fetch error for ${sport}/${league} on ${dateStr}:`, err);
    return [];
  }
}

// ─── Fuzzy Team Matcher ─────────────────────────────────────

function teamMatchScore(a: string, b: string): number {
  const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
  const na = normalize(a);
  const nb = normalize(b);
  if (na === nb) return 1.0;
  // Check last word (team nickname)
  const lastA = a.trim().split(/\s+/).pop()?.toLowerCase() || '';
  const lastB = b.trim().split(/\s+/).pop()?.toLowerCase() || '';
  if (lastA === lastB && lastA.length > 2) return 0.9;
  // Substring containment
  if (na.includes(nb) || nb.includes(na)) return 0.8;
  // First two words match (handles "Austin Peay State" vs "Austin Peay Governors")
  const wordsA = a.trim().split(/\s+/).slice(0, 2).join(' ').toLowerCase();
  const wordsB = b.trim().split(/\s+/).slice(0, 2).join(' ').toLowerCase();
  if (wordsA.length >= 6 && wordsA === wordsB) return 0.75;
  return 0;
}

// ─── Main Worker ────────────────────────────────────────────

export async function runNightlyScoreResolver(options: { closePool?: boolean } = {}) {
  const { closePool = false } = options;
  console.log(`[${new Date().toISOString()}] Nightly score resolver starting...`);

  // Get dates to process: today + last 21 days (to catch up on backlog)
  const now = new Date();
  const etOffset = new Date().toLocaleString('en-US', { timeZone: 'America/New_York' });
  const etDate = new Date(etOffset);
  const datesToCheck: string[] = [];
  for (let i = 0; i <= 21; i++) {
    const d = new Date(etDate.getTime() - i * 86400000);
    datesToCheck.push(d.toISOString().slice(0, 10).replace(/-/g, ''));
  }

  console.log(`Checking dates: ${datesToCheck[0]}...${datesToCheck[datesToCheck.length - 1]} (${datesToCheck.length} days)`);

  // 1. Find forecasts missing scores
  const { rows: missing } = await pool.query(`
    SELECT fa.id, fa.event_id, fa.league, fa.event_date, fa.actual_winner,
           fc.home_team, fc.away_team, fc.starts_at,
           fa.home_score, fa.away_score
    FROM rm_forecast_accuracy_v2 fa
    LEFT JOIN rm_forecast_cache fc ON fa.event_id = fc.event_id
    WHERE fa.actual_winner IS NOT NULL
      AND (fa.home_score IS NULL OR fa.away_score IS NULL)
    ORDER BY fa.event_date DESC
    LIMIT 500
  `);

  console.log(`Found ${missing.length} forecasts missing scores`);

  // 2. Also find unresolved forecasts (no accuracy record yet)
  const { rows: unresolved } = await pool.query(`
    SELECT fc.id, fc.event_id, fc.league, fc.home_team, fc.away_team,
           fc.starts_at, fc.forecast_data, fc.odds_data, fc.confidence_score
    FROM rm_forecast_cache fc
    LEFT JOIN rm_forecast_accuracy_v2 fa ON fc.id = fa.forecast_id
    WHERE fa.id IS NULL
      AND fc.expires_at < NOW()
      AND COALESCE(fc.is_minor_league, false) = false
    ORDER BY fc.starts_at DESC
    LIMIT 600
  `);

  console.log(`Found ${unresolved.length} unresolved forecasts`);

  // 3. Fetch scores from ESPN for each league + date combo
  const scoreCache: Map<string, ESPNScore[]> = new Map();
  const leagueDates = new Set<string>();

  for (const row of [...missing, ...unresolved]) {
    const league = row.league;
    if (!ESPN_LEAGUES[league]) continue;
    for (const d of datesToCheck) {
      leagueDates.add(`${league}:${d}`);
    }
  }

  for (const key of leagueDates) {
    const [league, dateStr] = key.split(':');
    const espn = ESPN_LEAGUES[league];
    if (!espn) continue;

    // NCAAB: use groups=50 for conference tournament coverage
    const groups = league === 'ncaab' ? '50' : undefined;
    const scores = await fetchESPNScores(espn.sport, espn.league, dateStr, groups);
    scoreCache.set(key, scores);
    if (scores.length > 0) {
      console.log(`  ${league} ${dateStr}: ${scores.length} final games`);
    }
    // Rate limit — 200ms between requests
    await new Promise(r => setTimeout(r, 200));
  }

  // 4. Backfill missing scores on existing accuracy records
  let scoresFilled = 0;
  for (const row of missing) {
    if (!ESPN_LEAGUES[row.league]) continue;
    const eventDateStr = row.event_date?.toISOString?.()?.slice(0, 10)?.replace(/-/g, '') ||
                         row.event_date?.replace?.(/-/g, '') || '';

    // Try each date
    for (const d of [eventDateStr, ...datesToCheck]) {
      if (!d) continue;
      const scores = scoreCache.get(`${row.league}:${d}`) || [];
      const match = scores.find(s => {
        const homeMatch = teamMatchScore(s.homeTeam, row.home_team || '') >= 0.8;
        const awayMatch = teamMatchScore(s.awayTeam, row.away_team || '') >= 0.8;
        return homeMatch && awayMatch;
      });

      // Also try reversed (ESPN sometimes swaps home/away)
      const matchReversed = !match ? scores.find(s => {
        const homeMatch = teamMatchScore(s.awayTeam, row.home_team || '') >= 0.8;
        const awayMatch = teamMatchScore(s.homeTeam, row.away_team || '') >= 0.8;
        return homeMatch && awayMatch;
      }) : null;

      const finalMatch = match || matchReversed;
      if (finalMatch) {
        const hs = match ? finalMatch.homeScore : finalMatch.awayScore;
        const as = match ? finalMatch.awayScore : finalMatch.homeScore;
        await pool.query(
          `UPDATE rm_forecast_accuracy_v2 SET home_score = $1, away_score = $2,
           actual_spread = $3, actual_total = $4
           WHERE id = $5`,
          [hs, as, hs - as, hs + as, row.id]
        );
        scoresFilled++;
        break;
      }
    }
  }
  console.log(`Backfilled scores on ${scoresFilled} existing records`);

  // 5. Resolve unresolved forecasts
  let resolved = 0;
  for (const fc of unresolved) {
    if (!ESPN_LEAGUES[fc.league]) continue;
    const startDate = fc.starts_at ? new Date(fc.starts_at) : null;
    const dateStr = startDate
      ? startDate.toLocaleDateString('en-US', { timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit' })
          .split('/').map((p: string, i: number) => i === 2 ? p : p.padStart(2, '0')).reverse().join('').replace(/\//g, '')
      : '';
    // Normalize to YYYYMMDD
    const eventDateISO = startDate
      ? new Date(startDate.toLocaleString('en-US', { timeZone: 'America/New_York' })).toISOString().slice(0, 10).replace(/-/g, '')
      : '';

    for (const d of [eventDateISO, ...datesToCheck]) {
      if (!d) continue;
      const scores = scoreCache.get(`${fc.league}:${d}`) || [];
      const match = scores.find(s => {
        return teamMatchScore(s.homeTeam, fc.home_team || '') >= 0.8 &&
               teamMatchScore(s.awayTeam, fc.away_team || '') >= 0.8;
      });
      const matchR = !match ? scores.find(s => {
        return teamMatchScore(s.awayTeam, fc.home_team || '') >= 0.8 &&
               teamMatchScore(s.homeTeam, fc.away_team || '') >= 0.8;
      }) : null;
      const fm = match || matchR;

      if (fm) {
        const hs = match ? fm.homeScore : fm.awayScore;
        const as = match ? fm.awayScore : fm.homeScore;
        const actualWinner = hs === as ? 'DRAW' : (hs > as ? fc.home_team : fc.away_team);
        const predictedWinner = fc.forecast_data?.winner_pick || fc.forecast_data?.projected_winner || '';
        if (!predictedWinner) continue; // Skip if no winner prediction
        const grade = actualWinner === 'DRAW'
          ? 'P'
          : predictedWinner.toLowerCase().includes((actualWinner || '').toLowerCase().split(' ').pop() || '')
            ? 'W'
            : 'L';
        const accuracyPct = grade === 'W' ? 100 : grade === 'P' ? 50 : 0;
        const bucket = grade === 'W' ? '90+' : grade === 'P' ? 'B' : '<50';
        const eventDate = startDate
          ? new Date(startDate.toLocaleString('en-US', { timeZone: 'America/New_York' })).toISOString().slice(0, 10)
          : new Date().toISOString().slice(0, 10);

        // Extract closing lines
        const isHome = predictedWinner === fc.home_team;
        const predictedSpread = fc.odds_data?.spread?.[isHome ? 'home' : 'away']?.line ?? null;
        const predictedTotal = fc.odds_data?.total?.over?.line ?? null;

        await upsertAccuracyV2FromRow({
          forecast_id: fc.id,
          event_id: fc.event_id,
          league: fc.league,
          predicted_winner: predictedWinner,
          actual_winner: actualWinner,
          predicted_spread: predictedSpread,
          actual_spread: hs - as,
          predicted_total: predictedTotal,
          actual_total: hs + as,
          accuracy_bucket: bucket,
          accuracy_pct: accuracyPct,
          resolved_at: new Date(),
          event_date: eventDate,
          home_score: hs,
          away_score: as,
          original_forecast: null,
          benchmark_forecast: null,
          closing_market: null,
          final_grade: grade,
          grading_policy: 'legacy_binary',
          forecast_type: 'spread',
          benchmark_source: 'legacy',
        });

        resolved++;
        console.log(`  Resolved: ${fc.away_team} @ ${fc.home_team} → ${grade} (${hs}-${as})`);
        break;
      }
    }
  }

  console.log(`Resolved ${resolved} new forecasts`);
  console.log(`[${new Date().toISOString()}] Nightly score resolver complete.`);

  if (closePool) {
    await pool.end();
  }
}

async function main() {
  await runNightlyScoreResolver({ closePool: true });
}

if (require.main === module) {
  main().catch(err => {
    console.error('Nightly score resolver fatal error:', err);
    process.exit(1);
  });
}
