/**
 * Phase 3c — shared dependencies factory for the AAO Gateway.
 *
 * Both deploy modes assemble the exact same Express handler tree
 * (created by `createGatewayApp`); what differs is *who owns the
 * supporting singletons*:
 *
 *   - separate-deploy (`AAO_MODE=gateway` + scripts/gateway.sh): the
 *     gateway process owns its BackendStatusRegistry, KeyCache, etc.
 *     and tears them down on SIGTERM.
 *   - same-process (Phase 3c): the worker bridge already owns a
 *     BackendStatusRegistry singleton (for the Side Info Panel /
 *     node-status widget); the gateway must *reuse* that registry and
 *     own only the cache/limiter/stream-registry it doesn't share.
 *
 * This module collapses that wiring into one factory so the two paths
 * stop diverging. Bootstrap (separate-deploy) and the bridge mount
 * (same-process) both call `createSharedGatewayDependencies()` and
 * receive a `start()` / `stop()` lifecycle pair plus the
 * pre-assembled middleware chain the gateway app needs.
 *
 * Hot reload note: a `gateway.enabled: false -> true` flip in the
 * bridge calls `start()`; the inverse calls `stop()`. Resources NOT
 * owned here (the BackendStatusRegistry passed in) are untouched on
 * stop — that's the worker bridge's job.
 */
import type { RequestHandler } from 'express';
import { logger } from '../logger.js';
import type { BackendStatusRegistry } from '../engine/backend-status-registry.js';
import type { Repository } from '../db/repository.js';
import type { Registry as PromRegistry } from 'prom-client';
import {
  createKeyCache,
  type KeyCache,
} from './key-cache.js';
import {
  createRateLimiter,
  type RateLimiter,
  buildRateLimitMiddleware,
} from './rate-limiter.js';
import { buildBudgetCheckMiddleware } from './budget-check.js';
import {
  createBackendInflightCounter,
  createStreamRegistry,
  type BackendInflightCounter,
  type StreamRegistry,
  type UsageRecorderDeps,
} from './stream-proxy.js';
import { createGatewayMetrics, type GatewayMetrics } from '../metrics/gateway-metrics.js';
import type { DbKeyMatch } from './auth.js';
import type { GatewayConfig } from './config.js';

export interface SharedGatewayDependenciesOptions {
  /** Snapshot of the gateway config at construction time. */
  config: GatewayConfig;
  /**
   * BackendStatusRegistry instance. The factory does NOT start or stop
   * this — same-process callers pass the worker bridge's singleton,
   * separate-deploy passes a freshly created (and started) one.
   */
  registry: BackendStatusRegistry;
  /**
   * Open Repository (DB-backed virtual keys / usage / rate counters).
   * Pass null to run the Phase 1 stateless deploy — budget + rate-limit
   * middleware will be omitted and `usageRecorder` will be null.
   */
  repo: Repository | null;
  /**
   * Prometheus registry. When non-null the factory builds a
   * GatewayMetrics handle bound to it and wires the
   * BackendStatusRegistry subscription that emits backend gauges.
   *
   * In separate-deploy mode bootstrap creates a fresh per-process
   * registry; in same-process mode the bridge passes its worker
   * registry so gateway counters land in the same `/metrics` scrape
   * the worker already serves (no port collision, integrated
   * observability).
   *
   * Pass null to disable metrics entirely.
   */
  promRegistry: PromRegistry | null;
  /**
   * Metric name prefix. Defaults to `aao_gateway` for separate-deploy;
   * same-process callers can pass a distinct prefix to keep gateway
   * counters scoped under their own namespace inside the shared worker
   * registry.
   */
  prefix?: string;
}

