/**
 * combatAdapter — ESPN MMA/UFC (FREE JSON, no auth) → normalized fight cards
 * + tale-of-the-tape (Wave 6, combat intelligence).
 *
 * DATA SEMANTICS: fighter records / physicals are REAL sourced facts. We never
 * fabricate. `Number(null) === 0` is the trap — an absent stat (reach, stance,
 * finish counts) stays ABSENT (null), never coerced to 0. ESPN's MMA striking/
 * grappling granularity is THINNER than ufcstats; when a field is absent the
 * tape says less, never invents.
 *
 * Source: site.api.espn.com/apis/site/v2/sports/mma/ufc/scoreboard
 *   - The scoreboard `events[]` are UFC CARDS. Each event carries many
 *     `competitions[]` — one per BOUT. Each bout has 2 competitors (fighters)
 *     with athlete name, W-L-D record, weight class, scheduled rounds, and the
 *     ESPN athlete id (parsed from the player-card link href). Stance/reach are
 *     NOT in the free scoreboard → left null (absent), wired for a later enrich.
 *
 * Odds (moneyline + round total) come from the odds-api MMA feed and are parsed
 * by the PURE `normalizeCombatOdds` here (odds-api event shape → per-bout ML +
 * round total). VYNDR never generates odds — these are REAL book numbers.
 *
 * Everything is defensive: an unrecognized shape yields an empty result, never
 * a throw. `fetchImpl` is injectable so tests never touch the network.
 */

const axios = require('axios');
const { cacheGet, cacheSet } = require('../../utils/redis');
const { ALLOWED_BOOKS } = require('../../utils/oddsNormalizer');

const ESPN_MMA_SCOREBOARD = 'https://site.api.espn.com/apis/site/v2/sports/mma/ufc/scoreboard';
const HTTP_TIMEOUT_MS = 10_000;
const CARDS_TTL = 15 * 60;     // 15 min — cards move slowly; keep it cheap
const STALE_TTL = 6 * 3600;    // stale-while-error fallback

/** Real finite number or null — the absent-beats-zero guard. */
function numOrNull(v) {
  if (v === null || v === undefined || v === '') return null;
  const n = Number(v);
  return Number.isFinite(n) ? n : null;
}

/** Today's ET date (YYYY-MM-DD). Fight days roll on ET like the other sports. */
function todayET() {
  return new Intl.DateTimeFormat('en-CA', {
    timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit',
  }).format(new Date());
}

/** ET date (YYYY-MM-DD) of an ISO timestamp, or null if unparseable. */
function dateET(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);
}

/** Extract the ESPN athlete id from a fighter's link hrefs (…/id/4801725/…). */
function parseAthleteId(links) {
  if (!Array.isArray(links)) return null;
  for (const l of links) {
    const m = /\/id\/(\d+)\//.exec(l && l.href ? String(l.href) : '');
    if (m) return m[1];
  }
  return null;
}

/** Parse a "W-L-D" record summary into structured parts. Absent → nulls. */
function parseRecord(competitor) {
  const recs = competitor && Array.isArray(competitor.records) ? competitor.records : [];
  const overall = recs.find((r) => r && (r.type === 'total' || r.name === 'overall')) || recs[0];
  const summary = overall && typeof overall.summary === 'string' ? overall.summary : null;
  let wins = null, losses = null, draws = null;
  if (summary) {
    const m = /^(\d+)\s*-\s*(\d+)(?:\s*-\s*(\d+))?/.exec(summary.trim());
    if (m) {
      wins = numOrNull(m[1]);
      losses = numOrNull(m[2]);
      draws = m[3] != null ? numOrNull(m[3]) : 0;
    }
  }
  // Display form (26–1 with an en-dash, or 26–1–0 when a draw exists).
  let display = null;
  if (wins != null && losses != null) {
    display = draws ? `${wins}–${losses}–${draws}` : `${wins}–${losses}`;
  }
  return { wins, losses, draws, summary, display };
}

