/**
 * football-data.org adapter.
 *
 * Free tier:
 *   - 10 requests per minute (HARD rate limit on the API side — 429 on overflow)
 *   - Fixtures, standings, squads, scorers only (NO per-player game stats)
 *   - Requires `FOOTBALL_DATA_API_KEY` env var
 *
 * Design:
 *   - All responses cached in Redis with tier-appropriate TTLs.
 *   - Built-in token bucket holds calls at 8 req/min (2-req safety margin).
 *   - When the bucket is empty, stale-while-revalidate returns whatever
 *     is in Redis even if the TTL has lapsed — better to serve old data
 *     than to crash the request path.
 *   - When the API key is missing, every method returns null without
 *     touching the network. Callers (feature extractor, poller) treat
 *     null as "no data available — degrade gracefully".
 *   - All errors are caught and logged, never thrown. Same contract as
 *     the existing intelligence services.
 *
 * Endpoints exposed:
 *   getWorldCupFixtures(),
 *   getWorldCupStandings(),
 *   getWorldCupScorers(),
 *   getTeamSquad(teamId),
 *   getLeagueFixtures(competitionCode),   // generic — EPL/PD/BL1/...
 *   getLeagueStandings(competitionCode),
 *   getLeagueScorers(competitionCode).
 *
 * Competition codes: WC (World Cup), PL (Premier League),
 * PD (La Liga), BL1 (Bundesliga), SA (Serie A), FL1 (Ligue 1),
 * CL (Champions League), MLS (MLS), LIGA (Liga MX).
 */

const axios = require('axios');
const { cacheGet, cacheSet } = require('../../utils/redis');
// Session 21 — gateway wrap. The adapter's in-process token bucket
// (8 req/min) is the FIRST line of defense (synchronous; no Redis
// round-trip), and the gateway's per-minute counter is the
// observability layer (cross-process, visible in /admin). The
// bucket short-circuits before the gateway in burst scenarios, so
// the counter only ticks up for calls that actually went over the
// wire.
const gateway = require('../providerGateway');

const BASE_URL = 'https://api.football-data.org/v4';
const HTTP_TIMEOUT_MS = 8_000;

// Cache TTLs (seconds) — tier matched to data volatility.
const TTL = Object.freeze({
  fixtures:  6 * 3600,    // 6h — drifts as match status changes
  standings: 12 * 3600,   // 12h — moves once per matchday at most
  squad:     24 * 3600,   // 24h — only changes between matchdays
  scorers:   6 * 3600,    // 6h — moves only on goal events
});

// Token bucket — refills 8 tokens per 60-second window. We hold 2 tokens
// below the 10 req/min ceiling so a burst from the poller can't 429 the
// adapter on the user request path.
const BUCKET_MAX = 8;
const BUCKET_REFILL_MS = 60_000;

let _tokens = BUCKET_MAX;
let _lastRefill = 0;

function nowMs() {
  // jest.fakeTimers compatible — process.uptime is monotonic.
  return Math.floor(process.uptime() * 1000);
}

function refillBucket() {
  const now = nowMs();
  if (_lastRefill === 0) _lastRefill = now;
  const elapsed = now - _lastRefill;
  if (elapsed >= BUCKET_REFILL_MS) {
    // Full refill on window boundary — simpler than fractional refills,
    // and matches how the API's own per-minute window resets.
    _tokens = BUCKET_MAX;
    _lastRefill = now;
  }
}

function tryConsumeToken() {
  refillBucket();
  if (_tokens <= 0) return false;
  _tokens -= 1;
  return true;
}

function hasApiKey() {
  return !!process.env.FOOTBALL_DATA_API_KEY;
}

