// MUST be the very first import — see apps/portal-bff/src/observability/tracing.ts
// for the reasoning. Anything `import`ed above this line bypasses the
// OpenTelemetry auto-instrumentations and is silently un-traced.
import './observability/tracing';

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import { Logger } from 'nestjs-pino';
import { AppModule } from './app/app.module';
import { readCorsAllowlist } from './config/check-cors-allowlist';
import { assertDatabaseUrl } from './config/check-database-url';
import { assertEntraConfig } from './config/check-entra-config';
import { assertJwksConfig } from './config/check-jwks-config';
import { assertRedisConfig } from './config/check-redis-config';
import { assertLogUserIdSalt } from './config/check-log-user-id-salt';
import { assertOboCacheEncryptionKey } from './config/check-obo-cache-encryption-key';
import { assertSessionEncryptionKey } from './config/check-session-encryption-key';
import { assertSessionSecret } from './config/check-session-secret';
import { createRateLimitMiddleware, readRateLimitConfig } from './security/rate-limit.middleware';
import { CSRF_MIDDLEWARE } from './security/security.token';
import { StructuredErrorFilter } from './security/structured-error.filter';
import { JwksPublisher } from './downstream/jwks.publisher';
import { setupOpenApi } from './openapi/openapi';
import type { NextFunction, Request, Response } from 'express';
import {
  ADMIN_SESSION_MIDDLEWARE,
  SESSION_ABSOLUTE_TIMEOUT_MIDDLEWARE,
  SESSION_MIDDLEWARE,
  type RequestHandler,
} from './session/session.token';

// Fail fast on a malformed DATABASE_URL (most often a special char in
// the password that needs URL-encoding) rather than letting Prisma
// surface a cryptic "invalid connection string" error mid-request.
assertDatabaseUrl();

// Same family of pre-flight check for the Entra app-registration env
// vars (per ADR-0009). Missing / placeholder values fail here rather
// than deep inside the first auth request.
assertEntraConfig();

// SESSION_SECRET signs the auth-flow cookies (pre-auth state +
// PKCE verifier today, session cookie next).
const sessionSecret = assertSessionSecret();

// REDIS_URL is the shared session / cache backend (ADR-0010). Boot-
// time guard so a malformed URL fails before `ioredis` enters its
// reconnect loop.
assertRedisConfig();

// SESSION_ENCRYPTION_KEY is the AES-256-GCM key for session payload
// at-rest encryption (ADR-0010). Same fail-fast policy as the other
// pre-flight validators — a missing / weak key here would only
// surface on the first authenticated request otherwise.
assertSessionEncryptionKey();

// LOG_USER_ID_SALT — per-environment SHA-256 salt for the actor-id
// hash that joins audit rows (ADR-0013) and Pino log lines
// (ADR-0012). Mandatory at boot.
assertLogUserIdSalt();

// OBO_CACHE_ENCRYPTION_KEY — dedicated AES-256-GCM key for the OBO
// downstream-token cache (ADR-0014 §"Token cache (for OBO)"). MUST
// differ from SESSION_ENCRYPTION_KEY — the validator refuses an
// identical value as defense in depth against copy-paste accidents.
assertOboCacheEncryptionKey();

// BFF_JWKS_PRIVATE_KEY_PATH + BFF_JWKS_KID — signing material for
// the ADR-0014 signed-assertion strategy. Reads the PEM file once
// here so a missing / unreadable / weak key fails the boot rather
// than the first downstream call. The same parsed config is
// re-used by `DownstreamModule`'s factory at app construction.
assertJwksConfig();

