/**
 * Tank01 feature augmentor (Session 14).
 *
 * Reads cache keys the Tank01 adapters write — never hits the network
 * directly. This keeps the user request path off the RapidAPI budget
 * (1000 req/mo on the free tier) and makes the seam explicit: a
 * future daily prefetch populates these keys, and `computeFeatures`
 * reads them through this module.
 *
 * Contract: every export returns a flat object suitable for
 * `Object.assign(features, augmentation)` — no nested structures, no
 * throws, no DB writes. An empty object on miss is the correct
 * "absent signal" response (the grading engine treats unknown
 * features as neutral, not penalizing).
 *
 * Cache keys consumed (written by the adapters in Session 9):
 *   tank01:nba:boxscore:{gameId}       — per-game player stats
 *   tank01:nba:games:{ymd}             — daily schedule with statuses
 *   tank01:nba:odds:{ymd}              — book-by-book market lines
 *   tank01:mlb:boxscore:{gameId}       — per-game batter + pitcher lines
 *   tank01:mlb:bvp:{batterId}:{pitcherId} — historical matchup
 *   tank01:mlb:scoreboard:{ymd}        — daily slate
 *
 * Player matching: Tank01 box scores key players by ESPN-ish IDs.
 * Without a Tank01-id → name index, we match by case-insensitive
 * `longName`. Best-effort — if the prefetch eventually writes an
 * index keyed by `tank01:player_by_name:{normalizedName}`, we'll
 * prefer that.
 */

const { cacheGet } = require('../../utils/redis');
const { normalizeName } = require('../../utils/normalize');

function nameMatches(a, b) {
  if (!a || !b) return false;
  return String(a).trim().toLowerCase() === String(b).trim().toLowerCase();
}

async function safeRead(key) {
  try {
    return await cacheGet(key);
  } catch {
    return null;
  }
}

// Find a player row inside a Tank01 NBA box score response. The
// adapter projects `{playerId, name, team, pts, reb, ast, ...}` so
// we walk the projected list rather than the raw Tank01 envelope.
function findPlayerInBoxScore(boxScoreList, playerName) {
  if (!Array.isArray(boxScoreList)) return null;
  for (const row of boxScoreList) {
    if (nameMatches(row.name, playerName)) return row;
  }
  return null;
}

// Find this player's market line + market average for the requested
// stat type, when Tank01's odds payload includes them.
function extractMarketLine(oddsPayload, _playerName, _statType) {
  if (!oddsPayload || typeof oddsPayload !== 'object') return null;
  // Tank01's odds schema isn't fully stable across versions; the
  // adapter passes the raw body through. Surface the upstream
  // payload under a namespaced key so downstream features can dig
  // when the schema firms up; for now, just confirm presence.
  return oddsPayload.body || oddsPayload || null;
}

/**
 * augmentNbaFeatures — merge Tank01 NBA cache reads into the
 * computeFeatures pipeline.
 *
 * @param {Object} params { gameId, playerName, ymd }
 *   gameId    — ESPN game ID (preferred Tank01 key) or null
 *   playerName — display name to match against box scores
 *   ymd       — YYYYMMDD for the daily odds/schedule lookups
 * @returns {Promise<Object>} flat feature additions (possibly empty)
 */
async function augmentNbaFeatures({ gameId, playerName, ymd } = {}) {
  const out = {};
  if (!playerName && !gameId) return out;

  // 1) Per-game box score (live mid-game, final post-game).
  if (gameId) {
    const box = await safeRead(`tank01:nba:boxscore:${gameId}`);
    if (Array.isArray(box) && playerName) {
      const row = findPlayerInBoxScore(box, playerName);
      if (row) {
        // Surface as `t01_*` so the grading engine + reasoning
        // builder can distinguish Tank01-sourced fields from ESPN-
        // sourced ones (audit trail in production logs).
        out.t01_minutes = row.mins ?? null;
        out.t01_pts = row.pts ?? null;
        out.t01_reb = row.reb ?? null;
        out.t01_ast = row.ast ?? null;
        out.t01_threes = row.threes ?? null;
        out.t01_blk = row.blk ?? null;
        out.t01_stl = row.stl ?? null;
        out.t01_tov = row.tov ?? null;
        out.t01_final = !!row._final;
        out.t01_source = 'tank01';
      }
    }
  }

  // 2) Market odds for the slate date — exposes consensus lines that
  // the trap detector can read alongside odds-api numbers.
  if (ymd) {
    const odds = await safeRead(`tank01:nba:odds:${ymd}`);
    if (odds) {
      const market = extractMarketLine(odds, playerName);
      if (market) out.t01_market_present = true;
    }
  }

  return out;
}

