'use strict';

/**
 * ENVIRONMENT + MATCHUP CONTEXT (Session 77) — the WIRING that feeds the
 * dormant adjusters. It changes NONE of their internal logic: it fetches the
 * inputs they need (venue, first-pitch, weather forecast, opposing-pitcher
 * hand, batter splits) and calls parkBase / weatherMod / platoonSplits /
 * composeEnvironment exactly as they are.
 *
 * Everything here is best-effort and HONEST-ABSENT: a missing venue → no park
 * or weather; an undeclared pitcher → no platoon. Nothing is fabricated, and a
 * failure anywhere degrades that prop to no-adjustment, never a crash — the
 * adjusters are measured inside the pipeline and must not be able to break it.
 *
 * POINT-IN-TIME: the weather is a FORECAST for first pitch fetched now (at
 * projection time), and the platoon split is the hitter's line entering the
 * game. Neither reads a settle-time value — no lookahead.
 */

const parkBase = require('./parkBase');
const weatherMod = require('./weatherMod');
const platoon = require('./platoonSplits');

/**
 * Park coordinates by team abbr — PUBLIC GEOMETRY, same class as the dome list
 * and the centre-field bearings in weatherMod. Lives in the WIRING, not the
 * adjuster, so the adjusters stay pure. A team missing here gets no weather (its
 * park factor still applies).
 */
const PARK_COORDS = Object.freeze({
  ARI: [33.4455, -112.0667], ATL: [33.8907, -84.4677], BAL: [39.2839, -76.6217],
  BOS: [42.3467, -71.0972], CHC: [41.9484, -87.6553], CWS: [41.83, -87.6339],
  CIN: [39.0975, -84.5069], CLE: [41.4962, -81.6852], COL: [39.7559, -104.9942],
  DET: [42.339, -83.0485], HOU: [29.7573, -95.3555], KC: [39.0517, -94.4803],
  LAA: [33.8003, -117.8827], LAD: [34.0739, -118.24], MIA: [25.7781, -80.2197],
  MIL: [43.028, -87.9712], MIN: [44.9817, -93.2776], NYM: [40.7571, -73.8458],
  NYY: [40.8296, -73.9262], ATH: [38.5806, -121.5136], PHI: [39.9061, -75.1665],
  PIT: [40.4469, -80.0057], SD: [32.7073, -117.157], SF: [37.7786, -122.3893],
  SEA: [47.5914, -122.3325], STL: [38.6226, -90.1928], TB: [27.7683, -82.6534],
  TEX: [32.7473, -97.0847], TOR: [43.6414, -79.3894], WSH: [38.873, -77.0074],
});

/** Full team name → abbr, so a schedule/stats "Los Angeles Dodgers" resolves to
 *  the abbr the coords + park tables key on. */
const NAME_TO_ABBR = Object.freeze({
  'arizona diamondbacks': 'ARI', 'atlanta braves': 'ATL', 'baltimore orioles': 'BAL',
  'boston red sox': 'BOS', 'chicago cubs': 'CHC', 'chicago white sox': 'CWS',
  'cincinnati reds': 'CIN', 'cleveland guardians': 'CLE', 'colorado rockies': 'COL',
  'detroit tigers': 'DET', 'houston astros': 'HOU', 'kansas city royals': 'KC',
  'los angeles angels': 'LAA', 'los angeles dodgers': 'LAD', 'miami marlins': 'MIA',
  'milwaukee brewers': 'MIL', 'minnesota twins': 'MIN', 'new york mets': 'NYM',
  'new york yankees': 'NYY', 'athletics': 'ATH', 'oakland athletics': 'ATH',
  'philadelphia phillies': 'PHI', 'pittsburgh pirates': 'PIT', 'san diego padres': 'SD',
  'san francisco giants': 'SF', 'seattle mariners': 'SEA', 'st. louis cardinals': 'STL',
  'tampa bay rays': 'TB', 'texas rangers': 'TEX', 'toronto blue jays': 'TOR',
  'washington nationals': 'WSH',
});