async function bootstrap() {
  // `bufferLogs: true` holds early-bootstrap log lines until the
  // Pino-based Logger is wired in below, so we don't lose anything
  // emitted before `app.useLogger()`.
  const app = await NestFactory.create(AppModule, { bufferLogs: true });
  app.useLogger(app.get(Logger));

  // Global exception filter — normalises every 4xx/5xx response to
  // `{ error: { code, message, traceId } }`. The Nest default
  // serialises HttpException's getResponse() at the top level,
  // which leaks the class name on 500s and produces an
  // inconsistent shape across exception types. Registering early
  // (before request middleware mounts) ensures even errors thrown
  // during route setup are caught.
  app.useGlobalFilters(new StructuredErrorFilter(app.get(Logger)));

  // Security headers (phase-2). Defaults from `helmet()` are good
  // for an API server returning JSON: X-Frame-Options=SAMEORIGIN,
  // X-Content-Type-Options=nosniff, Referrer-Policy=no-referrer,
  // X-Powered-By removed, etc. CSP defaults apply too but the BFF
  // doesn't render HTML, so they're inert here.
  //
  // Three overrides for our specific shape:
  //   - HSTS only in production (dev runs on plain HTTP).
  //   - crossOriginResourcePolicy: 'cross-origin' so the SPA on its
  //     own origin can read JSON from the BFF without being blocked
  //     by Spectre-class CORP protections.
  //   - contentSecurityPolicy: false in dev — Helmet's default CSP
  //     blocks `connect-src` from anything but 'self', which is
  //     fine for HTML pages but irrelevant for JSON responses and
  //     noisy in browser devtools.
  app.use(
    helmet({
      hsts: process.env['NODE_ENV'] === 'production',
      crossOriginResourcePolicy: { policy: 'cross-origin' },
      contentSecurityPolicy: process.env['NODE_ENV'] === 'production',
    }),
  );

  // CORS allowlist — env-driven via `CORS_ALLOWED_ORIGINS`, parsed
  // and validated at boot. No hardcoded localhost fallback: getting
  // CORS wrong silently is exactly the kind of "works in dev, breaks
  // in prod" issue this validator is meant to catch.
  app.enableCors({
    origin: [...readCorsAllowlist()],
    allowedHeaders: [
      'Content-Type',
      'Accept',
      'Authorization',
      'X-CSRF-Token',
      'traceparent',
      'tracestate',
    ],
    credentials: true,
  });

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  // Cookie parsing for the auth flow (per ADR-0009). `SESSION_SECRET`
  // signs the pre-auth cookie and the post-login session id cookie;
  // signed cookies are read from `req.signedCookies`, unsigned from
  // `req.cookies`.
  app.use(cookieParser(sessionSecret));

  // Session middlewares (per ADR-0010 + ADR-0020 §"Sessions — distinct
  // from `portal-shell`"). Two parallel express-session instances:
  //
  //   - `SESSION_MIDDLEWARE` carries `portal_session` / Redis prefix
  //     `session:` and binds to every path EXCEPT `/api/admin/*`.
  //   - `ADMIN_SESSION_MIDDLEWARE` carries `portal_admin_session` /
  //     Redis prefix `session:admin:` and binds to `/api/admin/*` only.
  //
  // The dispatch is a tiny wrapper that picks one or the other per
  // request — running both would have the second overwrite `req.session`
  // from the first, collapsing the two surfaces. Mounted after
  // `cookieParser` so the session-id cookie is parsed by the time the
  // selected middleware reads it.
  const userSession = app.get<RequestHandler>(SESSION_MIDDLEWARE);
  const adminSession = app.get<RequestHandler>(ADMIN_SESSION_MIDDLEWARE);
  app.use((req: Request, res: Response, next: NextFunction) => {
    if (req.path.startsWith('/api/admin')) {
      return adminSession(req, res, next);
    }
    return userSession(req, res, next);
  });

  // Absolute-timeout enforcement (ADR-0010 §"TTL policy"). Runs on
  // every request that survives `express-session`; if the session
  // is past its 12 h hard ceiling, destroy it + clear the cookie +
  // drop the per-user index entry, then let the request continue
  // anonymously (route-level guards turn it into a 401 where
  // needed).
  app.use(app.get<RequestHandler>(SESSION_ABSOLUTE_TIMEOUT_MIDDLEWARE));

  // Rate limiting (ADR-0015 §"DoS mitigation" + phase-2 follow-up).
  // Mounted after the session middleware so the bucket key falls
  // back to the session id for authenticated requests (preventing
  // a single attacker from rotating sessions to dodge the limit)
  // and to the remote IP otherwise. Default 120/min general, 10/min
  // on `/auth/login` and `/auth/callback` to slow brute-force /
  // replay attempts. `/api/health` is skipped — orchestrator polls
  // shouldn't burn the user quota.
  app.use(createRateLimitMiddleware(readRateLimitConfig()));

  // Double-submit CSRF (ADR-0009 §"CSRF defense"). Mounted after
  // the session middleware so `req.session.csrfToken` is available
  // for comparison with the `X-CSRF-Token` request header. Skips
  // safe methods (GET / HEAD / OPTIONS), anonymous requests, and
  // the auth entry routes that *mint* the token (`/auth/login`,
  // `/auth/callback`).
  app.use(app.get<RequestHandler>(CSRF_MIDDLEWARE));

  const globalPrefix = 'api';
  app.setGlobalPrefix(globalPrefix);

  // JWKS endpoint at the RFC 8615 bare-root path. Wired at the
  // Express layer rather than as a Nest `@Controller` because
  // path-to-regexp v8 (Nest 11's router) does not cleanly route a
  // leading-dot segment like `.well-known/jwks.json` to a bare-root
  // URL — the previous Nest-side attempt with `setGlobalPrefix`
  // `exclude` landed the route at neither `/api/.well-known/jwks.json`
  // nor `/.well-known/jwks.json` (both 404'd). Express's own
  // routing accepts the leading dot verbatim, and the Nest DI
  // container still owns the underlying `JwksPublisher` service.
  //
  // Public by design (no session, no CSRF) — the JWKS is the
  // downstream's verification anchor; gating it defeats the
  // purpose. Mounted before `app.listen()` so the route is live
  // by the time the BFF reports ready.
  const jwksPublisher = app.get(JwksPublisher);
  app.getHttpAdapter().get('/.well-known/jwks.json', (_req: Request, res: Response) => {
    res.json(jwksPublisher.jwks());
  });

  // OpenAPI spec + Scalar API Reference UI, dev-only. Mounted at
  // `/${globalPrefix}/openapi.json` and `/${globalPrefix}/docs`
  // respectively — the function short-circuits in production.
  setupOpenApi(app, globalPrefix);

  const port = process.env['PORT'] ?? 3000;
  await app.listen(port);

  app
    .get(Logger)
    .log(`Application is running on: http://localhost:${port}/${globalPrefix}`, 'Bootstrap');
}

bootstrap();
