/**
 * Soccer feature extractor — soccer's answer to the NBA feature stack.
 *
 * Reads from prefetch-populated Redis cache (NEVER hits external APIs on
 * the user-request path) and shapes the result into engine1's feature
 * vector plus a soccer-specific overlay. Engine1 ignores unknown keys
 * so the overlay is read by:
 *   - trapDetection (soccer traps)
 *   - analyzeViaEngine1 (soccer reasoning sentences)
 *   - downstream UI rendering
 *
 * Cache contract — keys written by `scripts/soccer-data-prefetch.js`
 * and `poller/soccer.js`:
 *   soccer:player:{normalizedName}           → per-player season aggregate
 *   soccer:nextmatch:{teamName}              → next fixture (opp, venue, ref, daysUntil)
 *   soccer:lastfixture:{teamName}            → most recent finished fixture (rest_days)
 *   soccer:referee:{refereeName}             → referee cards/penalties per game
 *   soccer:teamdefense:{league}:{teamName}   → opp defensive aggregates
 *
 * Any cache miss → that field stays null. Engine1 + reasoning handle
 * nulls gracefully (the trap, consistency, and grading pipeline all
 * default-skip missing signals rather than penalizing).
 *
 * No external HTTP. No throws. Every step independently fault-tolerant.
 */

const { cacheGet } = require('../../utils/redis');
const { normalizeName } = require('../../utils/normalize');
const wc = require('../../data/worldcup2026');
// Session 16 — World Cup venue weather. Open-Meteo lookup is cached
// 1h, has a 5s timeout, and degrades silently on failure. Dome
// venues skip the fetch entirely (operators close the roof when
// conditions warrant — weather doesn't drive grade in that case).
const { getWcVenueCoords } = require('../../data/venueCoordinates');
const weatherService = require('../weatherService');

const SOCCER_SPORTS = new Set(['soccer', 'football']);

async function safeCacheGet(key) {
  try {
    return await cacheGet(key);
  } catch (err) {
    console.warn('[soccerFeatures] cache read failed:', key, err.message);
    return null;
  }
}

// Source-priority cascade (Session 9). Each load function checks
// adapter-specific keys in priority order:
//   1. api-football.com (PRIMARY — 100 req/day, richest payload)
//   2. FootApi via RapidAPI (BACKUP — 50 req/day, Sofascore mirror)
//   3. football-data.org (TERTIARY — fixtures/standings only)
//
// The richer adapters write name-keyed aliases (`apifootball:player_by_name:…`,
// `footapi:player_by_name:…`) during the daily prefetch so the request
// path can resolve without an upstream call. When the alias key is
// missing — either because the prefetch hasn't run, the adapter has
// no key configured, or the player isn't covered — we fall through to
// the next source. Final fallback is the legacy `soccer:player:{name}`
// key the football-data prefetch already populates.
//
// Every value is tagged with `_source` so trap detection and reasoning
// can attribute the data origin (and so we can spot when a source is
// silently failing in production logs).
async function loadFromCascade(keys) {
  for (const { key, source } of keys) {
    const v = await safeCacheGet(key);
    if (v && typeof v === 'object') {
      return { ...v, _source: v._source || source };
    }
  }
  return null;
}

async function loadPlayerProfile(playerName) {
  if (!playerName) return null;
  const n = normalizeName(playerName);
  return loadFromCascade([
    { key: `apifootball:player_by_name:${n}`, source: 'api-football' },
    { key: `footapi:player_by_name:${n}`,     source: 'footapi' },
    { key: `soccer:player:${n}`,              source: 'football-data' },
  ]);
}

async function loadNextMatch(teamName) {
  if (!teamName) return null;
  return loadFromCascade([
    { key: `apifootball:nextmatch:${teamName}`, source: 'api-football' },
    { key: `soccer:nextmatch:${teamName}`,      source: 'football-data' },
  ]);
}

async function loadLastFixture(teamName) {
  if (!teamName) return null;
  return loadFromCascade([
    { key: `apifootball:lastfixture:${teamName}`, source: 'api-football' },
    { key: `soccer:lastfixture:${teamName}`,      source: 'football-data' },
  ]);
}

async function loadRefereeProfile(refName) {
  if (!refName) return null;
  return loadFromCascade([
    { key: `apifootball:referee_by_name:${refName}`, source: 'api-football' },
    { key: `footapi:referee_by_name:${refName}`,     source: 'footapi' },
    { key: `soccer:referee:${refName}`,              source: 'football-data' },
  ]);
}

