'use strict';

/**
 * SETTLEMENT SOURCE — date-targeted game resolution (Session 64, Order 1).
 *
 * THE BUG THIS REPLACES: settlement looked for a game inside the player's
 * ROLLING last-N window. Two mechanisms silently produced eternal pending:
 *   1. the player DNP'd, so no row for that date exists at all; and
 *   2. the window rolled past the game (verified live: Cooper Pratt's Jul 12
 *      game fell out of a last10 that now starts Jul 6).
 * Both hit one line — `if (!gameRow) { pending += 1; continue; }` — with no
 * terminal state, so a row that can NEVER settle looked exactly like one
 * settling tomorrow.
 *
 * THE FIX: resolve the SPECIFIC DATE, and read GAME STATE — not merely whether
 * the player appears.
 *
 * Cost note: for MLB this is FREE. `mlbStatsAdapter.getPlayerGameLog` already
 * returns the player's FULL SEASON log and `getPlayerStats` was discarding it
 * with `.slice(-10)`. Settlement now reads the full log (same URL, same cache,
 * no extra request). Projections keep using last10 — unchanged.
 *
 * States returned:
 *   settled       — the game is final and the player has a line → score it
 *   void          — no bet existed: confirmed DNP, or postponed/cancelled
 *   pending       — not final yet (scheduled / in progress / suspended). Retry.
 *   unknown       — we could not determine state; caller applies the retry cap
 *
 * A partial game (injured/ejected but with a line) is a REAL result and settles
 * normally — it is never a void.
 */

const FINAL_HINTS = ['final', 'completed', 'post', 'game over'];
const VOID_HINTS = ['postponed', 'cancelled', 'canceled', 'forfeit'];
const NOT_FINAL_HINTS = ['scheduled', 'pre', 'warmup', 'in progress', 'live', 'delayed', 'suspended', 'in'];

function textOf(...vals) {
  return vals.filter(Boolean).map((v) => String(v).toLowerCase()).join(' ');
}

/** Classify one schedule entry's state. Order matters: an explicitly void
 *  state must win over a "final"-looking wrapper. */
function classifyGameState(game) {
  const t = textOf(
    game && game.status,
    game && game.detailedState,
    game && game.abstractGameState,
    game && game.state,
    game && game.statusText,
  );
  if (!t) return 'unknown';
  if (VOID_HINTS.some((h) => t.includes(h))) return 'void';
  // 'suspended' must not be read as final even though it can pair with other
  // words — a suspended game resumes and settles later.
  if (t.includes('suspended')) return 'not_final';
  if (FINAL_HINTS.some((h) => t.includes(h))) return 'final';
  if (NOT_FINAL_HINTS.some((h) => t.includes(h))) return 'not_final';
  return 'unknown';
}

/**
 * Given the day's games for a sport, decide what the ABSENCE of a player row
 * means. Pure — the caller supplies the schedule.
 *   - any game still unplayed/suspended → 'pending' (do NOT void; it may settle)
 *   - every relevant game void          → 'void'
 *   - at least one final, none pending  → 'void' (confirmed DNP)
 *   - nothing known                     → 'unknown'
 */
function absenceMeaning(games) {
  const list = Array.isArray(games) ? games : [];
  if (list.length === 0) return 'unknown';
  const states = list.map(classifyGameState);
  if (states.some((s) => s === 'not_final')) return 'pending';
  if (states.every((s) => s === 'void')) return 'void';
  if (states.some((s) => s === 'final')) return 'void';   // played without them = DNP
  return 'unknown';
}

/** Find the player's row for an exact date in a full game log. */
function rowForDate(log, gameDate, matchesDate) {
  const rows = Array.isArray(log) ? log : [];
  return rows.find((r) => matchesDate(r, gameDate)) || null;
}

/**
 * Resolve one ledger row's outcome source.
 *
 * @param {Object} args
 *  - sport, playerName, gameDate, statType
 *  - getFullLog(playerName, sport)  → [{date, stat}]  (full season, not a window)
 *  - getSchedule(sport, gameDate)   → [{status,…}]
 *  - matchesDate(row, gameDate)     → bool
 *  - statValue(statObj, statType, sport) → number|null
 * @returns {Promise<{state:string, value:number|null, reason:string|null, source:string}>}
 */
async function resolveOutcome(args = {}) {
  const {
    sport, playerName, gameDate, statType,
    getFullLog, getSchedule, matchesDate, statValue,
  } = args;

  let log = [];
  try {
    log = (await getFullLog(playerName, sport)) || [];
  } catch {
    log = [];
  }

  const row = rowForDate(log, gameDate, matchesDate);
  if (row) {
    const value = statValue(row.stat, statType, sport);
    if (value == null) {
      // The player PLAYED but this stat isn't in the box row — a mapping gap,
      // not a void. Never void a real appearance.
      return { state: 'unknown', value: null, reason: 'stat_not_in_box_row', source: 'date_log' };
    }
    return { state: 'settled', value, reason: null, source: 'date_log' };
  }

  // No row for that date — the absence only means something once we know
  // whether the games were actually played.
  let games = [];
  try {
    games = (await getSchedule(sport, gameDate)) || [];
  } catch {
    games = [];
  }
  const meaning = absenceMeaning(games);
  if (meaning === 'pending') {
    return { state: 'pending', value: null, reason: 'game_not_final', source: 'schedule' };
  }
  if (meaning === 'void') {
    const allVoid = games.length > 0 && games.map(classifyGameState).every((s) => s === 'void');
    // POSITIVE evidence only: the games themselves were postponed/cancelled.
    if (allVoid) {
      return { state: 'void', value: null, reason: 'game_postponed_or_cancelled', source: 'schedule' };
    }
    // DNP VOIDING — RE-ENABLED (Order 2 Phase 4), deliberately reversing the
    // Order 1.5 disable because its premise was FALSE.
    //
    // Order 1.5 assumed a missing player row meant the ROW'S DATE was wrong.
    // The Phase 0 dry-run disproved that: across every bindable row the stored
    // date matched a real game (MIS-DATED: 0), and the specific players cited
    // as counter-evidence were genuine DNPs on their true dates (Freeman
    // 07-18, Kwan/Hedges/Davis 07-17 — their teams played, they did not).
    //
    // So the evidence here IS positive: the day's games are FINAL and the
    // player has no line in a full-season log. That is a DNP — no bet existed.
    // The date is trustworthy; the absence is real.
    return { state: 'void', value: null, reason: 'player_dnp', source: 'schedule' };
  }
  return { state: 'unknown', value: null, reason: 'state_undetermined', source: 'schedule' };
}

module.exports = {
  resolveOutcome,
  absenceMeaning,
  classifyGameState,
  rowForDate,
};
