/**
 * Daily Forecast Generator
 *
 * RULE: When the current day's forecasts become eligible for post,
 * automatically generate them with current market pricing from data feeds.
 *
 * This worker:
 * 1. Fetches all events from SGO across all leagues
 * 2. Filters to only today's games (eligible for post)
 * 3. For each eligible game without a cached forecast, generates one via Grok
 * 4. Stores the forecast + current market pricing in rm_forecast_cache
 * 5. For games that already have a cached forecast, refreshes their odds
 *
 * Runs via cron at 6:00 AM ET daily, with an optional refresh at 12:00 PM ET.
 *
 * Usage: npx tsx src/workers/daily-forecasts.ts [--dry-run]
 */

import 'dotenv/config';
import pool from '../db';
import { fetchAllLeagueEvents, SgoEvent, isNcaabD1 } from '../services/sgo';
import { getCachedForecast, resolveCanonicalEventId } from '../models/forecast';
import { generateForEvent, refreshForEvent } from '../services/forecast-runner';
import { runShadow } from './rm2-shadow-runner';
import { execSync } from 'child_process';

const dryRun = process.argv.includes('--dry-run');

function sleep(ms: number) {
  return new Promise(r => setTimeout(r, ms));
}

function isToday(dateStr: string): boolean {
  try {
    const eventDate = new Date(dateStr);
    const now = new Date();
    return (
      eventDate.getFullYear() === now.getFullYear() &&
      eventDate.getMonth() === now.getMonth() &&
      eventDate.getDate() === now.getDate()
    );
  } catch {
    return false;
  }
}

function isUpcoming(dateStr: string): boolean {
  try {
    return new Date(dateStr) > new Date();
  } catch {
    return false;
  }
}