async function loadTeamDefense(league, teamName) {
  if (!league || !teamName) return null;
  return safeCacheGet(`soccer:teamdefense:${String(league).toLowerCase()}:${teamName}`);
}

// Compute rest days from a `lastfixture` payload. Returns null if the
// payload is absent or malformed — engine1 reads null as "unknown" and
// neither rewards nor penalizes.
function computeRestDays(lastFixture) {
  if (!lastFixture || !lastFixture.utcDate) return null;
  const last = Date.parse(lastFixture.utcDate);
  if (!Number.isFinite(last)) return null;
  // Use Date.now() so tests can fake the clock via jest.useFakeTimers().
  const diffMs = Date.now() - last;
  if (diffMs < 0) return null; // future date — malformed
  return Math.floor(diffMs / (24 * 3600 * 1000));
}

// xG regression risk fires when actual goals significantly outpace
// expected goals — historically these regress to the mean within ~10
// matches. The 0.3 threshold is the standard analytics-community cutoff.
function xgRegressionRisk(xgDelta) {
  if (!Number.isFinite(xgDelta)) return false;
  return xgDelta > 0.3;
}

/**
 * extractSoccerFeatures — the public entry. Async (cache reads), never
 * throws, always returns the engine1-compatible shape even when every
 * lookup misses. Errors land in `meta.errors` so the route layer can
 * downgrade confidence and explain.
 *
 * @param {Object} input  { player, stat_type, line, direction, sport,
 *                          team?, opponent?, venue?, league? }
 * @returns {Object} { features, trap, consistency, prop, meta }
 */
