import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { Response } from 'express';

// 전역 Exception Filter — CLAUDE.md 에러 코드 맵에 따라 표준 응답으로 변환
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(HttpExceptionFilter.name);

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

    let status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
    let code = 'SYS_001';
    let message =
      '일시적인 시스템 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.';

    // multer 업로드 오류(크기 초과 등)는 HttpException 이 아니므로 별도 400(VAL_001) 매핑
    if (this.isMulterError(exception)) {
      const muMessage =
        exception.code === 'LIMIT_FILE_SIZE'
          ? '파일 크기가 허용 한도(25MB)를 초과했습니다.'
          : '파일 업로드에 실패했습니다. 파일을 확인해 주세요.';
      response.status(HttpStatus.BAD_REQUEST).json({
        success: false,
        error: { code: 'VAL_001', message: muMessage },
      });
      return;
    }

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const exceptionResponse = exception.getResponse();

      // 0) 명시적 { code, message } 를 가진 비즈니스 예외 — status 매핑보다 우선
      //    (BusinessException 등에서 사용. 코드/문구를 그대로 사용자에게 노출)
      const explicit = this.extractExplicitError(exceptionResponse);
      if (explicit) {
        response.status(status).json({ success: false, error: explicit });
        return;
      }

      // 1) HTTP 상태 → 에러 코드 매핑
      if (status === HttpStatus.UNAUTHORIZED) {
        code = 'AUTH_001';
        message = '로그인이 필요한 서비스입니다. 로그인 후 이용해 주세요.';
      } else if (status === HttpStatus.FORBIDDEN) {
        code = 'AUTH_003';
        message = '해당 메뉴나 기능에 접근할 수 있는 권한이 없습니다.';
      } else if (status === HttpStatus.NOT_FOUND) {
        code = 'RES_001';
        message = '요청하신 정보나 페이지를 찾을 수 없습니다.';
      } else if (
        status === HttpStatus.BAD_REQUEST ||
        status === HttpStatus.UNPROCESSABLE_ENTITY
      ) {
        code = 'VAL_001';
        message =
          '입력하신 정보를 다시 확인해 주세요. (필수값 누락 또는 형식 오류)';
      } else {
        code = 'BIZ_001';
        message = '요청하신 작업을 완료하지 못했습니다. 다시 시도해 주세요.';

        // 비즈니스 예외에서는 사용자에게 보일 메시지를 재정의 가능
        const overridden = this.extractMessage(exceptionResponse);
        if (overridden) {
          message = overridden;
        }
      }
    } else {
      // 정의되지 않은 예외 → 운영팀이 추적 가능하도록 로그 + SYS_001 응답 유지
      // NestJS Logger 2번째 인자는 "스택 문자열"이어야 한다. Error 객체를 그대로 넘기면
      // nest-winston 이 비열거 속성을 직렬화하며 메시지/스택을 잃어 `{ stack: [ {} ] }` 로 찍힌다.
      // → 메시지는 message 로, 스택은 문자열로 분리해 실제 원인이 로그에 남도록 한다.
      const message =
        exception instanceof Error ? exception.message : String(exception);
      this.logger.error(
        `Unhandled Exception: ${message}`,
        exception instanceof Error ? exception.stack : undefined,
      );
    }

    response.status(status).json({
      success: false,
      error: {
        code,
        message,
      },
    });
  }

  // multer 업로드 오류 판별 (name === 'MulterError', code 보유)
  private isMulterError(e: unknown): e is { name: string; code?: string } {
    return (
      typeof e === 'object' &&
      e !== null &&
      (e as { name?: unknown }).name === 'MulterError'
    );
  }

  // 예외 응답에서 명시적 { code, message } 추출 (둘 다 문자열일 때만)
  private extractExplicitError(
    res: string | object,
  ): { code: string; message: string } | null {
    if (typeof res === 'object' && res !== null) {
      const r = res as { code?: unknown; message?: unknown };
      if (typeof r.code === 'string' && typeof r.message === 'string') {
        return { code: r.code, message: r.message };
      }
    }
    return null;
  }

  // 예외 응답에서 사용자 노출 메시지(문자열) 추출
  private extractMessage(res: string | object): string | null {
    if (typeof res === 'object' && res !== null && 'message' in res) {
      const responseMessage = res.message;
      if (typeof responseMessage === 'string') {
        return responseMessage;
      }
    }
    return null;
  }
}
