/**
 * Error Handling Middleware
 *
 * Centralized error handling with custom error classes and logging
 */

import { Request, Response, NextFunction } from 'express';
import { ErrorResponse, ValidationError as ValidationErrorType } from '../types';
import { logger, logError } from '../utils/logger';
import { trackError } from '../utils/metrics';
import { isProduction } from '../config';

/**
 * Base API Error class
 */
export class ApiError extends Error {
  public statusCode: number;
  public code: string;
  public details?: Record<string, unknown>;
  public isOperational: boolean;

  constructor(
    statusCode: number,
    code: string,
    message: string,
    details?: Record<string, unknown>,
    isOperational = true
  ) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.details = details;
    this.isOperational = isOperational;

    // Maintains proper stack trace for where error was thrown
    Error.captureStackTrace(this, this.constructor);

    // Set the prototype explicitly
    Object.setPrototypeOf(this, ApiError.prototype);
  }
}

/**
 * Validation Error class
 */
export class ValidationError extends ApiError {
  public errors: ValidationErrorType[];

  constructor(errors: ValidationErrorType[], message = 'Validation failed') {
    super(400, 'VALIDATION_ERROR', message, { errors });
    this.errors = errors;
    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

/**
 * Authentication Error class
 */
export class AuthenticationError extends ApiError {
  constructor(message = 'Authentication failed', code = 'AUTHENTICATION_FAILED') {
    super(401, code, message);
    Object.setPrototypeOf(this, AuthenticationError.prototype);
  }
}

/**
 * Authorization Error class
 */
export class AuthorizationError extends ApiError {
  constructor(message = 'Access denied', code = 'ACCESS_DENIED') {
    super(403, code, message);
    Object.setPrototypeOf(this, AuthorizationError.prototype);
  }
}

/**
 * Insufficient Credits Error class
 */
export class InsufficientCreditsError extends ApiError {
  constructor(
    required: number,
    available: number,
    message = 'Insufficient credits to complete this request'
  ) {
    super(402, 'INSUFFICIENT_CREDITS', message, {
      required,
      available,
      deficit: required - available,
    });
    Object.setPrototypeOf(this, InsufficientCreditsError.prototype);
  }
}

/**
 * Not Found Error class
 */
export class NotFoundError extends ApiError {
  constructor(resource = 'Resource', message?: string) {
    super(404, 'NOT_FOUND', message || `${resource} not found`);
    Object.setPrototypeOf(this, NotFoundError.prototype);
  }
}

/**
 * Conflict Error class
 */
export class ConflictError extends ApiError {
  constructor(message = 'Resource conflict', code = 'CONFLICT') {
    super(409, code, message);
    Object.setPrototypeOf(this, ConflictError.prototype);
  }
}

/**
 * Rate Limit Error class
 */
export class RateLimitError extends ApiError {
  constructor(retryAfter?: number, message = 'Rate limit exceeded') {
    super(429, 'RATE_LIMIT_EXCEEDED', message, { retryAfter });
    Object.setPrototypeOf(this, RateLimitError.prototype);
  }
}

/**
 * Internal Server Error class
 */
export class InternalServerError extends ApiError {
  constructor(message = 'Internal server error', code = 'INTERNAL_ERROR', details?: Record<string, unknown>) {
    super(500, code, message, details, false); // Not operational
    Object.setPrototypeOf(this, InternalServerError.prototype);
  }
}

/**
 * Service Unavailable Error class
 */
export class ServiceUnavailableError extends ApiError {
  constructor(message = 'Service temporarily unavailable', code = 'SERVICE_UNAVAILABLE') {
    super(503, code, message);
    Object.setPrototypeOf(this, ServiceUnavailableError.prototype);
  }
}

/**
 * Format error response according to ErrorResponse type
 */
function formatErrorResponse(error: ApiError, requestId?: string): ErrorResponse {
  const response: ErrorResponse = {
    error: true,
    code: error.code,
    message: error.message,
    timestamp: new Date().toISOString(),
  };

  if (error.details && Object.keys(error.details).length > 0) {
    response.details = error.details;
  }

  if (requestId) {
    response.requestId = requestId;
  }

  return response;
}

/**
 * Error handler middleware
 *
 * This should be the last middleware in the chain
 */
export function errorHandler(
  error: Error | ApiError,
  req: Request,
  res: Response,
  _next: NextFunction
): void {
  // Log error
  if (error instanceof ApiError) {
    if (error.statusCode >= 500) {
      logError(error, {
        requestId: req.requestId,
        userId: req.user?.userId,
        apiKeyId: req.apiKey?.id,
        path: req.path,
        method: req.method,
      });
    } else {
      logger.warn('API error', {
        code: error.code,
        message: error.message,
        statusCode: error.statusCode,
        requestId: req.requestId,
        userId: req.user?.userId,
        path: req.path,
      });
    }
  } else {
    // Unhandled error
    logError(error, {
      requestId: req.requestId,
      userId: req.user?.userId,
      apiKeyId: req.apiKey?.id,
      path: req.path,
      method: req.method,
    });
  }

  // Track error in metrics
  if (error instanceof ApiError) {
    trackError(error.constructor.name, error.code);
  } else {
    trackError('UnhandledError', 'UNHANDLED_ERROR');
  }

  // Send error response
  if (error instanceof ApiError) {
    const errorResponse = formatErrorResponse(error, req.requestId);
    res.status(error.statusCode).json(errorResponse);
  } else {
    // Unhandled error - return generic 500 error
    const errorResponse: ErrorResponse = {
      error: true,
      code: 'INTERNAL_ERROR',
      message: isProduction() ? 'An unexpected error occurred' : error.message,
      timestamp: new Date().toISOString(),
    };

    if (req.requestId) {
      errorResponse.requestId = req.requestId;
    }

    // Include stack trace in development
    if (!isProduction() && error.stack) {
      errorResponse.details = {
        stack: error.stack.split('\n'),
      };
    }

    res.status(500).json(errorResponse);
  }
}

/**
 * 404 Not Found handler
 */
export function notFoundHandler(req: Request, res: Response): void {
  const error = new NotFoundError('Endpoint', `Endpoint ${req.method} ${req.path} not found`);
  const errorResponse = formatErrorResponse(error, req.requestId);
  res.status(404).json(errorResponse);
}

/**
 * Async handler wrapper to catch errors in async route handlers
 */
export function asyncHandler(
  fn: (_req: Request, _res: Response, _next: NextFunction) => Promise<void>
) {
  return (req: Request, res: Response, next: NextFunction): void => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}
