/**
 * SharpAPI — PRIMARY real-time odds source.
 *
 * Called by the GRADING PIPELINE (n8n) before a game tips. NOT used by the
 * resolution poller — closing-line capture goes through OddsPapi for the
 * Pinnacle benchmark.
 *
 * The adapter exposes three reads:
 *   getPlayerProps     — every player prop across books, de-vigged
 *   getGameOdds        — spread / total / moneyline for one game
 *   getConsensusLine   — median / min / max line across books (trap detector)
 *
 * Free-tier budget is 12 req/min. We cap at 10 to leave headroom for
 * incident retries. Responses cache in Redis for 60s — long enough to
 * coalesce duplicate grade requests for the same prop, short enough that a
 * line move propagates inside a minute.
 *
 * Failure modes:
 *   429        — back off, serve stale cache marked `{ stale: true }`
 *   5xx        — circuit breaker (3 fails → open 60s)
 *   timeout    — 10s connect/read, circuit breaker counts it as a failure
 */

const axios = require('axios');
const { createLimiter, createCircuitBreaker, API_BUDGETS } = require('../../utils/rateLimiter');
const { cacheGet, cacheSet } = require('../../utils/redis');
const { devig } = require('../../utils/odds');
const { getSupabaseServiceClient } = require('../../utils/supabase');

const SOURCE = 'sharpapi';
const HTTP_TIMEOUT_MS = 10_000;
const CACHE_TTL_SECONDS = 60;
const STALE_CACHE_TTL_SECONDS = 300;

const BASE_URL = process.env.SHARPAPI_BASE_URL || 'https://api.sharpapi.com/v1';

const SPORT_KEYS = Object.freeze({
  nba:     'basketball_nba',
  wnba:    'basketball_wnba',
  mlb:     'baseball_mlb',
  nfl:     'americanfootball_nfl',
  nhl:     'icehockey_nhl',
  ncaab:   'basketball_ncaab',
  ncaafb:  'americanfootball_ncaaf',
});

const limiter = createLimiter(API_BUDGETS.sharpApi);
const breaker = createCircuitBreaker({ failureThreshold: 3, resetTimeout: 60_000 });

function configured() {
  return !!process.env.SHARPAPI_KEY;
}

function authHeaders() {
  return { 'X-Api-Key': process.env.SHARPAPI_KEY };
}

function sportKey(sport) {
  const key = SPORT_KEYS[sport];
  if (!key) throw new Error(`Unsupported sport: ${sport}`);
  return key;
}

function median(nums) {
  if (!nums.length) return null;
  const sorted = [...nums].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}

async function fetchWithGuards(url, params, cacheKey) {
  if (!configured()) return null;

  // 1. Hot cache — fresh hit returns immediately.
  const cached = await cacheGet(cacheKey);
  if (cached && !cached.stale) return cached;

  await limiter.waitForToken();

  try {
    const data = await breaker.call(async () => {
      const res = await axios.get(url, {
        params,
        headers: authHeaders(),
        timeout: HTTP_TIMEOUT_MS,
        validateStatus: (s) => (s >= 200 && s < 300) || s === 429,
      });
      if (res.status === 429) {
        const err = new Error('sharpapi rate limited');
        err.code = 'SHARPAPI_429';
        throw err;
      }
      return res.data;
    });
    await cacheSet(cacheKey, data, CACHE_TTL_SECONDS);
    return data;
  } catch (err) {
    if (err?.code === 'SHARPAPI_429' && cached) {
      // Serve stale cache marked so callers can decide whether to trust it.
      const stale = { ...cached, stale: true };
      await cacheSet(cacheKey, stale, STALE_CACHE_TTL_SECONDS);
      return stale;
    }
    if (err?.code === 'CIRCUIT_OPEN') {
      // Don't spam logs while the breaker is open — one warn per minute is
      // enough; the snapshot tells ops the state.
      return cached ? { ...cached, stale: true } : null;
    }
    console.warn(`[sharpapi] fetch failed for ${cacheKey}:`, err?.message);
    return cached ? { ...cached, stale: true } : null;
  }
}

