'use strict';

/**
 * GAME-LEVEL CONTEXT CAPTURE (Layer 3, Step 4b).
 *
 * We store prop-level rows only, so park factors cannot be self-derived from
 * our own data. This starts the accrual that eventually can — and it is
 * FORWARD-ONLY: a day not captured is permanently lost, which is the entire
 * justification for storing anything before it is needed.
 *
 * ── WHY THIS IS JUSTIFIED STORAGE AND NOT HOARDING ───────────────────────
 * Three tests, all of which this passes and most speculative capture fails:
 *   1. NAMED NEED — every field maps to one specific future derivation
 *      (venue → park factors, runs → run environment, HR → HR park factors).
 *      A field with no named purpose is not stored.
 *   2. UNRECOVERABLE IF MISSED — statsapi serves historical schedules, but our
 *      *joined* view of which games our props sat in is not reconstructable
 *      cheaply, and the habit of capturing late is how the p_win gap happened.
 *   3. CHEAP — ONE schedule call per game DATE returns every game that day with
 *      venue, linescore and scoring plays. Not per game, and certainly not per
 *      prop.
 *
 * ── GRAIN ────────────────────────────────────────────────────────────────
 * One row per game, deduped on `game_id`. Never copied onto prop rows: a game's
 * totals belong to the game, and duplicating them per prop is how the same fact
 * starts disagreeing with itself.
 *
 * ── ACCRUAL TARGET (so "later" is a date, not a vibe) ─────────────────────
 * The derived park factor needs MIN_GAMES (150) home games per venue in the
 * current regime. At ~81 home games per park per season that is ~2 SEASONS of
 * capture before a self-derived factor can even be NOMINATED against the public
 * base. `accrualStatus()` reports live progress so the wait is measurable.
 */

const { MIN_GAMES } = require('./parkFactors');

const HOME_GAMES_PER_SEASON = 81;

const num = (v) => {
  if (v == null || v === '') return null;
  const n = typeof v === 'number' ? v : Number(v);
  return Number.isFinite(n) ? n : null;
};

/**
 * rowsFromSchedule(payload, opts) — PURE. statsapi schedule → game_context rows.
 *
 * Only FINAL games are stored: a suspended or in-progress game has totals that
 * are not the game's totals, and storing them would poison every factor derived
 * from them later.
 */
function rowsFromSchedule(payload, opts = {}) {
  const sport = opts.sport || 'mlb';
  const out = [];
  for (const dt of (payload && payload.dates) || []) {
    for (const g of dt.games || []) {
      const status = (g.status && g.status.detailedState) || null;
      if (status !== 'Final') continue; // in-progress totals are not results

      const ls = g.linescore || {};
      const teams = ls.teams || {};
      const home = (g.teams && g.teams.home) || {};
      const away = (g.teams && g.teams.away) || {};

      // Every home run scores at least the batter, so scoringPlays carries all
      // of them — no per-game boxscore fetch needed.
      const plays = g.scoringPlays || [];
      const hrPlays = plays.filter((p) => ((p.result || {}).event || '') === 'Home Run');
      const homeHr = hrPlays.filter((p) => (p.about || {}).halfInning === 'bottom').length;
      const awayHr = hrPlays.filter((p) => (p.about || {}).halfInning === 'top').length;

      const homeRuns = num((teams.home || {}).runs);
      const awayRuns = num((teams.away || {}).runs);

      out.push({
        game_id: `${sport}:${g.gamePk}`,
        sport,
        season: num(g.season) || (dt.date ? Number(String(dt.date).slice(0, 4)) : null),
        game_date: dt.date || (g.gameDate ? String(g.gameDate).slice(0, 10) : null),
        venue_id: num((g.venue || {}).id),
        venue_name: (g.venue || {}).name || null,
        home_team: ((home.team || {}).name) || null,
        away_team: ((away.team || {}).name) || null,
        home_runs: homeRuns,
        away_runs: awayRuns,
        total_runs: homeRuns == null || awayRuns == null ? null : homeRuns + awayRuns,
        home_hr: hrPlays.length ? homeHr : null,
        away_hr: hrPlays.length ? awayHr : null,
        total_hr: hrPlays.length ? hrPlays.length : null,
        status,
        // Session 75 — weather ACTUAL, named-purpose: raw material for FUTURE
        // self-derived weather factors. Deliberately absent here and filled by
        // captureWeatherActual, so a schedule capture never invents weather.
      });
    }
  }
  return out;
}

const SCHEDULE_URL = (date) =>
  `https://statsapi.mlb.com/api/v1/schedule?sportId=1&startDate=${date}&endDate=${date}`
  + '&gameType=R&hydrate=linescore,venue,scoringplays';

/**
 * captureDate(date, opts) — ONE call per game DATE, upserted on game_id.
 * Idempotent: re-running a date refreshes nothing that matters and never
 * duplicates.
 */