export interface SharedGatewayDependencies {
  /** LRU cache shared by auth + budget + rate-limit lookups. Null when no Repository. */
  keyCache: KeyCache | null;
  /** Sliding-window rate limiter (DB-backed). Null when no Repository. */
  rateLimiter: RateLimiter | null;
  /** In-memory registry of in-flight SSE streams (always created). */
  streamRegistry: StreamRegistry;
  /**
   * Real-time per-backend in-flight counter. Always created. Stream-proxy
   * inc/decs around the upstream fetch; health-endpoint reads its
   * snapshot so /health reports live busy state without waiting for
   * the next BackendStatusRegistry probe (probe runs at 5–30s cadence
   * and misses sub-5s chats). Shared across every chat request the
   * gateway handles, so multi-client AAO setups inherit a consistent
   * "X is busy because some other AAO is using it" view.
   */
  inflight: BackendInflightCounter;
  /** Per-key usage recorder bound to the Repository. Null when no Repository. */
  usageRecorder: UsageRecorderDeps | null;
  /** Prometheus metrics handle. Null when promRegistry was null. */
  metrics: GatewayMetrics | null;
  /**
   * Pre-assembled middleware chain mounted AFTER auth + json + model
   * allowlist but BEFORE the chat handler — budget check then rate
   * limit, in that order. Empty array when there's no Repository.
   */
  postAuthMiddleware: RequestHandler[];
  /**
   * Auth lookup callback for `createGatewayApp`. Reads the Repository
   * via the shared cache. Undefined when there's no Repository (Phase 1
   * stateless deploy uses the config-only path).
   */
  dbLookup?: (keyHash: string) => DbKeyMatch | null;
  /** Touches the last_used_at column. Dedup'd inside the auth middleware. */
  touchLastUsed?: (keyId: string) => void;
  /**
   * Start the lifecycle bits this factory owns: the rate-limiter flush
   * interval and the metrics subscription on the BackendStatusRegistry.
   * Idempotent — calling start twice is a no-op after the first call.
   */
  start(): void;
  /**
   * Stop everything `start()` started + drop any cached state. After
   * `stop()` the factory's caches / streams / counters are dead; callers
   * who want to re-enable the gateway must build a fresh
   * SharedGatewayDependencies.
   */
  stop(): Promise<void>;
}

/**
 * Build the cross-deploy gateway dependency bundle. See module doc for
 * ownership semantics — the BackendStatusRegistry is borrowed, not
 * owned, so the caller is responsible for starting / stopping it.
 */
