/**
 * Unified Exception Filter
 *
 * Single exception filter that handles all HTTP exceptions consistently.
 * Uses the shared error codes and messages from the domain package.
 */

import { Catch, HttpException, HttpStatus, Inject } from "@nestjs/common";
import type { ExceptionFilter, ArgumentsHost } from "@nestjs/common";
import type { Request, Response } from "express";
import { Logger } from "nestjs-pino";
import { ConfigService } from "@nestjs/config";
import {
  ErrorCode,
  ErrorMessages,
  ErrorMetadata,
  type ErrorCodeType,
  type ApiError,
} from "@customer-portal/domain/common";
import { generateRequestId } from "@bff/core/logging/request-id.util.js";

function mapHttpStatusToErrorCode(status?: number): ErrorCodeType {
  if (!status) return ErrorCode.UNKNOWN;

  switch (status) {
    case 401:
      return ErrorCode.SESSION_EXPIRED;
    case 403:
      return ErrorCode.FORBIDDEN;
    case 404:
      return ErrorCode.NOT_FOUND;
    case 409:
      return ErrorCode.ACCOUNT_EXISTS;
    case 400:
      return ErrorCode.VALIDATION_FAILED;
    case 429:
      return ErrorCode.RATE_LIMITED;
    case 503:
      return ErrorCode.SERVICE_UNAVAILABLE;
    default:
      return status >= 500 ? ErrorCode.INTERNAL_ERROR : ErrorCode.UNKNOWN;
  }
}

/**
 * Request context for error logging
 */
interface ErrorContext {
  requestId: string;
  userId?: string | undefined;
  method: string;
  path: string;
  userAgent?: string | undefined;
  ip?: string | undefined;
}

/**
 * Unified exception filter for all HTTP errors.
 * Provides consistent error responses and secure logging.
 */
@Catch()
export class UnifiedExceptionFilter implements ExceptionFilter {
  private readonly isDevelopment: boolean;

  constructor(
    @Inject(Logger) private readonly logger: Logger,
    private readonly configService: ConfigService
  ) {
    this.isDevelopment = this.configService.get("NODE_ENV") !== "production";
  }

  catch(exception: unknown, host: ArgumentsHost): void {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request & { user?: { id?: string }; requestId?: string }>();

    // Build error context for logging
    const errorContext = this.buildErrorContext(request);

    // Extract status code and error details
    const { status, errorCode, originalMessage } = this.extractErrorDetails(exception);

    // Get user-friendly message (with dev details if in development)
    const userMessage = this.getUserMessage(errorCode, originalMessage);

    // Log the error
    this.logError(errorCode, originalMessage, status, errorContext, exception);

    // Build and send response
    const errorResponse = this.buildErrorResponse(errorCode, userMessage, status, errorContext);
    response.status(status).json(errorResponse);
  }

  /**
   * Extract error details from exception
   */
  private extractErrorDetails(exception: unknown): {
    status: number;
    errorCode: ErrorCodeType;
    originalMessage: string;
  } {
    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let originalMessage = "An unexpected error occurred";
    let explicitCode: ErrorCodeType | undefined;

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const extracted = this.extractExceptionDetails(exception);
      originalMessage = extracted.message;
      explicitCode = extracted.code;
    } else if (exception instanceof Error) {
      originalMessage = exception.message;
    }

    // Map to error code
    const errorCode = explicitCode ?? mapHttpStatusToErrorCode(status);