/** Normalize ONE fighter (an ESPN competitor). Defensive; unknown fields null. */
function normalizeFighter(competitor) {
  if (!competitor || typeof competitor !== 'object') return null;
  const a = competitor.athlete || {};
  const name = a.displayName || a.fullName || a.shortName || null;
  if (!name) return null;
  return {
    id: parseAthleteId(a.links),
    name,
    shortName: a.shortName || null,
    record: parseRecord(competitor),
    winner: competitor.winner === true ? true : (competitor.winner === false ? false : null),
    // Physicals absent from the free scoreboard — kept for a future enrich.
    stance: null,
    reach: null,
  };
}

/** Normalize ONE bout (an ESPN competition). Returns null on an unusable shape. */
function normalizeBout(comp) {
  if (!comp || typeof comp !== 'object') return null;
  const competitors = Array.isArray(comp.competitors) ? comp.competitors : [];
  if (competitors.length < 2) return null;
  const ordered = [...competitors].sort((x, y) => (x.order ?? 0) - (y.order ?? 0));
  const fighters = ordered.map(normalizeFighter).filter(Boolean);
  if (fighters.length < 2) return null;
  const st = comp.status && comp.status.type ? comp.status.type : {};
  return {
    id: comp.id != null ? String(comp.id) : null,
    weightClass: (comp.type && (comp.type.text || comp.type.abbreviation)) || null,
    rounds: numOrNull(comp.format && comp.format.regulation && comp.format.regulation.periods),
    status: st.state || null,        // pre | in | post
    completed: st.completed === true,
    fighters,
  };
}

/**
 * Normalize a raw ESPN scoreboard payload into VYNDR fight cards. PURE +
 * defensive — a shape it doesn't recognize returns { events: [] }, never throws.
 */
function normalizeScoreboard(raw, { date } = {}) {
  const events = raw && Array.isArray(raw.events) ? raw.events : [];
  const cards = [];
  for (const ev of events) {
    if (!ev || typeof ev !== 'object') continue;
    const evDate = ev.date || null;
    // Date-pin defensively (same discipline as scheduleService S57): when a
    // date is requested, only events on that ET date survive.
    if (date && dateET(evDate) !== date) continue;
    const comps = Array.isArray(ev.competitions) ? ev.competitions : [];
    const bouts = comps.map(normalizeBout).filter(Boolean);
    if (bouts.length === 0 && !ev.id) continue;
    const comp0 = comps[0] || {};
    cards.push({
      id: ev.id != null ? String(ev.id) : null,
      name: ev.name || null,
      shortName: ev.shortName || null,
      date: evDate,
      dateET: dateET(evDate),
      venue: (comp0.venue && (comp0.venue.fullName || comp0.venue.shortName)) || null,
      bouts,
    });
  }
  return { events: cards };
}

async function doFetch(url, fetchImpl) {
  if (fetchImpl) return fetchImpl(url);
  const res = await axios.get(url, { timeout: HTTP_TIMEOUT_MS });
  return res.data;
}

/**
 * getFightCards(date, opts) — cache-aside UFC cards for an ET date.
 * opts: { fetchImpl, cacheGet, cacheSet }. NO odds-api credits (ESPN is free).
 * Off-card windows return { events: [] } (honest empty), never a throw.
 */