// One central HTTP wrapper — applies key, timeout, rate-limit check, and
// stale-while-revalidate fallback. Returns parsed JSON or null. Never throws.
async function fetchWithCache(path, cacheKey, ttl) {
  // 1. Try fresh cache (within TTL).
  const fresh = await cacheGet(cacheKey);
  if (fresh !== null) return fresh;

  // 2. No key → can't fetch. Return null (callers degrade).
  if (!hasApiKey()) return null;

  // 3. Token bucket — if we're rate-limited, try the stale-while-revalidate
  // key. If THAT misses too, give up rather than 429'ing the upstream API.
  if (!tryConsumeToken()) {
    const stale = await cacheGet(`${cacheKey}:stale`);
    if (stale !== null) return stale;
    return null;
  }

  // 4. Hit the network.
  try {
    const res = await gateway.fetch(
      'football-data',
      () => axios.get(`${BASE_URL}${path}`, {
        headers: { 'X-Auth-Token': process.env.FOOTBALL_DATA_API_KEY },
        timeout: HTTP_TIMEOUT_MS,
      }),
      { capability: 'fixtures', sport: 'soccer' },
    );
    const body = res.data;
    if (body && typeof body === 'object') {
      // Write to BOTH the live and stale keys. Stale key has a much
      // longer TTL so stale-while-revalidate always finds something.
      await cacheSet(cacheKey, body, ttl);
      await cacheSet(`${cacheKey}:stale`, body, ttl * 4);
    }
    return body;
  } catch (err) {
    console.warn('[footballData] fetch failed:', path, err.message);
    // Network failure — fall back to stale if we have it.
    const stale = await cacheGet(`${cacheKey}:stale`);
    if (stale !== null) return stale;
    return null;
  }
}

// ---- Public surface ----

async function getLeagueFixtures(competitionCode) {
  if (!competitionCode) return null;
  const code = String(competitionCode).toUpperCase();
  const data = await fetchWithCache(
    `/competitions/${code}/matches`,
    `soccer:${code.toLowerCase()}:fixtures`,
    TTL.fixtures,
  );
  // null → API unavailable (no key, fetch failure, drained bucket+no stale)
  if (data === null) return null;
  // Object present but no matches array → API returned nothing meaningful.
  if (!Array.isArray(data.matches)) return [];
  // Project to a stable shape so callers don't depend on API field names.
  return data.matches.map((m) => ({
    id: m.id,
    homeTeam: m.homeTeam?.name || m.homeTeam?.shortName || null,
    awayTeam: m.awayTeam?.name || m.awayTeam?.shortName || null,
    utcDate: m.utcDate || null,
    status: m.status || null,
    score: m.score || null,
    matchday: m.matchday ?? null,
    venue: m.venue || null,
    competition: code,
  }));
}

async function getLeagueStandings(competitionCode) {
  if (!competitionCode) return null;
  const code = String(competitionCode).toUpperCase();
  const data = await fetchWithCache(
    `/competitions/${code}/standings`,
    `soccer:${code.toLowerCase()}:standings`,
    TTL.standings,
  );
  if (data === null) return null;
  if (!Array.isArray(data.standings)) return [];
  return data.standings;
}

async function getLeagueScorers(competitionCode) {
  if (!competitionCode) return null;
  const code = String(competitionCode).toUpperCase();
  const data = await fetchWithCache(
    `/competitions/${code}/scorers`,
    `soccer:${code.toLowerCase()}:scorers`,
    TTL.scorers,
  );
  if (data === null) return null;
  if (!Array.isArray(data.scorers)) return [];
  // Project: { player: {name, position, nationality}, team, goals, assists, playedMatches, ... }
  return data.scorers.map((s) => ({
    name: s.player?.name || null,
    position: s.player?.position || null,
    nationality: s.player?.nationality || null,
    team: s.team?.name || null,
    goals: s.goals ?? 0,
    assists: s.assists ?? 0,
    playedMatches: s.playedMatches ?? 0,
    minutesPlayed: s.minutesPlayed ?? null,
  }));
}

async function getTeamSquad(teamId) {
  if (!teamId) return null;
  const data = await fetchWithCache(
    `/teams/${teamId}`,
    `soccer:team:${teamId}:squad`,
    TTL.squad,
  );
  if (data === null) return null;
  if (!Array.isArray(data.squad)) return [];
  return data.squad.map((p) => ({
    id: p.id,
    name: p.name,
    position: p.position || null,
    nationality: p.nationality || null,
    shirtNumber: p.shirtNumber ?? null,
    dateOfBirth: p.dateOfBirth || null,
  }));
}

// Convenience wrappers for the World Cup — most-used competition code.
async function getWorldCupFixtures()  { return getLeagueFixtures('WC'); }
async function getWorldCupStandings() { return getLeagueStandings('WC'); }
async function getWorldCupScorers()   { return getLeagueScorers('WC'); }

module.exports = {
  getLeagueFixtures,
  getLeagueStandings,
  getLeagueScorers,
  getTeamSquad,
  getWorldCupFixtures,
  getWorldCupStandings,
  getWorldCupScorers,
  hasApiKey,
  __internals: {
    BASE_URL,
    TTL,
    BUCKET_MAX,
    tryConsumeToken,
    refillBucket,
    resetBucketForTests: () => { _tokens = BUCKET_MAX; _lastRefill = 0; },
  },
};
