import { NestFactory } from '@nestjs/core';
import { Logger, ValidationPipe, ForbiddenException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as express from 'express';
import helmet from 'helmet';
import cookieParser from 'cookie-parser';
import type { Request, Response, NextFunction } from 'express';
import { AppModule } from './app.module';
import { buildCsrf, isExemptFromCsrf } from './auth/csrf';
import {
  isTrashedMediaPath,
  resolveStorageRoot,
  STORAGE_PUBLIC_MOUNT,
} from './config/storage.config';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = app.get(ConfigService);

  // Security headers (X-Frame-Options, X-Content-Type-Options, HSTS, etc.)
  app.use(
    helmet({
      contentSecurityPolicy: false,
      crossOriginResourcePolicy: { policy: 'cross-origin' },
    }),
  );

  // CORS — restricted to configured origin(s). The CORS_ORIGIN env
  // var is comma-separated so multiple frontends (dev, UAT, prod) can
  // share one server. Methods + allowedHeaders are listed explicitly
  // because the browser preflights any request carrying a custom
  // header (X-Tenant-Id) — without listing it the OPTIONS reply
  // doesn't echo it back and the actual call gets blocked.
  const corsOrigin = config.get<string>('CORS_ORIGIN', 'http://localhost:5173');
  app.enableCors({
    origin: corsOrigin.split(',').map((o) => o.trim()),
    credentials: true,
    methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS'],
    allowedHeaders: [
      'Content-Type',
      'Authorization',
      'X-Tenant-Id',
      'X-Requested-With',
      'X-Request-Id',
      'X-CSRF-Token',
    ],
    exposedHeaders: ['X-Request-Id'],
    maxAge: 86400, // 24h preflight cache
  });

  // Cookie parser — required for httpOnly access + refresh cookies
  // set by AuthController, and for the CSRF double-submit cookie.
  app.use(cookieParser());

  // CSRF (double-submit cookie via csrf-csrf) — guards state-changing
  // requests when the caller rides a session cookie. Bearer-token
  // clients and the auth surface itself are exempt (see csrf.ts).
  //
  // Header the FE echoes:  X-CSRF-Token
  // Cookie the token lives in:  dmc_csrf  (host-only, non-httpOnly)
  //
  // Endpoint to issue a fresh token pair:  GET /api/auth/csrf-token
  const csrfSecret = config.get<string>('CSRF_SECRET') || config.get<string>('JWT_SECRET') || '';
  const { doubleCsrfProtection, generateCsrfToken } = buildCsrf(csrfSecret);
  app.use((req: Request, res: Response, next: NextFunction) => {
    if (req.method === 'GET' && req.path === '/api/auth/csrf-token') {
      const token = generateCsrfToken(req, res);
      res.json({
        success: true,
        message: 'CSRF token issued',
        data: { csrf_token: token },
        extra_data: [],
      });
      return;
    }
    if (isExemptFromCsrf(req)) return next();
    // csrf-csrf raises its own ForbiddenError (code EBADCSRFTOKEN) on a
    // missing/invalid token — either thrown synchronously or forwarded
    // via next(err). That error is NOT a Nest HttpException, so it would
    // fall through GlobalExceptionFilter as a generic 500 ("Internal
    // server error"). Translate it into a proper 403 so callers get a
    // clear, correct status. Non-CSRF errors are passed through unchanged.
    const forwardCsrfError = (err?: unknown) => {
      if (!err) return next();
      if ((err as { code?: string }).code === 'EBADCSRFTOKEN') {
        return next(new ForbiddenException('Invalid or missing CSRF token'));
      }
      return next(err);
    };
    try {
      doubleCsrfProtection(req, res, forwardCsrfError);
    } catch (err) {
      forwardCsrfError(err);
    }
  });

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      // Reject (rather than silently strip) unknown body keys —
      // turns field-injection attempts into a 400 the caller can
      // see, instead of letting them slip past the DTO. Required by
      // backend.md.
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  // GlobalExceptionFilter is registered as APP_FILTER in AppModule so it
  // can inject ErrorAlertService — registering it here too would run it
  // twice.

  app.setGlobalPrefix('api');

  // Serve uploaded files. Mount is frozen at /api/uploads so it cannot
  // overlay the rest of the API. index/dotfiles off; nosniff so a
  // sniffed PDF/image cannot be re-interpreted as HTML.
  const localDir = config.get<string>('storage.localDir') || 'uploads';
  app.use(
    STORAGE_PUBLIC_MOUNT,
    (req: Request, res: Response, next: NextFunction) => {
      res.setHeader('X-Content-Type-Options', 'nosniff');
      if (isTrashedMediaPath(req.path)) {
        res.status(404).end();
        return;
      }
      next();
    },
    express.static(resolveStorageRoot(localDir), {
      index: false,
      dotfiles: 'deny',
      redirect: false,
    }),
  );

  const port = config.get<number>('app.port', 3000);

  await app.listen(port);
  Logger.log(`DMC Ops Platform running on http://localhost:${port}/api`, 'Bootstrap');
}

void bootstrap();
