import type { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule, type OpenAPIObject } from '@nestjs/swagger';
import { apiReference } from '@scalar/nestjs-api-reference';
import type { Request, Response } from 'express';

/**
 * Cookie names declared as security schemes. Match the production
 * cookie names — Scalar's "Try it" panel offers to send them and
 * the SPA-side cookies already flow on same-origin calls.
 */
export const USER_SESSION_COOKIE_AUTH = 'portal_session';
export const ADMIN_SESSION_COOKIE_AUTH = 'portal_admin_session';

/**
 * Builds the OpenAPI document from the running Nest application by
 * traversing the controller/DTO graph (Nest's standard
 * `SwaggerModule.createDocument`). Two cookie-auth schemes are
 * registered up front so endpoints can reference them via
 * `@ApiCookieAuth(name)`:
 *
 *   - `portal_session` — user-portal surface (ADR-0009).
 *   - `portal_admin_session` — admin-portal surface (ADR-0020).
 *
 * No `@ApiBearerAuth` — the BFF never exposes a bearer-auth surface
 * (per ADR-0009 the SPA never holds tokens; downstream OBO tokens
 * are server-side only per ADR-0014).
 */
export function buildOpenApiDocument(app: INestApplication): OpenAPIObject {
  const config = new DocumentBuilder()
    .setTitle('APF Portal BFF API')
    .setDescription(
      'OpenAPI specification of the portal-bff HTTP surface. Auto-generated from ' +
        'Nest controllers + class-validator DTOs. Mutating routes (POST / PUT / PATCH / ' +
        'DELETE) require the `X-CSRF-Token` header per ADR-0009 §"Double-submit CSRF" — ' +
        'set it manually in Scalar to the value of the `portal_csrf` cookie when ' +
        'exercising those routes from this page.',
    )
    .setVersion('0.0.0')
    .addCookieAuth(
      USER_SESSION_COOKIE_AUTH,
      { type: 'apiKey', in: 'cookie', name: USER_SESSION_COOKIE_AUTH },
      USER_SESSION_COOKIE_AUTH,
    )
    .addCookieAuth(
      ADMIN_SESSION_COOKIE_AUTH,
      { type: 'apiKey', in: 'cookie', name: ADMIN_SESSION_COOKIE_AUTH },
      ADMIN_SESSION_COOKIE_AUTH,
    )
    .build();
  return SwaggerModule.createDocument(app, config);
}

/**
 * Mounts the OpenAPI spec + the Scalar API Reference UI on the BFF.
 *
 * Routes:
 *   - `GET /<globalPrefix>/openapi.json` — the raw OpenAPI 3 spec.
 *     External tools (Bruno, Insomnia, Postman) import from here.
 *   - `GET /<globalPrefix>/docs` — Scalar API Reference UI page,
 *     loads the spec from the JSON endpoint at render time.
 *
 * **Dev-only by default.** Production deployments do not need a
 * docs surface served from the BFF itself — and exposing it would
 * give an attacker a curated map of every authenticated endpoint
 * + every DTO shape. Skipped entirely when `NODE_ENV === 'production'`.
 * If a future ops use-case wants the spec in prod (e.g. an internal
 * gateway), we can add an explicit `OPENAPI_PUBLISH=true` env knob
 * — opt-in, not default-on.
 */
export function setupOpenApi(app: INestApplication, globalPrefix: string): void {
  if (process.env['NODE_ENV'] === 'production') {
    return;
  }

  const document = buildOpenApiDocument(app);

  // Serve the JSON spec via a plain Express handler. Bypasses Nest's
  // router because the URL is a static asset — no DTO, no guard,
  // no middleware interaction worth threading through.
  const httpAdapter = app.getHttpAdapter();
  httpAdapter.get(`/${globalPrefix}/openapi.json`, (_req: Request, res: Response) => {
    res.json(document);
  });

  // Scalar API Reference UI. Same Express-level mount — its handler
  // emits a static HTML page that fetches the spec from the URL
  // above at render time. Lock the operationTitleSource to `path`
  // so endpoints sort + display by their URL (more useful for an
  // admin-API surface where the path encodes hierarchy).
  app.use(
    `/${globalPrefix}/docs`,
    apiReference({
      url: `/${globalPrefix}/openapi.json`,
      operationTitleSource: 'path',
    }),
  );
}
