import { ProcessingError } from "@app/types/processing";

export class ProcessingErrorHandler {
  private static readonly DEFAULT_MAX_RETRIES = 3;
  private static readonly RETRY_DELAYS = [1000, 2000, 4000]; // Progressive backoff in ms

  /**
   * Create a ProcessingError from an unknown error
   */
  static createProcessingError(
    error: unknown,
    retryCount: number = 0,
    maxRetries: number = this.DEFAULT_MAX_RETRIES,
  ): ProcessingError {
    const originalError =
      error instanceof Error ? error : new Error(String(error));
    const message = originalError.message;

    // Determine error type based on error message and properties
    const errorType = this.determineErrorType(originalError, message);

    // Determine if error is recoverable
    const recoverable = this.isRecoverable(errorType, retryCount, maxRetries);

    return {
      type: errorType,
      message: this.formatErrorMessage(errorType, message),
      recoverable,
      retryCount,
      maxRetries,
      originalError,
    };
  }

  /**
   * Determine the type of error based on error characteristics
   */
  private static determineErrorType(
    error: Error,
    message: string,
  ): ProcessingError["type"] {
    const lowerMessage = message.toLowerCase();

    // Network-related errors
    if (
      lowerMessage.includes("network") ||
      lowerMessage.includes("fetch") ||
      lowerMessage.includes("connection")
    ) {
      return "network";
    }

    // Memory-related errors
    if (
      lowerMessage.includes("memory") ||
      lowerMessage.includes("quota") ||
      lowerMessage.includes("allocation") ||
      error.name === "QuotaExceededError"
    ) {
      return "memory";
    }

    // Timeout errors
    if (
      lowerMessage.includes("timeout") ||
      lowerMessage.includes("aborted") ||
      error.name === "AbortError"
    ) {
      return "timeout";
    }

    // Cancellation
    if (
      lowerMessage.includes("cancel") ||
      lowerMessage.includes("abort") ||
      error.name === "AbortError"
    ) {
      return "cancelled";
    }

    // PDF corruption/parsing errors
    if (
      lowerMessage.includes("pdf") ||
      lowerMessage.includes("parse") ||
      lowerMessage.includes("invalid") ||
      lowerMessage.includes("corrupt") ||
      lowerMessage.includes("malformed")
    ) {
      return "corruption";
    }

    // Default to parsing error
    return "parsing";
  }

  /**
   * Determine if an error is recoverable based on type and retry count
   */
  private static isRecoverable(
    errorType: ProcessingError["type"],
    retryCount: number,
    maxRetries: number,
  ): boolean {
    // Never recoverable
    if (errorType === "cancelled" || errorType === "corruption") {
      return false;
    }

    // Recoverable if we haven't exceeded retry count
    if (retryCount >= maxRetries) {
      return false;
    }

    // Memory errors are usually not recoverable
    if (errorType === "memory") {
      return retryCount < 1; // Only one retry for memory errors
    }

    // Network and timeout errors are usually recoverable
    return (
      errorType === "network" ||
      errorType === "timeout" ||
      errorType === "parsing"
    );
  }

  /**
   * Format error message for user display
   */
  private static formatErrorMessage(
    errorType: ProcessingError["type"],
    originalMessage: string,
  ): string {
    switch (errorType) {
      case "network":
        return "Network connection failed. Please check your internet connection and try again.";

      case "memory":
        return "Insufficient memory to process this file. Try closing other applications or processing a smaller file.";

      case "timeout":
        return "Processing timed out. This file may be too large or complex to process.";

      case "cancelled":
        return "Processing was cancelled by user.";

      case "corruption":
        return "This PDF file appears to be corrupted or encrypted. Please try a different file.";

      case "parsing":
        return `Failed to process PDF: ${originalMessage}`;

      default:
        return `Processing failed: ${originalMessage}`;
    }
  }

  /**
   * Execute an operation with automatic retry logic
   */
  static async executeWithRetry<T>(
    operation: () => Promise<T>,
    onError?: (error: ProcessingError) => void,
    maxRetries: number = this.DEFAULT_MAX_RETRIES,
  ): Promise<T> {
    let lastError: ProcessingError | null = null;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = this.createProcessingError(error, attempt, maxRetries);

        // Notify error handler
        if (onError) {
          onError(lastError);
        }

        // Don't retry if not recoverable
        if (!lastError.recoverable) {
          break;
        }

        // Don't retry on last attempt
        if (attempt === maxRetries) {
          break;
        }

        // Wait before retry with progressive backoff
        const delay =
          this.RETRY_DELAYS[Math.min(attempt, this.RETRY_DELAYS.length - 1)];
        await this.delay(delay);

        console.log(
          `Retrying operation (attempt ${attempt + 2}/${maxRetries + 1}) after ${delay}ms delay`,
        );
      }
    }

    // All retries exhausted
    throw lastError || new Error("Operation failed after all retries");
  }

  /**
   * Create a timeout wrapper for operations
   */
  static withTimeout<T>(
    operation: () => Promise<T>,
    timeoutMs: number,
    timeoutMessage: string = "Operation timed out",
  ): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        reject(new Error(timeoutMessage));
      }, timeoutMs);

      operation()
        .then((result) => {
          clearTimeout(timeoutId);
          resolve(result);
        })
        .catch((error) => {
          clearTimeout(timeoutId);
          reject(error);
        });
    });
  }

  /**
   * Create an AbortController that times out after specified duration
   */
  static createTimeoutController(timeoutMs: number): AbortController {
    const controller = new AbortController();

    setTimeout(() => {
      controller.abort();
    }, timeoutMs);

    return controller;
  }

  /**
   * Check if an error indicates the operation should be retried
   */
  static shouldRetry(error: ProcessingError): boolean {
    return error.recoverable && error.retryCount < error.maxRetries;
  }

  /**
   * Get user-friendly suggestions based on error type
   */
  static getErrorSuggestions(error: ProcessingError): string[] {
    switch (error.type) {
      case "network":
        return [
          "Check your internet connection",
          "Try refreshing the page",
          "Try again in a few moments",
        ];

      case "memory":
        return [
          "Close other browser tabs or applications",
          "Try processing a smaller file",
          "Restart your browser",
          "Use a device with more memory",
        ];

      case "timeout":
        return [
          "Try processing a smaller file",
          "Break large files into smaller sections",
          "Check your internet connection speed",
        ];

      case "corruption":
        return [
          "Verify the PDF file opens in other applications",
          "Try re-downloading the file",
          "Try a different PDF file",
          "Contact the file creator if it appears corrupted",
        ];

      case "parsing":
        return [
          "Verify this is a valid PDF file",
          "Try a different PDF file",
          "Contact support if the problem persists",
        ];

      default:
        return [
          "Try refreshing the page",
          "Try again in a few moments",
          "Contact support if the problem persists",
        ];
    }
  }

  /**
   * Utility function for delays
   */
  private static delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}
