import type { Exception } from '@api-client/core/exceptions/exception.js'

/**
 * Defines the options for the retry mechanism.
 */
export interface RetryOptions {
  /**
   * The maximum number of retry attempts.
   * Defaults to 3.
   */
  retries?: number
  /**
   * The delay in milliseconds before the next retry.
   * Can be a fixed number or a function that returns a number (e.g., for exponential backoff).
   * Defaults to 1000ms.
   */
  delayMs?: number | ((attempt: number) => number)
  /**
   * An optional function that determines if a retry should be attempted based on the error.
   * If not provided, all errors will trigger a retry up to the maximum number of retries.
   * @param error The error that occurred.
   * @returns True if a retry should be attempted, false otherwise.
   */
  shouldRetry?: (error: Error) => boolean
}

/**
 * Calculates an exponential backoff delay for retry attempts.
 * The delay doubles with each attempt.
 *
 * @param attempt The current retry attempt number (should be 1-indexed).
 * @param initialDelay The initial delay in milliseconds. Defaults to 1000ms.
 * @returns The calculated delay in milliseconds.
 */
export function exponentialBackoffDelay(attempt: number, initialDelay = 1000): number {
  if (attempt <= 0) return initialDelay
  // For attempt 1, delay is initialDelay * 2^0 = initialDelay
  return initialDelay * Math.pow(2, attempt - 1)
}
/**
 * Checks if the API error should be retried based on its status code.
 * If the status code is 404, it returns false (do not retry).
 * For other status codes, it returns true (retry).
 * @param error The error to check.
 */
export function shouldRetryApiError(error: Error): boolean {
  const { status } = error as Exception
  if (typeof status === 'number' && status >= 400 && status < 500) {
    // Do not retry on 4xx errors (client errors)
    return false
  }
  return true
}

/**
 * Executes an asynchronous operation with a retry mechanism.
 *
 * @param operation A function that returns a Promise to be executed.
 * @param options Optional retry configurations.
 * @returns A Promise that resolves with the result of the operation or rejects if all retries fail.
 * @example
 * ```typescript
 * import { shouldRetryApiError, exponentialBackoffDelay, retry } from '../data/retry_logic.js'
 *
 * override async create(data: Partial<IUser>): Promise<IUser> {
 *   const operation = async (): Promise<UserData> => {
 *     // ... perform API call or other async operation
 *   }
 *   const retryOptions: RetryOptions = {
 *     retries: 5,
 *     delayMs: exponentialBackoffDelay,
 *     shouldRetry: shouldRetryApiError,
 *   }
 *   const data = await retry(operation, retryOptions)
 *   this.dispatchEvent(new CustomEvent('data-changed', { detail: data }))
 *   return data
 * }
 * ```
 */
export async function retry<R>(operation: () => Promise<R>, options: RetryOptions = {}): Promise<R> {
  const { retries = 3, delayMs = 1000, shouldRetry = () => true } = options

  let lastError: Error | undefined

  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      return await operation()
    } catch (e) {
      lastError = e as Error
      if (!shouldRetry(lastError) || attempt === retries - 1) {
        // Do not retry if shouldRetry returns false or if it's the last attempt
        throw lastError
      }

      const delay = typeof delayMs === 'function' ? delayMs(attempt + 1) : delayMs
      if (delay > 0) {
        await new Promise((resolve) => setTimeout(resolve, delay))
      }
    }
  }
  // This part should ideally not be reached if retries > 0,
  // as the loop would throw on the last attempt.
  // However, to satisfy TypeScript and guard against retries = 0:
  if (lastError) {
    throw lastError
  }
  // Should not happen if operation is called at least once.
  // Adding a generic error for completeness.
  const err = new Error('Retry operation failed without a specific error.')
  throw err
}
