import { TournamentState, type LiveMatch, type Source } from '../tournament';
import { pollDelay, fallbackAllowed } from './cadence';
import { fetchEspnScoreboard, fetchEspnSummary } from './espn';
import { fetchEspnOdds } from './espnOdds';
import { fetchFootballData } from '../footballData';
import { fetchSofascore } from '../sofascore';
import { setSourceMap, getSourceMap, setMatchExt, recordOdds, logIngest, prune, backupDb } from '../db/db';
import type { Fixture } from '../../../src/lib/types';

// The ingestion orchestrator. Replaces the old live loop: ESPN is the primary
// live + enrichment source (works from the VPS); football-data.org and SofaScore
// are fallbacks. Everything persists to SQLite; the UI reads the DB, so a blocked
// source only means staler data.

const LIVE_WINDOW_MS = 3 * 60 * 60 * 1000;
const ENRICH_MS = 5 * 60_000;
const ODDS_MS = 3 * 60 * 60 * 1000; // odds sweep cadence (line-movement history)
const ODDS_HORIZON_MS = 14 * 24 * 60 * 60 * 1000;

function dateUTC(iso: string): string {
  const d = new Date(iso);
  return `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, '0')}${String(d.getUTCDate()).padStart(2, '0')}`;
}

export function startScheduler(
  state: TournamentState,
  onChange: () => void,
  onTick?: () => void,
): () => void {
  const token = process.env.FOOTBALL_DATA_TOKEN?.trim();
  const sofa = process.env.ENABLE_SOFASCORE === 'true';
  let stopped = false;
  let liveTimer: ReturnType<typeof setTimeout> | undefined;
  let enrichTimer: ReturnType<typeof setTimeout> | undefined;

  console.log(`[ingest] scheduler — espn:on football-data:${token ? 'on' : 'off'} sofascore:${sofa ? 'on' : 'off'}`);

  // ---- one-time: map every fixture to its ESPN event id (across all dates).
  // Mappings persist in source_map, so on restarts we only fetch dates that
  // still contain unmapped fixtures — boots get fast, odds capture starts early.
  const mapAllFixtures = async (): Promise<void> => {
    const byDate = new Map<string, Fixture[]>();
    for (const f of state.allFixtures()) {
      const d = dateUTC(f.kickoff);
      if (!byDate.has(d)) byDate.set(d, []);
      byDate.get(d)!.push(f);
    }
    const dates = [...byDate.keys()].filter((d) => byDate.get(d)!.some((f) => !getSourceMap(f.num, 'espn')));
    let mapped = 0;
    for (const date of dates) {
      if (stopped) return;
      const t0 = Date.now();
      try {
        const evs = await fetchEspnScoreboard(date);
        for (const m of evs) {
          const num = state.matchFixtureNum(m.home, m.away, 'group', m.kickoff);
          if (num) { setSourceMap(num, 'espn', m.eventId); mapped++; }
        }
        logIngest('espn', 'map', true, Date.now() - t0, `${date}: ${evs.length} events`);
      } catch (e) {
        logIngest('espn', 'map', false, Date.now() - t0, e instanceof Error ? e.message : String(e));
      }
    }
    console.log(`[ingest] mapped ${mapped} fixtures to ESPN event ids`);
  };

  // ---- live scoreboard ----
  let lastFdAttempt = 0; // throttles the fallback independently of tick cadence
  const liveTick = async (): Promise<void> => {
    const t0 = Date.now();
    let matches: LiveMatch[] = [];
    let source: Source = 'seed';
    try {
      const espn = await fetchEspnScoreboard();
      matches = espn;
      source = 'espn';
      for (const m of espn) {
        const num = state.matchFixtureNum(m.home, m.away, 'group', m.kickoff);
        if (num) setSourceMap(num, 'espn', m.eventId);
      }
      logIngest('espn', 'scoreboard', true, Date.now() - t0, `${espn.length} events`);
    } catch (e) {
      logIngest('espn', 'scoreboard', false, Date.now() - t0, e instanceof Error ? e.message : String(e));
    }
    if (!matches.length && token && fallbackAllowed(lastFdAttempt, Date.now())) {
      lastFdAttempt = Date.now();
      try { matches = await fetchFootballData(token); source = 'football-data'; } catch { /* logged via health */ }
    }
    if (!matches.length && sofa) {
      try { matches = await fetchSofascore(); source = 'sofascore'; } catch { /* expected block */ }
    }
    if (matches.length) {
      const changed = state.mergeLive(matches, source);
      if (changed.length) { console.log(`[ingest] ${changed.length} fixture(s) updated via ${source}`); onChange(); }
    }
    onTick?.(); // e.g. freeze pre-kickoff forecast snapshots

    // Keep live matches' stats + event timeline fresh at the live cadence.
    for (const f of state.allFixtures()) {
      if (f.status !== 'live') continue;
      const eid = getSourceMap(f.num, 'espn');
      if (!eid) continue;
      try { setMatchExt(f.num, await fetchEspnSummary(eid, true)); } catch { /* logged via health */ }
    }
  };

  // ---- enrichment: ESPN summary for imminent/live fixtures ----
  const enrichTick = async (): Promise<void> => {
    const now = Date.now();
    const relevant = state.allFixtures().filter((f) => {
      const k = new Date(f.kickoff).getTime();
      return f.status === 'live' || (k > now - LIVE_WINDOW_MS && k < now + 3 * 24 * 60 * 60 * 1000);
    });
    for (const f of relevant) {
      if (stopped) return;
      const eid = getSourceMap(f.num, 'espn');
      if (!eid) continue;
      const t0 = Date.now();
      try {
        const summary = await fetchEspnSummary(eid);
        setMatchExt(f.num, summary);
        logIngest('espn', 'summary', true, Date.now() - t0, `M${f.num}`);
      } catch (e) {
        logIngest('espn', 'summary', false, Date.now() - t0, e instanceof Error ? e.message : String(e));
      }
    }
  };

  // ---- odds capture: every upcoming fixture inside the horizon, plus imminent
  // ones near kickoff. Insert-if-changed keeps a clean line-movement history.
  const oddsTick = async (): Promise<void> => {
    const now = Date.now();
    const relevant = state.allFixtures().filter((f) => {
      if (f.status === 'finished') return false;
      const k = new Date(f.kickoff).getTime();
      return k > now - 3 * 60 * 60 * 1000 && k < now + ODDS_HORIZON_MS;
    });
    let captured = 0;
    const t0 = Date.now();
    for (const f of relevant) {
      if (stopped) return;
      const eid = getSourceMap(f.num, 'espn');
      if (!eid) continue;
      try {
        for (const o of await fetchEspnOdds(eid)) {
          if (recordOdds({
            fixture_num: f.num, provider: o.provider,
            home_ml: o.homeMl, draw_ml: o.drawMl, away_ml: o.awayMl,
            over_under: o.overUnder, spread: o.spread,
          })) captured++;
        }
      } catch { /* breaker/health track it */ }
    }
    logIngest('espn-odds', 'sweep', true, Date.now() - t0, `${relevant.length} fixtures, ${captured} new lines`);
    if (captured) console.log(`[ingest] odds: ${captured} new line(s) captured`);
  };

  let oddsTimer: ReturnType<typeof setTimeout> | undefined;
  const scheduleOdds = (): void => {
    if (stopped) return;
    oddsTimer = setTimeout(async () => { await oddsTick().catch(() => {}); scheduleOdds(); }, ODDS_MS);
  };

  // Daily housekeeping: prune keeps the faster polling from growing
  // kv_cache/ingest_log forever; backupDb snapshots to the archive dir
  // (the storage box in prod) so history survives anything.
  const housekeeping = (): void => {
    try {
      const n = prune();
      if (n) console.log(`[db] pruned ${n} stale cache/log rows`);
    } catch { /* non-fatal */ }
    backupDb();
  };
  const pruneTimer = setInterval(housekeeping, 24 * 60 * 60 * 1000);

  const scheduleLive = (): void => {
    if (stopped) return;
    const delay = pollDelay(state.hasLive(), state.hasLiveActivity(LIVE_WINDOW_MS));
    liveTimer = setTimeout(async () => { await liveTick().catch(() => {}); scheduleLive(); }, delay);
  };
  const scheduleEnrich = (): void => {
    if (stopped) return;
    enrichTimer = setTimeout(async () => { await enrichTick().catch(() => {}); scheduleEnrich(); }, ENRICH_MS);
  };

  // boot: map ids, then run the loops (odds first after mapping — line history
  // is unrecoverable, so capture as early as possible)
  void (async () => {
    await mapAllFixtures().catch(() => {});
    await oddsTick().catch(() => {});
    await liveTick().catch(() => {});
    await enrichTick().catch(() => {});
    scheduleLive();
    scheduleEnrich();
    scheduleOdds();
    backupDb(); // boot snapshot — the daily timer covers the rest
  })();

  return () => {
    stopped = true;
    if (liveTimer) clearTimeout(liveTimer);
    if (enrichTimer) clearTimeout(enrichTimer);
    if (oddsTimer) clearTimeout(oddsTimer);
    clearInterval(pruneTimer);
  };
}
