import {
  type ArgumentsHost,
  Catch,
  type ExceptionFilter,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { type Request, type Response } from 'express';
import { DomainException, type ErrorResponseBody } from '../../domain/domain-exception';
import { ErrorCode } from '../../domain/error-codes';
import { LoggerService } from '../logger.service';

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

  catch(exception: unknown, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const correlationId = (request.headers['x-correlation-id'] as string) ?? undefined;

    const errorResponse = this.buildErrorResponse(exception, correlationId);

    this.logger.error(
      `[${errorResponse.errorCode}] ${errorResponse.message}`,
      exception instanceof Error ? exception.stack : undefined,
      'GlobalExceptionFilter',
    );

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

  private buildErrorResponse(exception: unknown, correlationId?: string): ErrorResponseBody {
    if (exception instanceof DomainException) {
      return {
        statusCode: exception.getStatus(),
        errorCode: exception.errorCode,
        message: exception.message,
        details: exception.details,
        correlationId,
        timestamp: new Date().toISOString(),
      };
    }

    if (exception instanceof HttpException) {
      const status = exception.getStatus();
      const exceptionResponse = exception.getResponse();
      const message =
        typeof exceptionResponse === 'string'
          ? exceptionResponse
          : (exceptionResponse as Record<string, unknown>)['message'] ?? exception.message;

      return {
        statusCode: status,
        errorCode: this.httpStatusToErrorCode(status),
        message: Array.isArray(message) ? message.join('; ') : String(message),
        correlationId,
        timestamp: new Date().toISOString(),
      };
    }

    // Handle Prisma-specific errors to avoid leaking 500s for known DB conditions
    if (exception instanceof Prisma.PrismaClientKnownRequestError) {
      return this.buildPrismaKnownErrorResponse(exception, correlationId);
    }

    if (exception instanceof Prisma.PrismaClientValidationError) {
      return {
        statusCode: HttpStatus.BAD_REQUEST,
        errorCode: ErrorCode.BAD_REQUEST,
        message: 'Invalid request parameters',
        correlationId,
        timestamp: new Date().toISOString(),
      };
    }

    return {
      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
      errorCode: ErrorCode.INTERNAL_ERROR,
      message: 'Internal server error',
      correlationId,
      timestamp: new Date().toISOString(),
    };
  }

  private buildPrismaKnownErrorResponse(
    exception: Prisma.PrismaClientKnownRequestError,
    correlationId?: string,
  ): ErrorResponseBody {
    switch (exception.code) {
      // P2025: Record not found (update/delete on non-existent row)
      case 'P2025':
        return {
          statusCode: HttpStatus.NOT_FOUND,
          errorCode: ErrorCode.NOT_FOUND,
          message: 'The requested resource was not found',
          correlationId,
          timestamp: new Date().toISOString(),
        };

      // P2002: Unique constraint violation
      case 'P2002':
        return {
          statusCode: HttpStatus.CONFLICT,
          errorCode: ErrorCode.CONFLICT,
          message: 'A record with the given identifier already exists',
          correlationId,
          timestamp: new Date().toISOString(),
        };

      // P2003: Foreign key constraint violation
      case 'P2003':
        return {
          statusCode: HttpStatus.BAD_REQUEST,
          errorCode: ErrorCode.BAD_REQUEST,
          message: 'Referenced record does not exist',
          correlationId,
          timestamp: new Date().toISOString(),
        };

      default:
        return {
          statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
          errorCode: ErrorCode.INTERNAL_ERROR,
          message: 'Internal server error',
          correlationId,
          timestamp: new Date().toISOString(),
        };
    }
  }

  private httpStatusToErrorCode(status: number): ErrorCode {
    const map: Record<number, ErrorCode> = {
      [HttpStatus.BAD_REQUEST]: ErrorCode.BAD_REQUEST,
      [HttpStatus.UNAUTHORIZED]: ErrorCode.UNAUTHORIZED,
      [HttpStatus.FORBIDDEN]: ErrorCode.FORBIDDEN,
      [HttpStatus.NOT_FOUND]: ErrorCode.NOT_FOUND,
      [HttpStatus.CONFLICT]: ErrorCode.CONFLICT,
      [HttpStatus.TOO_MANY_REQUESTS]: ErrorCode.TOO_MANY_REQUESTS,
    };
    return map[status] ?? ErrorCode.INTERNAL_ERROR;
  }
}
