import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { ErrorResponse } from 'src/common/types/response.types';
import { Logger } from 'nestjs-pino';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  constructor(private readonly logger: Logger) {}

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

    const { status, code, message } = this.normalizeException(exception);

    const requestId = request.requestId;
    const logData = {
      event: 'http.exception',
      requestId,
      statusCode: status,
      code,
      message,
      method: request.method,
      route: this.resolveRoute(request),
    };
    const error = exception instanceof Error ? exception : undefined;

    if (status >= Number(HttpStatus.INTERNAL_SERVER_ERROR)) {
      if (error) {
        this.logger.error(
          { ...logData, err: error },
          'http_exception',
          GlobalExceptionFilter.name,
        );
      } else {
        this.logger.error(
          logData,
          'http_exception',
          GlobalExceptionFilter.name,
        );
      }
    } else {
      this.logger.warn(logData, 'http_exception', GlobalExceptionFilter.name);
    }

    if (response.headersSent) {
      return;
    }

    // 에러 응답에 meta 포함
    const body: ErrorResponse = {
      success: false,
      error: {
        code,
        message,
      },
      meta: {
        requestId,
        timestamp: new Date().toISOString(),
        path: request.url,
        method: request.method,
        statusCode: status,
      },
    };

    response.status(status).json(body);
  }

  private normalizeException(exception: unknown): {
    status: number;
    code: string;
    message: string;
  } {
    if (exception instanceof HttpException) {
      const status = exception.getStatus();
      const response = exception.getResponse();

      return this.normalizeHttpException(response, status, exception.message);
    }

    const message =
      exception instanceof Error && exception.message
        ? exception.message
        : 'Internal server error';

    return {
      status: HttpStatus.INTERNAL_SERVER_ERROR,
      code: 'INTERNAL_SERVER_ERROR',
      message,
    };
  }

  private normalizeHttpException(
    response: string | object,
    status: number,
    fallbackMessage: string,
  ): {
    status: number;
    code: string;
    message: string;
  } {
    const code = this.pickCode(response, status);
    const message = this.pickMessage(response, fallbackMessage);

    return {
      status,
      code,
      message,
    };
  }

  private pickCode(response: string | object, status: number): string {
    if (this.isObject(response)) {
      const code = this.pickString(response, ['code', 'errorCode']);
      if (code) {
        return code;
      }
    }

    return HttpStatus[status] ?? 'HTTP_ERROR';
  }

  private pickMessage(
    response: string | object,
    fallbackMessage: string,
  ): string {
    if (typeof response === 'string') {
      return response;
    }

    if (!this.isObject(response)) {
      return fallbackMessage;
    }

    const message = this.parseMessage(response.message);
    if (message) {
      return message;
    }

    const errorMessage = this.pickString(response, ['error']);
    if (errorMessage) {
      return errorMessage;
    }

    return fallbackMessage;
  }

  private parseMessage(message: unknown): string | undefined {
    if (Array.isArray(message)) {
      const parts = message.filter((item) => typeof item === 'string');
      return parts.length > 0 ? parts.join(', ') : undefined;
    }

    if (typeof message === 'string') {
      return message;
    }

    return undefined;
  }

  private pickString(
    response: Record<string, unknown>,
    keys: string[],
  ): string | undefined {
    for (const key of keys) {
      const value = response[key];
      if (typeof value === 'string' && value.trim().length > 0) {
        return value;
      }
    }

    return undefined;
  }

  private isObject(value: unknown): value is Record<string, unknown> {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
  }

  private resolveRoute(request: Request): string {
    const route = (request as unknown as { route?: { path?: unknown } }).route;
    const rawPath = route?.path;
    const routePath = typeof rawPath === 'string' ? rawPath : undefined;

    if (typeof routePath === 'string') {
      const baseUrl = request.baseUrl ?? '';
      const combined = `${baseUrl}${routePath}`.replace(/\/{2,}/g, '/');
      return combined.startsWith('/') ? combined : `/${combined}`;
    }

    const path = request.path ?? request.url;
    if (typeof path === 'string') {
      return path.split('?')[0] ?? path;
    }

    return '';
  }
}
