import { Request, Response, NextFunction } from "express"
import * as Sentry from "@sentry/node"
import { logger, LogContext } from "../utils/logger.js"
import { AppErrorAction } from "./app-error-action.js"
import { AppError, getErrorMessage } from "./app-error.js"
import { AppException, DownstreamException } from "./app-exception.js"
import { BaseException } from "./base-exception.js"

/**
 * Error response DTO
 */
export interface ErrorDTO {
  code: string
  errorType: string
  message: string
  title: string
  referenceDoc?: string | null
  downstreamErrorMessage?: string | null
  downstreamErrorCode?: string | null
}

/**
 * API Response wrapper
 */
export interface ResponseDTO<T> {
  status: number
  data: T
  requestId?: string
  timestamp: string
}

/**
 * Global exception handler for Express
 * Similar to Java GlobalExceptionHandler with @ControllerAdvice
 */
export class GlobalExceptionHandler {
  /**
   * Log error to console and optionally to Sentry
   */
  private static doLog(error: Error, context: LogContext): void {
    // Configure Sentry tags
    Sentry.setExtra("Stack Trace", error.stack ?? "No stack trace available")
    Sentry.setTag("source", "cf-api")

    if (context.requestId) {
      Sentry.setTag("requestId", context.requestId)
    }
    if (context.endpoint) {
      Sentry.setTag("endpoint", context.endpoint)
    }
    if (context.userId) {
      Sentry.setUser({ id: context.userId })
    }

    // Check if we should log externally
    if (error instanceof BaseException) {
      const baseError = error as BaseException
      const shouldLogExternally = baseError.getErrorAction() === AppErrorAction.LOG_EXTERNALLY
      const forceLogToSentry = baseError.getShouldLogToSentry()

      if (shouldLogExternally || forceLogToSentry) {
        // Add context map as tags
        const contextMap = baseError.getContextMap()
        Object.entries(contextMap).forEach(([key, value]) => {
          if (value !== undefined && key !== "shouldLogToSentry") {
            Sentry.setTag(key, String(value))
          }
        })
        Sentry.captureException(error)
      }
    } else {
      // For non-BaseException errors, always log to Sentry
      Sentry.captureException(error)
    }
  }

  /**
   * Main error handling middleware
   * Should be registered as the last middleware in the Express app
   */
  static handle(error: Error, req: Request, res: Response, next: NextFunction): void {
    // If response was already sent, delegate to default Express error handler
    if (res.headersSent) {
      return next(error)
    }

    const errorContext: LogContext = {
      requestId: req.requestId,
      traceId: req.traceId,
      userId: (req as any).userId,
      endpoint: req.path,
      operation: "error_handling",
    }

    // Handle different types of exceptions
    if (error instanceof AppException) {
      GlobalExceptionHandler.handleAppException(error, req, res, errorContext)
    } else if (error instanceof DownstreamException) {
      GlobalExceptionHandler.handleDownstreamException(error, req, res, errorContext)
    } else if (error instanceof BaseException) {
      GlobalExceptionHandler.handleBaseException(error, req, res, errorContext)
    } else if (GlobalExceptionHandler.isServerWebInputException(error)) {
      GlobalExceptionHandler.handleServerWebInputException(error, req, res, errorContext)
    } else {
      GlobalExceptionHandler.handleUnknownException(error, req, res, errorContext)
    }
  }

  /**
   * Handle AppException (custom application errors)
   */
  private static handleAppException(
    e: AppException,
    req: Request,
    res: Response,
    context: LogContext,
  ): void {
    const status = e.getHttpStatus()

    GlobalExceptionHandler.doLog(e, context)

    logger.error(`AppException: ${e.message}`, context, {
      errorCode: e.getAppErrorCode(),
      errorType: e.getErrorType(),
      statusCode: status,
      title: e.getTitle(),
    })

    const response = GlobalExceptionHandler.createResponse<ErrorDTO>(
      status,
      {
        code: e.getAppErrorCode(),
        errorType: e.getErrorType(),
        message: e.getMessage(),
        title: e.getTitle(),
        referenceDoc: e.getReferenceDoc(),
      },
      req.requestId,
    )

    res.status(status).json(response)
  }

  /**
   * Handle DownstreamException (errors from external services)
   */
  private static handleDownstreamException(
    e: DownstreamException,
    req: Request,
    res: Response,
    context: LogContext,
  ): void {
    const status = e.getHttpStatus()

    GlobalExceptionHandler.doLog(e, context)

    logger.error(`DownstreamException: ${e.message}`, context, {
      errorCode: e.getAppErrorCode(),
      errorType: e.getErrorType(),
      statusCode: status,
      downstreamMessage: e.getDownstreamErrorMessage(),
      downstreamCode: e.getDownstreamErrorCode(),
      service: e.getServiceName(),
    })

    const response = GlobalExceptionHandler.createResponse<ErrorDTO>(
      status,
      {
        code: e.getAppErrorCode(),
        errorType: e.getErrorType(),
        message: e.message,
        title: e.getTitle(),
        downstreamErrorMessage: e.getDownstreamErrorMessage(),
        downstreamErrorCode: e.getDownstreamErrorCode(),
      },
      req.requestId,
    )

    res.status(status).json(response)
  }