async function getFightCards(date = todayET(), opts = {}) {
  const cGet = opts.cacheGet || cacheGet;
  const cSet = opts.cacheSet || cacheSet;
  const key = `combat:cards:${date}`;
  try {
    const cached = await cGet(key);
    if (cached && Array.isArray(cached.events)) return { ...cached, source: 'cache' };
  } catch { /* cache miss → live */ }

  try {
    const sep = ESPN_MMA_SCOREBOARD.includes('?') ? '&' : '?';
    const url = `${ESPN_MMA_SCOREBOARD}${sep}dates=${String(date).replace(/-/g, '')}`;
    const raw = await doFetch(url, opts.fetchImpl);
    const normalized = normalizeScoreboard(raw, { date });
    const payload = { date, events: normalized.events, source: 'espn' };
    try { await cSet(key, payload, CARDS_TTL); } catch { /* best-effort */ }
    return payload;
  } catch (err) {
    // Stale-while-error, else honest empty.
    try {
      const stale = await cGet(key);
      if (stale && Array.isArray(stale.events)) return { ...stale, source: 'stale' };
    } catch { /* ignore */ }
    console.warn('[combatAdapter] getFightCards failed:', err && err.message);
    return { date, events: [], source: 'espn' };
  }
}

/**
 * getFightCard(eventId, opts) — one UFC card (all its bouts). The scoreboard
 * carries every bout, so we locate the event by id. opts may pass `date` to
 * pin the scoreboard fetch. Returns null when the id isn't found.
 */
async function getFightCard(eventId, opts = {}) {
  const id = String(eventId || '');
  if (!id) return null;
  const cGet = opts.cacheGet || cacheGet;
  const cSet = opts.cacheSet || cacheSet;
  try {
    const sep = ESPN_MMA_SCOREBOARD.includes('?') ? '&' : '?';
    const url = opts.date
      ? `${ESPN_MMA_SCOREBOARD}${sep}dates=${String(opts.date).replace(/-/g, '')}`
      : ESPN_MMA_SCOREBOARD;
    const key = `combat:card:${id}`;
    if (!opts.fetchImpl) {
      try {
        const cached = await cGet(key);
        if (cached && cached.id) return { ...cached, source: 'cache' };
      } catch { /* miss */ }
    }
    const raw = await doFetch(url, opts.fetchImpl);
    // Don't date-filter here — we're locating a specific event by id.
    const normalized = normalizeScoreboard(raw, {});
    const card = normalized.events.find((e) => e.id === id) || null;
    if (card) { try { await cSet(key, card, CARDS_TTL); } catch { /* ignore */ } }
    return card;
  } catch (err) {
    console.warn('[combatAdapter] getFightCard failed:', err && err.message);
    return null;
  }
}

/** Best (highest) American-odds price wins for a given side. Absent → null. */
function bestPrice(current, price) {
  const p = numOrNull(price);
  if (p == null) return current;
  if (current == null) return p;
  return p > current ? p : current; // best payout for the bettor
}

/**
 * normalizeCombatOdds(eventsWithOdds) — PURE parse of the odds-api MMA event
 * odds array into per-bout moneyline + round total. VYNDR never generates
 * these — they are REAL book numbers. Only allow-listed US books count.
 * Returns a map keyed by matchup ("fighterA|fighterB", lowercased) so the
 * combat surface can join odds to the ESPN bout. Absent market → absent side.
 */
function normalizeCombatOdds(eventsWithOdds) {
  const out = {};
  const list = Array.isArray(eventsWithOdds) ? eventsWithOdds : [];
  for (const ev of list) {
    if (!ev || typeof ev !== 'object') continue;
    const home = ev.home_team || null;
    const away = ev.away_team || null;
    if (!home && !away) continue;
    const ml = { home: null, away: null };
    const roundTotal = { line: null, over: null, under: null };
    const books = Array.isArray(ev.bookmakers) ? ev.bookmakers : [];
    for (const bk of books) {
      if (!bk || !ALLOWED_BOOKS.has(bk.key)) continue;
      const markets = Array.isArray(bk.markets) ? bk.markets : [];
      for (const mk of markets) {
        const outcomes = Array.isArray(mk && mk.outcomes) ? mk.outcomes : [];
        if (mk.key === 'h2h') {
          for (const o of outcomes) {
            if (o.name === home) ml.home = bestPrice(ml.home, o.price);
            else if (o.name === away) ml.away = bestPrice(ml.away, o.price);
          }
        } else if (mk.key === 'totals') {
          for (const o of outcomes) {
            const point = numOrNull(o.point);
            if (point == null) continue;
            if (roundTotal.line == null) roundTotal.line = point;
            // Only pair the primary posted line (first seen).
            if (point !== roundTotal.line) continue;
            if (o.name === 'Over') roundTotal.over = bestPrice(roundTotal.over, o.price);
            else if (o.name === 'Under') roundTotal.under = bestPrice(roundTotal.under, o.price);
          }
        }
      }
    }
    const key = `${String(home || '').toLowerCase()}|${String(away || '').toLowerCase()}`;
    out[key] = {
      eventId: ev.id != null ? String(ev.id) : null,
      home,
      away,
      commence_time: ev.commence_time || null,
      moneyline: ml,
      roundTotal: (roundTotal.over != null || roundTotal.under != null) ? roundTotal : null,
    };
  }
  return out;
}

