/**
 * computeFeaturesForProp — the ONE permitted architectural addition of
 * Session 7f. Bridges raw single-prop input (`{player, stat_type, line,
 * direction, book, sport}`) to the feature-vector shape `engine1.gradeProp()`
 * expects.
 *
 * The orchestrator does this same work inline, tied to its batch loop +
 * grade_history persistence. This module lifts only the per-prop logic
 * so single-prop callers (`/api/analyze/prop`, batch entries,
 * `/api/scan/parlay` legs, `/api/bets/*`) can produce engine1 input
 * without re-implementing the resolution chain.
 *
 * Never throws. Every step is independently fault-tolerant:
 *   - player_id_map miss        → team/opponent unknown, features still partial
 *   - no game tonight           → no gameId, gameId-dependent features omitted
 *   - feature fetch fails       → features {} returned, engine1 lands C
 *   - trap fetch fails          → trap defaults to no signals firing
 *   - game logs unavailable     → consistency defaults to 'unknown'
 *
 * The caller (analyzeViaEngine1) reads the returned `errors` array and
 * surfaces them in the reasoning string. NOTE (Session 63): this comment used
 * to claim confidence is "downgraded accordingly" — it never was. No
 * data-sufficiency penalty exists in the live path; confidence is a pure
 * function of the grade letter (see gradeAdapter `confidence_basis`). The one
 * real penalty lived in the dead `mlbGrader.js`, now removed. Insufficient data
 * produces a REFUSAL (grade null + insufficient_data), not a softened grade.
 *
 * ─────────────────────────────────────────────────────────────────────
 * Signal provenance (Session 15 audit)
 * ─────────────────────────────────────────────────────────────────────
 * Every signal the engine reads has a documented source. Phantom
 * signals — referenced in reasoning but populated by nothing — would
 * be a trust failure. As of Session 15 there are none.
 *
 * • injury_severity_score (engine1.js:126 reads it; analyzeViaEngine1
 *   surfaces it in reasoning at line 156)
 *   ← `src/services/intelligence/injuryParser.js` (ESPN injury feed)
 *   Populated by the grading orchestrator in batch mode; in the
 *   single-prop path it lives in the `featureCache` payload.
 *
 * • coach_pace_delta + coach_player_interaction
 *   ← `src/services/intelligence/coachSignals.js` reads the
 *     `coach_profiles` Supabase table (migration 017), with a
 *     `src/config/coaches.json` seed file as the cold-start fallback.
 *
 * • consistency (boom_bust / reliable / elite labels + numeric score)
 *   ← `src/services/intelligence/consistencyScore.js` operating on
 *     game logs from `gameLogService` (ESPN). When game logs are
 *     unavailable, defaults to `{consistency:'unknown', score:null}`
 *     which engine1 treats as neutral (does not penalize).
 *
 * • Tank01 t01_* fields (added Session 14)
 *   ← `src/services/intelligence/tank01Augment.js` reads cache keys
 *     written by `scripts/tank01-prefetch.js` (Session 15 — added
 *     this session) which calls the Tank01 NBA/MLB RapidAPI adapters.
 *
 * • Soccer features (10 of them — goals_per_90, xG, altitude, etc.)
 *   ← `src/services/intelligence/soccerFeatureExtractor.js` cascade
 *     across api-football → footapi → football-data cache keys.
 *
 * • Park factors (Session 15 — MLB)
 *   ← `src/data/parkFactors.js` — static FanGraphs 2024-25 data.
 *
 * • Weather (Session 15 — MLB + soccer)
 *   ← `src/services/weatherService.js` calls Open-Meteo (no key),
 *     cached 1h in Redis. Skipped for dome stadiums.
 *
 * • Pace factors (Session 15 — NBA)
 *   ← `src/data/paceFactors.js` — static NBA team pace data.
 *
 * No signal currently surfaces in user-facing reasoning that isn't
 * populated by one of the sources above. When a source is down, the
 * signal returns null and reasoning omits it gracefully — never
 * fabricated.
 */

