/**
 * OddsPapi — Pinnacle closing-line capture for CLV.
 *
 * Closing lines are immutable facts. Once captured at tip-off they live in
 * Supabase forever; we never re-read them, never cache them in Redis (would
 * be wasted space — they don't change).
 *
 * Called from the resolution poller the FIRST time it sees a game flip to
 * STATUS_IN_PROGRESS. One row per (game_id, player_espn_id, stat_type) via
 * the UNIQUE constraint in migration 016, so repeated triggers no-op
 * cleanly.
 */

const axios = require('axios');
const { createLimiter, createCircuitBreaker, API_BUDGETS } = require('../../utils/rateLimiter');
const { devig } = require('../../utils/odds');
const { getSupabaseServiceClient } = require('../../utils/supabase');
// Session 21 — gateway wrap. ODDSPAPI is the Pinnacle-close
// provider, not a live-odds source. Each call hits ONCE per game
// at tip-off, so traffic is bounded — the gateway counter mostly
// gives the admin dashboard visibility rather than acting as
// hot-path throttle (the in-process limiter does that).
const gateway = require('../providerGateway');

const HTTP_TIMEOUT_MS = 10_000;
const BASE_URL = process.env.ODDSPAPI_BASE_URL || 'https://api.oddspapi.io/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.oddsPapi);
const breaker = createCircuitBreaker({ failureThreshold: 3, resetTimeout: 60_000 });

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

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

async function fetchPinnacleProp(sport, gameId, playerName, statType) {
  if (!configured()) return null;
  await limiter.waitForToken();
  try {
    return await breaker.call(async () => {
      const res = await gateway.fetch(
        'oddspapi',
        () => axios.get(`${BASE_URL}/sports/${sportKey(sport)}/events/${gameId}/odds`, {
          params: { bookmaker: 'pinnacle', market: 'player_props' },
          headers: { 'X-Api-Key': process.env.ODDSPAPI_KEY },
          timeout: HTTP_TIMEOUT_MS,
        }),
        { capability: 'closing_lines', sport },
      );
      const props = res.data?.props || res.data?.data || [];
      return Array.isArray(props)
        ? props.find(
            (p) =>
              (p.player ?? p.player_name)?.toLowerCase() === playerName.toLowerCase()
              && (p.stat_type ?? p.market) === statType
          ) || null
        : null;
    });
  } catch (err) {
    if (err?.code !== 'CIRCUIT_OPEN') {
      console.warn(`[oddspapi] fetch failed for ${sport}/${gameId}/${playerName}/${statType}:`, err?.message);
    }
    return null;
  }
}

async function getPinnacleClosingLine(sport, gameId, playerEspnId, statType, playerName) {
  if (!configured()) return null;
  const prop = await fetchPinnacleProp(sport, gameId, playerName, statType);
  if (!prop) return null;
  const line = Number(prop.line ?? prop.point);
  const overOdds = Number(prop.over_price ?? prop.overOdds);
  const underOdds = Number(prop.under_price ?? prop.underOdds);
  if (!Number.isFinite(line) || !Number.isFinite(overOdds) || !Number.isFinite(underOdds)) return null;
  const fair = devig(overOdds, underOdds);
  return {
    line,
    overOdds,
    underOdds,
    fairOver: fair?.fairOver ?? null,
    fairUnder: fair?.fairUnder ?? null,
    capturedAt: new Date().toISOString(),
  };
}

async function batchCapture(sport, gameId) {
  if (!configured()) return { captured: 0, skipped: 0, reason: 'not_configured' };
  const supabase = getSupabaseServiceClient();

  // Pull every unresolved prop for this game from the grading pipeline.
  // resolved_at IS NULL prevents double-capture for games we've already
  // processed (matters for retries from the poller).
  const { data: graded, error } = await supabase
    .from('grade_history')
    .select('player_id, player_name, stat_type')
    .eq('game_id', gameId)
    .is('resolved_at', null);

  if (error) {
    console.warn('[oddspapi] grade_history lookup failed:', error.message);
    return { captured: 0, error: error.message };
  }
  if (!graded || graded.length === 0) {
    return { captured: 0, skipped: 0, reason: 'no_graded_props' };
  }

  // Deduplicate by (player, stat) — same player can be graded twice on
  // different lines but we only need one Pinnacle reference per stat.
  const seen = new Set();
  const targets = [];
  for (const row of graded) {
    const key = `${row.player_id}|${row.stat_type}`;
    if (seen.has(key)) continue;
    seen.add(key);
    targets.push(row);
  }

  let captured = 0;
  let skipped = 0;
  for (const t of targets) {
    const line = await getPinnacleClosingLine(sport, gameId, t.player_id, t.stat_type, t.player_name);
    if (!line) { skipped += 1; continue; }
    const { error: upsertErr } = await supabase
      .from('closing_lines')
      .upsert({
        game_id: gameId,
        sport,
        player_name: t.player_name,
        player_espn_id: t.player_id,
        stat_type: t.stat_type,
        pinnacle_line: line.line,
        pinnacle_over_odds: line.overOdds,
        pinnacle_under_odds: line.underOdds,
        fair_over_probability: line.fairOver,
        fair_under_probability: line.fairUnder,
      }, { onConflict: 'game_id,player_espn_id,stat_type' });
    if (upsertErr) {
      console.warn('[oddspapi] closing_lines upsert failed:', upsertErr.message);
      skipped += 1;
      continue;
    }
    captured += 1;
  }
  return { captured, skipped, total: targets.length };
}

module.exports = {
  configured,
  getPinnacleClosingLine,
  batchCapture,
  __internals: { limiter, breaker, SPORT_KEYS },
};