const abbrOf = (team) => {
  if (!team) return null;
  const s = String(team).trim();
  if (/^[A-Z]{2,3}$/.test(s)) return s.toUpperCase();
  return NAME_TO_ABBR[s.toLowerCase()] || null;
};

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

/** Injectable JSON GET — tests never hit the network. */
async function fetchJson(url, opts = {}) {
  if (opts.fetchJson) return opts.fetchJson(url);
  const res = await require('axios').get(url, { timeout: 20_000, headers: { Accept: 'application/json' } });
  return res.data;
}

const DEFAULT_SEASON = Number(process.env.STATCAST_SEASON) || 2026;

/**
 * buildContext(sport, deps) — fetches everything ONCE per snapshot and returns
 * a resolver. All fetches are optional; whatever fails becomes honest-absent.
 *
 * Returns { contextFor(grade), stats } where contextFor gives
 * { environment, matchup } for one grade, ready to hand to attachChallenger.
 */
async function buildContext(sport, deps = {}) {
  const sp = String(sport || 'mlb').toLowerCase();
  if (sp !== 'mlb') {
    // Park/weather/platoon are MLB-only today. Everything else honest-absents.
    return { contextFor: () => ({ environment: null, matchup: null }), stats: { sport: sp, applicable: false } };
  }
  const season = deps.season || DEFAULT_SEASON;

  // ── 1. schedule: team → { venue, gameTime, homeAbbr, awayAbbr } ─────────
  const gameByTeam = new Map();
  try {
    const sched = deps.schedule || await fetchJson(`${deps.origin || ''}/api/schedule/mlb`, deps).catch(() => null)
      || await fetchJson('https://statsapi.mlb.com/api/v1/schedule?sportId=1&hydrate=venue', deps);
    const games = (sched && (sched.games || (sched.dates || []).flatMap((d) => d.games))) || [];
    for (const g of games) {
      const homeAbbr = abbrOf(g.homeTeam?.abbreviation || g.homeTeam?.name || g.teams?.home?.team?.name);
      const awayAbbr = abbrOf(g.awayTeam?.abbreviation || g.awayTeam?.name || g.teams?.away?.team?.name);
      const gameTime = g.gameTime || g.gameDate || g.date || null;
      const venue = g.venue?.name || g.venue || null;
      const rec = { homeAbbr, awayAbbr, gameTime, venue };
      if (homeAbbr) gameByTeam.set(homeAbbr, rec);
      if (awayAbbr) gameByTeam.set(awayAbbr, rec);
    }
  } catch { /* honest-absent everywhere below */ }

  // ── 2. probable pitchers: team → { oppPitcherId } ───────────────────────
  //    A team's OPPOSING pitcher is the other side's probable.
  const oppPitcherByTeam = new Map();
  const pitcherIds = new Set();
  try {
    const pp = deps.pitchers || await fetchJson(`${deps.origin || ''}/api/schedule/mlb/pitchers`, deps).catch(() => null);
    for (const g of (pp && pp.games) || []) {
      const homeAbbr = abbrOf(g.home?.team);
      const awayAbbr = abbrOf(g.away?.team);
      const homePid = num(g.home?.pitcherId);
      const awayPid = num(g.away?.pitcherId);
      if (homeAbbr && awayPid) { oppPitcherByTeam.set(homeAbbr, awayPid); pitcherIds.add(awayPid); }
      if (awayAbbr && homePid) { oppPitcherByTeam.set(awayAbbr, homePid); pitcherIds.add(homePid); }
    }
  } catch { /* platoon honest-absents */ }

  // ── 3. pitcher handedness: ONE batched statsapi call ────────────────────
  const handById = new Map();
  if (pitcherIds.size) {
    try {
      const ids = [...pitcherIds].join(',');
      const people = deps.people || await fetchJson(`https://statsapi.mlb.com/api/v1/people?personIds=${ids}`, deps);
      for (const p of (people && people.people) || []) {
        const code = p.pitchHand?.code;
        if (code) handById.set(num(p.id), String(code).toUpperCase());
      }
    } catch { /* platoon honest-absents */ }
  }

  // ── 4. weather forecast: ONE Open-Meteo call per HOME park ──────────────
  const homeAbbrs = new Set([...gameByTeam.values()].map((r) => r.homeAbbr).filter(Boolean));
  const forecastByHome = new Map();
  await Promise.all([...homeAbbrs].map(async (homeAbbr) => {
    const coords = PARK_COORDS[homeAbbr];
    if (!coords) return; // no coords → no weather (park still applies)
    const rec = gameByTeam.get(homeAbbr);
    try {
      const payload = await fetchJson(weatherMod.FORECAST_URL(coords[0], coords[1]), deps);
      const f = weatherMod.pickHour(payload, rec && rec.gameTime);
      if (f) forecastByHome.set(homeAbbr, f);
    } catch { /* forecast_absent */ }
  }));

  // ── 5. batter splits: per unique batter, bounded, best-effort ───────────
  //    Only fetched for players who have a graded prop AND a resolvable id.
  const splitsById = new Map();
  const fetchSplits = async (playerId) => {
    if (playerId == null || splitsById.has(playerId)) return;
    try {
      const payload = await fetchJson(platoon.SPLITS_URL(playerId, season), deps);
      splitsById.set(playerId, platoon.parseSplits(payload));
    } catch { splitsById.set(playerId, null); }
  };

  const stats = {
    sport: sp, applicable: true,
    games: gameByTeam.size / 2, venues_with_weather: forecastByHome.size,
    pitchers_with_hand: handById.size, opp_declared: oppPitcherByTeam.size,
  };

  /**
   * contextFor(grade) — { environment, matchup } for one graded prop.
   * grade must carry: team (player's real team), statType, direction, bats,
   * playerId. Anything missing → that half is null.
   */
  const contextFor = async (grade) => {
    const teamAbbr = abbrOf(grade && grade.team);
    const game = teamAbbr ? gameByTeam.get(teamAbbr) : null;
    const stat = grade && (grade.stat_type || grade.stat);

    // ENVIRONMENT = park_base × weather_mod (composable coefficient).
    let environment = null;
    if (game && game.homeAbbr) {
      const park = parkBase.resolveParkBase({ teamAbbr: game.homeAbbr, venueName: game.venue });
      const wx = weatherMod.weatherMod({
        forecast: forecastByHome.get(game.homeAbbr) || null,
        teamAbbr: game.homeAbbr, venueName: game.venue,
        statType: stat, weatherNa: park.weather_na,
      });
      const env = weatherMod.composeEnvironment({ park, weather: wx, statType: stat });
      if (env.multiplier !== 1) {
        environment = {
          multiplier: env.multiplier, label: env.label, venue: env.venue,
          weather_na: env.weather_na, park_base: env.park_base,
          weather_mod: env.weather_mod, weather_state: env.weather_state,
        };
      }
    }

    // MATCHUP = platoon split, hitter hand + opposing-SP hand.
    let matchup = null;
    const batterHand = grade && grade.bats;
    const oppPid = teamAbbr ? oppPitcherByTeam.get(teamAbbr) : null;
    const pitcherHand = oppPid != null ? handById.get(oppPid) : null;
    if (batterHand && pitcherHand && grade.playerId != null) {
      await fetchSplits(grade.playerId);
      const splits = splitsById.get(grade.playerId);
      if (splits) {
        const e = platoon.platoonEstimate({ splits, batterHand, pitcherHand, statType: stat });
        if (e.multiplier !== 1) {
          matchup = {
            multiplier: e.multiplier, label: 'PLATOON',
            batter_hand: e.batter_hand, pitcher_hand: e.pitcher_hand,
            observed_pa: e.observed_pa, observed_weight: e.observed_weight,
          };
        }
      }
    }

    return { environment, matchup };
  };

  return { contextFor, stats, _internals: { gameByTeam, forecastByHome, handById, oppPitcherByTeam } };
}

module.exports = {
  buildContext,
  abbrOf,
  PARK_COORDS,
  NAME_TO_ABBR,
};
