import { cacheGet, cacheSet, healthGet, healthRecord } from '../db/db';

// One resilient fetch path for every source: DB-cached, rate-limited, and
// circuit-broken. A blocked source (e.g. SofaScore 403 from the VPS) trips its
// breaker and is skipped; callers transparently get stale cache or a thrown
// error they already handle — the UI, which reads the DB, never blocks.

const CIRCUIT_THRESHOLD = 4; // consecutive failures before opening
const CIRCUIT_COOLDOWN = 10 * 60_000; // base cooldown, escalates with failures

const lastCall = new Map<string, number>();

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export function browserHeaders(extra: Record<string, string> = {}): Record<string, string> {
  return {
    'User-Agent':
      'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
    Accept: 'application/json, text/plain, */*',
    'Accept-Language': 'en-US,en;q=0.9',
    ...extra,
  };
}

export interface FetchOpts {
  /** Cache freshness window (ms). */
  ttl: number;
  /** Minimum interval between calls to this source (ms). */
  minInterval?: number;
  headers?: Record<string, string>;
  timeoutMs?: number;
  /** Parse as text instead of JSON. */
  text?: boolean;
}

export function circuitOpen(source: string): boolean {
  const h = healthGet(source);
  return !!h && h.open_until > Date.now();
}

async function rateLimit(source: string, minInterval: number): Promise<void> {
  if (!minInterval) return;
  const last = lastCall.get(source) ?? 0;
  const wait = minInterval - (Date.now() - last);
  if (wait > 0) await sleep(wait + Math.random() * 250); // jitter
  lastCall.set(source, Date.now());
}

/**
 * Fetch JSON (or text) for `source` at `url`, keyed in the cache by `key`.
 * Returns fresh cache when valid, otherwise fetches; on failure returns stale
 * cache if present, else throws.
 */
export async function cachedFetch<T = unknown>(
  source: string,
  key: string,
  url: string,
  opts: FetchOpts,
): Promise<T> {
  const cached = cacheGet(key);
  if (cached?.fresh) return (opts.text ? cached.value : JSON.parse(cached.value)) as T;

  if (circuitOpen(source)) {
    if (cached) return (opts.text ? cached.value : JSON.parse(cached.value)) as T;
    throw new Error(`${source}: circuit open`);
  }

  await rateLimit(source, opts.minInterval ?? 0);

  try {
    const res = await fetch(url, {
      headers: browserHeaders(opts.headers),
      signal: AbortSignal.timeout(opts.timeoutMs ?? 12_000),
    });
    if (!res.ok) throw new Error(`${res.status}`);
    const body = await res.text();
    cacheSet(key, body, opts.ttl);
    healthRecord(source, true);
    return (opts.text ? body : JSON.parse(body)) as T;
  } catch (e) {
    const note = e instanceof Error ? e.message : String(e);
    const consec = (healthGet(source)?.consec_fail ?? 0) + 1;
    const openMs = consec >= CIRCUIT_THRESHOLD ? CIRCUIT_COOLDOWN * Math.min(6, consec - CIRCUIT_THRESHOLD + 1) : undefined;
    healthRecord(source, false, note, openMs);
    if (cached) return (opts.text ? cached.value : JSON.parse(cached.value)) as T; // serve stale
    throw new Error(`${source} fetch failed: ${note}`);
  }
}
