/**
 * Phase 3b — Gateway-side Prometheus metrics.
 *
 * 11 metrics, all prefixed with `aao_gateway_` by default. Labels are
 * kept narrow to bound cardinality: `team` and `backend` come from the
 * static config (small N), `model` from a request body (bounded by the
 * router's allowlist), `status` from a closed enum.
 *
 * Hot-path emission order:
 *   - `requestsTotal` + `requestDurationSeconds`: fired in
 *     stream-proxy.ts's finally for every response (success and error).
 *   - `tokensTotal`: fired alongside requestsTotal when usage was
 *     observed (skipped when zero — we don't want labels with 0 sum).
 *   - `backendBusySlots` / `backendTotalSlots` / `backendOnline`: pushed
 *     from a BackendStatusRegistry.subscribe() callback in bootstrap.
 *   - `cacheHitTotal` / `cacheMissTotal`: bootstrap's dbLookup wrapper
 *     inc()s these on each path through the keyCache.
 *   - `budgetUsedRatio`: pushed when a usage write completes
 *     (recordUsage callback in bootstrap).
 *   - `rateLimitRejectionsTotal`: fired by the rate-limit middleware
 *     reject branch.
 *   - `activeStreams`: bootstrap subscribes a tick that reads the
 *     StreamRegistry size every 5s (gauge — not delta).
 *
 * Cardinality budget (5 backends × 3 teams × 5 models × 5 statuses):
 *   - requestsTotal: 375 series — fine
 *   - requestDurationSeconds: same × Prom default 10 buckets = 3750
 *     observation series — still well under the 100k informal cap
 */
import { Counter, Gauge, Histogram, type Registry } from 'prom-client';

export interface GatewayMetrics {
  /** Total chat/completions requests fired. Labels: team, backend, model, status. */
  requestsTotal: Counter<'team' | 'backend' | 'model' | 'status'>;
  /** Request processing latency seconds (default buckets). */
  requestDurationSeconds: Histogram<'team' | 'backend' | 'model' | 'status'>;
  /** Cumulative tokens routed through the gateway. direction is in/out. */
  tokensTotal: Counter<'team' | 'backend' | 'model' | 'direction'>;
  /** Latest BackendStatusRegistry busySlots for each backend. */
  backendBusySlots: Gauge<'backend'>;
  /** Latest totalSlots for each backend (constant per backend, but expose for completeness). */
  backendTotalSlots: Gauge<'backend'>;
  /** 1 = online, 0 = offline (registry probe failed). */
  backendOnline: Gauge<'backend'>;
  /** keyCache hit counter. Label `cache` keeps room for budget / rate / key sub-caches. */
  cacheHitTotal: Counter<'cache'>;
  /** keyCache miss counter. */
  cacheMissTotal: Counter<'cache'>;
  /**
   * Per-key budget usage ratio (used / budget) in [0, 1+]. Reset monthly
   * when the underlying gateway_key_usage period rolls over.
   */
  budgetUsedRatio: Gauge<'team' | 'key_prefix'>;
  /** 429s emitted by the rate-limit middleware. */
  rateLimitRejectionsTotal: Counter<'team'>;
  /** Currently in-flight SSE streams (StreamRegistry size). */
  activeStreams: Gauge;
}

/**
 * Per-(registry, prefix) memoization cache.
 *
 * prom-client throws on duplicate metric name registration. In
 * same-process gateway mode the bridge owns one shared Registry but
 * `createGatewayMetrics` is invoked again every time `startGateway()`
 * runs (e.g. `enabled: false → true → false → true` bounce, or a
 * backend list edit triggering a stop+start cycle). Without
 * memoization the second start throws and crashes the bridge process.
 *
 * Cache key: the Registry instance × the metric-name prefix. Using a
 * WeakMap on the Registry lets the cache GC naturally when the
 * registry is dropped (tests typically discard registries between
 * cases). The inner `Map<prefix, GatewayMetrics>` allows multiple
 * prefixes against the same registry (rare but legitimate — e.g. two
 * gateway mounts on different prefixes in test).
 *
 * Stop()/teardown semantics: callers MUST NOT delete metric instances
 * from the registry on stop — doing so would break the next start
 * (the cache would still hold a Counter whose internal registry
 * pointer dangles). Stop should reset label values via `.remove(label)`
 * or `.reset()` only.
 */
const metricsCache = new WeakMap<Registry, Map<string, GatewayMetrics>>();

