type Fetch = typeof fetch;

export class HttpError extends Error {
  constructor(
    message: string,
    public readonly status: number,
    public readonly responseBody: string,
    public readonly retryable: boolean,
  ) {
    super(message);
    this.name = "HttpError";
  }
}

export interface RetryOptions {
  attempts?: number;
  allow404?: boolean;
}

function retryDelayMs(response: Response | undefined, attempt: number): number {
  const retryAfter = response?.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
    const date = Date.parse(retryAfter);
    if (Number.isFinite(date)) return Math.max(0, date - Date.now());
  }
  return Math.min(30_000, 1000 * 2 ** attempt);
}

export function sleep(milliseconds: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

export async function fetchWithRetry(
  makeRequest: (attempt: number) => Promise<Response>,
  options: RetryOptions = {},
): Promise<Response> {
  const attempts = options.attempts ?? 5;
  let lastError: unknown;
  for (let attempt = 0; attempt < attempts; attempt += 1) {
    let response: Response | undefined;
    try {
      response = await makeRequest(attempt);
      if (response.ok || (options.allow404 && response.status === 404)) return response;
      const responseBody = await response.text();
      const retryable = response.status === 408 || response.status === 429 || response.status >= 500;
      if (!retryable || attempt === attempts - 1) {
        throw new HttpError(
          `HTTP request failed with status ${response.status}.`,
          response.status,
          responseBody.slice(0, 4000),
          retryable,
        );
      }
    } catch (error) {
      if (error instanceof HttpError) throw error;
      lastError = error;
      if (attempt === attempts - 1) {
        throw error;
      }
    }
    await sleep(retryDelayMs(response, attempt));
  }
  throw lastError instanceof Error ? lastError : new Error("HTTP request failed after retries.");
}

export const defaultFetch: Fetch = fetch;
