import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import * as Sentry from '@sentry/nestjs';
import type { Response } from 'express';
import { AppException, type AppErrorResponse } from './app.exception';
import { ErrorCode, type ErrorCodeValue } from './error-codes';

interface ClientErrorBody {
  success: false;
  error: {
    code: ErrorCodeValue;
    statusCode: number;
    details?: unknown;
  };
}

const STATUS_FALLBACK_CODES: Partial<Record<number, ErrorCodeValue>> = {
  [HttpStatus.BAD_REQUEST]: ErrorCode.BAD_REQUEST,
  [HttpStatus.UNAUTHORIZED]: ErrorCode.AUTH_UNAUTHORIZED,
  [HttpStatus.FORBIDDEN]: ErrorCode.PERMISSION_DENIED,
  [HttpStatus.NOT_FOUND]: ErrorCode.NOT_FOUND,
  [HttpStatus.CONFLICT]: ErrorCode.CONFLICT,
  [HttpStatus.INTERNAL_SERVER_ERROR]: ErrorCode.INTERNAL_ERROR,
};

@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>();

    const { statusCode, body } = this.normalizeException(exception);

    if (statusCode >= 500) {
      this.logger.error(
        exception instanceof Error ? exception.stack : String(exception),
      );
      if (Sentry.getClient()) {
        Sentry.captureException(exception);
      }
    }

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

  private normalizeException(exception: unknown): {
    statusCode: number;
    body: ClientErrorBody;
  } {
    if (!(exception instanceof HttpException)) {
      return {
        statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
        body: this.buildBody(ErrorCode.INTERNAL_ERROR, HttpStatus.INTERNAL_SERVER_ERROR),
      };
    }

    const statusCode = exception.getStatus();
    const rawResponse = exception.getResponse();

    if (exception instanceof AppException || this.isAppErrorResponse(rawResponse)) {
      const appError = rawResponse as AppErrorResponse;
      return {
        statusCode,
        body: this.buildBody(appError.code, statusCode, appError.details),
      };
    }

    const code = this.resolveFallbackCode(rawResponse, statusCode);
    return {
      statusCode,
      body: this.buildBody(code, statusCode),
    };
  }

  private isAppErrorResponse(value: unknown): value is AppErrorResponse {
    return (
      typeof value === 'object' &&
      value !== null &&
      'code' in value &&
      typeof (value as AppErrorResponse).code === 'string'
    );
  }

  /** Nest/Passport leftovers: validation arrays, otherwise HTTP status. */
  private resolveFallbackCode(rawResponse: string | object, statusCode: number): ErrorCodeValue {
    const message = this.extractMessage(rawResponse);
    if (Array.isArray(message)) {
      return ErrorCode.VALIDATION_FAILED;
    }
    return STATUS_FALLBACK_CODES[statusCode] ?? ErrorCode.INTERNAL_ERROR;
  }

  private extractMessage(rawResponse: string | object): string | string[] | undefined {
    if (typeof rawResponse === 'string') {
      return rawResponse;
    }

    if (typeof rawResponse === 'object' && rawResponse !== null && 'message' in rawResponse) {
      const message = (rawResponse as { message?: string | string[] }).message;
      return message;
    }

    return undefined;
  }

  private buildBody(
    code: ErrorCodeValue,
    statusCode: number,
    details?: unknown,
  ): ClientErrorBody {
    return {
      success: false,
      error: {
        code,
        statusCode,
        ...(details !== undefined ? { details } : {}),
      },
    };
  }
}
