import { Request, Response, NextFunction } from "express"

/**
 * Interface for application errors with additional information
 */
export interface AppError extends Error {
  statusCode?: number;
  code?: string;
  isOperational?: boolean;
  details?: any;
}

/**
 * Base class for operational errors (expected errors)
 */
export class OperationalError extends Error implements AppError {
  statusCode: number
  code: string
  isOperational: boolean
  details?: any

  constructor(
    message: string,
    details?: any,
    statusCode = 500,
    code = "OPERATIONAL_ERROR"
  ) {
    super(message)
    this.name = this.constructor.name
    this.statusCode = statusCode
    this.code = code
    this.isOperational = true
    this.details = details

    // Maintains correct stack trace
    Error.captureStackTrace(this, this.constructor)
  }
}

/**
 * Validation error (400)
 */
export class ValidationError extends OperationalError {
  constructor(message: string, details?: any) {
    super(message, details, 400, "VALIDATION_ERROR")
  }
}

/**
 * Authentication error (401)
 */
export class UnauthorizedError extends OperationalError {
  constructor(message: string, details?: any) {
    super(message, details, 401, "UNAUTHORIZED")
  }
}

/**
 * Authorization error (403)
 */
export class ForbiddenError extends OperationalError {
  constructor(message: string, details?: any) {
    super(message, details, 403, "FORBIDDEN")
  }
}

/**
 * Resource not found error (404)
 */
export class NotFoundError extends OperationalError {
  constructor(message: string, details?: any) {
    super(message, details, 404, "NOT_FOUND")
  }
}

/**
 * Conflict error (409)
 */
export class ConflictError extends OperationalError {
  constructor(message: string, details?: any) {
    super(message, details, 409, "CONFLICT")
  }
}

/**
 * Internal server error (500)
 */
export class InternalServerError extends OperationalError {
  constructor(message: string, details?: any) {
    super(message, details, 500, "INTERNAL_SERVER_ERROR")
  }
}

/**
 * Determines if an error is operational (expected) or programming (unexpected)
 * @param error - The error to check
 * @returns true if the error is operational, false otherwise
 */
function isOperationalError(error: any): boolean {
  if (error instanceof OperationalError) {
    return error.isOperational === true
  }

  return false
}

/**
 * Formats the error for HTTP response
 * @param error - The error to format
 * @param isDevelopment - Whether in development mode
 * @returns Object with the formatted response
 */
function formatError(error: AppError | Error, isDevelopment = false) {
  const isOperational = isOperationalError(error)
  const appError = error as AppError

  const baseResponse: any = {
    status: "error",
    message: appError.message || "An unexpected error occurred",
    code: appError.code || "INTERNAL_ERROR",
  }

  // In development, include more details
  if (isDevelopment) {
    baseResponse.stack = appError.stack
    baseResponse.details = appError.details
  }

  // If it's an operational error, include details if they exist
  if (isOperational && appError.details) {
    baseResponse.details = appError.details
  }

  // If not operational, don't expose details in production
  if (!isOperational && !isDevelopment) {
    baseResponse.message = "An unexpected error occurred"
    baseResponse.code = "INTERNAL_ERROR"
  }

  return baseResponse
}

/**
 * Determines the appropriate HTTP status code
 * @param error - The error from which to get the status code
 * @returns The HTTP status code
 */
function getStatusCode(error: AppError | Error): number {
  if ((error as AppError).statusCode) {
    return (error as AppError).statusCode!
  }

  // Common Node.js errors
  if (error.name === "ValidationError") return 400
  if (error.name === "UnauthorizedError") return 401
  if (error.name === "ForbiddenError") return 403
  if (error.name === "NotFoundError") return 404
  if (error.name === "ConflictError") return 409

  // Default to server error
  return 500
}

/**
 * Logs the error in a structured format
 * @param error - The error to log
 * @param req - Express Request object
 * @returns void
 */
function logError(error: AppError | Error, req: Request): void {
  const appError = error as AppError
  const isOperational = isOperationalError(error)

  const logData = {
    timestamp: new Date().toISOString(),
    method: req.method,
    url: req.originalUrl,
    statusCode: getStatusCode(error),
    error: {
      name: error.name,
      message: error.message,
      code: appError.code,
      isOperational,
      stack: error.stack,
      details: appError.details,
    },
    ip: req.ip || req.socket.remoteAddress,
    userAgent: req.get("user-agent"),
  }

  // Different log based on error type
  if (isOperational) {
    console.error("⚠️  Operational Error:", JSON.stringify(logData, null, 2))
  } else {
    console.error("❌ Unexpected Error:", JSON.stringify(logData, null, 2))
  }
}

/**
 * Wrapper for async handlers that automatically catches errors
 *
 * Usage:
 * app.get('/route', asyncHandler(async (req, res) => {
 *   // your async code here
 * }))
 * @param fn - Async handler function to wrap
 * @returns Express middleware that automatically catches errors
 */
export const asyncHandler = (
  fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
) => {
  return (req: Request, res: Response, next: NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch(next)
  }
}

/**
 * Middleware to handle not found routes (404)
 * Must be placed after all routes but before the error handler
 * @param req - Express Request object
 * @param res - Express Response object
 * @param next - Express next function
 * @returns void
 */
export const notFoundHandler = (
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  const error = new NotFoundError(
    `Route ${req.method} ${req.originalUrl} not found`
  )
  next(error)
}

/**
 * Global error handling middleware
 * Must be the last middleware in the chain
 *
 * Catches all unhandled synchronous and asynchronous errors
 * @param error - The caught error
 * @param req - Express Request object
 * @param res - Express Response object
 * @param next - Express next function
 * @returns void
 */
export const errorHandler = (
  error: AppError | Error,
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  // If response already sent, delegate to Express default handler
  if (res.headersSent) {
    return next(error)
  }

  // Determine if we're in development mode
  const isDevelopment =
    process.env.NODE_ENV === "development" || process.env.DEBUG === "true"

  // Log the error
  logError(error, req)

  // Get status code
  const statusCode = getStatusCode(error)

  // Format response
  const response = formatError(error, isDevelopment)

  // Send response
  res.status(statusCode).json(response)
}

/**
 * Helper to quickly create operational errors
 * @param message - Error message
 * @param details - Additional error details
 * @param statusCode - HTTP status code
 * @param code - Custom error code
 * @returns New OperationalError instance
 */
export const createError = (
  message: string,
  details?: any,
  statusCode?: number,
  code?: string
): OperationalError => {
  return new OperationalError(
    message,
    details,
    statusCode || 500,
    code || "OPERATIONAL_ERROR"
  )
}
