/**
 * 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
 * downgrades confidence accordingly via the adapter's reasoning string.
 *
 * ─────────────────────────────────────────────────────────────────────
 * 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.
async function lookupTodayGame({ sport, teamAbbr }) {
  if (!sport || !teamAbbr) return null;
  let sportCfg;
  try { sportCfg = getSportConfig(sport); } catch { return null; }
  try {
    const res = await axios.get(sportCfg.espnScoreboard, { 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 }) {
  const fallback = { consistency: 'unknown', score: null, games: 0 };
  try {
    const logs = await gameLogService.getGameLogs(playerName, sport, 20);
    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;

  const game = teamAbbr ? await lookupTodayGame({ sport, teamAbbr }) : null;
  if (!game) errors.push('no_game_scheduled_today');

  const gameContext = {
    home_away: game ? (game.isHome ? 'home' : 'away') : null,
  };

  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.
  const ymd = 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,
  });

  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,
    lookupTodayGame,
    safeGetFeatures,
    safeGetTrap,
    safeGetConsistency,
  },
};