async function main() {
  const startTime = Date.now();
  console.log('='.repeat(60));
  console.log('RAINMAKER DAILY FORECAST GENERATOR');
  console.log(`Date: ${new Date().toISOString()}`);
  console.log(`Mode: ${dryRun ? 'DRY RUN' : 'LIVE'}`);
  console.log('='.repeat(60));

  // 0. PRE-FORECAST DATA REFRESH — injuries, lineups, transactions
  console.log('\n📋 Running pre-forecast data refresh...');
  const refreshScripts = [
    { name: 'Injuries', cmd: 'cd /var/www/html/eventheodds && python3 scripts/scrape_injuries_all.py' },
    { name: 'Lineups (NBA)', cmd: 'cd /home/administrator/.openclaw/agents/sportsclaw/workspace && node scripts/scrape_rotowire_lineups.js nba' },
    { name: 'Lineups (MLB)', cmd: 'cd /var/www/html/rainmaker/backend && npx tsx src/scripts/sync-mlb-lineups.ts' },
    { name: 'Bullpen Usage (MLB)', cmd: 'cd /var/www/html/rainmaker/backend && npx tsx src/scripts/sync-mlb-bullpen-usage.ts' },
    { name: 'Transactions', cmd: 'python3 /var/www/html/eventheodds/scripts/fetch_espn_transactions.py --days 7' },
  ];
  for (const script of refreshScripts) {
    try {
      console.log(`  ↳ Refreshing ${script.name}...`);
      execSync(script.cmd, { timeout: 120000, stdio: 'pipe' });
      console.log(`  ✅ ${script.name} updated`);
    } catch (err: any) {
      console.warn(`  ⚠️ ${script.name} refresh failed: ${err.message?.slice(0, 100)}`);
    }
  }
  // Cross-validate: deactivate stale sportsclaw.injuries where PlayerInjury says Active
  try {
    const { rowCount } = await pool.query(`
      UPDATE sportsclaw.injuries si
      SET is_active = false
      FROM "PlayerInjury" pi
      WHERE LOWER(si.player_name) = LOWER(pi."playerName")
        AND si.team = pi.team
        AND si.league = pi.league
        AND si.is_active = true
        AND LOWER(si.status) IN ('out', 'ir', 'doubtful', 'questionable')
        AND LOWER(pi.status) = 'active'
    `);
    if (rowCount && rowCount > 0) {
      console.log(`  🔄 Cross-validated: deactivated ${rowCount} stale injury entries (players now Active)`);
    }
  } catch (err: any) {
    console.warn(`  ⚠️ Injury cross-validation failed: ${err.message?.slice(0, 100)}`);
  }

  console.log('📋 Pre-forecast data refresh complete.\n');

  // 1. Fetch all events across all leagues
  console.log('\nFetching events from all leagues...');
  const eventsByLeague = await fetchAllLeagueEvents();

  // 2. Filter to today's eligible games
  const eligible: Array<{ event: SgoEvent; league: string }> = [];
  let totalEvents = 0;

  for (const [lg, events] of Object.entries(eventsByLeague)) {
    const valid = events.filter(e => e.teams?.home?.names && e.teams?.away?.names);
    totalEvents += valid.length;

    for (const event of valid) {
      const startsAt = event.status?.startsAt || '';
      // Eligible = today's games that haven't ended yet
      if (isToday(startsAt) && isUpcoming(startsAt)) {
        eligible.push({ event, league: lg });
      }
    }

    console.log(`  ${lg.toUpperCase()}: ${valid.length} total, ${eligible.filter(e => e.league === lg).length} eligible today`);
  }

  // MLB preseason filter removed — regular season is live and MLB games should publish.
  // Keep split-squad detection below as the safer duplicate/exhibition guard.
  const preseasonFiltered = eligible;

  // Also detect split-squad: if a team appears in 2+ games, skip the duplicates
  const teamGameCount: Record<string, number> = {};
  for (const { event } of preseasonFiltered) {
    const home = event.teams?.home?.names?.long || '';
    const away = event.teams?.away?.names?.long || '';
    teamGameCount[home] = (teamGameCount[home] || 0) + 1;
    teamGameCount[away] = (teamGameCount[away] || 0) + 1;
  }
  const splitSquadTeams = Object.entries(teamGameCount).filter(([_, c]) => c > 1).map(([t]) => t);
  if (splitSquadTeams.length > 0) {
    console.log(`\n⚠️ Split-squad detected (teams in 2+ games): ${splitSquadTeams.join(', ')}`);
    console.log('  These are likely preseason exhibition games.');
  }

  // Filter out non-D1 NCAAB (D2/D3/NAIA have no grading source)
  const d1Filtered: typeof preseasonFiltered = [];
  for (const entry of preseasonFiltered) {
    if (entry.league === 'ncaab') {
      const home = entry.event.teams?.home?.names?.long || '';
      const away = entry.event.teams?.away?.names?.long || '';
      const [homeD1, awayD1] = await Promise.all([isNcaabD1(home), isNcaabD1(away)]);
      if (!homeD1 || !awayD1) {
        console.log(`  [SKIP] Non-D1 NCAAB: ${away} @ ${home}`);
        continue;
      }
    }
    d1Filtered.push(entry);
  }
  if (preseasonFiltered.length - d1Filtered.length > 0) {
    console.log(`\nFiltered ${preseasonFiltered.length - d1Filtered.length} non-D1 NCAAB games`);
  }

  const finalEligible = d1Filtered;

  // Sort by game start time — earliest games cached first so they're ready before tipoff
  finalEligible.sort((a, b) => {
    const aStart = a.event.status?.startsAt || '';
    const bStart = b.event.status?.startsAt || '';
    return aStart.localeCompare(bStart);
  });

  console.log(`\nTotal events: ${totalEvents}`);
  console.log(`Eligible today: ${eligible.length} (sorted by start time — earliest first)`);

  if (finalEligible.length === 0) {
    console.log('\nNo eligible games for today. Exiting.');
    await pool.end();
    return;
  }

  // 3. Process eligible games
  let generated = 0;
  let oddsRefreshed = 0;
  let failed = 0;

  console.log('\n' + '-'.repeat(60));

  for (let i = 0; i < finalEligible.length; i++) {
    const { event, league } = finalEligible[i];
    const homeTeam = event.teams.home.names.long;
    const awayTeam = event.teams.away.names.long;
    const label = `[${i + 1}/${eligible.length}] ${awayTeam} @ ${homeTeam} (${league.toUpperCase()})`;

    // Resolve SGO event ID to canonical rm_events ID so cache lookups always match
    const canonicalId = await resolveCanonicalEventId(
      event.eventID, homeTeam, awayTeam, event.status?.startsAt || ''
    );
    event.eventID = canonicalId;

    // Check if forecast already exists
    const cached = await getCachedForecast(canonicalId);

    if (cached && cached.forecast_data?.summary) {
      // ─── CRUNCH THE NUMBERS ───
      // Forecast & blog already exist — refresh odds + re-crunch composite.
      if (!dryRun) {
        try {
          await refreshForEvent(event, league, cached, label);
        } catch (err) {
          console.log(`  [ODDS REFRESHED] ${label} (composite re-crunch failed)`);
        }
      } else {
        console.log(`  [WOULD CRUNCH] ${label}`);
      }
      oddsRefreshed++;
      continue;
    }

    // No forecast — generate one
    if (dryRun) {
      console.log(`  [WOULD GENERATE] ${label}`);
      generated++;
      continue;
    }

    try {
      console.log(`  [GENERATING] ${label}...`);
      await generateForEvent(event, league, label);
      generated++;
    } catch (err: any) {
      console.error(`  [FAIL] ${label}: ${err.message}`);
      failed++;
    }

    // Rate limit: 3s between Grok calls
    if (i < eligible.length - 1) {
      await sleep(3000);
    }
  }

  // ── Catch-up: generate forecasts for rm_events missing from SGO feed ──
  console.log('\n' + '-'.repeat(60));
  console.log('CATCH-UP: Checking rm_events for missing forecasts...');

  const { rows: orphanEvents } = await pool.query(`
    SELECT e.event_id, e.league, e.home_team, e.away_team, e.home_short, e.away_short,
           e.starts_at, e.moneyline, e.spread, e.total
    FROM rm_events e
    LEFT JOIN rm_forecast_cache fc ON fc.event_id = e.event_id
    WHERE DATE(e.starts_at AT TIME ZONE 'America/New_York') = (NOW() AT TIME ZONE 'America/New_York')::date
      AND fc.id IS NULL
      AND (
        ((e.moneyline->>'home')::text IS NOT NULL AND (e.moneyline->>'home')::text != 'null')
        OR ((e.spread->>'home')::text IS NOT NULL AND (e.spread->>'home')::text != 'null')
      )
      -- Skip MLB preseason/spring training (before regular season ~March 25)
      AND NOT (e.league = 'mlb' AND EXTRACT(MONTH FROM e.starts_at) < 4
               AND EXTRACT(DAY FROM e.starts_at) < 25)
    ORDER BY e.starts_at ASC
  `);

  if (orphanEvents.length > 0) {
    console.log(`Found ${orphanEvents.length} seeded events without forecasts — generating...`);
    for (let i = 0; i < orphanEvents.length; i++) {
      const oe = orphanEvents[i];
      const label = `[CATCH-UP ${i + 1}/${orphanEvents.length}] ${oe.away_team} @ ${oe.home_team} (${oe.league.toUpperCase()})`;

      // Build a minimal SgoEvent from rm_events data
      const startsAtStr = typeof oe.starts_at === 'object' && oe.starts_at.toISOString
        ? oe.starts_at.toISOString() : String(oe.starts_at);
      const syntheticEvent: SgoEvent = {
        eventID: oe.event_id,
        teams: {
          home: { names: { long: oe.home_team, short: oe.home_short } },
          away: { names: { long: oe.away_team, short: oe.away_short } },
        },
        status: { startsAt: startsAtStr, displayShort: '', oddsPresent: true },
        odds: {},
      };

      // Attach odds from rm_events to the synthetic event
      if (oe.moneyline) {
        const ml = oe.moneyline;
        syntheticEvent.odds['moneyline-home-american'] = { bookOdds: String(ml.home) };
        syntheticEvent.odds['moneyline-away-american'] = { bookOdds: String(ml.away) };
      }
      if (oe.spread) {
        const sp = oe.spread;
        if (sp.home) syntheticEvent.odds['spread-home-american'] = { bookOdds: String(sp.home.odds), bookSpread: String(sp.home.line) };
        if (sp.away) syntheticEvent.odds['spread-away-american'] = { bookOdds: String(sp.away.odds), bookSpread: String(sp.away.line) };
      }
      if (oe.total) {
        const tot = oe.total;
        if (tot.over) syntheticEvent.odds['total-over-american'] = { bookOdds: String(tot.over.odds), bookOverUnder: String(tot.over.line) };
        if (tot.under) syntheticEvent.odds['total-under-american'] = { bookOdds: String(tot.under.odds), bookOverUnder: String(tot.under.line) };
      }

      if (dryRun) {
        console.log(`  [WOULD GENERATE] ${label}`);
        generated++;
        continue;
      }

      try {
        console.log(`  [GENERATING] ${label}...`);
        await generateForEvent(syntheticEvent, oe.league, label);
        generated++;
      } catch (err: any) {
        console.error(`  [FAIL] ${label}: ${err.message}`);
        failed++;
      }

      if (i < orphanEvents.length - 1) await sleep(3000);
    }
  } else {
    console.log('No orphan events found — all seeded events have forecasts.');
  }

  const elapsed = Math.round((Date.now() - startTime) / 1000);

  console.log('\n' + '='.repeat(60));
  console.log('DAILY FORECAST COMPLETE');
  console.log('='.repeat(60));
  console.log(`Eligible today:  ${eligible.length}`);
  console.log(`Catch-up:        ${orphanEvents.length}`);
  console.log(`Generated:       ${generated}`);
  console.log(`Odds refreshed:  ${oddsRefreshed}`);
  console.log(`Failed:          ${failed}`);
  console.log(`Time:            ${elapsed}s`);
  console.log('='.repeat(60));

  // ── Auto-replenish Weatherman daily passes ──────────────────────
  // All weathermen get 999 picks refreshed every time forecasts are generated.
  const { rows: weathermen } = await pool.query(
    `SELECT email FROM rm_users WHERE is_weatherman = true`
  );
  for (const { email } of weathermen) {
    try {
      const { rowCount } = await pool.query(
        `UPDATE rm_users
         SET daily_pass_picks = 999,
             daily_pass_date = (NOW() AT TIME ZONE 'America/New_York')::date
         WHERE lower(email) = lower($1)`,
        [email]
      );
      if (rowCount && rowCount > 0) {
        console.log(`[VIP] Replenished daily pass for ${email}`);
      }
    } catch (err) {
      console.error(`[VIP] Failed to replenish ${email}:`, err);
    }
  }

  // ── Rain Man 2.0 Shadow Run ──────────────────────────────────
  // After RM 1.0 completes, run RM 2.0 shadow to copy forecasts with v2 weights
  try {
    console.log('\n[RM 2.0] Starting shadow run...');
    const shadowResult = await runShadow();
    console.log(`[RM 2.0] Shadow complete — copied: ${shadowResult.copied}, skipped: ${shadowResult.skipped}, failed: ${shadowResult.failed}`);
  } catch (err: any) {
    console.error(`[RM 2.0] Shadow run failed: ${err.message}`);
  }

  await pool.end();
  process.exit(failed > 0 ? 1 : 0);
}

main().catch((err) => {
  console.error('Fatal error:', err);
  process.exit(1);
});
