/**
 * Schedule service (Session 23).
 *
 * Today's game schedule from FREE ESPN scoreboards. NO odds-api credits
 * burned. Cache-aside: reads `schedule:{sport}:{date}` from Redis first;
 * on a miss it fetches the ESPN scoreboard directly (the same free
 * endpoint the PM2 pollers hit every 60s), normalizes, caches, returns.
 *
 * This dual path is deliberate. The pollers warm the cache during game
 * hours, but the endpoint must NEVER be empty just because a poller is
 * down or off-hours — so it self-heals by fetching ESPN on a cache miss.
 *
 * Everything here is free. The only paid/quota'd layer (Tank01 game
 * lines, odds-api props) is checked separately via the hasGameLines /
 * hasOdds flags, which read OTHER caches without ever triggering a fetch.
 */

const axios = require('axios');
const { cacheGet, cacheSet } = require('../utils/redis');
const { SPORT_CONFIG } = require('../config/sports');

function getSportConfig(sport) {
  return SPORT_CONFIG[String(sport || '').toLowerCase()] || null;
}

const HTTP_TIMEOUT_MS = 10_000;
const SCHEDULE_TTL = 60;          // 60s — mirrors poller cadence; live scores stay fresh
const STALE_TTL = 6 * 3600;       // stale-while-error fallback

/**
 * Today's date in ET as YYYY-MM-DD. Sports days roll over on ET, not UTC,
 * so a late west-coast game still counts as "today" past midnight UTC.
 */
function todayET() {
  const fmt = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'America/New_York',
    year: 'numeric', month: '2-digit', day: '2-digit',
  });
  return fmt.format(new Date()); // en-CA → YYYY-MM-DD
}

/**
 * Normalize one ESPN scoreboard event into VYNDR's schedule shape.
 * Defensive throughout — ESPN omits fields freely (no venue for neutral
 * sites, no broadcast until close to tip). A missing field becomes null,
 * never a throw.
 */
function normalizeEvent(ev) {
  if (!ev) return null;
  const comp = ev.competitions?.[0] || {};
  const competitors = comp.competitors || [];
  const home = competitors.find((c) => c.homeAway === 'home') || competitors[0] || {};
  const away = competitors.find((c) => c.homeAway === 'away') || competitors[1] || {};

  const team = (c) => ({
    name: c?.team?.displayName || c?.team?.name || c?.team?.shortDisplayName || null,
    abbreviation: c?.team?.abbreviation || null,
  });

  const score = (c) => {
    const n = Number(c?.score);
    return Number.isFinite(n) ? n : 0;
  };

  const state = ev.status?.type?.state || comp.status?.type?.state || null; // pre|in|post
  const hasScore = state === 'in' || state === 'post';

  // Broadcast: ESPN scatters this across competitions[].broadcasts and
  // geoBroadcasts. Take the first network name we can find.
  let broadcast = null;
  const bcasts = comp.broadcasts || [];
  if (bcasts[0]?.names?.[0]) broadcast = bcasts[0].names[0];
  else if (comp.geoBroadcasts?.[0]?.media?.shortName) broadcast = comp.geoBroadcasts[0].media.shortName;

  return {
    id: String(ev.id),
    homeTeam: team(home),
    awayTeam: team(away),
    gameTime: ev.date || comp.date || null,
    status: state,
    score: hasScore ? { home: score(home), away: score(away) } : null,
    venue: comp.venue?.fullName || null,
    broadcast,
    hasOdds: false,        // filled by enrichFlags
    hasGameLines: false,   // filled by enrichFlags
  };
}

/**
 * A game's ET date (YYYY-MM-DD) from its ISO start time, or null when the
 * timestamp is missing/unparseable.
 */
function gameDateET(iso) {
  if (!iso) return null;
  const t = new Date(iso);
  if (Number.isNaN(t.getTime())) return null;
  return new Intl.DateTimeFormat('en-CA', {
    timeZone: 'America/New_York',
    year: 'numeric', month: '2-digit', day: '2-digit',
  }).format(t);
}

/**
 * Fetch + normalize the ESPN scoreboard for a sport. Free endpoint.
 *
 * Session 57 (Phase 0) — MUST be pinned to the requested date. Without a
 * `?dates=` param ESPN returns the NEAREST slate in the off-season (the Jun 13
 * NYK@SA Finals game kept rendering as "tonight" all July). We pass the param
 * AND filter defensively: only events whose ET date matches `date` survive;
 * events with no parseable date are dropped — the slate derives from the live
 * feed only, never from whatever ESPN felt like returning.
 */
async function fetchScheduleFromEspn(sport, date) {
  const cfg = getSportConfig(sport);
  if (!cfg || !cfg.espnScoreboard) return null;
  const sep = cfg.espnScoreboard.includes('?') ? '&' : '?';
  const url = date
    ? `${cfg.espnScoreboard}${sep}dates=${String(date).replace(/-/g, '')}`
    : cfg.espnScoreboard;
  const res = await axios.get(url, { timeout: HTTP_TIMEOUT_MS });
  const events = res.data?.events || [];
  const games = events.map(normalizeEvent).filter(Boolean);
  return date ? games.filter((g) => gameDateET(g.gameTime) === date) : games;
}

/**
 * Cache-aside schedule read. Returns an array of normalized games
 * (possibly empty — empty is a valid "no games today", not an error).
 * Returns null only when the sport is unknown / unsupported.
 */