/**
 * augmentMlbFeatures — merge Tank01 MLB cache reads into computeFeatures.
 *
 * BvP is the headline signal: a batter's historical line against a
 * specific pitcher. Pitcher resolution is best-effort — we check the
 * daily scoreboard for the opposing starter when `pitcherName` isn't
 * supplied by the caller.
 */
async function augmentMlbFeatures({ gameId, batterName, pitcherName, batterId, pitcherId, ymd } = {}) {
  const out = {};
  if (!batterName && !gameId) return out;

  // 1) Per-game box score — live batter line during play, final after.
  if (gameId) {
    const box = await safeRead(`tank01:mlb:boxscore:${gameId}`);
    if (Array.isArray(box) && batterName) {
      const row = box.find((r) => r.role === 'batter' && nameMatches(r.name, batterName));
      if (row) {
        out.t01_box_present = true;
        out.t01_team = row.team;
        out.t01_final = !!row._final;
      }
    }
  }

  // 2) Batter-vs-pitcher historical line. Tank01 keys BvP by IDs.
  //    Without IDs, fall through silently — name-based BvP lookup
  //    needs a separate name→id index the prefetch can build.
  if (batterId && pitcherId) {
    const bvp = await safeRead(`tank01:mlb:bvp:${batterId}:${pitcherId}`);
    if (bvp && typeof bvp === 'object' && !Array.isArray(bvp)) {
      out.t01_bvp_pa = bvp.plateAppearances ?? 0;
      out.t01_bvp_ab = bvp.atBats ?? 0;
      out.t01_bvp_h = bvp.hits ?? 0;
      out.t01_bvp_hr = bvp.homeRuns ?? 0;
      out.t01_bvp_rbi = bvp.rbi ?? 0;
      out.t01_bvp_so = bvp.strikeouts ?? 0;
      out.t01_bvp_avg = bvp.avg;
      out.t01_bvp_ops = bvp.ops;
      // K-rate as a friendly derived signal — trap detection reads
      // strikeout-prone matchups directly off this.
      if (out.t01_bvp_ab > 0) {
        out.t01_bvp_so_rate = Math.round((out.t01_bvp_so / out.t01_bvp_ab) * 1000) / 1000;
      }
      out.t01_source = 'tank01';
    }
  } else if (batterName && pitcherName) {
    // Future: if a name→id index lands in the prefetch, resolve
    // here. For now we drop a marker so reasoning can say
    // "BvP unavailable — names not yet indexed."
    out.t01_bvp_name_only = true;
    out.t01_bvp_batter_name = batterName;
    out.t01_bvp_pitcher_name = pitcherName;
  }

  // 3) Daily scoreboard — read the opposing pitcher when the caller
  //    didn't pass one. This is the "best-effort pitcher detection"
  //    the spec called out.
  if (!pitcherName && !pitcherId && ymd && batterName) {
    const slate = await safeRead(`tank01:mlb:scoreboard:${ymd}`);
    if (Array.isArray(slate) && slate.length > 0) {
      // We don't have a batter→team map here (that lives in the
      // box score we may not have hit yet). Mark the slate as
      // present so reasoning can say "starting pitcher data
      // available — call augmentMlbFeatures with pitcherName."
      out.t01_slate_present = true;
    }
  }

  return out;
}

module.exports = {
  augmentNbaFeatures,
  augmentMlbFeatures,
  __internals: {
    nameMatches,
    safeRead,
    findPlayerInBoxScore,
    extractMarketLine,
    normalizeName,
  },
};