  /**
   * Handle generic BaseException
   */
  private static handleBaseException(
    e: BaseException,
    req: Request,
    res: Response,
    context: LogContext,
  ): void {
    const status = e.getHttpStatus()

    GlobalExceptionHandler.doLog(e, context)

    logger.error(`BaseException: ${e.message}`, context, {
      errorCode: e.getAppErrorCode(),
      errorType: e.getErrorType(),
      statusCode: status,
    })

    const response = GlobalExceptionHandler.createResponse<ErrorDTO>(
      status,
      {
        code: e.getAppErrorCode(),
        errorType: e.getErrorType(),
        message: e.message,
        title: e.getTitle(),
      },
      req.requestId,
    )

    res.status(status).json(response)
  }

  /**
   * Check if error is similar to ServerWebInputException (Express body parser errors)
   */
  private static isServerWebInputException(error: Error): boolean {
    return (
      error.name === "SyntaxError" ||
      error.name === "PayloadTooLargeError" ||
      (error as any).type === "entity.parse.failed" ||
      (error as any).type === "entity.too.large"
    )
  }

  /**
   * Handle server input exceptions (malformed JSON, etc.)
   */
  private static handleServerWebInputException(
    e: Error,
    req: Request,
    res: Response,
    context: LogContext,
  ): void {
    const appError = AppError.GENERIC_BAD_REQUEST
    const status = appError.httpErrorCode

    GlobalExceptionHandler.doLog(e, context)

    let errorMessage = e.message
    if ((e as any).body) {
      errorMessage = `Malformed request body: ${e.message}`
    }

    logger.warn(`ServerWebInputException: ${errorMessage}`, context, {
      errorCode: appError.appErrorCode,
      errorType: appError.errorType,
      statusCode: status,
    })

    const response = GlobalExceptionHandler.createResponse<ErrorDTO>(
      status,
      {
        code: appError.appErrorCode,
        errorType: appError.errorType,
        message: getErrorMessage(appError, errorMessage),
        title: appError.title,
      },
      req.requestId,
    )

    res.status(status).json(response)
  }

  /**
   * Handle unknown/unexpected exceptions
   */
  private static handleUnknownException(
    e: Error,
    req: Request,
    res: Response,
    context: LogContext,
  ): void {
    const appError = AppError.GENERIC_INTERNAL_SERVER_ERROR
    const status = appError.httpErrorCode

    GlobalExceptionHandler.doLog(e, context)

    logger.errorWithSentry(`UnknownException: ${e.message}`, context, {
      errorCode: appError.appErrorCode,
      errorType: appError.errorType,
      statusCode: status,
      stack: e.stack,
    })

    // In production, hide internal error details
    const message =
      process.env.NODE_ENV === "production"
        ? getErrorMessage(appError, "An unexpected error occurred")
        : getErrorMessage(appError, e.message)

    const response = GlobalExceptionHandler.createResponse<ErrorDTO>(
      status,
      {
        code: appError.appErrorCode,
        errorType: appError.errorType,
        message,
        title: appError.title,
      },
      req.requestId,
    )

    res.status(status).json(response)
  }

  /**
   * Create a standard response DTO
   */
  private static createResponse<T>(status: number, data: T, requestId?: string): ResponseDTO<T> {
    return {
      status,
      data,
      requestId,
      timestamp: new Date().toISOString(),
    }
  }

  /**
   * Handle unhandled promise rejections
   *
   * NOTE: We intentionally do NOT exit the process here. Unhandled promise rejections
   * typically don't corrupt process state, and the event loop continues normally.
   * With proper async route handlers (postAsync), this should rarely be triggered.
   * We log to Sentry for monitoring but keep the service running.
   */
  static async handleUnhandledRejection(reason: unknown, promise: Promise<unknown>): Promise<void> {
    const context: LogContext = {
      operation: "unhandled_rejection",
    }

    const error = reason instanceof Error ? reason : new Error(String(reason))

    logger.errorWithSentry(
      "Unhandled Promise Rejection",
      context,
      {
        reason: String(reason),
        promise: String(promise),
        isOperational: false,
      },
      error,
    )

    // Do NOT exit the process - unhandled rejections don't corrupt process state
    // The error has been logged to Sentry for monitoring
  }

  /**
   * Handle uncaught exceptions
   *
   * NOTE: We intentionally do NOT exit the process here either. While traditional
   * guidance suggests exiting on uncaught exceptions, modern Node.js error handling
   * with proper try-catch and async handlers makes this less necessary.
   * We log to Sentry and let the service continue running.
   */
  static async handleUncaughtException(error: Error): Promise<void> {
    const context: LogContext = {
      operation: "uncaught_exception",
    }

    logger.errorWithSentry(
      "Uncaught Exception",
      context,
      {
        stack: error.stack,
        isOperational: false,
      },
      error,
    )

    // Do NOT exit the process - log to Sentry and continue running
    // This keeps the service available even when unexpected errors occur
  }
}