const axios = require('axios');
const { getSportConfig } = require('../../config/sports');
const { getSupabaseServiceClient } = require('../../utils/supabase');
const { normalizeName } = require('../../utils/normalize');
const featureCache = require('./featureCache');
const trapDetection = require('./trapDetection');
const consistencyScore = require('./consistencyScore');
const gameLogService = require('./gameLogService');
// Session 7j — soccer branch. The extractor reads from prefetched
// Redis cache; no external HTTP on the user request path.
const { extractSoccerFeatures, isSoccerSport } = require('./soccerFeatureExtractor');
// Session 14 — Tank01 augmentor. Reads cache keys the Tank01
// adapters write; no network from this path. Daily prefetch (future)
// populates the cache. Until that lands, the augmentor returns
// empty objects and the existing ESPN-derived features stand alone.
const tank01Augment = require('./tank01Augment');
// Session 15 — static lookup tables (MLB park factors, NBA pace
// factors). Pure synchronous reads, no network, no cache. Merged
// into the feature map alongside the per-sport ESPN payload.
const { getParkFactor } = require('../../data/parkFactors');
const { getPaceFactor } = require('../../data/paceFactors');
// Session 15 — Open-Meteo weather fetch. 1h Redis cache, 5s timeout,
// silent on failure. Skipped for dome stadiums via the venue index.
const weatherService = require('../weatherService');
const { getMlbVenue, getWcVenueCoords } = require('../../data/venueCoordinates');

const HTTP_TIMEOUT_MS = 8_000;

// Resolve a free-form player + sport to a roster row. Returns null on
// any failure so callers can still proceed with partial features.
async function lookupPlayer({ player, sport }) {
  if (!player || !sport) return null;
  try {
    const supabase = getSupabaseServiceClient();
    const norm = normalizeName(player);
    const { data, error } = await supabase
      .from('player_id_map')
      .select('display_name, normalized_name, espn_id, team_abbr, sport')
      .eq('sport', sport)
      .eq('normalized_name', norm)
      .limit(1)
      .maybeSingle();
    if (error || !data) return null;
    return data;
  } catch (err) {
    console.warn('[computeFeatures] player lookup failed:', err.message);
    return null;
  }
}

// Pull today's scoreboard for the sport and find the game the player's
// team plays in. Returns { gameId, opponentAbbr, isHome } or null.
/**
 * Session 64 (Order 1.6) — resolve the player's game for a SPECIFIC DATE.
 *
 * This used to be `lookupTodayGame`, calling the ESPN scoreboard with NO date
 * param — it took whatever ESPN calls "today". At the late slots (01:00/03:00
 * UTC = 21:00/23:00 ET) that is the PREVIOUS day's card, so a prop for
 * tonight bound `opponentAbbr` and `home_away` to YESTERDAY'S opponent. Those
 * feed the ±1.0 opponent-defense factor and the home/away factor, so it is a
 * MODEL-OUTPUT bug, not bookkeeping.
 *
 * `gameDate` (YYYY-MM-DD, ET) comes from the prop's BOUND game — the same game
 * the ledger, retention and settlement now use, so all four agree.
 * Absent gameDate → we do NOT guess a day; the caller degrades honestly.
 */
/** ET calendar date of an ISO timestamp. */
function dateETOf(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);
}

async function lookupGameOnDate({ sport, teamAbbr, gameDate }) {
  if (!sport || !teamAbbr) return null;
  let sportCfg;
  try { sportCfg = getSportConfig(sport); } catch { return null; }
  try {
    const url = gameDate
      ? `${sportCfg.espnScoreboard}${sportCfg.espnScoreboard.includes('?') ? '&' : '?'}dates=${String(gameDate).replace(/-/g, '')}`
      : sportCfg.espnScoreboard;
    const res = await axios.get(url, { timeout: HTTP_TIMEOUT_MS });
    const events = res.data?.events || [];
    for (const ev of events) {
      const comp = ev?.competitions?.[0];
      if (!comp) continue;
      const competitors = comp.competitors || [];
      const home = competitors.find((c) => c.homeAway === 'home');
      const away = competitors.find((c) => c.homeAway === 'away');
      const homeAbbr = home?.team?.abbreviation;
      const awayAbbr = away?.team?.abbreviation;
      if (homeAbbr === teamAbbr) {
        return { gameId: String(ev.id), opponentAbbr: awayAbbr, isHome: true };
      }
      if (awayAbbr === teamAbbr) {
        return { gameId: String(ev.id), opponentAbbr: homeAbbr, isHome: false };
      }
    }
    return null;
  } catch (err) {
    console.warn('[computeFeatures] scoreboard fetch failed:', err.message);
    return null;
  }
}

