/**
 * Trap detection — 7 independent signals → one composite trap score.
 *
 *   reverse_line_movement      — sharp money moved AGAINST public side
 *   historical_hit_rate_paradox — high hit-rate AND line moving against them
 *   new_context_trap            — first game in a new context (playoffs G1)
 *   recency_inflation           — L5 dramatically above L20 (chasing hot)
 *   juice_degradation           — vig got worse while line stayed flat
 *   teammate_return_trap        — key teammate returning from injury
 *   line_consensus_divergence   — one book's line ≠ the consensus
 *
 * Composite formula:
 *   composite = average(active_signal_scores)
 *
 * Only ACTIVE signals (the ones with enough data to compute) average in.
 * A null/inactive signal does NOT dilute the score — this prevents thin
 * data from producing an artificially-low trap score on new deployments.
 *
 *   < 0.25 → proceed
 *   < 0.50 → caution
 *   ≥ 0.50 → avoid
 */

const { reverseLineMovement, juiceDegradation, getLineMovement } = require('./lineMovement');
const { getSupabaseServiceClient } = require('../../utils/supabase');
// DUP-1 (Session 7e): consolidated into src/utils/normalize.js.
// Trap matching strips digits — keepDigits stays at the default false.
const { normalizeName } = require('../../utils/normalize');

function inactive(reason) {
  return { score: 0, active: false, explanation: reason };
}

// Detect whether a key teammate transitioned from OUT in the recent past
// to AVAILABLE now. Called by the orchestrator (Section 2) before invoking
// the trap detector — the orchestrator owns the injury-history context.
// priorInjuriesByGame is an array of injury snapshots (most recent first).
// Each entry: array of { playerId, status }. Returns the highest-usage
// teammate that has flipped from OUT/DOUBTFUL to PROBABLE/active, or null.
function detectReturningTeammate(currentInjuries, priorInjuriesByGame, usageMap = {}) {
  if (!Array.isArray(priorInjuriesByGame) || priorInjuriesByGame.length === 0) return null;
  const currentOutIds = new Set(
    (currentInjuries || [])
      .filter((i) => i.status === 'OUT' || i.status === 'DOUBTFUL')
      .map((i) => String(i.playerId)),
  );
  // A player was "previously out" if they appeared as OUT/DOUBTFUL in any
  // of the last 1-3 games' snapshots.
  const priorOutIds = new Set();
  for (const snap of priorInjuriesByGame.slice(0, 3)) {
    for (const inj of snap || []) {
      if (inj.status === 'OUT' || inj.status === 'DOUBTFUL') {
        priorOutIds.add(String(inj.playerId));
      }
    }
  }
  let best = null;
  for (const id of priorOutIds) {
    if (currentOutIds.has(id)) continue;
    const usage = Number(usageMap[id]) || 0;
    if (!best || usage > best.usage) best = { playerId: id, usage };
  }
  return best;
}

// 1. Reverse line movement.
async function signalReverseLineMovement(input) {
  const { gameId, playerName, statType, publicBetPct } = input;
  if (!gameId || !playerName || !statType) return inactive('missing inputs');
  const r = await reverseLineMovement(gameId, playerName, statType, publicBetPct);
  if (!r) return inactive('not enough snapshots');
  if (!r.isReverse) return { score: 0, active: true, explanation: 'line moved with public' };
  return {
    score: r.score,
    active: true,
    explanation: `line moved toward ${r.lineDirection} while public was on ${r.publicSide}`,
  };
}

// 2. Historical hit-rate paradox — high hit rate AND line moving against the
// player. Uses resolution_results history. Confidence-scaled: thin history
// gets a proportional penalty.
async function signalHistoricalHitRateParadox(input) {
  const { playerName, statType, sport, gameId } = input;
  if (!playerName || !statType || !sport) return inactive('missing inputs');
  const supabase = getSupabaseServiceClient();
  const { data, error } = await supabase
    .from('resolution_results')
    .select('result, direction, line')
    .eq('sport', sport)
    .eq('stat_type', statType)
    .eq('player_name', playerName);
  if (error || !data || data.length < 10) {
    return inactive(`only ${data?.length ?? 0} historical resolves`);
  }
  const hits = data.filter((r) => r.result === 'hit').length;
  const hitRate = hits / data.length;

  const lm = gameId ? await getLineMovement(gameId, playerName, statType) : null;
  if (!lm) return inactive('no line movement context');

  // "Against direction" — if the player generally bets OVER and the line
  // moves DOWN, that's a trap; flip for UNDER.
  const directionGuess = data.filter((r) => r.direction === 'over').length >= data.length / 2 ? 'over' : 'under';
  const againstDirection = (directionGuess === 'over' && lm.movement < 0)
    || (directionGuess === 'under' && lm.movement > 0);
  if (!againstDirection) return { score: 0, active: true, explanation: 'line moving with the player\'s usual side' };

  const confidence = Math.min(data.length / 20, 1.0);
  const score = Math.min(1.0, hitRate * Math.abs(lm.movement)) * confidence;
  return {
    score,
    active: true,
    explanation: `hit rate ${(hitRate * 100).toFixed(0)}% (${data.length} resolves) but line moved ${lm.movement} against ${directionGuess}`,
  };
}

