/**
 * Token-bucket limiter + circuit breaker, factory-style.
 *
 * Why this exists alongside src/services/rateLimiter.js:
 *   - services/rateLimiter.js is a SHARED registry of upstream-keyed buckets
 *     ('espn', 'fanduel', etc.) used by the original odds adapters.
 *   - This file is a PER-INSTANCE factory used by the new pipeline adapters
 *     (SharpAPI, OddsPapi) where each adapter owns its own bucket and
 *     circuit breaker, paired together so a 5xx storm flips the breaker
 *     without polluting other adapters.
 *
 * The two coexist; new code should prefer this file.
 */

const DEFAULT_WAIT_TIMEOUT_MS = 30_000;

function createLimiter({ tokensPerInterval, interval }) {
  if (!(tokensPerInterval > 0) || !(interval > 0)) {
    throw new Error('createLimiter requires positive tokensPerInterval and interval');
  }
  const refillPerMs = tokensPerInterval / interval;
  let tokens = tokensPerInterval;
  let lastRefill = Date.now();

  function refill() {
    const now = Date.now();
    const elapsed = now - lastRefill;
    if (elapsed <= 0) return;
    tokens = Math.min(tokensPerInterval, tokens + elapsed * refillPerMs);
    lastRefill = now;
  }

  async function waitForToken(timeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
    const deadline = Date.now() + timeoutMs;
    while (true) {
      refill();
      if (tokens >= 1) {
        tokens -= 1;
        return true;
      }
      if (Date.now() >= deadline) {
        // CRITICAL: never block forever. A stuck limiter must not kill the
        // poller — log loud, proceed, let the caller hit the upstream rate
        // limit naturally if it's actually exhausted.
        console.warn('[rateLimiter] token wait exceeded timeout, proceeding without token');
        return false;
      }
      const needed = 1 - tokens;
      const waitMs = Math.min(250, Math.ceil(needed / refillPerMs));
      await new Promise((r) => setTimeout(r, Math.max(10, waitMs)));
    }
  }

  function snapshot() {
    refill();
    return { tokens: Math.floor(tokens), capacity: tokensPerInterval, interval };
  }

  return { waitForToken, snapshot };
}

function createCircuitBreaker({ failureThreshold = 3, resetTimeout = 60_000 } = {}) {
  // States:
  //   closed     — calls flow through, failures accumulate
  //   open       — calls reject immediately until resetTimeout elapses
  //   half_open  — one test call permitted; success closes, failure re-opens
  let state = 'closed';
  let failures = 0;
  let openedAt = 0;
  let halfOpenInFlight = false;

  function transitionIfReady() {
    if (state === 'open' && Date.now() - openedAt >= resetTimeout) {
      state = 'half_open';
      halfOpenInFlight = false;
    }
  }

  async function call(fn) {
    transitionIfReady();

    if (state === 'open') {
      const err = new Error('circuit breaker is open');
      err.code = 'CIRCUIT_OPEN';
      throw err;
    }

    if (state === 'half_open') {
      if (halfOpenInFlight) {
        const err = new Error('circuit breaker is half-open (test in flight)');
        err.code = 'CIRCUIT_HALF_OPEN_BUSY';
        throw err;
      }
      halfOpenInFlight = true;
    }

    try {
      const result = await fn();
      // Success: close the circuit and clear the failure count.
      failures = 0;
      state = 'closed';
      halfOpenInFlight = false;
      return result;
    } catch (err) {
      halfOpenInFlight = false;
      if (state === 'half_open') {
        state = 'open';
        openedAt = Date.now();
        throw err;
      }
      failures += 1;
      if (failures >= failureThreshold) {
        state = 'open';
        openedAt = Date.now();
      }
      throw err;
    }
  }

  function snapshot() {
    transitionIfReady();
    return { state, failures, openedAt };
  }

  return { call, snapshot };
}

const API_BUDGETS = Object.freeze({
  sharpApi:   { tokensPerInterval: 10, interval: 60_000 },
  espn:       { tokensPerInterval: 2,  interval: 60_000 },
  mlbStats:   { tokensPerInterval: 2,  interval: 60_000 },
  oddsPapi:   { tokensPerInterval: 5,  interval: 60_000 },
  openRouter: { tokensPerInterval: 15, interval: 60_000 },
});

module.exports = { createLimiter, createCircuitBreaker, API_BUDGETS };
