import { INestApplication, ValidationPipe } from '@nestjs/common';
import { HttpAdapterHost, NestFactory } from '@nestjs/core';

import { ConfigService } from '@nestjs/config';
import * as bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import { NextFunction, Request, Response } from 'express';
import helmet from 'helmet';
import { Logger, LoggerErrorInterceptor } from 'nestjs-pino';
import { AppModule } from './app.module';
import { AllExceptionFilter } from './fliters/all-exception.filter';
import { applyHttpServerTimeouts } from './helpers/http-server.helper';
import { AdminAuditInterceptor } from './interceptors/admin-audit.interceptor';
import { RequestCountInterceptor } from './interceptors/request-count.interceptor';
import { setupKernel } from './kernel';
import { runSeeds } from './seeds/run-seeds';
import { AlertSignalService } from './modules/alert/alert-signal.service';
import { AuditService } from './modules/audit/audit.service';
import { isSwaggerEnabled, setupSwagger } from './swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { bufferLogs: true });
  const httpAdapterHost = app.get(HttpAdapterHost);
  //* A4 신호 수집(M2 §D3) — 필터(분자)와 인터셉터(분모)가 같은 인스턴스를 쓴다.
  const alertSignals = app.get(AlertSignalService);
  //* 관리자 행위 감사(T2·DENIED) — 인터셉터가 통과 요청을, 예외 필터가 가드 거부를 남긴다.
  //*  두 층이 같은 인스턴스를 쓰므로 줄 형태와 버전이 갈리지 않는다.
  const audit = app.get(AuditService);

  setupCors(app);
  //* rank24: 보안 헤더. 항상 ON. 기본 helmet 에서 "교차 출처 임베딩/로딩"을 막는 3개를 끈다 —
  //*  이 API 는 admin↔site 교차 출처 iframe(admin-view)과 청구서 PDF 를 inline 으로 내보내므로
  //*  CSP(frame-ancestors 포함)·X-Frame-Options(frameguard)·Cross-Origin-Resource-Policy 기본값이
  //*  기존 클라이언트를 깨뜨린다. CORS(setupCors) 설정도 그대로 보존된다. 나머지 안전 헤더
  //*  (X-Content-Type-Options nosniff·HSTS·Referrer-Policy·X-Powered-By 제거 등)는 유지한다.
  //*  Cross-Origin-Opener-Policy 도 끈다. 본인인증 창은 POST /mok-auth/result 의 302 를 지나 사이트로
  //*  돌아오는데, 이 응답에 same-origin 이 붙으면 브라우저가 창의 opener 를 끊어 결과가 찾기·가입
  //*  화면으로 전달되지 않는다(2026-10-20 stg 실측).
  app.use(
    helmet({
      contentSecurityPolicy: false,
      crossOriginOpenerPolicy: false,
      crossOriginResourcePolicy: false,
      frameguard: false,
    }),
  );
  app.use(bodyParser.json({ limit: '10mb' }));
  app.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
  app.use(cookieParser());
  //* rank24: CSRF. 기본 OFF. 관리자 인증 쿠키 _aaT 가 SameSite=None 이라(교차 출처 폼 POST 에도
  //*  실림) 전역 CSRF 를 보류한 팀 결정 — policy-json-body.guard 가 정책 라우트만 방어 중, 전역화는
  //*  backlog abp-m1p-impl-sec-admin-csrf-global. CSRF_ENFORCE==='true' 일 때만 최소 더블서밋 쿠키
  //*  가드를 켠다. cookieParser 뒤에 둬야 req.cookies 를 읽는다. 기본값에선 미등록 = 동작 변화 없음.
  if (process.env.CSRF_ENFORCE === 'true') {
    app.use(csrfDoubleSubmit);
  }
  //* rank24: 전역 rate-limit 은 두지 않는다(신규 의존 + 동작 변화 우려). 라우트별 가드가 이미 있다
  //*  (auth AdminSigninRateLimitGuard, treat CertifyRateLimitGuard). 전역 throttler 는 후속 과제.
  // app.useGlobalPipes(new ValidationPipe({ transform: true }));
  app.useGlobalFilters(
    new AllExceptionFilter(httpAdapterHost, alertSignals, audit),
  );

  app.useLogger(app.get(Logger));
  app.useGlobalInterceptors(
    new LoggerErrorInterceptor(),
    new RequestCountInterceptor(alertSignals),
    new AdminAuditInterceptor(audit),
  );

  //* swagger 는 stg·운영에서 닫는다. NODE_ENV 가 아니라 SWAGGER_ENABLED=true 일 때만 연다(로컬 opt-in).
  if (isSwaggerEnabled()) {
    setupSwagger(app);
  }
  if (process.platform === 'win32') {
    await setupKernel();
  }

  applyHttpServerTimeouts(app.getHttpServer());

  //* 기본값은 7200 그대로. PORT 가 있을 때만 그 값을 쓴다 — 같은 머신에서 worktree별
  //* 인스턴스를 동시에 띄우려면 포트가 하드코딩돼 있으면 안 된다. 미설정/비수치면 7200.
  const port = Number(process.env.PORT) || 7200;
  await app.listen(port);

  //* 기동 seed — **listen 뒤**다(DD2). 앞에 두면 seed 가 늦거나 멈출 때 기동·헬스체크가 함께 기다린다.
  //*   모듈 수명주기 훅에 걸지 않는 이유도 같다 — 스케줄러와 invoice.seeder 도 같은 AppModule 을 띄운다.
  //*   `runSeeds` 는 던지지 않는다. 끝나기 전에 온 요청은 최고관리자 판정이 거짓(닫힘)이다.
  void runSeeds(app);
}

bootstrap();

function setupCors(app: INestApplication) {
  const configService = app.get(ConfigService);
  const rawOrigins = configService.get<string>('allowOrigins');
  if (!rawOrigins) {
    return;
  }

  const origins = rawOrigins.split(',');
  app.enableCors({
    origin: origins,
    credentials: true,
    exposedHeaders: ['Content-Disposition'],
  });
}

//* rank24: 최소 더블서밋 쿠키 CSRF 가드. CSRF_ENFORCE==='true' 일 때만 app.use 로 연결된다(기본 OFF).
//*  안전 메서드(GET/HEAD/OPTIONS)는 통과. 상태변경 메서드는 'x-csrf-token' 헤더와 'csrf-token' 쿠키가
//*  일치해야 통과한다(헤더가 배열이면 string 쿠키와 불일치 → 거부). 토큰 발급(쿠키 세팅)은 활성화 시
//*  프런트/클라이언트에서 연결한다. 기본 OFF 이므로 켜기 전까지 완전히 비활성 — 동작 변화 없음.
function csrfDoubleSubmit(req: Request, res: Response, next: NextFunction) {
  if (
    req.method === 'GET' ||
    req.method === 'HEAD' ||
    req.method === 'OPTIONS'
  ) {
    return next();
  }
  const header = req.headers['x-csrf-token'];
  const cookie = req.cookies?.['csrf-token'];
  if (typeof cookie === 'string' && cookie.length > 0 && header === cookie) {
    return next();
  }
  res.status(403).json({ statusCode: 403, message: 'CSRF token invalid' });
}
