import {
  ArgumentsHost,
  BadRequestException,
  Catch,
  ConflictException,
  ExceptionFilter,
  ForbiddenException,
  HttpException,
  HttpStatus,
  Logger,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import * as Sentry from '@sentry/nestjs';
import { ApiCode, describeApiCode } from '@pac/types';
import { ZodValidationException } from 'nestjs-zod';
import { ZodError } from 'zod';
import type { Request, Response } from 'express';
import { ContractDriftError } from '../../modules/sync/assembler/field-mapper';
import { BizError } from '../errors/biz-error';

/**
 * Universal exception filter — emits the wrapped envelope shape:
 *   { code, msg, data: null, details? }
 *
 * HTTP status policy:
 *   - All known business / HTTP exceptions → HTTP 200 (clients always parse body.code).
 *   - Truly uncaught Errors                → HTTP 500 (so probes / LB still see dead instance).
 *
 * Code mapping policy (priority highest → lowest):
 *   1. BizError              → use its explicit 5-digit code
 *   2. ZodValidationException → CLIENT_VALIDATION_FAILED (10002)
 *   3. PayloadTooLargeError  → CLIENT_VALIDATION_FAILED (10002), HTTP 200 —— 裸 Error,
 *                              必须排在第 5 条之前拦下,否则对面只看到 500/90000
 *   4. ContractDriftError    → SYNC_CONTRACT_DRIFT (30802)
 *   5. NestJS HttpException  → mapped by class (UnauthorizedException → 10106, etc.)
 *   6. Plain Error           → INTERNAL_ERROR (90000), HTTP 500
 */
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name);

  catch(exception: unknown, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const res = ctx.getResponse<Response>();
    const req = ctx.getRequest<Request>();

    let httpStatus = HttpStatus.OK;
    let code: number = ApiCode.INTERNAL_ERROR;
    let msg = describeApiCode(ApiCode.INTERNAL_ERROR);
    let details: unknown;

    if (exception instanceof BizError) {
      code = exception.code;
      msg = exception.msg;
      details = exception.details;
    } else if (exception instanceof ZodValidationException) {
      code = ApiCode.CLIENT_VALIDATION_FAILED;
      msg = describeApiCode(code);
      const err = exception.getZodError();
      if (err instanceof ZodError) {
        details = err.issues.map((i) => ({
          path: i.path.join('.'),
          code: i.code,
          message: i.message,
        }));
      }
    } else if (isPayloadTooLargeError(exception)) {
      // body-parser 的 PayloadTooLargeError 是**裸 Error**(不是 HttpException)→ 不拦的话掉进
      //   下面 500/90000 分支,对面只看到"PAC 内部错误"、按文档去退避重试,永远重试不好
      //   (2026-07 FRIDAY 增量 push 整点重试连挂 25 次就是这么来的)。
      // 归 10002:接入文档 §9.1 把「字段校验失败 / 批量超限」定为 10002「修正后重发」——
      //   包超限正是"这批得改小了再发",语义与处置动作都对得上。
      // HTTP 保持 200:同文档「HTTP 恒为 200(仅进程级故障返回 5xx)」,客户端只认 body.code。
      // 也**不上报 Sentry**:这是对面发包过大,不是 PAC 故障。
      code = ApiCode.CLIENT_VALIDATION_FAILED;
      msg = `请求体超出上限(${PAC_BODY_LIMIT_HINT}),请减小单批行数后重发`;
      details = { limit: PAC_BODY_LIMIT_HINT, hint: '病历类行含自由文本,建议单批 50-100 行' };
      this.logger.warn(`${req.method} ${req.url} → 请求体超限,已按 10002 回执`);
    } else if (exception instanceof ContractDriftError) {
      code = ApiCode.SYNC_CONTRACT_DRIFT;
      msg = exception.message;
      details = { hostId: exception.hostId, resource: exception.resource };
    } else if (exception instanceof HttpException) {
      const httpStatusFromExc = exception.getStatus();
      const body = exception.getResponse();
      msg = exception.message;
      if (typeof body === 'object' && body !== null) {
        const b = body as Record<string, unknown>;
        msg = (b.message as string) ?? msg;
        details = b.details ?? (Array.isArray(b.message) ? b.message : undefined);
      } else if (typeof body === 'string') {
        msg = body;
      }
      code = mapNestExceptionToCode(exception, httpStatusFromExc);
    } else if (exception instanceof Error) {
      // Truly unexpected — keep HTTP 500 so infra notices.
      httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
      code = ApiCode.INTERNAL_ERROR;
      msg = exception.message || describeApiCode(code);
      this.logger.error(exception.stack ?? msg);
      // 只上报"真·未预期错误"(500 分支)给 Sentry;业务错误(BizError/Zod/Http)不上报,免噪音。
      // captureException 在未 init(无 DSN)时是 no-op,本地无 Sentry 也安全。
      Sentry.captureException(exception, {
        tags: { path: `${req.method} ${req.url}` },
      });
    }

    if (httpStatus >= 500) {
      this.logger.error(`${req.method} ${req.url} → 5xx code=${code} msg=${msg}`);
    }

    res.status(httpStatus).json({
      code,
      msg,
      data: null,
      ...(details !== undefined ? { details } : {}),
    });
  }
}

function mapNestExceptionToCode(exc: HttpException, status: number): number {
  if (exc instanceof BadRequestException) return ApiCode.CLIENT_BAD_REQUEST;
  if (exc instanceof UnauthorizedException) return ApiCode.AUTH_TOKEN_INVALID;
  if (exc instanceof ForbiddenException) return ApiCode.AUTH_PERMISSION_DENIED;
  if (exc instanceof NotFoundException) return ApiCode.CLIENT_NOT_FOUND_GENERIC;
  if (exc instanceof ConflictException) return ApiCode.PLAN_ALREADY_ASSIGNED; // best-guess; refactor target uses BizError

  // Fallback by HTTP status family
  if (status === 400) return ApiCode.CLIENT_BAD_REQUEST;
  if (status === 401) return ApiCode.AUTH_TOKEN_INVALID;
  if (status === 403) return ApiCode.AUTH_PERMISSION_DENIED;
  if (status === 404) return ApiCode.CLIENT_NOT_FOUND_GENERIC;
  if (status === 409) return ApiCode.PLAN_ALREADY_ASSIGNED;
  if (status === 422) return ApiCode.CLIENT_VALIDATION_FAILED;
  if (status === 429) return ApiCode.CLIENT_RATE_LIMITED;
  return ApiCode.INTERNAL_ERROR;
}

/// 请求体上限的展示串 —— 仅用于回执文案 / details。真值在 main.ts 的 PAC_BODY_LIMIT,
/// 两处都改才对得上(这里不 import main.ts:那会把 bootstrap 副作用拖进过滤器)。
const PAC_BODY_LIMIT_HINT = '10MB';

/**
 * body-parser(raw-body)在请求体超过 limit 时抛的错 —— **裸 Error,不是 HttpException**,
 * 也不导出类型可供 instanceof。稳定特征是 `type === 'entity.too.large'`(raw-body 固定写入),
 * 辅以 status/statusCode 413 兜底(不同版本字段名有出入)。
 */
function isPayloadTooLargeError(e: unknown): boolean {
  if (!(e instanceof Error)) return false;
  const x = e as Error & { type?: string; status?: number; statusCode?: number };
  return x.type === 'entity.too.large' || x.status === 413 || x.statusCode === 413;
}