async function getSchedule(sport, date) {
  const cfg = getSportConfig(sport);
  if (!cfg || !cfg.espnScoreboard) return null;
  const key = `schedule:${sport}:${date}`;

  const cached = await cacheGet(key);
  if (cached !== null) return cached;

  try {
    const games = await fetchScheduleFromEspn(sport, date);
    if (Array.isArray(games)) {
      await cacheSet(key, games, SCHEDULE_TTL);
      await cacheSet(`${key}:stale`, games, STALE_TTL);
      return games;
    }
    return [];
  } catch (err) {
    console.warn(`[schedule] ESPN fetch failed for ${sport}:`, err.message);
    const stale = await cacheGet(`${key}:stale`);
    return stale !== null ? stale : [];
  }
}

/**
 * Per-game enrichment: set hasOdds / hasGameLines by peeking at the OTHER
 * caches. Reads only — never triggers a fetch, never burns quota. The
 * odds-api props cache and the Tank01 game-lines cache are date-keyed
 * (one blob per sport+date), so a single read tells us whether ANY game
 * that day has data; we apply it to every game in the slate.
 *
 * A future refinement could match per-game, but the date-level flag is
 * the honest signal today: "props exist for this slate" / "lines exist
 * for this slate".
 */
async function enrichFlags(sport, date, games) {
  if (!Array.isArray(games) || games.length === 0) return games;
  const ymd = String(date).replace(/-/g, '');

  // odds-api props cache — oddsService writes `odds:{sport}:{utcDate}`
  // as `{ updated_at, props, spreads }`. The slate `date` is ET, so try
  // the ET key first then the UTC key (they differ only past midnight).
  const utcDate = new Date().toISOString().split('T')[0];
  const oddsCache =
    (await cacheGet(`odds:${sport}:${date}`)) ??
    (await cacheGet(`odds:${sport}:${utcDate}`)) ??
    (await cacheGet(`odds:${sport}`));
  const hasOdds = hasPropsData(oddsCache);

  // Tank01 game-lines cache — adapters write tank01:{sport}:odds:{ymd}.
  const linesCache = await cacheGet(`tank01:${sport}:odds:${ymd}`);
  const hasGameLines = hasLinesData(linesCache);

  return games.map((g) => ({ ...g, hasOdds, hasGameLines }));
}

function hasPropsData(cache) {
  if (!cache) return false;
  if (Array.isArray(cache)) return cache.length > 0;
  if (Array.isArray(cache.props)) return cache.props.length > 0;
  if (Array.isArray(cache.games)) return cache.games.length > 0;
  if (typeof cache === 'object') return Object.keys(cache).length > 0;
  return false;
}

function hasLinesData(cache) {
  if (!cache) return false;
  const body = cache.body || cache;
  if (Array.isArray(body)) return body.length > 0;
  if (typeof body === 'object') return Object.keys(body).length > 0;
  return false;
}

// ---------------------------------------------------------------------
// ESPN Summary enrichment (Session 30).
//
// The ESPN `summary?event=` endpoint returns rich per-game data the
// scoreboard doesn't: real-time injuries, ESPN Bet odds, ATS records,
// stat leaders, and the full box score. FREE, no auth, unlimited. We
// cache it briefly (10 min) since injuries/odds shift pre-game.
// ---------------------------------------------------------------------

const ESPN_SPORT_PATHS = Object.freeze({
  nba: 'basketball/nba',
  wnba: 'basketball/wnba',
  mlb: 'baseball/mlb',
  nfl: 'football/nfl',
  nhl: 'hockey/nhl',
  ncaab: 'basketball/mens-college-basketball',
});

const SUMMARY_TTL = 10 * 60; // 10 min

/**
 * Enriched per-game data for one ESPN event. Returns a defensive shape
 * with empty defaults — some games lack some sections, and an invalid
 * eventId must NOT crash (returns the empty defaults).
 */
async function getGameSummary(sport, eventId) {
  const path = ESPN_SPORT_PATHS[String(sport || '').toLowerCase()];
  const empty = { injuries: [], odds: [], ats: null, leaders: [], boxscore: null, rosters: [] };
  if (!path || !eventId) return empty;

  const key = `espn:summary:${sport}:${eventId}`;
  const cached = await cacheGet(key);
  if (cached !== null) return cached;

  try {
    const url = `https://site.api.espn.com/apis/site/v2/sports/${path}/summary?event=${encodeURIComponent(eventId)}`;
    const res = await axios.get(url, { timeout: HTTP_TIMEOUT_MS });
    const data = res && res.data ? res.data : {};
    const out = {
      injuries: Array.isArray(data.injuries) ? data.injuries : [],
      odds: Array.isArray(data.odds) ? data.odds : [],
      ats: data.againstTheSpread || null,
      leaders: Array.isArray(data.leaders) ? data.leaders : [],
      boxscore: data.boxscore || null,
      // Wave 2B — pre-game lineups carry athlete id + direct headshot href, the
      // reliable NBA/WNBA headshot source (espnAthleteIndex harvests it). Passed
      // through defensively; absent → [] (no fabricated roster).
      rosters: Array.isArray(data.rosters) ? data.rosters : [],
    };
    await cacheSet(key, out, SUMMARY_TTL);
    return out;
  } catch (err) {
    console.warn(`[schedule] ESPN summary fetch failed for ${sport}/${eventId}:`, err.message);
    return empty;
  }
}

module.exports = {
  getSchedule,
  enrichFlags,
  todayET,
  getGameSummary,
  __internals: { normalizeEvent, fetchScheduleFromEspn, hasPropsData, hasLinesData, ESPN_SPORT_PATHS },
};