/**
 * Wire all 11 metrics onto the supplied registry and return the typed
 * handle. Pass the same registry instance to every call site so a
 * `/metrics` scrape sees a single coherent snapshot.
 *
 * `prefix` only changes the metric name root; we keep it as a function
 * arg so tests can use a unique prefix per case and avoid the
 * "duplicate registration" prom-client error.
 *
 * Idempotent on `(registry, prefix)` — a second call with the same
 * args returns the cached handle instead of re-registering counters
 * (which prom-client would reject by throwing). See `metricsCache`
 * doc for why same-process gateway bounces need this.
 */
export function createGatewayMetrics(
  registry: Registry,
  prefix: string = 'aao_gateway',
): GatewayMetrics {
  let perRegistry = metricsCache.get(registry);
  if (perRegistry) {
    const cached = perRegistry.get(prefix);
    if (cached) return cached;
  } else {
    perRegistry = new Map<string, GatewayMetrics>();
    metricsCache.set(registry, perRegistry);
  }

  const handle = buildGatewayMetrics(registry, prefix);
  perRegistry.set(prefix, handle);
  return handle;
}

function buildGatewayMetrics(registry: Registry, prefix: string): GatewayMetrics {
  const requestsTotal = new Counter({
    name: `${prefix}_requests_total`,
    help: 'Total chat/completions requests processed by the gateway.',
    labelNames: ['team', 'backend', 'model', 'status'] as const,
    registers: [registry],
  });

  const requestDurationSeconds = new Histogram({
    name: `${prefix}_request_duration_seconds`,
    help: 'Gateway request processing time in seconds (incl. upstream).',
    labelNames: ['team', 'backend', 'model', 'status'] as const,
    // Prom-client default buckets are tuned for HTTP latency.
    registers: [registry],
  });

  const tokensTotal = new Counter({
    name: `${prefix}_tokens_total`,
    help: 'Cumulative tokens routed through the gateway (in / out).',
    labelNames: ['team', 'backend', 'model', 'direction'] as const,
    registers: [registry],
  });

  const backendBusySlots = new Gauge({
    name: `${prefix}_backend_busy_slots`,
    help: 'BackendStatusRegistry busySlots for each backend.',
    labelNames: ['backend'] as const,
    registers: [registry],
  });

  const backendTotalSlots = new Gauge({
    name: `${prefix}_backend_total_slots`,
    help: 'BackendStatusRegistry totalSlots for each backend.',
    labelNames: ['backend'] as const,
    registers: [registry],
  });

  const backendOnline = new Gauge({
    name: `${prefix}_backend_online`,
    help: 'Backend health: 1 = reachable, 0 = probe failed.',
    labelNames: ['backend'] as const,
    registers: [registry],
  });

  const cacheHitTotal = new Counter({
    name: `${prefix}_cache_hit_total`,
    help: 'Key cache hits by sub-cache (key/backends).',
    labelNames: ['cache'] as const,
    registers: [registry],
  });

  const cacheMissTotal = new Counter({
    name: `${prefix}_cache_miss_total`,
    help: 'Key cache misses by sub-cache.',
    labelNames: ['cache'] as const,
    registers: [registry],
  });

  const budgetUsedRatio = new Gauge({
    name: `${prefix}_virtual_key_budget_used_ratio`,
    help: 'Per-virtual-key budget usage ratio (used / budget); 0 means unlimited.',
    labelNames: ['team', 'key_prefix'] as const,
    registers: [registry],
  });

  const rateLimitRejectionsTotal = new Counter({
    name: `${prefix}_rate_limit_rejections_total`,
    help: 'Total 429 rejections by the rate-limit middleware.',
    labelNames: ['team'] as const,
    registers: [registry],
  });

  const activeStreams = new Gauge({
    name: `${prefix}_active_streams`,
    help: 'In-flight SSE streams managed by the StreamRegistry.',
    registers: [registry],
  });

  return {
    requestsTotal,
    requestDurationSeconds,
    tokensTotal,
    backendBusySlots,
    backendTotalSlots,
    backendOnline,
    cacheHitTotal,
    cacheMissTotal,
    budgetUsedRatio,
    rateLimitRejectionsTotal,
    activeStreams,
  };
}

/**
 * Closed enum of request status labels — exported so call sites get a
 * compile-time check rather than scattering string literals.
 */
export type GatewayRequestStatus =
  | 'success'
  | 'upstream_error'
  | 'gateway_timeout'
  | 'gateway_shutdown'
  | 'client_aborted'
  | 'auth_fail'
  | 'budget_exhausted'
  | 'rate_limited'
  | 'no_backend';
