/**
 * /api/streaks/:sport (Session 23; Session 60 night2/B — the lens).
 *
 * Computed player streaks + form heat from cached game logs, every row
 * interpreted through the VYNDR lens: what the streak was built against,
 * tonight's matchup (opponent + MLB opposing SP w/ ERA), difficulty, and a
 * one-line read. NO paid API calls — warm Redis + the free ESPN/statsapi
 * caches only. Where a snapshot grade exists for a streaking player's
 * category, the grade letter rides along (the slate already shows locked
 * grades publicly — consistent, not a leak).
 *
 * Response: { sport, stat, streaks: [...], source: 'computed' }
 * An empty array is a valid, non-error state.
 */

const express = require('express');
const streaksService = require('../services/streaksService');
const { applyLens } = require('../services/streakLens');
const { loadRosterLogs } = require('../services/rosterLogs');
const { nameKey } = require('../utils/playerName');
const { createRateLimit } = require('../middleware/rateLimit');

// Lens context reads are best-effort: a dead cache or slow upstream must
// NEVER hang the public route — the lens just says less. Timer is unref'd
// so it can't keep the process (or Jest) alive.
function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((resolve) => {
      const t = setTimeout(() => resolve(null), ms);
      if (t.unref) t.unref();
    }),
  ]).catch(() => null);
}

const LENS_BUDGET_MS = 1500;
const inTest = () => process.env.NODE_ENV === 'test';

const router = express.Router();
// Session 32 — public throttle (60/min; pure engine over cached logs).
router.use(createRateLimit({ windowMs: 60_000, max: 60 }));

const MISSION_HEADER = { 'X-VYNDR-Mission': 'Streaks are the heartbeat' };

const SUPPORTED = new Set(['nba', 'wnba', 'mlb', 'nfl', 'soccer']);

function todayET() {
  return new Intl.DateTimeFormat('en-CA', {
    timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit',
  }).format(new Date());
}

/** Tonight's context for the lens — best-effort, time-bounded, degrades to
 *  less context rather than hanging. Skipped under NODE_ENV=test (the lens
 *  builder itself is unit-tested; route tests exercise engine + shape). */
async function lensContext(sport) {
  const ctx = { scheduleGames: [], pitcherGames: [] };
  if (inTest()) return ctx;
  const { cacheGet } = require('../utils/redis');
  const sched = await withTimeout(cacheGet(`schedule:${sport}:${todayET()}`), LENS_BUDGET_MS);
  if (Array.isArray(sched)) ctx.scheduleGames = sched;
  if (sport === 'mlb') {
    // Warm in prod (the slate's /pitchers endpoint fills the same caches);
    // hard-capped so a cold cache costs at most the budget, never a hang.
    const { getProbablePitchers } = require('../services/probablePitchers');
    const pg = await withTimeout(getProbablePitchers(todayET()), LENS_BUDGET_MS);
    if (Array.isArray(pg)) ctx.pitcherGames = pg;
  }
  return ctx;
}

/** Grade letters for streaking players (snapshot cache; public data). */
async function gradeJoin(sport) {
  if (inTest()) return {};
  try {
    const { cacheGet } = require('../utils/redis');
    const env = await withTimeout(cacheGet(`grades:${sport}`), LENS_BUDGET_MS);
    const map = {};
    for (const g of (env && env.grades) || []) {
      map[nameKey(g.player || g.player_name)] = map[nameKey(g.player || g.player_name)] || g.grade;
    }
    return map;
  } catch { return {}; }
}

router.get('/:sport', async (req, res) => {
  const sport = String(req.params.sport || '').toLowerCase();
  if (!SUPPORTED.has(sport)) {
    return res.status(404).set(MISSION_HEADER).json({ error: `No streaks for sport: ${sport}` });
  }
  const stat = req.query.stat ? String(req.query.stat).toLowerCase() : 'all';
  const limit = req.query.limit ? Math.max(0, parseInt(req.query.limit, 10) || 0) : 0;
  // Session 60 (night2/C) — a player's active streaks for the dossier page.
  const playerKey = req.query.player ? nameKey(String(req.query.player).slice(0, 60)) : null;

  try {
    let roster = await loadRosterLogs(sport);
    if (playerKey) roster = roster.filter((p) => nameKey(p.name) === playerKey);
    const streaks = streaksService.computeStreaks(roster, sport, { stat });
    // Session 60 — form heat (hot hitters/sluggers/shooters) joins the feed.
    const heat = streaksService.computeFormHeat(roster, sport, {})
      .filter((h) => stat === 'all' || h.category === stat);
    let rows = [...streaks, ...heat];
    if (limit > 0) rows = rows.slice(0, limit);

    // THE LENS — no raw streak renders alone.
    const [ctx, grades] = await Promise.all([lensContext(sport), gradeJoin(sport)]);
    rows = applyLens(rows, ctx).map((r) => ({ ...r, grade: grades[nameKey(r.player)] || null }));

    return res.set(MISSION_HEADER).json({ sport, stat, streaks: rows, source: 'computed' });
  } catch (err) {
    console.error(`[streaks/${sport}]`, err.message);
    return res.set(MISSION_HEADER).json({ sport, stat, streaks: [], source: 'computed' });
  }
});

module.exports = router;
