/**
 * Express app wiring for AAO Gateway mode.
 *
 * Endpoint map:
 *   - POST /v1/chat/completions  (auth, json body parser, model allowlist) → stream-proxy
 *   - GET  /v1/models            (auth)                                    → models-endpoint
 *   - GET  /health               (no auth, LiteLLM-shaped)                 → health-endpoint
 *   - GET  /health/liveness      (no auth, bare 200)                       → health-endpoint
 *
 * Design notes:
 *  - CORS is intentionally NOT enabled. The expected caller is an LLM
 *    client (AAO worker / curl), not a browser. If you need browser
 *    access add a reverse proxy in front; we'd rather flag the unusual
 *    case than silently allow drive-by requests.
 *  - express.json() is mounted only on /v1/chat/completions (with a
 *    10 MB limit) so /v1/models and /health stay GET-only — body parsing
 *    is wasted work there.
 *  - The middleware order matters: auth before json before allow-list,
 *    because the allow-list reads `req.body.model` and we want the 401
 *    timing to be independent of body size.
 */
import express, { type Express, type RequestHandler } from 'express';
import { logger } from '../logger.js';
import type { GatewayConfig } from './config.js';
import {
  buildAuthMiddleware,
  buildAllowedModelsMiddleware,
  type DbKeyMatch,
} from './auth.js';
import { createRouter, type Router as GatewayRouter } from './router.js';
import { buildChatCompletionsHandler, type BackendInflightCounter, type UsageRecorderDeps, type StreamRegistry } from './stream-proxy.js';
import { buildModelsHandler } from './models-endpoint.js';
import { buildHealthHandler, buildLivenessHandler } from './health-endpoint.js';
import type { BackendStatusRegistry } from '../engine/backend-status-registry.js';
import type { GatewayMetrics } from '../metrics/gateway-metrics.js';
import { createMetricsHandler } from '../metrics/http-handler.js';
import type { Registry as PromRegistry } from 'prom-client';

export interface GatewayServerDeps {
  config: GatewayConfig;
  registry: BackendStatusRegistry;
  /**
   * Optional fetch override — exposed so integration tests can stand
   * up a fake upstream without monkey-patching globalThis.fetch.
   */
  fetchImpl?: typeof fetch;
  /**
   * Phase 2a: when present, the auth middleware consults DB-backed
   * keys first. Supplied by gateway/bootstrap.ts after opening the
   * Repository; tests can pass a fake. Omit for the Phase 1
   * config-only deployment.
   */
  dbLookup?: (keyHash: string) => DbKeyMatch | null;
  /** Best-effort touch on successful DB match. Dedup'd in auth. */
  touchLastUsed?: (keyId: string) => void;
  /**
   * Phase 2b: optional middleware chain mounted on /v1/chat/completions
   * AFTER auth + json + model-allowlist but BEFORE the chat handler.
   * Bootstrap wires this up to budget-check + rate-limiter when a
   * Repository is open; tests can pass any RequestHandler[] sequence.
   */
  postAuthMiddleware?: RequestHandler[];
  /** Phase 2b: optional usage recorder forwarded to the chat handler. */
  usageRecorder?: UsageRecorderDeps;
  /**
   * Phase 3a F8: optional stream registry. When supplied, the chat
   * handler registers each SSE stream so a graceful shutdown can abort
   * it with a `gateway_shutdown` SSE event before force-disconnect.
   */
  streamRegistry?: StreamRegistry;
  /**
   * Real-time per-backend in-flight counter. Threaded from
   * shared-dependencies to both the chat handler (which inc/decs
   * around the upstream fetch) and the /health handler (which reads
   * the snapshot to populate aao_busy_slots without waiting for the
   * next registry probe cycle).
   */
  inflight?: BackendInflightCounter;
  /**
   * Phase 3b: optional Prometheus metrics handle + scrape registry.
   * When supplied:
   *  - GET /metrics is mounted with NO auth (Prometheus convention)
   *  - stream-proxy emits requests/duration/tokens counters
   *  - auth emits auth_fail
   *  - rate-limit / budget-check reject paths increment rejection counters
   * When omitted, /metrics is not mounted and no counters fire. Tests
   * that don't care about metrics can leave both fields undefined.
   */
  metrics?: GatewayMetrics;
  metricsRegistry?: PromRegistry;
}