    return { status, errorCode, originalMessage };
  }

  /**
   * Extract message (and optionally an explicit error code) from HttpException.
   */
  private extractExceptionDetails(exception: HttpException): {
    message: string;
    code?: ErrorCodeType | undefined;
  } {
    const response = exception.getResponse();

    if (typeof response === "string") {
      return { message: response };
    }

    if (typeof response === "object" && response !== null) {
      const responseObj = response as Record<string, unknown>;
      const code = this.extractExplicitCode(responseObj);

      // Handle NestJS validation errors (array of messages)
      const messageField = responseObj["message"];
      if (Array.isArray(messageField)) {
        const firstMessage = messageField.find((m): m is string => typeof m === "string");
        if (firstMessage) return { message: firstMessage, code };
      }

      // Handle standard message field
      if (typeof messageField === "string") {
        return { message: messageField, code };
      }

      // Handle error field
      const errorField = responseObj["error"];
      if (typeof errorField === "string") {
        return { message: errorField, code };
      }

      return { message: exception.message, code };
    }

    return { message: exception.message };
  }

  /**
   * Extract explicit error code from HttpException response bodies.
   */
  private extractExplicitCode(responseObj: Record<string, unknown>): ErrorCodeType | undefined {
    // 1) Preferred: { code: "AUTH_003" }
    const codeField = responseObj["code"];
    if (typeof codeField === "string" && this.isKnownErrorCode(codeField)) {
      return codeField as ErrorCodeType;
    }

    // 2) Standard API error format: { error: { code: "AUTH_003" } }
    const maybeError = responseObj["error"];
    if (maybeError && typeof maybeError === "object") {
      const errorObj = maybeError as Record<string, unknown>;
      const errorCode = errorObj["code"];
      if (typeof errorCode === "string" && this.isKnownErrorCode(errorCode)) {
        return errorCode as ErrorCodeType;
      }
    }

    return undefined;
  }

  private isKnownErrorCode(value: string): boolean {
    // ErrorMessages is keyed by ErrorCodeType
    return Object.prototype.hasOwnProperty.call(ErrorMessages, value);
  }

  /**
   * Get user-friendly message, with dev details in development mode
   */
  private getUserMessage(errorCode: ErrorCodeType, originalMessage: string): string {
    const userMessage = ErrorMessages[errorCode] ?? ErrorMessages[ErrorCode.UNKNOWN];

    if (this.isDevelopment && originalMessage !== userMessage) {
      // In dev mode, append original message for debugging
      const sanitized = this.sanitizeForDev(originalMessage);
      if (sanitized && sanitized !== userMessage) {
        return `${userMessage} (Dev: ${sanitized})`;
      }
    }

    return userMessage;
  }

  /**
   * Sanitize message for development display
   */
  private sanitizeForDev(message: string): string {
    return message
      .replace(/password[=:]\s*[^\s]+/gi, "password=[HIDDEN]")
      .replace(/secret[=:]\s*[^\s]+/gi, "secret=[HIDDEN]")
      .replace(/token[=:]\s*[^\s]+/gi, "token=[HIDDEN]")
      .replace(/key[=:]\s*[^\s]+/gi, "key=[HIDDEN]")
      .slice(0, 200); // Limit length
  }

  /**
   * Build error context from request
   */
  private buildErrorContext(
    request: Request & { user?: { id?: string }; requestId?: string }
  ): ErrorContext {
    const userAgentHeader = request.headers["user-agent"];

    return {
      requestId: request.requestId ?? this.generateRequestId(),
      userId: request.user?.id,
      method: request.method,
      path: request.url,
      userAgent:
        typeof userAgentHeader === "string"
          ? userAgentHeader
          : Array.isArray(userAgentHeader)
            ? userAgentHeader[0]
            : undefined,
      ip: request.ip,
    };
  }

  /**
   * Log error with appropriate level based on metadata
   */
  private logError(
    errorCode: ErrorCodeType,
    originalMessage: string,
    status: number,
    context: ErrorContext,
    exception: unknown
  ): void {
    const metadata = ErrorMetadata[errorCode] ?? ErrorMetadata[ErrorCode.UNKNOWN];

    const logData = {
      errorCode,
      category: metadata.category,
      severity: metadata.severity,
      statusCode: status,
      originalMessage: this.sanitizeForLogging(originalMessage),
      ...context,
      stack: exception instanceof Error ? exception.stack : undefined,
    };

    // Log based on severity
    switch (metadata.logLevel) {
      case "error":
        this.logger.error(`HTTP ${status} Error [${errorCode}]`, logData);
        break;
      case "warn":
        this.logger.warn(`HTTP ${status} Warning [${errorCode}]`, logData);
        break;
      case "info":
        this.logger.log(`HTTP ${status} Info [${errorCode}]`, logData);
        break;
      case "debug":
        this.logger.debug(`HTTP ${status} Debug [${errorCode}]`, logData);
        break;
    }
  }

  /**
   * Sanitize message for logging (remove sensitive info)
   */
  private sanitizeForLogging(message: string): string {
    return message
      .replace(/password[=:]\s*[^\s]+/gi, "password=[REDACTED]")
      .replace(/secret[=:]\s*[^\s]+/gi, "secret=[REDACTED]")
      .replace(/token[=:]\s*[^\s]+/gi, "token=[REDACTED]")
      .replace(/\b[A-Za-z0-9]{32,}\b/g, "[TOKEN]")
      .replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "[IP]");
  }

  /**
   * Build standard error response
   */
  private buildErrorResponse(
    errorCode: ErrorCodeType,
    message: string,
    status: number,
    context: ErrorContext
  ): ApiError {
    const metadata = ErrorMetadata[errorCode] ?? ErrorMetadata[ErrorCode.UNKNOWN];

    return {
      success: false,
      error: {
        code: errorCode,
        message,
        details: {
          statusCode: status,
          category: metadata.category,
          timestamp: new Date().toISOString(),
          path: context.path,
          requestId: context.requestId,
        },
      },
    };
  }

  /**
   * Generate unique request ID
   */
  private generateRequestId(): string {
    return generateRequestId("req");
  }
}