export function createSharedGatewayDependencies(
  opts: SharedGatewayDependenciesOptions,
): SharedGatewayDependencies {
  const { config, registry, repo, promRegistry } = opts;
  const prefix = opts.prefix ?? 'aao_gateway';

  // --- Metrics ---------------------------------------------------------
  let metrics: GatewayMetrics | null = null;
  if (promRegistry) {
    metrics = createGatewayMetrics(promRegistry, prefix);
  }

  // --- Key cache + admin-cross-process hook ---------------------------
  // Shared by auth-by-hash + budget-by-id + rate-by-id lookups.
  const keyCache: KeyCache | null = repo ? createKeyCache() : null;
  if (repo && keyCache) {
    (repo as unknown as { __gatewayKeyCache?: KeyCache }).__gatewayKeyCache = keyCache;
  }
  if (repo && metrics) {
    (repo as unknown as { __gatewayMetrics?: GatewayMetrics }).__gatewayMetrics = metrics;
  }

  const incCacheHit = (cache: 'key' | 'backends'): void => {
    if (!metrics) return;
    try { metrics.cacheHitTotal.labels({ cache }).inc(); } catch { /* noop */ }
  };
  const incCacheMiss = (cache: 'key' | 'backends'): void => {
    if (!metrics) return;
    try { metrics.cacheMissTotal.labels({ cache }).inc(); } catch { /* noop */ }
  };

  // --- Auth dbLookup + touchLastUsed ----------------------------------
  const dbLookup = repo
    ? (keyHash: string) => {
        const cached = keyCache?.get(keyHash);
        if (cached) {
          incCacheHit('key');
          if (cached.revokedAt !== null) return null;
          return {
            id: cached.id,
            team: cached.team,
            allowedModels: cached.allowedModels,
          };
        }
        incCacheMiss('key');
        const row = repo.findGatewayVirtualKeyByHash(keyHash);
        if (!row) return null;
        keyCache?.set(row);
        return {
          id: row.id,
          team: row.team,
          allowedModels: row.allowedModels,
        };
      }
    : undefined;
  const touchLastUsed = repo
    ? (keyId: string) => {
        try {
          repo.touchGatewayVirtualKeyLastUsed(keyId);
        } catch (e) {
          logger.warn(
            `[gateway-shared] touchGatewayVirtualKeyLastUsed failed: ${e instanceof Error ? e.message : String(e)}`,
          );
        }
      }
    : undefined;

  // --- Post-auth middleware (budget + rate limit) ---------------------
  const findByIdCached = repo
    ? (id: string) => {
        const cached = keyCache?.get(id);
        if (cached) {
          incCacheHit('key');
          return cached;
        }
        incCacheMiss('key');
        const row = repo.findGatewayVirtualKeyById(id);
        if (row) keyCache?.set(row);
        return row;
      }
    : null;

  const postAuthMiddleware: RequestHandler[] = [];
  let rateLimiter: RateLimiter | null = null;
  let usageRecorder: UsageRecorderDeps | null = null;
  if (repo && findByIdCached) {
    postAuthMiddleware.push(
      buildBudgetCheckMiddleware({
        repo: {
          getGatewayKeyUsage: repo.getGatewayKeyUsage.bind(repo),
          findGatewayVirtualKeyById: findByIdCached,
        },
        metrics: metrics ?? undefined,
      }),
    );
    rateLimiter = createRateLimiter({ repo });
    postAuthMiddleware.push(
      buildRateLimitMiddleware({
        limiter: rateLimiter,
        getRateLimit: (keyId: string) => {
          const row = findByIdCached(keyId);
          return row?.rateLimitRpm ?? null;
        },
        metrics: metrics ?? undefined,
      }),
    );
    const rl = rateLimiter;
    usageRecorder = {
      recordUsage: (params: {
        keyId: string;
        period: string;
        tokensIn: number;
        tokensOut: number;
        requests: number;
      }): void => {
        repo.incrementGatewayKeyUsage({
          keyId: params.keyId,
          period: params.period,
          tokensIn: params.tokensIn,
          tokensOut: params.tokensOut,
          requests: 0,
        });
        try {
          rl.recordSuccess(params.keyId);
        } catch (e) {
          logger.warn(
            `[gateway-shared] rateLimiter.recordSuccess failed: ${e instanceof Error ? e.message : String(e)}`,
          );
        }
        if (metrics) {
          try {
            const row = findByIdCached(params.keyId);
            const budget = row?.tokensBudget ?? null;
            const team = row?.team ?? 'unknown';
            const keyPrefix = (row?.id ?? params.keyId).slice(0, 8);
            if (budget && budget > 0) {
              const usage = repo.getGatewayKeyUsage(params.keyId, params.period);
              const used = (usage?.tokensIn ?? 0) + (usage?.tokensOut ?? 0);
              metrics.budgetUsedRatio.labels({ team, key_prefix: keyPrefix }).set(used / budget);
            }
          } catch {
            /* gauge update best-effort */
          }
        }
      },
    };
  }

  // --- Stream registry (always present, cheap) ------------------------
  const streamRegistry = createStreamRegistry();

  // --- Real-time in-flight counter (always present, cheap) ------------
  const inflight = createBackendInflightCounter();

  // --- Lifecycle handles ----------------------------------------------
  let started = false;
  let unsubscribeRegistry: (() => void) | null = null;
  let activeStreamsTimer: ReturnType<typeof setInterval> | null = null;
  let prevBackendIds = new Set<string>();

  const start = (): void => {
    if (started) return;
    started = true;

    if (rateLimiter) {
      rateLimiter.startFlushInterval();
    }

    if (metrics) {
      // backend gauges (busy / total / online) keyed by backend id
      const unsubscribe = registry.subscribe(snapshot => {
        const currentIds = new Set<string>();
        for (const node of snapshot) {
          currentIds.add(node.nodeId);
          try {
            metrics!.backendBusySlots.labels({ backend: node.nodeId }).set(node.busySlots);
            metrics!.backendTotalSlots.labels({ backend: node.nodeId }).set(node.totalSlots);
            metrics!.backendOnline.labels({ backend: node.nodeId }).set(node.online ? 1 : 0);
          } catch {
            /* gauge update best-effort */
          }
        }
        for (const goneId of prevBackendIds) {
          if (currentIds.has(goneId)) continue;
          try {
            metrics!.backendBusySlots.remove({ backend: goneId });
            metrics!.backendTotalSlots.remove({ backend: goneId });
            metrics!.backendOnline.remove({ backend: goneId });
          } catch {
            /* label removal best-effort */
          }
        }
        prevBackendIds = currentIds;
      });
      // `subscribe` may not return an unsubscribe in older versions; guard.
      unsubscribeRegistry = typeof unsubscribe === 'function' ? unsubscribe : null;

      activeStreamsTimer = setInterval(() => {
        try {
          metrics!.activeStreams.set(streamRegistry.size());
        } catch { /* noop */ }
      }, 5_000);
      if (typeof activeStreamsTimer.unref === 'function') activeStreamsTimer.unref();
    }

    logger.info(
      `[gateway-shared] started backends=${config.backends.length} virtual_keys=${config.virtualKeys.length} ` +
        `repo=${repo ? 'open' : 'stateless'} metrics=${metrics ? 'on' : 'off'}`,
    );
  };

  const stop = async (): Promise<void> => {
    if (!started) return;
    started = false;

    // Signal in-flight streams first so they emit `gateway_shutdown`
    // SSE before we tear down the rate-limiter / cache state.
    try {
      streamRegistry.signalShutdown();
    } catch (e) {
      logger.warn(
        `[gateway-shared] streamRegistry.signalShutdown threw: ${e instanceof Error ? e.message : String(e)}`,
      );
    }

    if (rateLimiter) {
      try {
        await rateLimiter.flushToDb();
      } catch (e) {
        logger.warn(
          `[gateway-shared] rateLimiter.flushToDb threw: ${e instanceof Error ? e.message : String(e)}`,
        );
      }
      try { rateLimiter.stop(); } catch { /* noop */ }
    }

    if (activeStreamsTimer) {
      clearInterval(activeStreamsTimer);
      activeStreamsTimer = null;
    }
    if (unsubscribeRegistry) {
      try { unsubscribeRegistry(); } catch { /* noop */ }
      unsubscribeRegistry = null;
    }

    // Drop the per-backend gauge labels we accumulated so a fresh
    // start() doesn't see stale series.
    if (metrics) {
      for (const id of prevBackendIds) {
        try {
          metrics.backendBusySlots.remove({ backend: id });
          metrics.backendTotalSlots.remove({ backend: id });
          metrics.backendOnline.remove({ backend: id });
        } catch { /* noop */ }
      }
    }
    prevBackendIds = new Set<string>();

    // Detach the cross-process admin handles we hung off the Repository.
    if (repo && keyCache) {
      try {
        delete (repo as unknown as { __gatewayKeyCache?: KeyCache }).__gatewayKeyCache;
      } catch { /* noop */ }
    }
    if (repo && metrics) {
      try {
        delete (repo as unknown as { __gatewayMetrics?: GatewayMetrics }).__gatewayMetrics;
      } catch { /* noop */ }
    }

    logger.info('[gateway-shared] stopped');
  };

  return {
    keyCache,
    rateLimiter,
    streamRegistry,
    inflight,
    usageRecorder,
    metrics,
    postAuthMiddleware,
    dbLookup,
    touchLastUsed,
    start,
    stop,
  };
}
