/**
 * Retry Utility
 *
 * Provides retry logic with exponential backoff for HTTP requests.
 */

import { Logger } from "@nestjs/common";
import type { AxiosError } from "axios";

const logger = new Logger("RetryUtil");

/**
 * Configuration for retry logic
 */
export interface RetryConfig {
  /** Maximum number of retry attempts (default: 3) */
  maxRetries?: number;
  /** Initial backoff delay in milliseconds (default: 1000) */
  initialDelay?: number;
  /** Maximum backoff delay in milliseconds (default: 8000) */
  maxDelay?: number;
  /** Backoff multiplier (default: 2 for exponential) */
  backoffMultiplier?: number;
}

/**
 * Default retry configuration
 */
const DEFAULT_CONFIG: Required<RetryConfig> = {
  maxRetries: 3,
  initialDelay: 1000, // 1 second
  maxDelay: 8000, // 8 seconds
  backoffMultiplier: 2,
};

/**
 * Check if error is retryable (network errors, timeouts, 5xx errors)
 * Do NOT retry on 4xx errors (client errors)
 */
export function isRetryableError(error: unknown): boolean {
  // Check if it's a plain object (for testing) or Error instance
  if (!error || (typeof error !== "object" && !(error instanceof Error))) {
    return false;
  }

  const axiosError = error as AxiosError;

  // Retry on network errors (no response received)
  if (!axiosError.response) {
    // Check for network error codes
    const networkErrorCodes = [
      "ECONNREFUSED",
      "ETIMEDOUT",
      "ENOTFOUND",
      "ENETUNREACH",
      "EAI_AGAIN",
    ];

    if (axiosError.code && networkErrorCodes.includes(axiosError.code)) {
      return true;
    }

    // Retry on timeout
    if (axiosError.message.includes("timeout")) {
      return true;
    }

    return false;
  }

  // Retry on 5xx server errors
  const status = axiosError.response.status;
  if (status >= 500 && status < 600) {
    return true;
  }

  // Retry on 429 (Too Many Requests) with backoff
  if (status === 429) {
    return true;
  }

  // Do NOT retry on 4xx client errors
  return false;
}

/**
 * Execute a function with retry logic and exponential backoff
 */
export async function withRetry<T>(
  operation: () => Promise<T>,
  config: RetryConfig = {}
): Promise<T> {
  const finalConfig: Required<RetryConfig> = {
    ...DEFAULT_CONFIG,
    ...config,
  };

  let lastError: Error | undefined;
  let delay = finalConfig.initialDelay;

  for (let attempt = 0; attempt <= finalConfig.maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error as Error;

      // If this is the last attempt, don't retry
      if (attempt === finalConfig.maxRetries) {
        break;
      }

      // Check if error is retryable
      if (!isRetryableError(error)) {
        logger.warn(`Non-retryable error, aborting retry: ${lastError.message}`);
        throw error;
      }

      // Log retry attempt
      const errorMessage = lastError instanceof Error ? lastError.message : "Unknown error";
      logger.warn(
        `Retry attempt ${String(attempt + 1)}/${String(finalConfig.maxRetries)} after error: ${errorMessage}. Retrying in ${String(delay)}ms...`
      );

      // Wait with exponential backoff
      await sleep(delay);

      // Calculate next delay with exponential backoff
      delay = Math.min(delay * finalConfig.backoffMultiplier, finalConfig.maxDelay);
    }
  }

  // All retries exhausted
  if (lastError) {
    throw lastError;
  }

  // Should never reach here, but satisfy TypeScript
  throw new Error("Operation failed after retries");
}

/**
 * Sleep for specified milliseconds
 */
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
