"use strict";

/**
 * espnAthleteIndex — Wave 2B: reliable ESPN athlete-id + DIRECT headshot capture
 * from feeds VYNDR already calls for a slate.
 *
 * THE GAP it closes: NBA/WNBA `espnId` was captured ONLY from
 * `espnStatsAdapter.getSeasonAverages` (the offline-Python fallback), which is
 * unreliable in prod. ESPN's own summary / boxscore / leaders / injuries / roster
 * payloads carry, per athlete, `athlete.id` AND often a direct
 * `athlete.headshot.href` (the exact image URL). This harvests both into a
 *   { nameKey -> { espnId, headshotHref } }
 * index — reusing `scheduleService.getSchedule` + `getGameSummary` (no new
 * endpoint), bounded (mapLimit), cached per sport+date, and NEVER throwing.
 *
 * Doctrine (unchanged): a REAL photo where ESPN gives an id/href; a
 * team-colored monogram where it can't. Nothing here fabricates a face. MLB's
 * MLBAM path is untouched (this returns {} for MLB — MLB owns its own id).
 *
 * A direct `headshotHref` is PREFERRED over a constructed `(sport,id)` URL: it
 * is the exact URL ESPN serves, so it never 404s on a league whose CDN path we
 * would otherwise guess. This is the ONLY honest route for soccer (ESPN soccer
 * headshots are inconsistent → we trust a direct href only, never a constructed
 * soccer URL). All parsing is pure + defensive so a malformed shape yields {}.
 */

const { nameKey } = require('../utils/playerName');

const INDEX_TTL = 3600;            // 1h — an athlete's id/href is stable within a day
const GAME_CONCURRENCY = 4;        // bounded per-game summary fan-out
const MAX_DEPTH = 8;               // recursion guard on the ESPN payload walk

// Sports whose headshots ESPN hosts by athlete id (a.espncdn CDN). MLB is
// deliberately excluded (its MLBAM path owns headshots). Soccer is included
// best-effort: ESPN soccer headshots are inconsistent, so only a DIRECT href is
// trusted downstream — snapshotService never constructs a soccer URL from an id.
const ESPN_INDEX_SPORTS = new Set(['nba', 'wnba', 'nfl', 'nhl', 'soccer']);

// Athlete-specific markers that distinguish an athlete object from a TEAM object
// (teams also carry displayName + numeric id, but never these). Guards the
// recursive harvest from tagging a team name as a player.
const ATHLETE_MARKERS = ['headshot', 'position', 'jersey', 'guid'];

function isNumericId(v) {
  return v != null && /^\d+$/.test(String(v));
}

