/**
 * Line movement signals built on top of line_snapshots.
 *
 * Two derived signals power the trap detector:
 *
 *   reverseLineMovement
 *     The line moved AGAINST where the public is betting. If the public is
 *     hammering OVER but the line drops (toward UNDER), sharp money is on
 *     the under and the over is a trap.
 *
 *   juiceDegradation
 *     The line didn't move but the vig on one side got worse (e.g. -110 →
 *     -130). Books are charging more for the same number — that side is
 *     the trap.
 *
 * Both signals require at least two snapshots. If snapshots are missing we
 * return null so trap detection can mark the signal "inactive" instead of
 * scoring zero (which would dilute the composite).
 */

const { getSupabaseServiceClient } = require('../../utils/supabase');
const { oddsToImplied } = require('../../utils/odds');

async function fetchSnapshots(gameId, playerName, statType) {
  const supabase = getSupabaseServiceClient();
  const { data, error } = await supabase
    .from('line_snapshots')
    .select('line, over_odds, under_odds, consensus_median, snapshot_at')
    .eq('game_id', gameId)
    .eq('stat_type', statType)
    .eq('player_name', playerName)
    .order('snapshot_at', { ascending: true });
  if (error) {
    console.warn('[lineMovement] snapshot lookup failed:', error.message);
    return [];
  }
  return data || [];
}

async function getLineMovement(gameId, playerName, statType) {
  const snaps = await fetchSnapshots(gameId, playerName, statType);
  if (snaps.length < 2) return null;
  const open = snaps[0];
  const close = snaps[snaps.length - 1];
  const movement = Number(close.line) - Number(open.line);
  const overJuiceOpen = Number(open.over_odds);
  const overJuiceClose = Number(close.over_odds);
  const underJuiceOpen = Number(open.under_odds);
  const underJuiceClose = Number(close.under_odds);
  return {
    opening_line: Number(open.line),
    current_line: Number(close.line),
    movement,
    direction: movement > 0 ? 'up' : movement < 0 ? 'down' : 'flat',
    opening_over_odds: Number.isFinite(overJuiceOpen) ? overJuiceOpen : null,
    current_over_odds: Number.isFinite(overJuiceClose) ? overJuiceClose : null,
    opening_under_odds: Number.isFinite(underJuiceOpen) ? underJuiceOpen : null,
    current_under_odds: Number.isFinite(underJuiceClose) ? underJuiceClose : null,
    juice_change_over: Number.isFinite(overJuiceClose - overJuiceOpen) ? overJuiceClose - overJuiceOpen : null,
    juice_change_under: Number.isFinite(underJuiceClose - underJuiceOpen) ? underJuiceClose - underJuiceOpen : null,
    snapshots_count: snaps.length,
    first_seen: open.snapshot_at,
    last_seen: close.snapshot_at,
  };
}

// publicBetPct is the public-money percentage on the OVER (0-100). If we
// don't have it, we estimate from odds movement direction: when the over
// got more expensive (smaller positive / bigger negative), the public was
// on the over.
async function reverseLineMovement(gameId, playerName, statType, publicBetPct) {
  const lm = await getLineMovement(gameId, playerName, statType);
  if (!lm) return null;

  // Estimate public side if not provided.
  let publicSide;
  if (Number.isFinite(publicBetPct)) {
    publicSide = publicBetPct >= 50 ? 'over' : 'under';
  } else if (Number.isFinite(lm.juice_change_over)) {
    // If over juice got worse (became more negative), book is shading away
    // from over — public was on over.
    publicSide = lm.juice_change_over < 0 ? 'over' : 'under';
  } else {
    return null;
  }

  // Line movement direction tells us where sharp money went.
  const lineDirection = lm.movement > 0 ? 'over' : lm.movement < 0 ? 'under' : 'flat';
  if (lineDirection === 'flat') return null;

  const isReverse = publicSide !== lineDirection;
  if (!isReverse) return { score: 0, isReverse: false, publicSide, lineDirection };

  // Magnitude normalized to typical movement (1 point is meaningful for
  // basketball points; everything bigger gets capped at 1.0).
  const magnitude = Math.min(Math.abs(lm.movement), 1.0);
  const publicWeight = Number.isFinite(publicBetPct)
    ? Math.max(0.5, Math.abs(publicBetPct - 50) / 50)
    : 0.6;
  return {
    score: Math.min(1.0, magnitude * publicWeight),
    isReverse: true,
    publicSide,
    lineDirection,
    movement: lm.movement,
  };
}

async function juiceDegradation(gameId, playerName, statType) {
  const lm = await getLineMovement(gameId, playerName, statType);
  if (!lm) return null;
  // Only meaningful when the line itself barely moved — if both line and
  // juice shifted, that's regular line movement, captured by RLM instead.
  if (Math.abs(lm.movement) > 0.5) return { score: 0, applicable: false };

  const overShift = Number.isFinite(lm.juice_change_over) ? lm.juice_change_over : 0;
  const underShift = Number.isFinite(lm.juice_change_under) ? lm.juice_change_under : 0;
  // Worst-side degradation: the side whose implied-prob increase is bigger
  // is the one the books are pulling money to.
  const overImpliedShift = (oddsToImplied(lm.current_over_odds) ?? 0) - (oddsToImplied(lm.opening_over_odds) ?? 0);
  const underImpliedShift = (oddsToImplied(lm.current_under_odds) ?? 0) - (oddsToImplied(lm.opening_under_odds) ?? 0);
  const worstSide = overImpliedShift >= underImpliedShift ? 'over' : 'under';
  // Normalize to a 20-cent (e.g. -110 → -130) max move.
  const magnitude = Math.max(Math.abs(overShift), Math.abs(underShift));
  return {
    score: Math.min(1.0, magnitude / 20),
    applicable: true,
    worstSide,
    overShift,
    underShift,
  };
}

module.exports = { getLineMovement, reverseLineMovement, juiceDegradation, fetchSnapshots };
