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. ContractDriftError    → SYNC_CONTRACT_DRIFT (30802)
 *   4. NestJS HttpException  → mapped by class (UnauthorizedException → 10106, etc.)
 *   5. 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 (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;
}