// 3. New context trap — first game in a context where stats may not transfer.
function signalNewContextTrap(input) {
  const { gameContext = {} } = input;
  let flags = 0;
  const reasons = [];
  if (gameContext.game_in_series === 1) { flags += 1; reasons.push('series_g1'); }
  if (gameContext.first_playoff_game) { flags += 1; reasons.push('first_playoff_game'); }
  if (gameContext.new_opponent_in_series) { flags += 1; reasons.push('new_opponent_in_series'); }
  if (gameContext.new_venue) { flags += 1; reasons.push('new_venue'); }
  if (flags === 0) return inactive('no context flags');
  return {
    score: flags / 4,
    active: true,
    explanation: `new context: ${reasons.join(', ')}`,
  };
}

// 4. Recency inflation — L5 dramatically above L20.
function signalRecencyInflation(input) {
  const f = input.features || {};
  const l5 = Number(f.l5_avg);
  const l20 = Number(f.l20_avg);
  if (!Number.isFinite(l5) || !Number.isFinite(l20) || l20 <= 0) {
    return inactive('l5_avg or l20_avg missing');
  }
  const ratio = (l5 - l20) / l20;
  if (ratio <= 0) return { score: 0, active: true, explanation: 'L5 not hotter than L20' };
  return {
    score: Math.min(1.0, ratio),
    active: true,
    explanation: `L5 (${l5.toFixed(1)}) ${(ratio * 100).toFixed(0)}% above L20 (${l20.toFixed(1)})`,
  };
}

// 5. Juice degradation — vig got worse while the line stayed flat.
async function signalJuiceDegradation(input) {
  const { gameId, playerName, statType } = input;
  if (!gameId || !playerName || !statType) return inactive('missing inputs');
  const r = await juiceDegradation(gameId, playerName, statType);
  if (!r) return inactive('not enough snapshots');
  if (!r.applicable) return inactive('line moved too much for juice signal');
  return { score: r.score, active: true, explanation: `juice worsening on ${r.worstSide}` };
}

// 6. Teammate return trap — key teammate returning → suppression.
function signalTeammateReturnTrap(input) {
  const { gameContext = {} } = input;
  const returning = gameContext.returning_teammate_usage_rate;
  if (!Number.isFinite(returning) || returning <= 0) return inactive('no returning teammate');
  return {
    score: Math.min(1.0, returning * 0.5),
    active: true,
    explanation: `teammate returning with ${(returning * 100).toFixed(0)}% usage`,
  };
}

// 7. Line consensus divergence — one book's line differs from the consensus.
function signalLineConsensusDivergence(input) {
  const { odds = {} } = input;
  const consensus = odds.consensus;
  const playerLine = Number(odds.playerLine);
  if (!consensus || !Number.isFinite(playerLine)) return inactive('no consensus or player line');
  const median = Number(consensus.median);
  if (!Number.isFinite(median)) return inactive('consensus median missing');
  // Standard deviation across books; fall back to a 0.5 floor so even a
  // tight consensus produces a meaningful divisor.
  const stddev = Math.max(Number(consensus.stddev) || 0.5, 0.5);
  const score = Math.min(1.0, Math.abs(playerLine - median) / stddev);
  return {
    score,
    active: true,
    explanation: `player line ${playerLine} vs consensus median ${median} (σ=${stddev})`,
  };
}

const SIGNALS = [
  ['reverse_line_movement', signalReverseLineMovement],
  ['historical_hit_rate_paradox', signalHistoricalHitRateParadox],
  ['new_context_trap', signalNewContextTrap],
  ['recency_inflation', signalRecencyInflation],
  ['juice_degradation', signalJuiceDegradation],
  ['teammate_return_trap', signalTeammateReturnTrap],
  ['line_consensus_divergence', signalLineConsensusDivergence],
];

// ---------------------------------------------------------------
// Soccer trap signals (Session 7j).
//
// All soccer signals are synchronous — they read pre-computed feature
// values straight off `input.features`. The feature extractor and the
// daily prefetch are responsible for filling those fields; nothing
// here touches the network. Each signal returns the same
// `{score, active, explanation}` shape as the NBA path.
//
// `positive: true` signals (e.g. referee_card_heavy on a CARDS over)
// are visible in the signals map but DO NOT contribute to the
// composite — they're favorable to the bet, not a trap reason.
// ---------------------------------------------------------------

function signalXgRegression(input) {
  const xgDelta = input?.features?.xg_delta;
  if (!Number.isFinite(xgDelta)) return inactive('no xG data');
  if (xgDelta > 0.3) {
    return {
      score: Math.min(1, xgDelta),
      active: true,
      explanation: `scoring ${(xgDelta * 100).toFixed(0)}% above expected goals — regression risk`,
    };
  }
  return { score: 0, active: true, explanation: 'xG tracks actual goals' };
}