export interface GatewayServer {
  app: Express;
  router: GatewayRouter;
}

const CHAT_COMPLETIONS_BODY_LIMIT_MB = 10;

export function createGatewayApp(deps: GatewayServerDeps): GatewayServer {
  const { config, registry } = deps;
  const app = express();
  // Defensive: disable the X-Powered-By header so we don't leak Express
  // version to any client reachable on the gateway port.
  app.disable('x-powered-by');

  const router = createRouter({
    getBackends: () => config.backends,
    registry,
    // Wire the live in-flight counter so pick() enforces per-host
    // max_slots in real time (reservation on select) rather than relying
    // on the 5–30s-stale probe snapshot alone. Released in stream-proxy's
    // finally via decInflightOnce().
    inflight: deps.inflight,
  });

  const authMw = buildAuthMiddleware({
    keys: config.virtualKeys,
    dbLookup: deps.dbLookup,
    touchLastUsed: deps.touchLastUsed,
    metrics: deps.metrics,
  });
  const jsonMw = express.json({ limit: `${CHAT_COMPLETIONS_BODY_LIMIT_MB}mb` });
  const modelsMw = buildAllowedModelsMiddleware({ getBackends: () => config.backends });

  const chatHandler = buildChatCompletionsHandler({
    router,
    upstreamTimeoutSec: config.upstreamTimeoutSec,
    requestTimeoutSec: config.requestTimeoutSec,
    fetchImpl: deps.fetchImpl,
    usageRecorder: deps.usageRecorder,
    streamRegistry: deps.streamRegistry,
    metrics: deps.metrics,
    inflight: deps.inflight,
  });
  const modelsHandler = buildModelsHandler({ getBackends: () => config.backends });
  const healthHandler = buildHealthHandler({ router, inflight: deps.inflight });
  const livenessHandler = buildLivenessHandler();

  // Liveness MUST be the very first route registered: a k8s liveness
  // probe should never be gated by middleware, parser, or registry warm-up.
  app.get('/health/liveness', livenessHandler);

  // Phase 3b: Prometheus /metrics endpoint. The handler is mounted
  // before any auth-bearing routes (so /metrics doesn't collide with
  // bearer-virtual-key auth used on /v1/*), but it has its OWN auth
  // model:
  //  - bearer token (`gateway.metrics.bearer_token`) if configured
  //  - otherwise client-IP allowlist (`gateway.metrics.allowed_hosts`,
  //    default localhost-only)
  // This stops Prometheus labels (team, backend, key_prefix) from
  // leaking to anyone who can reach the gateway port.
  if (deps.metricsRegistry) {
    const metricsCfg = config.metrics;
    const metricsAuth = {
      bearerToken: metricsCfg?.bearerToken,
      allowedHosts: metricsCfg?.allowedHosts,
    };
    app.get('/metrics', createMetricsHandler(deps.metricsRegistry, metricsAuth));
  }

  app.get('/health', healthHandler);

  app.get('/v1/models', authMw, modelsHandler);

  // Phase 2b: budget + rate-limit middleware are injected by bootstrap
  // when a Repository is open. The order matters: auth must run first
  // (so `req.gatewayAuth` exists), then model-allowlist (cheap reject),
  // then budget (DB read), then rate-limit (in-memory check), only
  // then the handler that contacts upstream. Keeping rate after budget
  // means a budget-exhausted client can't burn its rpm quota.
  const postAuth: RequestHandler[] = deps.postAuthMiddleware ?? [];
  app.post(
    '/v1/chat/completions',
    jsonMw,
    authMw,
    modelsMw,
    ...postAuth,
    chatHandler,
  );

  // Generic 404 — return the same {error: ...} shape the rest of the
  // gateway uses so clients don't get an HTML page from Express's default.
  app.use((req, res) => {
    res.status(404).json({ error: `not found: ${req.method} ${req.path}` });
  });

  // Error handler: log + 500. Express needs the 4-arg signature here.
  app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
    logger.warn(`[gateway-server] unhandled error: ${err instanceof Error ? err.message : String(err)}`);
    if (!res.headersSent) {
      res.status(500).json({ error: 'internal error' });
    } else {
      try { res.end(); } catch { /* ignore */ }
    }
  });

  return { app, router };
}
