import { HaloError } from "../services/error";

export interface RetryOptions {
  maxRetries?: number;
  delayMs?: number;
  backoffMultiplier?: number;
  retryableStatuses?: number[];
}

const DEFAULT_OPTIONS: Required<RetryOptions> = {
  maxRetries: 3,
  delayMs: 1000,
  backoffMultiplier: 2,
  retryableStatuses: [408, 429, 500, 502, 503, 504],
};

export async function withRetry<T>(
  fn: () => Promise<T>,
  options: RetryOptions = {},
): Promise<T> {
  const opts = { ...DEFAULT_OPTIONS, ...options };
  let lastError: unknown;
  let delay = opts.delayMs;

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

      if (attempt === opts.maxRetries) {
        break;
      }

      if (error instanceof HaloError && !error.isNetworkError()) {
        if (!error.isAuthError() && !error.isNotFound()) {
          const statusCode = error.statusCode;
          if (statusCode && opts.retryableStatuses.includes(statusCode)) {
            await sleep(delay);
            delay *= opts.backoffMultiplier;
            continue;
          }
        }
        throw error;
      }

      if (error instanceof TypeError) {
        await sleep(delay);
        delay *= opts.backoffMultiplier;
        continue;
      }

      throw error;
    }
  }

  throw lastError;
}

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

export class RetryError extends Error {
  constructor(
    message: string,
    public readonly attempts: number,
    public readonly lastError?: unknown,
  ) {
    super(message);
    this.name = "RetryError";
  }
}
