/**
 * Priority-Aware AI Concurrency Limiter (Bekky, 2026-09-03).
 *
 * WHY: the app makes many AI calls to the proxy — on-open cohort/alert passes,
 * background enrichment/care refresh, and user-initiated scans. We need:
 *   1. Scans (user-initiated) to NEVER wait behind background calls — waiting
 *      is a buzzkill. They jump the queue and run immediately.
 *   2. Background calls (cohort care, alert advice, enrichment, care refresh)
 *      to serialize among themselves so they don't cross-talk or hammer the
 *      proxy.
 *   3. A concurrency cap so the proxy is never overwhelmed (no timeout pile-up).
 *
 * This is a semaphore with two priority classes:
 *   - HIGH (scans): grab a slot immediately, never wait.
 *   - NORMAL (background): fill remaining slots; if all busy, queue FIFO.
 *
 * PURELY ADDITIVE: it wraps existing calls; it does not change what they do.
 * A call that fails still fails — the limiter just bounds concurrency.
 */
export type AiPriority = 'high' | 'normal';

/** Max concurrent AI calls to the proxy. Scans + 1-2 background = no hammering. */
const MAX_CONCURRENT = 3;

/** Gap between consecutive NORMAL calls (ms) — lets the previous write settle. */
const GAP_MS = 300;

/** Number of slots currently in use. */
let active = 0;

/** FIFO waiters for NORMAL calls (high-priority calls bypass this). */
const normalQueue: Array<() => void> = [];

/** Resolve the next NORMAL waiter when a slot frees. */
function pumpNormal(): void {
  while (active < MAX_CONCURRENT && normalQueue.length) {
    const next = normalQueue.shift();
    if (next) {
      active++;
      next();
    }
  }
}

/** Acquire a slot. HIGH calls take one immediately; NORMAL calls wait FIFO. */
function acquire(priority: AiPriority): Promise<void> {
  if (priority === 'high') {
    // High-priority (scan): if a slot is free, take it now. If all slots are
    // busy (rare — a scan + 2 background), wait for the next free slot but
    // jump ahead of any queued NORMAL calls.
    if (active < MAX_CONCURRENT) {
      active++;
      return Promise.resolve();
    }
    // All slots busy — wait for a free slot, but ahead of NORMAL waiters.
    return new Promise<void>(resolve => {
      const tryAcquire = () => {
        if (active < MAX_CONCURRENT) {
          active++;
          resolve();
        } else {
          setTimeout(tryAcquire, 50);
        }
      };
      tryAcquire();
    });
  }
  // NORMAL (background): wait FIFO behind other NORMAL calls.
  return new Promise<void>(resolve => {
    normalQueue.push(() => {
      resolve();
    });
    pumpNormal();
  });
}

/** Release a slot and pump the next waiter. */
function release(): void {
  active = Math.max(0, active - 1);
  pumpNormal();
}

/**
 * Run `fn` through the concurrency limiter. Returns the same value `fn` would,
 * or throws the same error. `priority`:
 *   - 'high'  → scans / user-initiated (never wait behind background)
 *   - 'normal' → background care/enrichment (serialize among themselves)
 */
export function runAiCall<T>(fn: () => Promise<T>, priority: AiPriority = 'normal'): Promise<T> {
  return acquire(priority).then(async () => {
    try {
      // Small settle gap for NORMAL calls so the previous write lands before
      // the next call reads state. HIGH (scan) calls skip the gap — instant.
      if (priority === 'normal' && GAP_MS > 0) {
        await new Promise(r => setTimeout(r, GAP_MS));
      }
      return await fn();
    } finally {
      release();
    }
  });
}

/**
 * Backward-compatible alias: the old strict-FIFO `enqueueAiCall` is now a
 * NORMAL-priority call through the limiter (one-at-a-time among background
 * calls, but a scan can still jump ahead). Existing call sites keep working.
 */
export function enqueueAiCall<T>(fn: () => Promise<T>): Promise<T> {
  return runAiCall(fn, 'normal');
}
