'use strict';

/**
 * GAME BINDER (Session 64, Order 1.5) — bind a prop to its REAL game.
 *
 * THE ROOT BUG THIS FIXES. `ledgerService` dated a row with
 * `dateET(prop.game_time) || dateET(gradedTs)`. Our PRIMARY feed (PropLine)
 * emits no `commence_time` at all, so the first term was always null and every
 * row was dated by the GRADE timestamp. A 01:00/03:00 UTC snapshot is
 * 21:00/23:00 ET the PREVIOUS day, so props for tonight's games were filed
 * under yesterday. Settlement then correctly found no game on that date, and
 * Order 1's void logic turned the mislabel into 64 destroyed results
 * (Freeman/Bellinger/Tucker "DNP" on days they played).
 *
 * THE FIX: match each prop to a scheduled game by TEAMS across the plausible
 * date window and take the GAME'S OWN date. A grade timestamp is never again
 * allowed to name a game date.
 *
 * CONTRACT: if a prop cannot be bound to a real game, it returns NOTHING. The
 * caller must treat that as UNRESOLVED — never fall back to a derived date. A
 * mis-dated row is fabricated data, and the ledger's standing rule is that it
 * holds real market values or nothing.
 *
 * DOUBLEHEADERS are reported, not guessed. When two games share the same teams
 * on one date, a prop with no game id cannot be attributed to one of them; the
 * binding is marked `ambiguous` so settlement can decline rather than pick.
 */

const norm = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]/g, '');

/** Does a scheduled game involve both of the prop's teams? */
function gameMatchesTeams(game, homeTeam, awayTeam) {
  const h = norm(homeTeam);
  const a = norm(awayTeam);
  if (!h && !a) return false;
  const gh = norm(game && (game.homeTeam?.abbreviation || game.homeTeam?.name || game.home));
  const ga = norm(game && (game.awayTeam?.abbreviation || game.awayTeam?.name || game.away));
  if (!gh && !ga) return false;
  const hit = (x, y) => !!x && !!y && (x === y || x.includes(y) || y.includes(x));
  // Accept either orientation — feeds disagree about which side is "home".
  return (hit(gh, h) && hit(ga, a)) || (hit(gh, a) && hit(ga, h));
}

/** ET calendar date of an ISO timestamp. */
function etDate(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);
}

function addDays(dateStr, n) {
  const d = new Date(`${dateStr}T12:00:00Z`);
  if (Number.isNaN(d.getTime())) return null;
  d.setUTCDate(d.getUTCDate() + n);
  return d.toISOString().slice(0, 10);
}

/**
 * Candidate ET dates for a prop graded at `gradedAt`.
 *
 * A late-UTC snapshot (01:00/03:00 UTC = 21:00/23:00 ET) is grading TONIGHT'S
 * or TOMORROW'S slate while the ET clock still reads yesterday, so the real
 * game is the grade's ET date or the day after. We also look one day BACK to
 * cover an early-UTC slot grading a game already in progress.
 */
function candidateDates(gradedAt) {
  const base = etDate(gradedAt);
  if (!base) return [];
  return [base, addDays(base, 1), addDays(base, -1)].filter(Boolean);
}

/**
 * Bind ONE prop to a real scheduled game.
 * @returns {Promise<{game_time, game_date, game_id, ambiguous}|null>} null = unresolved
 */
async function bindGame({ sport, prop, gradedAt, getSchedule }) {
  if (!prop) return null;
  // Already carries a real game time (odds-api path) — trust the feed.
  if (prop.game_time && etDate(prop.game_time)) {
    return {
      game_time: prop.game_time,
      game_date: etDate(prop.game_time),
      game_id: prop.game_id || null,
      ambiguous: false,
    };
  }
  for (const date of candidateDates(gradedAt)) {
    let games = [];
    try { games = (await getSchedule(sport, date)) || []; } catch { games = []; }
    const matches = games.filter((g) => gameMatchesTeams(g, prop.home_team, prop.away_team));
    if (matches.length === 0) continue;
    const g = matches[0];
    const gt = g.gameTime || g.date || null;
    const gd = etDate(gt) || date;
    return {
      game_time: gt,
      game_date: gd,
      game_id: g.id ? `${sport}:${gd}:${g.id}` : null,
      // Two games, same teams, same day: a prop with no game id cannot be
      // attributed to one. Report it; never guess.
      ambiguous: matches.length > 1,
    };
  }
  return null;   // UNRESOLVED — caller must not invent a date
}

/**
 * Attach real game times to a slate's props. Returns counts so a silent
 * binding collapse is visible instead of quietly re-introducing bad dates.
 */
async function attachGameTimes(sport, props, deps = {}) {
  const getSchedule = deps.getSchedule
    || ((sp, d) => require('./scheduleService').getSchedule(sp, d));
  const gradedAt = deps.gradedAt || new Date().toISOString();
  const list = Array.isArray(props) ? props : [];
  let bound = 0;
  let alreadyHad = 0;
  let unresolved = 0;
  let ambiguous = 0;

  // One schedule fetch per candidate date, reused across every prop.
  const cache = new Map();
  const cachedSchedule = async (sp, d) => {
    const k = `${sp}:${d}`;
    if (!cache.has(k)) {
      try { cache.set(k, (await getSchedule(sp, d)) || []); } catch { cache.set(k, []); }
    }
    return cache.get(k);
  };

  for (const p of list) {
    if (!p) continue;
    if (p.game_time && etDate(p.game_time)) { alreadyHad += 1; continue; }
    const b = await bindGame({ sport, prop: p, gradedAt, getSchedule: cachedSchedule });
    if (!b) { unresolved += 1; continue; }
    p.game_time = b.game_time;
    p.game_date = b.game_date;
    if (b.game_id) p.bound_game_id = b.game_id;
    if (b.ambiguous) { p.game_ambiguous = true; ambiguous += 1; }
    bound += 1;
  }
  return { total: list.length, bound, alreadyHad, unresolved, ambiguous };
}

module.exports = {
  bindGame, attachGameTimes, candidateDates, gameMatchesTeams, etDate, addDays,
};