async function captureDate(date, opts = {}) {
  const sport = opts.sport || 'mlb';
  if (!date) return { ok: false, reason: 'no date' };

  let payload;
  try {
    payload = opts.fetchJson
      ? await opts.fetchJson(SCHEDULE_URL(date))
      : (await require('axios').get(SCHEDULE_URL(date), { timeout: 60_000 })).data;
  } catch (err) {
    return { ok: false, reason: `fetch failed: ${err.message}`, date };
  }

  const rows = rowsFromSchedule(payload, { sport });
  if (!rows.length) return { ok: true, date, games: 0, written: 0, reason: 'no final games' };

  const sb = opts.sb || getClient();
  if (!sb) return { ok: false, reason: 'supabase not configured', games: rows.length, written: 0 };

  const { error } = await sb.from('game_context').upsert(rows, { onConflict: 'game_id' });
  if (error) return { ok: false, reason: `upsert failed: ${error.message}`, games: rows.length, written: 0 };

  return {
    ok: true, date, games: rows.length, written: rows.length,
    venues: new Set(rows.map((r) => r.venue_id)).size,
    with_hr: rows.filter((r) => r.total_hr != null).length,
  };
}

function getClient() {
  try { return require('../utils/supabase').getSupabaseServiceClient(); } catch { return null; }
}

/**
 * accrualStatus(opts) — how far from a self-derived factor are we, per venue?
 * Makes "later" measurable instead of a promise.
 */
async function accrualStatus(opts = {}) {
  const sb = opts.sb || getClient();
  if (!sb) return { ok: false, reason: 'supabase not configured' };
  const { data, error } = await sb
    .from('game_context')
    .select('venue_id, venue_name')
    .eq('sport', opts.sport || 'mlb')
    .limit(20000);
  if (error) return { ok: false, reason: error.message };

  const byVenue = new Map();
  for (const r of data || []) {
    if (r.venue_id == null) continue;
    const v = byVenue.get(r.venue_id) || { venue: r.venue_name, games: 0 };
    v.games += 1;
    byVenue.set(r.venue_id, v);
  }
  const venues = [...byVenue.entries()].map(([id, v]) => ({
    venue_id: id, venue: v.venue, games: v.games,
    ready: v.games >= MIN_GAMES,
    remaining: Math.max(0, MIN_GAMES - v.games),
  })).sort((a, b) => b.games - a.games);

  return {
    ok: true,
    total_games: (data || []).length,
    venues_tracked: venues.length,
    venues_ready: venues.filter((v) => v.ready).length,
    min_games_per_venue: MIN_GAMES,
    seasons_needed_estimate: Math.round((MIN_GAMES / HOME_GAMES_PER_SEASON) * 10) / 10,
    venues: venues.slice(0, 5),
  };
}

/**
 * captureWeatherActual(date, opts) — the ACTUAL at game time, written ONLY to
 * game_context. Never read back into a projection or a measurement.
 *
 * Open-Meteo's ARCHIVE endpoint is the honest source for an actual: asking the
 * forecast endpoint after the fact would return a re-forecast, not what
 * happened. Unavailable → the columns stay NULL. Never imputed.
 */
const ARCHIVE_URL = (lat, lon, date) =>
  `https://archive-api.open-meteo.com/v1/archive?latitude=${lat}&longitude=${lon}`
  + `&start_date=${date}&end_date=${date}`
  + '&hourly=temperature_2m,wind_speed_10m,wind_direction_10m,precipitation'
  + '&temperature_unit=fahrenheit&wind_speed_unit=mph';

async function captureWeatherActual(date, opts = {}) {
  const sb = opts.sb || getClient();
  if (!sb) return { ok: false, reason: 'supabase not configured' };
  const { data: games, error } = await sb.from('game_context')
    .select('game_id, venue_id, game_date')
    .eq('game_date', date).is('wx_temp_f', null).limit(500);
  if (error) return { ok: false, reason: error.message };
  if (!games || !games.length) return { ok: true, date, updated: 0, absent: 0 };

  const coords = opts.venueCoords || {};
  let updated = 0; let absent = 0;
  for (const g of games) {
    const c = coords[g.venue_id];
    if (!c) { absent += 1; continue; } // no coordinates → honest-absent
    let wx = null;
    try {
      const payload = opts.fetchJson
        ? await opts.fetchJson(ARCHIVE_URL(c.lat, c.lon, date))
        : (await require('axios').get(ARCHIVE_URL(c.lat, c.lon, date), { timeout: 60_000 })).data;
      const h = payload && payload.hourly;
      if (h && Array.isArray(h.time) && h.time.length) {
        const i = Math.min(h.time.length - 1, 19); // ~7pm local, typical first pitch
        wx = {
          wx_temp_f: h.temperature_2m ? h.temperature_2m[i] : null,
          wx_wind_speed_mph: h.wind_speed_10m ? h.wind_speed_10m[i] : null,
          wx_wind_direction_deg: h.wind_direction_10m ? h.wind_direction_10m[i] : null,
          wx_precip_mm: h.precipitation ? h.precipitation[i] : null,
          wx_source: 'open_meteo_archive',
          wx_captured_at: new Date().toISOString(),
        };
      }
    } catch { wx = null; }
    if (!wx) { absent += 1; continue; } // never imputed
    const { error: e } = await sb.from('game_context').update(wx).eq('game_id', g.game_id);
    if (!e) updated += 1;
  }
  return { ok: true, date, updated, absent, candidates: games.length };
}

module.exports = {
  captureWeatherActual,
  ARCHIVE_URL,
  captureDate,
  rowsFromSchedule,
  accrualStatus,
  SCHEDULE_URL,
  HOME_GAMES_PER_SEASON,
};