async function extractSoccerFeatures(input = {}) {
  const errors = [];
  const player = input.player;
  const statType = input.stat_type || input.statType;
  const line = Number(input.line);
  const direction = input.direction || 'over';
  const league = input.league || 'WC';

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

  // Player profile — drives base stats, xG.
  const profile = await loadPlayerProfile(player);
  if (!profile) errors.push('player_not_found_in_cache');

  // Team — explicit if provided, otherwise inferred from the profile.
  const team = input.team || profile?.team || null;
  if (!team) errors.push('team_not_resolved');

  // Next match context — drives opponent, venue, referee.
  const nextMatch = team ? await loadNextMatch(team) : null;
  if (!nextMatch) errors.push('no_match_scheduled');

  const opponent = input.opponent || nextMatch?.opponent || null;
  const venueName = input.venue || nextMatch?.venue || null;
  const refereeName = nextMatch?.referee || null;
  const isHome = nextMatch?.isHome ?? null;

  // Rest days — from last finished fixture.
  const lastFixture = team ? await loadLastFixture(team) : null;
  const restDays = computeRestDays(lastFixture);

  // Opponent defensive aggregate.
  const oppDefense = opponent ? await loadTeamDefense(league, opponent) : null;

  // Referee profile (cards + penalties per game).
  const refProfile = refereeName ? await loadRefereeProfile(refereeName) : null;

  // Venue → altitude impact.
  const venue = wc.getVenue(venueName);
  const altitudeFt = venue?.altitude_ft ?? null;
  const climate = venue?.climate ?? null;
  const homeContinent = wc.isHomeContinent(team);
  const altImpact = wc.altitudeImpact(altitudeFt);

  // Session 16 — weather for outdoor World Cup venues. The venue
  // coords file is keyed by venue name; dome venues (BC Place,
  // AT&T, etc.) are skipped via the `dome` flag rather than the
  // network call so we don't burn the 5s timeout on stadiums that
  // will close the roof anyway.
  let weather = null;
  const coords = venueName ? getWcVenueCoords(venueName) : null;
  if (coords && !coords.dome && Number.isFinite(coords.lat) && Number.isFinite(coords.lon)) {
    try {
      weather = await weatherService.getWeather(coords.lat, coords.lon);
    } catch (err) {
      // Best-effort — never block the grade on the weather fetch.
      console.warn('[soccerFeatureExtractor] weather skipped:', err.message);
    }
  }

  // Set-piece + penalty roles (static data — no async).
  const isPK = wc.isPenaltyTaker(player, team);
  const isCorner = wc.isCornerTaker(player, team);
  const isFK = wc.isFreeKickTaker(player, team);
  const tournamentHistory = wc.getTournamentHistory(player);

  // ---- Feature vector ----
  // The engine1-known keys (l5_avg, l20_avg, home_away, opp_rank_stat,
  // rest_days) are filled where we have data so the legacy grading
  // logic still produces a grade. Soccer-specific fields are passed
  // through (engine1 ignores unknown keys).
  const features = {
    // engine1-canonical
    l5_avg: profile?.recent_form_per_90 ?? null,        // last 5 matches of THIS stat type, per 90
    l20_avg: profile?.season_per_90 ?? profile?.goals_per_90 ?? null,
    l10_stddev: null,                                   // Day 1: no rolling stddev
    home_away: isHome === true ? 1.0 : (isHome === false ? 0.0 : null),
    opp_rank_stat: oppDefense?.defensive_rank_norm ?? null, // 0..1, 1=worst D
    rest_days: restDays,
    injury_severity_score: 0, // soccer Day 1 — injuries surface differently
    game_count_in_7d: null,

    // soccer-specific overlay (engine1 passes through; trap + reasoning read)
    goals_per_90:           profile?.goals_per_90 ?? null,
    assists_per_90:         profile?.assists_per_90 ?? null,
    minutes_per_game:       profile?.minutes_per_game ?? null,
    start_rate:             profile?.start_rate ?? null,
    xg_per_90:              profile?.xg_per_90 ?? null,
    xa_per_90:              profile?.xa_per_90 ?? null,
    xg_delta:               profile?.xg_delta ?? null,
    xg_regression_risk:     xgRegressionRisk(profile?.xg_delta),
    is_penalty_taker:       isPK,
    takes_corners:          isCorner,
    takes_free_kicks:       isFK,
    home_continent:         homeContinent,
    venue_altitude_ft:      altitudeFt,
    altitude_impact:        altImpact,
    climate,
    // Session 16 — weather. Null when venue is a dome / not in the
    // WC venue index / Open-Meteo fetch failed. Trap detection +
    // reasoning surface these signals when present.
    weather_temp_f:         weather?.temp_f ?? null,
    weather_wind_mph:       weather?.wind_mph ?? null,
    weather_wind_dir:       weather?.wind_dir ?? null,
    weather_precip_mm:      weather?.precip_mm ?? null,
    opp_goals_conceded_per_game: oppDefense?.goals_conceded_per_game ?? null,
    opp_clean_sheet_rate:        oppDefense?.clean_sheet_rate ?? null,
    opp_defensive_rank:          oppDefense?.defensive_rank ?? null,
    referee_name:                refereeName,
    referee_cards_per_game:      refProfile?.cards_per_game ?? null,
    referee_penalties_per_game:  refProfile?.penalties_per_game ?? null,
    wc_goals_career:        tournamentHistory?.wc_goals_career ?? null,
    wc_appearances:         tournamentHistory?.wc_appearances ?? null,
    tournament_player:      !!(tournamentHistory && (tournamentHistory.wc_goals_career || 0) >= 3),
    stat_type:              statType, // trap detection peeks at this
  };

  // ---- Trap / consistency placeholders ----
  // Soccer trap detection runs in trapDetection.js (Fix 4). For now,
  // pass a neutral default — analyzeViaEngine1 calls trap detection
  // explicitly via the same path NBA uses.
  const trap = { composite: 0, signals: {}, active_count: 0, recommendation: 'proceed' };
  const consistency = { consistency: 'unknown', score: null, games: 0 };

  return {
    features,
    trap,
    consistency,
    prop: { line, direction },
    meta: {
      player,
      statType,
      line,
      direction,
      book: input.book || 'unknown',
      sport: 'soccer',
      league,
      teamAbbr: team,
      opponentAbbr: opponent,
      venue: venueName,
      referee: refereeName,
      isHome,
      gameLogs: [],
      errors,
      // Session 9 — which source the cascade actually resolved for
      // each load. Useful for debugging "why does Messi's profile
      // look thin?" — if `player_source: 'football-data'` it means
      // the api-football prefetch hasn't populated his row yet.
      sources: {
        player: profile?._source || null,
        nextMatch: nextMatch?._source || null,
        lastFixture: lastFixture?._source || null,
        referee: refProfile?._source || null,
      },
    },
  };
}

function isSoccerSport(sport) {
  return SOCCER_SPORTS.has(String(sport || '').toLowerCase());
}

module.exports = {
  extractSoccerFeatures,
  isSoccerSport,
  __internals: {
    SOCCER_SPORTS,
    computeRestDays,
    xgRegressionRisk,
    loadPlayerProfile,
    loadNextMatch,
    loadLastFixture,
    loadRefereeProfile,
    loadTeamDefense,
  },
};
