'use strict';

/**
 * OPPONENT STRENGTH — `opp_rank_stat` (Session 64, Phase 3).
 *
 * ONE COLUMN, ONE MEANING, ACROSS SPORTS. This is the shared contract, taken
 * from WNBA's live behaviour, which is the reference implementation:
 *
 *    scale    : 0–1
 *    polarity : HIGH (>= 0.70) = WEAK opponent defence  → favourable to the
 *                                hitter/scorer, lifts an OVER
 *               LOW  (<= 0.30) = TOUGH opponent defence → fades an OVER
 *
 * MLB previously had NO opp_rank_stat at all (ESPN's MLB team endpoint carries
 * no defensive metric), so engine1's ±1.0 opponent factor never fired for the
 * sport carrying most of our volume. This derives it from data we ALREADY
 * ingest, free and unauthenticated: statsapi's team pitching splits — one call
 * returns all 30 teams with avg (opponent batting average against), slg, ops,
 * homeRuns, strikeOuts, era, whip.
 *
 * POLARITY IS THE HIGHEST-RISK PART. A batting average AGAINST is a
 * "higher = worse pitching" stat, so it maps DIRECTLY to our scale: a team that
 * allows a high BAA is a weak opponent → high opp_rank_stat. Getting this
 * backwards would silently mis-adjust every MLB grade in the wrong direction,
 * which is far worse than having no value — so a test asserts MLB polarity
 * equals WNBA polarity, not merely that a number exists.
 *
 * HONEST NULLS: below the sample floor — either the opponent's own games or the
 * league baseline being too thin — this returns NULL. We are FIXING a silent
 * null here; we do not get to replace it with a confident guess off three games.
 */

// Which pitching field expresses "how easy is this opponent for THIS stat".
// Each is a higher = weaker-opponent measure, matching the shared polarity.
const MLB_STAT_FIELD = {
  hits: 'avg',                 // opponent batting average against
  total_bases: 'slg',          // slugging against
  home_runs: 'homeRuns',       // HR allowed
  runs: 'runs',
  rbi: 'runs',
  doubles: 'slg',
  triples: 'slg',
  walks: 'baseOnBalls',
  // Strikeouts are INVERTED: a staff that strikes out MORE batters is a TOUGHER
  // opponent for a batter's hits/TB props, but for a BATTER-strikeouts prop a
  // high-K staff makes the over EASIER. Handled by `invert` below.
  strikeouts: 'strikeOuts',
};
// Fields where a HIGHER raw value means a TOUGHER opponent for the graded side,
// so the percentile must be flipped to preserve "high = weak".
const INVERTED_FOR_BATTER = new Set([]);
// For a batter's own strikeout prop, a high-K staff HELPS the over — so it is
// NOT inverted. Listed explicitly so the intent is readable rather than implied.
const NOT_INVERTED = new Set(['strikeouts']);

const MIN_OPPONENT_GAMES = Number(process.env.OPP_RANK_MIN_GAMES || 20);
const MIN_LEAGUE_TEAMS = Number(process.env.OPP_RANK_MIN_TEAMS || 20);

function num(v) {
  if (v == null || v === '') return null;
  const n = Number(v);
  return Number.isFinite(n) ? n : null;
}

/**
 * Percentile of `value` within `all` (0–1), where a HIGHER raw value yields a
 * HIGHER percentile. Ties share the midpoint.
 */
function percentile(value, all) {
  const xs = all.filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
  if (xs.length < 2) return null;
  let below = 0;
  let equal = 0;
  for (const x of xs) {
    if (x < value) below += 1;
    else if (x === value) equal += 1;
  }
  return (below + equal / 2) / xs.length;
}

/**
 * Derive MLB opp_rank_stat.
 *
 * @param {Array}  teams    [{ teamId, stat: { avg, slg, gamesPlayed, ... } }] — every team
 * @param {string|number} opponentTeamId
 * @param {string} statType graded stat (hits, total_bases, ...)
 * @returns {{value:number|null, reason:string|null, field:string|null, raw:number|null}}
 */
function deriveMlbOppRank(teams, opponentTeamId, statType, opts = {}) {
  const field = MLB_STAT_FIELD[String(statType || '').toLowerCase()];
  if (!field) return { value: null, reason: 'stat_not_mapped', field: null, raw: null };

  const list = Array.isArray(teams) ? teams : [];
  // The league baseline must itself be real. Early season / partial feeds
  // produce a baseline that cannot rank anything honestly.
  if (list.length < (opts.minTeams ?? MIN_LEAGUE_TEAMS)) {
    return { value: null, reason: 'league_baseline_too_thin', field, raw: null };
  }

  const opp = list.find((t) => String(t.teamId) === String(opponentTeamId));
  if (!opp || !opp.stat) return { value: null, reason: 'opponent_not_found', field, raw: null };

  const games = num(opp.stat.gamesPlayed);
  if (games != null && games < (opts.minGames ?? MIN_OPPONENT_GAMES)) {
    return { value: null, reason: 'opponent_sample_too_thin', field, raw: null };
  }

  const raw = num(opp.stat[field]);
  if (raw == null) return { value: null, reason: 'field_absent', field, raw: null };

  const all = list
    .filter((t) => {
      const g = num(t.stat && t.stat.gamesPlayed);
      return g == null || g >= (opts.minGames ?? MIN_OPPONENT_GAMES);
    })
    .map((t) => num(t.stat && t.stat[field]))
    .filter((v) => v != null);
  if (all.length < (opts.minTeams ?? MIN_LEAGUE_TEAMS)) {
    return { value: null, reason: 'league_baseline_too_thin', field, raw };
  }

  let p = percentile(raw, all);
  if (p == null) return { value: null, reason: 'percentile_undefined', field, raw };
  // Preserve the shared polarity: HIGH = weak opponent.
  const invert = INVERTED_FOR_BATTER.has(String(statType).toLowerCase())
    && !NOT_INVERTED.has(String(statType).toLowerCase());
  if (invert) p = 1 - p;
  return { value: Math.round(p * 1000) / 1000, reason: null, field, raw };
}

/**
 * Health check. We are fixing a SILENT null — so an empty source AND an
 * unexpected null for a sport we expect to derive both page. A derived metric
 * that quietly stops deriving is the failure mode this whole session has been
 * about.
 */
function opponentStrengthHealth({ sport, teamsLoaded = 0, derived = 0, attempted = 0 } = {}) {
  if (teamsLoaded === 0) {
    return { alarm: true, reason: `${String(sport).toUpperCase()} opponent-strength source returned NO teams — opp_rank_stat cannot be derived` };
  }
  if (attempted > 0 && derived === 0) {
    return { alarm: true, reason: `${String(sport).toUpperCase()} opponent-strength derived NULL for all ${attempted} props despite ${teamsLoaded} teams loaded` };
  }
  return { alarm: false, reason: null };
}

module.exports = {
  deriveMlbOppRank, percentile, opponentStrengthHealth,
  MLB_STAT_FIELD, MIN_OPPONENT_GAMES, MIN_LEAGUE_TEAMS,
};
