import { PlatformError } from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import { ErrorCategory, ErrorRecoverability, ErrorSeverity } from './error.codes.js';
import { logger } from '../logger.js';

/**
 * Error handler configuration
 */
export interface IErrorHandlerConfig {
  /** Whether to log errors automatically */
  logErrors: boolean;
  
  /** Whether to include stack traces in prod environment */
  includeStacksInProd: boolean;
  
  /** Default retry options */
  retry: {
    /** Maximum retry attempts */
    maxAttempts: number;
    
    /** Base delay between retries in ms */
    baseDelay: number;
    
    /** Maximum delay between retries in ms */
    maxDelay: number;
    
    /** Backoff factor for exponential backoff */
    backoffFactor: number;
  };
}

/**
 * Global error handler configuration
 */
const config: IErrorHandlerConfig = {
  logErrors: true,
  includeStacksInProd: false,
  retry: {
    maxAttempts: 3,
    baseDelay: 1000,
    maxDelay: 30000,
    backoffFactor: 2
  }
};

/**
 * Error handler utility
 * Provides methods for consistent error handling across the platform
 */
export class ErrorHandler {
  /**
   * Current configuration
   */
  public static config = config;
  
  /**
   * Update error handler configuration
   * 
   * @param newConfig New configuration (partial)
   */
  public static configure(newConfig: Partial<IErrorHandlerConfig>): void {
    ErrorHandler.config = {
      ...ErrorHandler.config,
      ...newConfig,
      retry: {
        ...ErrorHandler.config.retry,
        ...(newConfig.retry || {})
      }
    };
  }

  /**
   * Convert any error to a PlatformError
   * 
   * @param error Error to convert
   * @param defaultCode Default error code if not a PlatformError
   * @param context Additional context
   * @returns PlatformError instance
   */
  public static toPlatformError(
    error: any,
    defaultCode: string,
    context: IErrorContext = {}
  ): PlatformError {
    // If already a PlatformError, just add context
    if (error instanceof PlatformError) {
      // Add context if provided
      if (Object.keys(context).length > 0) {
        return new (error.constructor as typeof PlatformError)(
          error.message,
          error.code,
          error.severity,
          error.category,
          error.recoverability,
          {
            ...error.context,
            ...context,
            data: {
              ...(error.context.data || {}),
              ...(context.data || {})
            }
          }
        );
      }
      return error;
    }
    
    // Convert standard Error to PlatformError
    if (error instanceof Error) {
      return new PlatformError(
        error.message,
        defaultCode,
        ErrorSeverity.MEDIUM,
        ErrorCategory.OPERATION,
        ErrorRecoverability.NON_RECOVERABLE,
        {
          ...context,
          data: {
            ...(context.data || {}),
            originalError: {
              name: error.name,
              message: error.message,
              stack: error.stack
            }
          }
        }
      );
    }
    
    // Not an Error instance
    return new PlatformError(
      typeof error === 'string' ? error : 'Unknown error',
      defaultCode,
      ErrorSeverity.MEDIUM,
      ErrorCategory.OPERATION,
      ErrorRecoverability.NON_RECOVERABLE,
      context
    );
  }

  /**
   * Format an error for API responses
   * Sanitizes errors for safe external exposure
   * 
   * @param error Error to format
   * @param includeDetails Whether to include detailed information
   * @returns Formatted error object
   */
  public static formatErrorForResponse(
    error: any,
    includeDetails: boolean = false
  ): Record<string, any> {
    const platformError = ErrorHandler.toPlatformError(
      error, 
      'PLATFORM_OPERATION_ERROR'
    );
    
    // Basic error information
    const responseError: Record<string, any> = {
      code: platformError.code,
      message: platformError.getUserMessage(),
      requestId: platformError.context.requestId
    };
    
    // Include more details if requested
    if (includeDetails) {
      responseError.details = {
        severity: platformError.severity,
        category: platformError.category,
        rawMessage: platformError.message,
        data: platformError.context.data
      };
      
      // Only include stack trace in non-production or if explicitly enabled
      if (process.env.NODE_ENV !== 'production' || ErrorHandler.config.includeStacksInProd) {
        responseError.details.stack = platformError.stack;
      }
    }
    
    return responseError;
  }

  /**
   * Handle an error with consistent logging and formatting
   * 
   * @param error Error to handle
   * @param defaultCode Default error code if not a PlatformError
   * @param context Additional context
   * @returns Formatted error for response
   */
  public static handleError(
    error: any,
    defaultCode: string,
    context: IErrorContext = {}
  ): Record<string, any> {
    const platformError = ErrorHandler.toPlatformError(
      error,
      defaultCode,
      context
    );
    
    // Log the error if enabled
    if (ErrorHandler.config.logErrors) {
      logger.error(platformError.message, {
        error_code: platformError.code,
        error_name: platformError.name,
        error_severity: platformError.severity,
        error_category: platformError.category,
        error_recoverability: platformError.recoverability,
        ...platformError.context,
        stack: platformError.stack
      });
    }
    
    // Return formatted error for response
    const isDetailedMode = process.env.NODE_ENV !== 'production';
    return ErrorHandler.formatErrorForResponse(platformError, isDetailedMode);
  }