async function safeGetFeatures(input) {
  try {
    const payload = await featureCache.getFeatures(input);
    return payload?.features || {};
  } catch (err) {
    console.warn('[computeFeatures] feature fetch failed:', err.message);
    return {};
  }
}

async function safeGetTrap(input) {
  const fallback = { composite: 0, signals: {}, active_count: 0, recommendation: 'proceed' };
  try {
    return (await trapDetection.getTrapScore(input)) || fallback;
  } catch (err) {
    console.warn('[computeFeatures] trap detection failed:', err.message);
    return fallback;
  }
}

async function safeGetConsistency({ playerName, sport, statType, statRows }) {
  const fallback = { consistency: 'unknown', score: null, games: 0 };
  try {
    // Session 63 — normalized rows from the REAL per-sport sources (MLB
    // statsapi / ESPN gamelog), not the NBA-WNBA-only Python service. This one
    // call feeds BOTH the consistency factor and (via meta.gameLogs) the
    // probability estimator, which had no rows at all in production.
    // `statRows` is passed in by computeFeaturesForProp so the fetch happens
    // ONCE per prop (it also powers game_count_in_7d, built before features).
    const logs = Array.isArray(statRows)
      ? statRows
      : await featureCache.getStatRows(playerName, sport, statType);
    if (!logs || logs.length === 0) return { result: fallback, gameLogs: [] };
    const result = await consistencyScore.getConsistency({
      playerName, sport, statType, gameLogs: logs,
    });
    return { result: result || fallback, gameLogs: logs };
  } catch (err) {
    console.warn('[computeFeatures] consistency failed:', err.message);
    return { result: fallback, gameLogs: [] };
  }
}

