'use strict';

/**
 * Line-snapshot service (Session 28).
 *
 * Lightweight, Redis-only line-movement tracking that complements the
 * Supabase-backed `lineMovementService` (which captures opening baselines
 * + sharp indicators for grading). This layer records a rolling history
 * of a prop's line through the day so the UI can draw a sparkline and a
 * "biggest movers" board — with ZERO odds-api credits (it only stores
 * what an odds fetch already returned).
 *
 * Key shape:
 *   linehistory:{sport}:{gameId}:{player}:{stat} → Redis list of
 *     JSON { time, line, book } snapshots (cap 100, 48h TTL).
 *
 * Everything is defensive: Redis down → no-op / empty, never a throw.
 */

const { getRedisClient, isDegraded } = require('../utils/redis');

const MAX_SNAPSHOTS = 100;
const TTL_SECONDS = 48 * 3600;
const SCAN_COUNT = 200;
const MAX_KEYS = 1000;
const SHARP_THRESHOLD = 1.5;   // points of movement that flags sharp money
const STABLE_THRESHOLD = 0.5;  // < this = "stable"

function snapshotKey(sport, gameId, player, stat) {
  return `linehistory:${sport}:${gameId}:${player}:${stat}`;
}

function safeNum(v) {
  const n = Number(v);
  return Number.isFinite(n) ? n : null;
}

/**
 * Append a line snapshot for each prop. `now` is injectable for tests.
 * Props need { gameId|game_id, player, stat|stat_type, line, book }.
 */
async function recordSnapshots(sport, props, now = Date.now()) {
  if (isDegraded && isDegraded()) return 0;
  if (!Array.isArray(props) || props.length === 0) return 0;
  const redis = getRedisClient();
  if (!redis || typeof redis.rpush !== 'function') return 0;

  let written = 0;
  for (const p of props) {
    const gameId = p.gameId || p.game_id;
    const player = p.player;
    const stat = p.stat || p.stat_type;
    const line = safeNum(p.line);
    if (!gameId || !player || !stat || line === null) continue;
    const key = snapshotKey(sport, gameId, player, stat);
    const snap = JSON.stringify({ time: now, line, book: p.book || null });
    try {
      await redis.rpush(key, snap);
      await redis.ltrim(key, -MAX_SNAPSHOTS, -1);
      await redis.expire(key, TTL_SECONDS);
      written += 1;
    } catch {
      /* swallow — snapshot recording must never break an odds fetch */
    }
  }
  return written;
}

async function getLineHistory(sport, gameId, player, stat) {
  if (isDegraded && isDegraded()) return [];
  const redis = getRedisClient();
  if (!redis || typeof redis.lrange !== 'function') return [];
  try {
    const raw = await redis.lrange(snapshotKey(sport, gameId, player, stat), 0, -1);
    return (raw || [])
      .map((s) => { try { return JSON.parse(s); } catch { return null; } })
      .filter(Boolean);
  } catch {
    return [];
  }
}

/**
 * Classify a list of snapshots (oldest → newest) into a movement summary.
 * Empty / single-snapshot → stable, never an error.
 */
function classifyMovement(snapshots) {
  if (!Array.isArray(snapshots) || snapshots.length < 2) {
    const only = snapshots && snapshots[0] ? safeNum(snapshots[0].line) : null;
    return { opening: only, current: only, delta: 0, movement: 'stable', sharpSignal: false, snapshots: snapshots || [] };
  }
  const opening = safeNum(snapshots[0].line) ?? 0;
  const current = safeNum(snapshots[snapshots.length - 1].line) ?? 0;
  const delta = Math.round((current - opening) * 100) / 100;
  const abs = Math.abs(delta);
  return {
    opening,
    current,
    delta,
    movement: abs < STABLE_THRESHOLD ? 'stable' : delta > 0 ? 'rising' : 'dropping',
    sharpSignal: abs >= SHARP_THRESHOLD,
    snapshots,
  };
}

function parseKey(key) {
  // linehistory:{sport}:{gameId}:{player}:{stat}
  const parts = String(key).split(':');
  if (parts.length < 5 || parts[0] !== 'linehistory') return null;
  // player names may contain no colons in practice; stat is the last part.
  const [, sport, gameId] = parts;
  const stat = parts[parts.length - 1];
  const player = parts.slice(3, parts.length - 1).join(':');
  return { sport, gameId, player, stat };
}

async function scanKeys(match) {
  if (isDegraded && isDegraded()) return [];
  const redis = getRedisClient();
  if (!redis || typeof redis.scan !== 'function') return [];
  const keys = [];
  let cursor = '0';
  try {
    do {
      const [next, batch] = await redis.scan(cursor, 'MATCH', match, 'COUNT', SCAN_COUNT);
      cursor = next;
      for (const k of batch) {
        if (!keys.includes(k)) keys.push(k);
        if (keys.length >= MAX_KEYS) return keys;
      }
    } while (cursor !== '0');
  } catch {
    return keys;
  }
  return keys;
}

/**
 * Biggest movers for a sport — every tracked prop classified, filtered to
 * meaningful moves, sorted by absolute delta desc. `limit` caps the list.
 */
async function getBiggestMovers(sport, { limit = 20, minDelta = STABLE_THRESHOLD } = {}) {
  const keys = await scanKeys(`linehistory:${sport}:*`);
  const movers = [];
  for (const key of keys) {
    const meta = parseKey(key);
    if (!meta) continue;
    const history = await getLineHistory(sport, meta.gameId, meta.player, meta.stat);
    const cls = classifyMovement(history);
    if (Math.abs(cls.delta) < minDelta) continue;
    movers.push({
      sport,
      gameId: meta.gameId,
      player: meta.player,
      stat: meta.stat,
      opening: cls.opening,
      current: cls.current,
      delta: cls.delta,
      movement: cls.movement,
      sharpSignal: cls.sharpSignal,
      snapshots: cls.snapshots,
    });
  }
  movers.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
  return limit > 0 ? movers.slice(0, limit) : movers;
}

module.exports = {
  recordSnapshots,
  getLineHistory,
  classifyMovement,
  getBiggestMovers,
  __internals: { snapshotKey, parseKey, scanKeys, SHARP_THRESHOLD, STABLE_THRESHOLD },
};