  /**
   * Execute a function with error handling
   * 
   * @param fn Function to execute
   * @param defaultCode Default error code if the function throws
   * @param context Additional context
   * @returns Function result or error
   */
  public static async execute<T>(
    fn: () => Promise<T>,
    defaultCode: string,
    context: IErrorContext = {}
  ): Promise<T> {
    try {
      return await fn();
    } catch (error) {
      throw ErrorHandler.toPlatformError(error, defaultCode, context);
    }
  }

  /**
   * Execute a function with retries and exponential backoff
   * 
   * @param fn Function to execute
   * @param defaultCode Default error code if the function throws
   * @param options Retry options
   * @param context Additional context
   * @returns Function result or error after max retries
   */
  public static async executeWithRetry<T>(
    fn: () => Promise<T>,
    defaultCode: string,
    options: {
      maxAttempts?: number;
      baseDelay?: number;
      maxDelay?: number;
      backoffFactor?: number;
      retryableErrorCodes?: string[];
      retryableErrorPatterns?: RegExp[];
      onRetry?: (error: PlatformError, attempt: number, delay: number) => void;
    } = {},
    context: IErrorContext = {}
  ): Promise<T> {
    const {
      maxAttempts = ErrorHandler.config.retry.maxAttempts,
      baseDelay = ErrorHandler.config.retry.baseDelay,
      maxDelay = ErrorHandler.config.retry.maxDelay,
      backoffFactor = ErrorHandler.config.retry.backoffFactor,
      retryableErrorCodes = [],
      retryableErrorPatterns = [],
      onRetry = () => {}
    } = options;
    
    let lastError: PlatformError;
    
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      try {
        return await fn();
      } catch (error) {
        // Convert to PlatformError
        const platformError = ErrorHandler.toPlatformError(
          error, 
          defaultCode,
          {
            ...context,
            retry: {
              currentRetry: attempt,
              maxRetries: maxAttempts,
              nextRetryAt: 0 // Will be set below if retrying
            }
          }
        );
        
        lastError = platformError;
        
        // Check if we should retry
        const isLastAttempt = attempt >= maxAttempts - 1;
        
        if (isLastAttempt) {
          // No more retries
          throw platformError;
        }
        
        // Check if error is retryable
        const isRetryable = 
          // Built-in recoverability
          platformError.recoverability === ErrorRecoverability.RECOVERABLE ||
          platformError.recoverability === ErrorRecoverability.MAYBE_RECOVERABLE ||
          platformError.recoverability === ErrorRecoverability.TRANSIENT ||
          // Specifically included error codes
          retryableErrorCodes.includes(platformError.code) ||
          // Matches error message patterns
          retryableErrorPatterns.some(pattern => pattern.test(platformError.message));
        
        if (!isRetryable) {
          throw platformError;
        }
        
        // Calculate delay with exponential backoff
        const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
        
        // Add jitter to prevent thundering herd problem (±20%)
        const jitter = 0.8 + Math.random() * 0.4;
        const actualDelay = Math.floor(delay * jitter);
        
        // Update nextRetryAt in error context
        const nextRetryAt = Date.now() + actualDelay;
        platformError.context.retry!.nextRetryAt = nextRetryAt;
        
        // Log retry attempt
        logger.warn(`Retrying operation after error (attempt ${attempt + 1}/${maxAttempts}): ${platformError.message}`, {
          error_code: platformError.code,
          retry_attempt: attempt + 1,
          retry_max_attempts: maxAttempts,
          retry_delay_ms: actualDelay,
          retry_next_at: new Date(nextRetryAt).toISOString()
        });
        
        // Call onRetry callback
        onRetry(platformError, attempt + 1, actualDelay);
        
        // Wait before next retry
        await new Promise(resolve => setTimeout(resolve, actualDelay));
      }
    }
    
    // This should never happen, but TypeScript needs it
    throw lastError!;
  }
}

/**
 * Create a middleware for handling errors in HTTP requests
 * 
 * @returns Middleware function
 */
export function createErrorHandlerMiddleware() {
  return (error: any, req: any, res: any, next: any) => {
    // Add request context
    const context: IErrorContext = {
      requestId: req.headers['x-request-id'] || req.headers['x-correlation-id'],
      component: 'HttpServer',
      operation: `${req.method} ${req.url}`,
      data: {
        method: req.method,
        url: req.url,
        query: req.query,
        params: req.params,
        ip: req.ip || req.connection.remoteAddress,
        userAgent: req.headers['user-agent']
      }
    };
    
    // Handle the error
    const formattedError = ErrorHandler.handleError(
      error,
      'PLATFORM_OPERATION_ERROR',
      context
    );
    
    // Set status code based on error type
    let statusCode = 500;
    
    if (error instanceof PlatformError) {
      // Map error categories to HTTP status codes
      switch (error.category) {
        case ErrorCategory.VALIDATION:
          statusCode = 400;
          break;
        case ErrorCategory.AUTHENTICATION:
          statusCode = 401;
          break;
        case ErrorCategory.RESOURCE:
          statusCode = 429;
          break;
        case ErrorCategory.OPERATION:
          statusCode = 400;
          break;
        default:
          statusCode = 500;
      }
    } else if (error.statusCode) {
      // Use provided status code if available
      statusCode = error.statusCode;
    }
    
    // Send error response
    res.status(statusCode).json({
      success: false,
      error: formattedError
    });
  };
}