/**
 * Fetch wrapper with automatic retry and exponential backoff for auth requests.
 *
 * Only retries errors classified as retryable by {@link parseAuthError}:
 * `network_error` and `server_error`. Non-retryable errors (401, 403, 429, etc.)
 * are returned or thrown immediately without retry.
 */

import { parseAuthError } from "./auth-errors";
import { sleep } from "./sleep";

export interface RetryOptions {
  /** Maximum number of retries after the initial attempt. Default: 3. */
  maxRetries?: number;
  /** Base delay in milliseconds before the first retry. Default: 1000. */
  baseDelayMs?: number;
  /** Multiplicative factor applied to the delay after each retry. Default: 2. */
  backoffFactor?: number;
}

const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_BASE_DELAY_MS = 1000;
const DEFAULT_BACKOFF_FACTOR = 2;

/**
 * Compute the backoff delay for a given retry attempt.
 *
 * @param attempt - Zero-based retry index (0 = first retry)
 * @param baseDelayMs - Starting delay in milliseconds
 * @param backoffFactor - Multiplicative factor per retry
 * @returns Delay in milliseconds
 */
function computeDelay(attempt: number, baseDelayMs: number, backoffFactor: number): number {
  return baseDelayMs * Math.pow(backoffFactor, attempt);
}

/**
 * Fetch a URL with automatic retries and exponential backoff for retryable errors.
 *
 * - Network errors (fetch throws `TypeError`) are retried if classified as retryable.
 * - HTTP error responses (e.g. 500, 502, 503) are retried if the status maps to a
 *   retryable error code.
 * - Non-retryable errors (401, 403, 429) are returned or thrown immediately.
 * - On exhausted retries for network errors, the last error is re-thrown.
 * - On exhausted retries for HTTP errors, the last response is returned.
 *
 * @param url - The URL to fetch
 * @param options - Standard `RequestInit` options forwarded to `fetch`
 * @param retryOptions - Controls retry behaviour (max retries, delay, backoff)
 * @returns The successful (or final non-retryable) `Response`
 */
export async function fetchWithRetry(
  url: string,
  options?: RequestInit,
  retryOptions?: RetryOptions
): Promise<Response> {
  const maxRetries = Math.max(0, Math.floor(retryOptions?.maxRetries ?? DEFAULT_MAX_RETRIES));
  const baseDelayMs = Math.max(100, retryOptions?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS);
  const backoffFactor = Math.max(1, retryOptions?.backoffFactor ?? DEFAULT_BACKOFF_FACTOR);

  let lastError: unknown = null;
  let lastResponse: Response | null = null;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (response.ok) {
        return response;
      }

      // Non-ok response: check if we should retry based on status code
      const parsed = parseAuthError({ status: response.status });

      if (!parsed.retryable || attempt === maxRetries) {
        return response;
      }

      // Retryable HTTP error with retries remaining
      lastResponse = response;
      const delay = computeDelay(attempt, baseDelayMs, backoffFactor);

      console.warn(
        `[Auth] Retry ${String(attempt + 1)}/${String(maxRetries)} after HTTP ${String(response.status)}, waiting ${String(delay)}ms...`
      );

      await sleep(delay);
    } catch (error: unknown) {
      const parsed = parseAuthError(error);

      if (!parsed.retryable || attempt === maxRetries) {
        throw error;
      }

      // Retryable thrown error with retries remaining
      lastError = error;
      const delay = computeDelay(attempt, baseDelayMs, backoffFactor);

      console.warn(
        `[Auth] Retry ${String(attempt + 1)}/${String(maxRetries)} after ${parsed.code}, waiting ${String(delay)}ms...`
      );

      await sleep(delay);
    }
  }

  // Should not be reached due to the loop logic, but satisfy TypeScript
  if (lastError) {
    if (lastError instanceof Error) {
      throw lastError;
    }
    throw new Error("fetchWithRetry: retries exhausted after non-Error failure");
  }
  if (lastResponse) {
    return lastResponse;
  }

  throw new Error("fetchWithRetry: unexpected state");
}