/** Pull a direct absolute headshot URL from an athlete node, else null. */
function extractHeadshotHref(node) {
  const h = node && node.headshot;
  if (!h) return null;
  if (typeof h === 'string') return /^https?:\/\//i.test(h) ? h : null;
  if (typeof h === 'object' && typeof h.href === 'string' && /^https?:\/\//i.test(h.href)) return h.href;
  return null;
}

function looksLikeAthlete(o) {
  if (!o || typeof o !== 'object') return false;
  if (!(o.displayName || o.fullName)) return false;
  return ATHLETE_MARKERS.some((m) => o[m] != null);
}

/** Record one athlete into the index, preferring the richest data on collision. */
function recordAthlete(out, athlete) {
  if (!athlete || typeof athlete !== 'object') return;
  const name = athlete.displayName || athlete.fullName || athlete.name;
  if (!name || typeof name !== 'string') return;
  const espnId = isNumericId(athlete.id) ? String(athlete.id) : null;
  const headshotHref = extractHeadshotHref(athlete);
  if (!espnId && !headshotHref) return; // nothing useful — absent beats noise
  const key = nameKey(name);
  if (!key) return;
  const existing = out[key];
  if (!existing) {
    out[key] = { espnId: espnId || null, headshotHref: headshotHref || null };
    return;
  }
  // A direct href is the reliable asset — fill it if a later source has one.
  if (headshotHref && !existing.headshotHref) existing.headshotHref = headshotHref;
  if (espnId && !existing.espnId) existing.espnId = espnId;
}

/**
 * PURE: walk any ESPN payload (summary, boxscore, leaders, injuries, roster)
 * and harvest every athlete-like object into { nameKey -> { espnId, headshotHref } }.
 * Records ONLY explicit `.athlete` wrappers or objects carrying an athlete
 * marker (never bare team objects). Defensive: a malformed shape yields {} and
 * NEVER throws.
 */
function harvestAthletes(payload, out, depth, seen) {
  out = out || {};
  depth = depth || 0;
  seen = seen || new Set();
  if (payload == null || depth > MAX_DEPTH) return out;
  if (Array.isArray(payload)) {
    for (const item of payload) harvestAthletes(item, out, depth + 1, seen);
    return out;
  }
  if (typeof payload !== 'object') return out;
  if (seen.has(payload)) return out;
  seen.add(payload);

  // Canonical ESPN wrapper: { athlete: {...}, stats|value|... }.
  if (payload.athlete && typeof payload.athlete === 'object') recordAthlete(out, payload.athlete);
  // A bare athlete object surfaced directly (roster/injury shapes vary).
  else if (looksLikeAthlete(payload)) recordAthlete(out, payload);

  for (const k of Object.keys(payload)) {
    const v = payload[k];
    if (v && typeof v === 'object') harvestAthletes(v, out, depth + 1, seen);
  }
  return out;
}

async function mapLimit(items, concurrency, fn) {
  let i = 0;
  async function worker() {
    while (i < items.length) {
      const idx = i++;
      // eslint-disable-next-line no-await-in-loop
      await fn(items[idx], idx);
    }
  }
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length || 1) }, worker));
}

/** Look up an athlete's { espnId, headshotHref } by (possibly raw) name. */
function lookup(index, name) {
  if (!index || typeof index !== 'object') return null;
  const k = nameKey(name || '');
  return (k && index[k]) || null;
}

/**
 * Build the per-sport { nameKey -> { espnId, headshotHref } } index from the
 * ESPN feeds the pipeline already fetches. Cached (`espnindex:{sport}:{date}`,
 * 1h). Returns {} for MLB and for any error (never throws). All deps injectable
 * → unit tests hit no network.
 */
async function buildEspnAthleteIndex(sport, opts = {}) {
  const sp = String(sport || '').toLowerCase();
  if (!ESPN_INDEX_SPORTS.has(sp)) return {};

  const cacheGet = opts.cacheGet || require('../utils/redis').cacheGet;
  const cacheSet = opts.cacheSet || require('../utils/redis').cacheSet;
  const sched = require('./scheduleService');
  const getSchedule = opts.getSchedule || sched.getSchedule;
  const getGameSummary = opts.getGameSummary || sched.getGameSummary;
  const date = opts.date || sched.todayET();
  const key = `espnindex:${sp}:${date}`;

  try {
    const cached = await cacheGet(key);
    if (cached && typeof cached === 'object') return cached;
  } catch { /* ignore cache read */ }

  const out = {};
  try {
    const games = await getSchedule(sp, date).catch(() => null);
    const ids = Array.isArray(games) ? games.map((g) => g && g.id).filter(Boolean) : [];
    await mapLimit(ids, GAME_CONCURRENCY, async (id) => {
      try {
        const summary = await getGameSummary(sp, id);
        harvestAthletes(summary, out);
      } catch { /* one game failing must not sink the index */ }
    });
    try { await cacheSet(key, out, INDEX_TTL); } catch { /* ignore cache write */ }
  } catch {
    return {}; // total failure → empty → every player falls to a monogram
  }
  return out;
}

module.exports = {
  buildEspnAthleteIndex,
  harvestAthletes,
  lookup,
  extractHeadshotHref,
  __internals: { recordAthlete, looksLikeAthlete, isNumericId, ESPN_INDEX_SPORTS, INDEX_TTL },
};
