/**
 * Team stats cache — daily refresh, Redis-backed.
 *
 * Source priority for each sport:
 *   nba / wnba / ncaab    : ESPN team statistics endpoint
 *   mlb                   : ESPN + MLB Stats API team totals
 *   nfl / ncaafb          : ESPN + CFBD talent composite (college)
 *   nhl                   : ESPN team statistics endpoint
 *
 * The cache key is `team_stats:{sport}:{teamAbbr}` with a 24h TTL. The
 * refresh function (called from n8n or app startup) walks every team in
 * the sport and writes one cache entry per team. Rate-limited at 1
 * request per 2 seconds to be respectful to ESPN.
 *
 * Per-team payload normalizes into a uniform shape; values not available
 * for a sport are simply omitted (mirrors the feature-cache philosophy).
 *
 *   {
 *     offensive_rating, defensive_rating, pace, opponent_ppg,
 *     team_fg_pct, team_3pt_pct, team_ft_rate,
 *     opponent_fg_pct, opponent_3pt_pct,
 *     team_k_rate,            // MLB only
 *     defensive_rank,         // 1-N (1 = best D)
 *     by_stat: { points: { allowed: N, rank: 1-30 }, ... }
 *   }
 */

const axios = require('axios');
const { cacheGet, cacheSet } = require('../../utils/redis');
const { createLimiter, createCircuitBreaker } = require('../../utils/rateLimiter');

const ESPN_BASE = 'https://site.api.espn.com/apis/site/v2/sports';
const CACHE_TTL_SECONDS = 24 * 60 * 60;
const HTTP_TIMEOUT_MS = 10_000;

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

const limiter = createLimiter({ tokensPerInterval: 30, interval: 60_000 }); // 1/2s
const breaker = createCircuitBreaker({ failureThreshold: 3, resetTimeout: 60_000 });

function teamCacheKey(sport, teamAbbr) {
  return `team_stats:${sport}:${String(teamAbbr).toUpperCase()}`;
}

// Pull a numeric value out of ESPN's labeled statistics arrays. ESPN
// returns categories with .stats[] of { name, value, displayValue, abbreviation }.
function pickStat(categoryStats, name) {
  if (!Array.isArray(categoryStats)) return null;
  const match = categoryStats.find(
    (s) =>
      (s?.name || '').toLowerCase() === name.toLowerCase()
      || (s?.abbreviation || '').toLowerCase() === name.toLowerCase()
  );
  if (!match) return null;
  const v = Number(match.value);
  return Number.isFinite(v) ? v : null;
}

function flattenTeamStats(payload) {
  // ESPN returns: { team, season, splits: [...], stats: [...] } depending on
  // endpoint. Most commonly: payload.results.stats[]/categories[] for
  // /teams/{id}/statistics
  // Session 64 — ESPN's CURRENT shape is `results.stats` = an OBJECT carrying
  // `categories[]` (general / offensive / defensive), each with `stats[]`. The
  // old code assumed an ARRAY and did `for...of` on it, which threw "buckets is
  // not iterable" for EVERY team — verified live: captured 0, errored 15/15.
  // Because `refreshTeamStats` had no production callers until S63, that crash
  // was invisible. Normalize every known shape to an array of buckets.
  const raw = payload?.results?.stats ?? payload?.stats ?? [];
  const buckets = Array.isArray(raw)
    ? raw
    : (Array.isArray(raw?.categories) ? raw.categories : []);
  const all = [];
  for (const b of buckets) {
    if (Array.isArray(b?.stats)) all.push(...b.stats);
    if (Array.isArray(b?.splits)) {
      for (const split of b.splits) {
        if (Array.isArray(split?.stats)) all.push(...split.stats);
      }
    }
  }
  return all;
}

function normalizeBasketball(payload) {
  const all = flattenTeamStats(payload);
  return {
    offensive_rating: pickStat(all, 'offensiveRating') ?? pickStat(all, 'oRtg'),
    defensive_rating: pickStat(all, 'defensiveRating') ?? pickStat(all, 'dRtg'),
    pace: pickStat(all, 'pace'),
    opponent_ppg: pickStat(all, 'avgPointsAgainst') ?? pickStat(all, 'oppPPG'),
    team_fg_pct: pickStat(all, 'fieldGoalPct'),
    team_3pt_pct: pickStat(all, 'threePointFieldGoalPct') ?? pickStat(all, 'threePtPct'),
    team_ft_rate: pickStat(all, 'freeThrowAttemptRate'),
    opponent_fg_pct: pickStat(all, 'opponentFieldGoalPct'),
    opponent_3pt_pct: pickStat(all, 'opponentThreePointFieldGoalPct'),
  };
}

function normalizeMlb(payload) {
  const all = flattenTeamStats(payload);
  return {
    team_k_rate: pickStat(all, 'strikeOutRate') ?? pickStat(all, 'strikeoutsPerNine'),
    opponent_ppg: pickStat(all, 'runsAgainst'),
  };
}