async function computeFeaturesForProp(rawProp = {}) {
  // Default to NBA when caller omits — matches what legacy analyzeProp does.
  const sport = String(rawProp.sport || 'nba').toLowerCase();

  // Soccer routes to a different extractor — different data sources
  // (football-data.org + cache vs ESPN scoreboard), different feature
  // set (xG, altitude, referee, set-piece role). The extractor returns
  // the same {features, trap, consistency, prop, meta} shape engine1
  // consumes, so analyzeViaEngine1 is sport-agnostic downstream.
  if (isSoccerSport(sport)) {
    const soccerResult = await extractSoccerFeatures(rawProp);
    // Soccer extractor returns a placeholder trap object. Run the real
    // soccer-branch trap detection here using the freshly computed
    // features so analyzeViaEngine1 sees a populated trap composite.
    const soccerTrap = await safeGetTrap({
      sport: 'soccer',
      playerName: rawProp.player,
      statType: soccerResult.meta?.statType,
      gameId: null,
      gameContext: { home_away: soccerResult.features?.home_away === 1.0 ? 'home' : (soccerResult.features?.home_away === 0.0 ? 'away' : null) },
      features: soccerResult.features,
      odds: { playerLine: soccerResult.prop?.line, consensus: null },
    });
    return { ...soccerResult, trap: soccerTrap };
  }

  const errors = [];
  const player = rawProp.player;
  const statType = rawProp.stat_type || rawProp.statType;
  const line = Number(rawProp.line);
  const direction = rawProp.direction || 'over';
  const book = rawProp.book || 'unknown';

  if (!player || !statType || !Number.isFinite(line)) {
    errors.push('missing required fields (player, stat_type, or line)');
  }

  const roster = await lookupPlayer({ player, sport });
  if (!roster) errors.push('player_not_found_in_id_map');

  const teamAbbr = roster?.team_abbr ?? null;
  const playerId = roster?.espn_id ?? null;

  // Session 64 (Order 1.6) — bind opponent/home-away features to the prop's
  // REAL game. `gameBinder` attaches game_date in snapshotService before
  // grading, so grading references the same game as the ledger/retention.
  // With no bound date we do NOT fall back to a dateless "today" lookup —
  // that is exactly what bound the wrong opponent. The features simply stay
  // absent, and engine1 omits the factors rather than scoring a wrong matchup.
  const boundGameDate = rawProp.game_date
    || (rawProp.game_time ? dateETOf(rawProp.game_time) : null);
  const game = (teamAbbr && boundGameDate)
    ? await lookupGameOnDate({ sport, teamAbbr, gameDate: boundGameDate })
    : null;
  if (teamAbbr && !boundGameDate) errors.push('no_bound_game_date');
  if (!game) errors.push('no_game_scheduled_today');

  // Session 63 — fetch the normalized per-game rows ONCE. They feed three
  // consumers that were all starving: the consistency factor, the probability
  // estimator (via meta.gameLogs), and game_count_in_7d below.
  const statRows = await featureCache.getStatRows(player, sport, statType);

  const gameContext = {
    home_away: game ? (game.isHome ? 'home' : 'away') : null,
    // `game_count_in_7d` gates engine1's heavy_workload_7d (-0.5). Nothing ever
    // populated it, so that factor could not fire. Derived from real logged
    // game dates; null (omitted) when we have no dated rows.
    game_count_in_7d: featureCache.gameCountInWindow(statRows, 7),
    // DELIBERATELY NOT SET: `teamId`. It was tempting to thread it here to
    // unlock injuryFeatures, but that would be dead code dressed as a fix —
    // three things block that factor and none is solved by a teamId here:
    //   1. getFeatures reads `teamId` as a TOP-LEVEL input, not off gameContext;
    //   2. `player_id_map` has no team_id column (lookupPlayer selects
    //      espn_id/team_abbr only), so there is no id to pass;
    //   3. injury_severity_score counts MISSING KNOWN STARTERS and no starter-id
    //      list exists, so it resolves to 0 and engine1's factor (needs >= 2)
    //      still cannot fire.
    // There is also an unresolved semantic: the factor is documented as
    // OPPONENT injuries but getFeatures passes `teamId`, with `opponentTeamId`
    // sitting unused beside it. Left alone on purpose — see
    // specs/audit-data/grade-collapse.md.
    // DELIBERATELY NOT SET: `season_type`. engine1's playoff factors gate on
    // `season_type >= 2`, but ESPN's season_type 2 means REGULAR season — so
    // threading it raw would fire "veteran_in_playoffs" in July. The factor also
    // needs career_playoff_games, which only the offline Python service
    // provides. Left unset on purpose; see specs/audit-data/grade-collapse.md.
  };

  const features = await safeGetFeatures({
    playerId,
    playerName: player,
    statType,
    sport,
    teamAbbr,
    opponentAbbr: game?.opponentAbbr ?? null,
    gameId: game?.gameId ?? null,
    gameContext,
  });

  if (!features || Object.keys(features).length === 0) {
    errors.push('no_features_computed');
  }

  // Session 14 — Tank01 augmentation. Sport-specific. Both calls are
  // cache-only (no network), Promise.allSettled-style isolated so a
  // Redis hiccup on the Tank01 read doesn't fail the whole grade.
  // The `t01_*` fields land alongside the ESPN-derived features;
  // grading + reasoning + trap detection read them when present and
  // ignore them when absent.
  // Session 64 (Order 1.6) — same class of bug as the scoreboard lookup: this
  // was TODAY's UTC date, so a late-slot grade read the wrong day's Tank01
  // cache. Use the prop's BOUND game date; fall back to today only when there
  // is no bound game (the t01_* fields are additive and simply stay absent).
  const ymd = (boundGameDate || new Date().toISOString().slice(0, 10)).replace(/-/g, '');
  try {
    if (sport === 'nba') {
      const aug = await tank01Augment.augmentNbaFeatures({
        gameId: game?.gameId ?? null,
        playerName: player,
        ymd,
      });
      Object.assign(features, aug);
    } else if (sport === 'mlb') {
      const aug = await tank01Augment.augmentMlbFeatures({
        gameId: game?.gameId ?? null,
        batterName: player,
        // batterId/pitcherId/pitcherName not yet plumbed through
        // computeFeatures — the augmentor returns name-only markers
        // when IDs are absent.
        ymd,
      });
      Object.assign(features, aug);
    }
  } catch (err) {
    // Never let augmentation failure poison the grade.
    console.warn('[computeFeatures] Tank01 augmentation skipped:', err.message);
  }

  // Session 15 — static context augmentation. Park factors (MLB),
  // pace factors (NBA). Synchronous, can't fail; the lookups return
  // null on miss, which we treat as "no signal — drop the field".
  try {
    if (sport === 'mlb') {
      // Home team in this matchup hosts the game; if the player's
      // team is home, use their abbr — otherwise use the opponent's.
      const homeAbbr = game?.isHome ? teamAbbr : game?.opponentAbbr;
      const park = getParkFactor(homeAbbr);
      if (park) {
        features.park_hr = park.hr;
        features.park_h = park.h;
        features.park_r = park.r;
        features.park_home = homeAbbr;
      }
    } else if (sport === 'nba') {
      // Pace factors are per-team — use the player's own team (fast
      // teams up the count regardless of opponent, slow teams
      // compress). Opponent pace effect is a separate signal we
      // could layer in a follow-up.
      const pace = getPaceFactor(teamAbbr);
      if (pace != null) features.pace_factor = pace;
      const oppPace = getPaceFactor(game?.opponentAbbr);
      if (oppPace != null) features.opp_pace_factor = oppPace;
    }
  } catch (err) {
    console.warn('[computeFeatures] static context augmentation skipped:', err.message);
  }

  // Session 15 — weather. Open-Meteo via weatherService. 5s timeout,
  // 1h Redis cache, dome-aware skip. Outdoor MLB + soccer benefit;
  // basketball indoor venues skip entirely.
  try {
    if (sport === 'mlb') {
      const homeAbbr = game?.isHome ? teamAbbr : game?.opponentAbbr;
      const venue = homeAbbr ? getMlbVenue(homeAbbr) : null;
      if (venue && !venue.dome && Number.isFinite(venue.lat) && Number.isFinite(venue.lon)) {
        const w = await weatherService.getWeather(venue.lat, venue.lon);
        if (w) {
          features.weather_temp_f = w.temp_f ?? null;
          features.weather_wind_mph = w.wind_mph ?? null;
          features.weather_wind_dir = w.wind_dir ?? null;
          features.weather_precip = w.precip_mm ?? null;
        }
      }
    }
    // Soccer weather slots in via the soccer branch (handled earlier
    // for the soccer sport — the venue is part of the cascade).
  } catch (err) {
    console.warn('[computeFeatures] weather lookup skipped:', err.message);
  }

  const trap = await safeGetTrap({
    playerName: player,
    statType,
    sport,
    gameId: game?.gameId ?? null,
    gameContext,
    features,
    odds: { playerLine: line, consensus: null },
  });

  const { result: consistency, gameLogs } = await safeGetConsistency({
    playerName: player, sport, statType, statRows,
  });

  return {
    // Shape engine1.gradeProp() consumes.
    features,
    trap,
    consistency,
    prop: { line, direction },
    // Extra context the wiring helper (Fix 2) uses to build human-readable
    // reasoning sentences. Not consumed by engine1 itself.
    meta: {
      player,
      statType,
      line,
      direction,
      book,
      sport,
      teamAbbr,
      playerId,
      opponentAbbr: game?.opponentAbbr ?? null,
      gameId: game?.gameId ?? null,
      isHome: game?.isHome ?? null,
      gameLogs,
      errors,
    },
  };
}

module.exports = {
  computeFeaturesForProp,
  __internals: {
    lookupPlayer,
    lookupGameOnDate,
    safeGetFeatures,
    safeGetTrap,
    safeGetConsistency,
  },
};
