/**
 * Retry Utility
 *
 * Executes an async operation with exponential backoff and jitter.
 * Only retries on retryable ProviderErrors (5xx, 429, network errors)
 * or raw HTTP errors with status >= 500 or status === 429.
 *
 * Default configuration:
 *   - maxRetries: 3
 *   - baseDelayMs: 1000 (1 second)
 *   - maxDelayMs: 30000 (30 seconds)
 *   - Jitter: random value between 0 and baseDelayMs added to each delay
 *
 * Delay formula: min(baseDelay * 2^attempt + random * baseDelay, maxDelay)
 *
 * Usage:
 *   const result = await withRetry(
 *     () => provider.createContact(data),
 *     'addContact',
 *     'brevo',
 *     { maxRetries: 3 }
 *   );
 */

import { ProviderError } from './errorNormalizer';
import type { ProviderName } from './types';

// ============================================
// Configuration
// ============================================

export interface RetryOptions {
  /** Maximum number of retry attempts (default: 3) */
  maxRetries: number;
  /** Base delay in milliseconds (default: 1000) */
  baseDelayMs: number;
  /** Maximum delay cap in milliseconds (default: 30000) */
  maxDelayMs: number;
  /** Custom function to determine if an error should trigger a retry */
  shouldRetry?: (error: unknown) => boolean;
}

const DEFAULT_OPTIONS: RetryOptions = {
  maxRetries: 3,
  baseDelayMs: 1000,
  maxDelayMs: 30000,
  shouldRetry: defaultShouldRetry,
};

// ============================================
// Retry function
// ============================================

/**
 * Execute an async operation with exponential backoff retry.
 *
 * @param operation - The async function to execute
 * @param operationName - Human-readable name for logging (e.g. 'addContact')
 * @param providerName - Provider name for logging (e.g. 'brevo')
 * @param options - Optional retry configuration overrides
 * @returns The result of the operation
 * @throws The last error if all retries are exhausted or the error is not retryable
 */
export async function withRetry<T>(
  operation: () => Promise<T>,
  operationName: string,
  providerName: ProviderName | string,
  options?: Partial<RetryOptions>
): Promise<T> {
  const maxRetries = options?.maxRetries ?? DEFAULT_OPTIONS.maxRetries;
  const baseDelayMs = options?.baseDelayMs ?? DEFAULT_OPTIONS.baseDelayMs;
  const maxDelayMs = options?.maxDelayMs ?? DEFAULT_OPTIONS.maxDelayMs;
  const shouldRetry = options?.shouldRetry ?? DEFAULT_OPTIONS.shouldRetry;

  let lastError: unknown;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const result = await operation();
      return result;
    } catch (error) {
      lastError = error;

      // Don't retry if we've exhausted our attempts
      if (attempt >= maxRetries) {
        throw error;
      }

      // Don't retry if the error is not retryable
      const retryCheck = shouldRetry || defaultShouldRetry;
      if (!retryCheck(error)) {
        throw error;
      }

      // Calculate delay with exponential backoff + jitter
      const jitter = Math.random() * baseDelayMs;
      const delay = Math.min(
        baseDelayMs * Math.pow(2, attempt) + jitter,
        maxDelayMs
      );

      console.log(
        `[ProviderGateway] Retrying ${operationName} for ${providerName} ` +
        `(attempt ${attempt + 1}/${maxRetries}, delay ${Math.round(delay)}ms)`
      );

      await sleep(delay);
    }
  }

  // This should never be reached, but TypeScript needs it
  throw lastError;
}

// ============================================
// Default shouldRetry predicate
// ============================================

/**
 * Determine whether an error should trigger a retry.
 *
 * Retries on:
 *   - ProviderError with retryable=true
 *   - HTTP errors with status >= 500
 *   - HTTP errors with status === 429 (rate limited)
 *   - Network errors (no response object)
 *
 * Does NOT retry on:
 *   - 4xx client errors (except 429)
 *   - ProviderError with retryable=false
 *   - Unknown errors without a status code
 */
function defaultShouldRetry(error: unknown): boolean {
  // ProviderError — check the retryable flag
  if (error instanceof ProviderError) {
    return error.retryable;
  }

  // Raw HTTP errors with a status code
  if (error && typeof error === 'object') {
    const err = error as Record<string, any>;

    // Axios-style errors: error.response.status
    if (err.response && typeof err.response === 'object' && typeof err.response.status === 'number') {
      const status = err.response.status;
      return status >= 500 || status === 429;
    }

    // Brevo SDK errors: error.statusCode
    if (typeof err.statusCode === 'number') {
      return err.statusCode >= 500 || err.statusCode === 429;
    }

    // Mailchimp SDK errors: error.status
    if (typeof err.status === 'number') {
      return err.status >= 500 || err.status === 429;
    }

    // Network errors (ECONNREFUSED, ETIMEDOUT, etc.)
    if (typeof err.code === 'string') {
      const networkCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EPIPE', 'ENETUNREACH'];
      if (networkCodes.includes(err.code)) {
        return true;
      }
    }
  }

  // Default: don't retry unknown errors
  return false;
}

// ============================================
// Utility
// ============================================

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}