/**
 * Express middleware wiring for the management-plane auth mode.
 *
 * Split into two pieces by design — the cookie-parsing middleware always
 * runs (so identity-aware handlers like /api/me always see req.session),
 * and the protected-route gate is mounted explicitly on the routes that
 * need it. There is no `if (publicPath) next()` shortcut anywhere — the
 * decision of "what is public" is encoded by which middleware Express
 * registers on which route, not by a string match at request time.
 *
 * When `OMCP_AUTH` is unset or "anonymous" (the default) both middlewares
 * are no-ops and every existing handler behaves exactly as before.
 */

import type { NextFunction, Request, RequestHandler, Response } from "express";

import { readCookie, verifySession, type SessionPayload, type SessionConfig } from "./session.js";
// Type-only import: erased at compile time, so it does NOT pull
// node:crypto or any other oidc runtime dependency into the
// middleware surface. Restores type safety on `runtime.oidc`
// without changing the module graph.
import type { OidcRuntime } from "./oidc/runtime.js";
// Type-only — the store instance is injected at wire-up time; importing
// the type adds no runtime coupling to node:fs here.
import type { RevocationStore } from "./revocation.js";

export type AuthMode = "anonymous" | "basic" | "oidc";

export interface AuthRuntime {
  mode: AuthMode;
  /** Present when mode is "basic" or "oidc" — both mint OMCP session
   *  cookies the same way, just sourced from local creds vs. an IdP. */
  session?: SessionConfig;
  /** When true and `secret` not provided, the server generated one for this
   * process — sessions will not survive a restart. The wire-up code logs a
   * warning once when this happens. */
  secretEphemeral?: boolean;
  /** OIDC runtime, present only when mode === "oidc". The OIDC HTTP
   *  endpoints in src/index.ts consume it. The import above is
   *  type-only and erased at compile time, so this typing adds zero
   *  runtime coupling — middleware.ts still doesn't depend on the
   *  OIDC sub-module's node:crypto path. */
  oidc?: OidcRuntime;
  /** Session revocation blocklist. Present in basic / oidc mode; the
   *  session attacher consults it on every request so a revoked but
   *  not-yet-expired cookie is treated as logged out. */
  revocation?: RevocationStore;
}

export interface AuthedRequest extends Request {
  session?: SessionPayload;
}

/**
 * Best-effort cookie resolver. Attaches `req.session` when present and
 * valid; otherwise leaves it undefined. Always calls `next()`. Mount this
 * globally so every handler can read the identity.
 */
export function buildSessionAttacher(runtime: AuthRuntime): RequestHandler {
  const sessionCfg = runtime.session;
  // In anonymous mode the secret is meaningless — verifySession would
  // throw on an empty secret — so install a true no-op middleware and
  // skip every per-request branch.
  if (runtime.mode === "anonymous" || !sessionCfg) {
    return function noopAttacher(_req: Request, _res: Response, next: NextFunction): void {
      next();
    };
  }
  return function sessionAttacher(req: Request, _res: Response, next: NextFunction): void {
    // verifySession is a pure parse + HMAC verify. It safely returns
    // null on empty / null / malformed input, so call it unconditionally
    // and let the result speak for itself — no attacker-controlled
    // branch decides whether the check runs.
    const raw = readCookie(req.headers.cookie || "");
    const payload = verifySession(raw, sessionCfg);
    // A valid signature is necessary but not sufficient: a session whose
    // sid (or whose subject, before the revocation cutoff) is on the
    // blocklist is treated as absent. isRevoked is a synchronous in-memory
    // lookup, so this stays a no-await hot path.
    if (payload && !runtime.revocation?.isRevoked(payload)) {
      (req as AuthedRequest).session = payload;
    }
    next();
  };
}

/**
 * Gate. Rejects requests that lack a valid session with HTTP 401 + a JSON
 * body the UI's fetch wrapper recognises. Mount this on each protected
 * route or router, NOT globally — paths the operator wants public
 * (login, /api/me, /api/info, /healthz, ...) simply don't register it.
 */
export function buildRequireSession(runtime: AuthRuntime): RequestHandler {
  return function requireSession(req: Request, res: Response, next: NextFunction): void {
    if (runtime.mode === "anonymous" || !runtime.session) {
      next();
      return;
    }
    if ((req as AuthedRequest).session) {
      next();
      return;
    }
    res.status(401).json({
      error: "authentication required",
      mode: runtime.mode,
      // Recognised by the UI's fetch wrapper to trigger the login modal.
      code: "OMCP_AUTH_REQUIRED",
    });
  };
}