function normalizeFootball(payload) {
  const all = flattenTeamStats(payload);
  return {
    offensive_rating: pickStat(all, 'totalPoints'),
    defensive_rating: pickStat(all, 'pointsAgainst'),
    opponent_ppg: pickStat(all, 'avgPointsAgainst'),
  };
}

function normalize(sport, payload) {
  switch (sport) {
    case 'nba':
    case 'wnba':
    case 'ncaab':
      return normalizeBasketball(payload);
    case 'mlb':
      return normalizeMlb(payload);
    case 'nfl':
    case 'ncaafb':
      return normalizeFootball(payload);
    case 'nhl':
    default:
      return flattenTeamStats(payload).reduce((acc, s) => {
        if (s?.name && Number.isFinite(Number(s.value))) acc[s.name] = Number(s.value);
        return acc;
      }, {});
  }
}

async function fetchTeamStatsRaw(sport, teamId) {
  const path = SPORT_PATH[sport];
  if (!path) return null;
  await limiter.waitForToken();
  return breaker.call(async () => {
    const res = await axios.get(`${ESPN_BASE}/${path}/teams/${teamId}/statistics`, {
      timeout: HTTP_TIMEOUT_MS,
    });
    return res.data;
  });
}

async function listTeams(sport) {
  const path = SPORT_PATH[sport];
  if (!path) return [];
  await limiter.waitForToken();
  const res = await axios.get(`${ESPN_BASE}/${path}/teams`, { timeout: HTTP_TIMEOUT_MS });
  const groups = res.data?.sports?.[0]?.leagues?.[0]?.teams || [];
  return groups
    .map((t) => t?.team)
    .filter(Boolean)
    .map((t) => ({ id: String(t.id), abbr: t.abbreviation, name: t.displayName }));
}

async function refreshTeamStats(sport) {
  const teams = await listTeams(sport);
  // Two-pass: fetch every team's stats first, then rank across the league
  // so we can normalize opponent rank to 0..1. A raw defensive_rating
  // means different things across sports (NBA ~100-120, NHL ~2.5-3.5
  // goals/game), so the cache stores both: raw + normalized.
  const fetched = [];
  let captured = 0;
  let errored = 0;
  for (const team of teams) {
    try {
      const raw = await fetchTeamStatsRaw(sport, team.id);
      if (!raw) { errored += 1; continue; }
      const stats = normalize(sport, raw);
      fetched.push({ team, stats });
      captured += 1;
    } catch (err) {
      if (err?.code !== 'CIRCUIT_OPEN') {
        console.warn(`[teamStats] ${sport}/${team.abbr} failed: ${err?.message}`);
      }
      errored += 1;
    }
  }

  // Rank teams by defensive_rating ascending (lower allowed = better D).
  // Then map each team's rank to [0, 1] — 0 = best D (hardest matchup),
  // 1 = worst D (easiest matchup). The feature cache uses this directly.
  const withDef = fetched.filter((f) => Number.isFinite(Number(f.stats.defensive_rating)));
  withDef.sort((a, b) => Number(a.stats.defensive_rating) - Number(b.stats.defensive_rating));
  const total = withDef.length;
  for (let i = 0; i < withDef.length; i += 1) {
    withDef[i].stats.defensive_rank_normalized = total > 1 ? i / (total - 1) : 0.5;
  }

  for (const { team, stats } of fetched) {
    await cacheSet(
      teamCacheKey(sport, team.abbr),
      { ...stats, team_id: team.id, team_name: team.name },
      CACHE_TTL_SECONDS,
    );
  }
  return { captured, errored, total: teams.length };
}

async function getTeamStats(sport, teamAbbr) {
  return cacheGet(teamCacheKey(sport, teamAbbr));
}

// Returns the opponent's normalized defensive rank on a 0..1 scale.
//   0.0 = best defense in the league (hardest matchup)
//   1.0 = worst defense (easiest matchup)
// Comparable across sports — NBA, NHL, NFL all collapse to the same
// scale even though their raw defensive_rating values differ by orders
// of magnitude. Returns null when we have no cache entry yet.
async function getOpponentRank(sport, teamAbbr, _statType) {
  const stats = await getTeamStats(sport, teamAbbr);
  if (!stats) return null;
  if (Number.isFinite(Number(stats.defensive_rank_normalized))) {
    return Number(stats.defensive_rank_normalized);
  }
  // Backward-compat: if the cache predates the normalization upgrade, we
  // can't normalize a single-team read in isolation — return null and
  // let the feature cache omit the feature rather than emit a raw value.
  return null;
}

module.exports = {
  refreshTeamStats,
  getTeamStats,
  getOpponentRank,
  __internals: { listTeams, normalize, teamCacheKey, limiter, breaker },
};