function normalizePlayerProp(raw) {
  // Defensive shape — SharpAPI returns slightly different field names across
  // markets. We only surface the fields downstream consumers actually need.
  const overOdds = raw.over_price ?? raw.overOdds ?? null;
  const underOdds = raw.under_price ?? raw.underOdds ?? null;
  const fair = (overOdds != null && underOdds != null) ? devig(overOdds, underOdds) : null;
  return {
    book: raw.book ?? raw.bookmaker ?? null,
    player: raw.player ?? raw.player_name ?? null,
    statType: raw.stat_type ?? raw.market ?? null,
    line: Number(raw.line ?? raw.point ?? null),
    overOdds,
    underOdds,
    fairOver: fair?.fairOver ?? null,
    fairUnder: fair?.fairUnder ?? null,
  };
}

// Fire-and-forget snapshot writer. Persists each line we observe so we can
// later detect reverse line movement + juice degradation. Never blocks the
// caller — a Supabase outage must not stop the grading pipeline.
function snapshotProps(sport, gameId, normalized, consensusMedian) {
  if (!normalized || normalized.length === 0) return;
  const rows = normalized
    .filter((p) => Number.isFinite(p.line))
    .map((p) => ({
      game_id: gameId,
      sport,
      player_name: p.player,
      player_id: null,
      stat_type: p.statType,
      line: p.line,
      over_odds: p.overOdds,
      under_odds: p.underOdds,
      book: p.book,
      consensus_median: consensusMedian ?? null,
    }));
  if (rows.length === 0) return;
  // Run after the response — Promise.resolve().then keeps it off the
  // caller's critical path without leaking unhandled rejections.
  Promise.resolve().then(async () => {
    try {
      const supabase = getSupabaseServiceClient();
      const { error } = await supabase.from('line_snapshots').insert(rows);
      if (error) console.warn('[sharpapi] snapshot insert failed:', error.message);
    } catch (err) {
      console.warn('[sharpapi] snapshot insert threw:', err?.message);
    }
  });
}

async function getPlayerProps(sport, gameId) {
  const key = sportKey(sport);
  const cacheKey = `odds:${sport}:${gameId}:player_props`;
  const data = await fetchWithGuards(
    `${BASE_URL}/sports/${key}/events/${gameId}/odds`,
    { markets: 'player_props' },
    cacheKey
  );
  if (!data) return [];
  const raw = data.props || data.markets || data.data || [];
  const normalized = Array.isArray(raw) ? raw.map(normalizePlayerProp) : [];

  // Only snapshot fresh data — stale-cache fallbacks are previously stored
  // already; re-snapshotting would mint duplicate "now" rows on every call.
  if (!data.stale && normalized.length) {
    snapshotProps(sport, gameId, normalized);
  }

  return data.stale ? Object.assign(normalized, { stale: true }) : normalized;
}

async function getGameOdds(sport, gameId) {
  const key = sportKey(sport);
  const cacheKey = `odds:${sport}:${gameId}:game`;
  const data = await fetchWithGuards(
    `${BASE_URL}/sports/${key}/events/${gameId}/odds`,
    { markets: 'spreads,totals,h2h' },
    cacheKey
  );
  if (!data) return null;
  return {
    spread: data.spread ?? null,
    total: data.total ?? null,
    moneyline: data.h2h ?? data.moneyline ?? null,
    stale: !!data.stale,
  };
}

async function getConsensusLine(sport, gameId, playerName, statType) {
  const props = await getPlayerProps(sport, gameId);
  const matches = props.filter(
    (p) => p.player && p.statType
      && p.player.toLowerCase() === playerName.toLowerCase()
      && p.statType === statType
      && Number.isFinite(p.line)
  );
  if (!matches.length) return null;
  const lines = matches.map((p) => p.line);
  return {
    median: median(lines),
    min: Math.min(...lines),
    max: Math.max(...lines),
    bookCount: matches.length,
    stale: !!props.stale,
  };
}

module.exports = {
  configured,
  getPlayerProps,
  getGameOdds,
  getConsensusLine,
  // Exported for tests so they can poke the circuit breaker / limiter state.
  __internals: { limiter, breaker, SPORT_KEYS },
};