function signalAltitudeRisk(input) {
  const f = input?.features || {};
  if (f.altitude_impact !== 'high') return inactive('not high altitude');
  if (f.home_continent) return inactive('host-continent team — assumed acclimated');
  return {
    score: 0.6,
    active: true,
    explanation: `non-acclimatized team at ${f.venue_altitude_ft || 'high'}ft altitude — historical goal reduction`,
  };
}

function signalRotationRisk(input) {
  const f = input?.features || {};
  if (!Number.isFinite(f.start_rate) || !Number.isFinite(f.rest_days)) {
    return inactive('missing start_rate or rest_days');
  }
  if (f.start_rate < 0.7 && f.rest_days <= 2) {
    return {
      score: 0.7,
      active: true,
      explanation: `${(f.start_rate * 100).toFixed(0)}% start rate on ${f.rest_days}-day rest — rotation candidate`,
    };
  }
  return { score: 0, active: true, explanation: 'start rate / rest acceptable' };
}

function signalMinuteDiscount(input) {
  const mpg = input?.features?.minutes_per_game;
  if (!Number.isFinite(mpg)) return inactive('no minutes-per-game');
  if (mpg < 70) {
    return {
      score: 0.5,
      active: true,
      explanation: `averages ${mpg.toFixed(0)} minutes/match — line assumes full 90`,
    };
  }
  return { score: 0, active: true, explanation: 'plays full matches' };
}

function signalRefereeCardBias(input) {
  const f = input?.features || {};
  const cpg = f.referee_cards_per_game;
  if (!Number.isFinite(cpg)) return inactive('no referee data');
  // Positive signal — applies only when the prop is about CARDS and the
  // referee is card-heavy. Surface but exclude from composite.
  const statType = f.stat_type || input?.statType;
  if (cpg > 5 && statType === 'cards') {
    return {
      score: 0, active: false, positive: true,
      explanation: `${f.referee_name || 'referee'} averages ${cpg.toFixed(1)} cards/match — favorable for card over`,
    };
  }
  return inactive('referee card rate not a positive signal for this stat type');
}

function signalStrongDefense(input) {
  const f = input?.features || {};
  const statType = f.stat_type || input?.statType;
  if (!['goals', 'shots_on_target', 'shots'].includes(statType)) {
    return inactive('only applies to scoring/shot stats');
  }
  const rank = f.opp_defensive_rank;
  if (!Number.isFinite(rank)) return inactive('no opponent defensive rank');
  if (rank <= 5) {
    return {
      score: 0.6,
      active: true,
      explanation: `top-${rank} defense — scoring/shot props face headwinds`,
    };
  }
  return { score: 0, active: true, explanation: 'opponent defense not elite' };
}

const SOCCER_SIGNALS = [
  ['xg_regression', signalXgRegression],
  ['altitude_risk', signalAltitudeRisk],
  ['rotation_risk', signalRotationRisk],
  ['minute_discount', signalMinuteDiscount],
  ['referee_card_bias', signalRefereeCardBias], // positive — excluded from composite
  ['strong_defense', signalStrongDefense],
];

function recommend(composite) {
  if (composite >= 0.5) return 'avoid';
  if (composite >= 0.25) return 'caution';
  return 'proceed';
}

async function getTrapScore(input = {}) {
  // Soccer runs a different signal set (xG regression, altitude, rotation,
  // referee bias). NBA/WNBA/MLB run the line-movement-centric set.
  const sport = String(input?.sport || '').toLowerCase();
  const isSoccer = sport === 'soccer' || sport === 'football';
  const signalList = isSoccer ? SOCCER_SIGNALS : SIGNALS;

  const signals = {};
  for (const [name, fn] of signalList) {
    try {
      const result = await fn(input);
      signals[name] = result;
    } catch (err) {
      signals[name] = { score: 0, active: false, explanation: `error: ${err?.message || 'unknown'}` };
    }
  }
  // Composite excludes signals flagged `positive: true` — those are
  // favorable to the bet, not trap reasons.
  const activeScores = Object.values(signals)
    .filter((s) => s.active && !s.positive)
    .map((s) => s.score);
  const composite = activeScores.length === 0
    ? 0
    : activeScores.reduce((a, b) => a + b, 0) / activeScores.length;
  return {
    composite,
    signals,
    active_count: activeScores.length,
    recommendation: recommend(composite),
  };
}

module.exports = {
  getTrapScore,
  normalizeName,
  detectReturningTeammate,
  __internals: {
    signalReverseLineMovement,
    signalHistoricalHitRateParadox,
    signalNewContextTrap,
    signalRecencyInflation,
    signalJuiceDegradation,
    signalTeammateReturnTrap,
    signalLineConsensusDivergence,
    signalXgRegression,
    signalAltitudeRisk,
    signalRotationRisk,
    signalMinuteDiscount,
    signalRefereeCardBias,
    signalStrongDefense,
    recommend,
  },
};
