import type { RetryPolicy } from './rate-limiter-types'
import { ToolingError } from '../../execution/engine/tools/types'

/**
 * Calculate retry delay with exponential backoff and jitter
 *
 * Formula: delay = min(maxDelay, initialDelay * 2^attempt) * (1 +/- jitter)
 *
 * @param attempt - Retry attempt number (0-indexed)
 * @param policy - Retry policy configuration
 * @returns Delay in milliseconds
 */
export function calculateRetryDelay(attempt: number, policy: RetryPolicy): number {
  const exponentialDelay = policy.initialDelayMs * Math.pow(2, attempt)
  const cappedDelay = Math.min(policy.maxDelayMs, exponentialDelay)

  // Add jitter: random factor between (1 - jitter) and (1 + jitter)
  const jitterRange = policy.jitterFactor * 2
  const jitterMultiplier = 1 - policy.jitterFactor + Math.random() * jitterRange

  return Math.floor(cappedDelay * jitterMultiplier)
}

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

/**
 * Execute function with retry logic
 *
 * Retries on ToolingError where isRetryable() returns true.
 * Uses exponential backoff with jitter to prevent thundering herd.
 *
 * Pattern: Stateless function (processor pattern)
 *
 * @param fn - Function to execute
 * @param policy - Retry policy
 * @returns Function result
 * @throws ToolingError if all retries exhausted
 *
 * @example
 * const result = await withRetry(
 *   () => adapter.call('sendEmail', params, creds),
 *   DEFAULT_RETRY_POLICY
 * )
 */
export async function withRetry<T>(
  fn: () => Promise<T>,
  policy: RetryPolicy
): Promise<T> {
  let lastError: Error | undefined

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

      // Don't retry non-ToolingErrors (programming bugs, TypeErrors, etc.)
      if (!(error instanceof ToolingError)) {
        throw error
      }

      // Don't retry if the ToolingError is marked non-retryable
      if (!error.isRetryable()) {
        throw error
      }

      // Don't retry if max retries reached
      if (attempt === policy.maxRetries) {
        break
      }

      // Calculate delay and sleep before retry
      const delay = calculateRetryDelay(attempt, policy)
      await sleep(delay)
    }
  }

  // All retries exhausted
  throw new ToolingError(
    'api_error',
    `Failed after ${policy.maxRetries} retries: ${lastError?.message}`,
    { originalError: lastError }
  )
}