const ODDS_API_MMA_ODDS = 'https://api.the-odds-api.com/v4/sports/mma_mixed_martial_arts/odds';
const ODDS_TTL = 30 * 60; // 30 min — combat ML/round totals move slowly; conserve odds-api credits

/**
 * getCombatOdds(opts) — BEST-EFFORT, CACHED combat moneyline + round totals.
 * Cache-aside on `combat:odds`; on a cold cache it hits the odds-api MMA BULK
 * odds endpoint ONCE (h2h + totals in a single request) rather than per-event,
 * to conserve the paid odds-api quota. No key / any error → {} (odds absent;
 * cards still render, the ML cell shows an honest "—"). Never throws.
 * opts: { fetchImpl, cacheGet, cacheSet, apiKey }.
 */
async function getCombatOdds(opts = {}) {
  const cGet = opts.cacheGet || cacheGet;
  const cSet = opts.cacheSet || cacheSet;
  const key = 'combat:odds';
  try {
    const cached = await cGet(key);
    if (cached && typeof cached === 'object' && !opts.fetchImpl) return cached;
  } catch { /* miss → live */ }

  const apiKey = opts.apiKey || process.env.ODDS_API_KEY;
  if (!apiKey && !opts.fetchImpl) return {};
  try {
    const url = `${ODDS_API_MMA_ODDS}?apiKey=${encodeURIComponent(apiKey || '')}&regions=us&markets=h2h,totals&oddsFormat=american`;
    const raw = await doFetch(url, opts.fetchImpl);
    const map = normalizeCombatOdds(Array.isArray(raw) ? raw : []);
    try { await cSet(key, map, ODDS_TTL); } catch { /* best-effort */ }
    return map;
  } catch (err) {
    console.warn('[combatAdapter] getCombatOdds failed:', err && err.message);
    return {};
  }
}

/**
 * matchBoutOdds(bout, oddsMap) — join the odds map onto an ESPN bout by fighter
 * names (either orientation). Returns the odds record or null. Never throws.
 */
function matchBoutOdds(bout, oddsMap) {
  if (!bout || !oddsMap || typeof oddsMap !== 'object') return null;
  const fs = Array.isArray(bout.fighters) ? bout.fighters : [];
  if (fs.length < 2) return null;
  const a = String(fs[0].name || '').toLowerCase();
  const b = String(fs[1].name || '').toLowerCase();
  return oddsMap[`${a}|${b}`] || oddsMap[`${b}|${a}`] || null;
}

module.exports = {
  // read paths
  getFightCards,
  getFightCard,
  getCombatOdds,
  matchBoutOdds,
  // pure, tested transforms
  normalizeScoreboard,
  normalizeBout,
  normalizeFighter,
  normalizeCombatOdds,
  parseAthleteId,
  parseRecord,
  // helpers
  todayET,
  dateET,
  numOrNull,
  ESPN_MMA_SCOREBOARD,
};
