/*
 * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
 */

import { isConnectionError, isTimeoutError } from "./http.js";

export type BackoffStrategy = {
  initialInterval: number;
  maxInterval: number;
  exponent: number;
  maxElapsedTime: number;
};

const defaultBackoff: BackoffStrategy = {
  initialInterval: 500,
  maxInterval: 60000,
  exponent: 1.5,
  maxElapsedTime: 3600000,
};

export type RetryConfig =
  | { strategy: "none" }
  | {
      strategy: "backoff";
      backoff?: BackoffStrategy;
      retryConnectionErrors?: boolean;
    };

class PermanentError extends Error {
  inner: unknown;

  constructor(inner: unknown) {
    super("Permanent error");
    this.inner = inner;

    Object.setPrototypeOf(this, PermanentError.prototype);
  }
}

class TemporaryError extends Error {
  res: Response;

  constructor(res: Response) {
    super("Temporary error");
    this.res = res;

    Object.setPrototypeOf(this, TemporaryError.prototype);
  }
}

export async function retry(
  fetchFn: () => Promise<Response>,
  options: {
    config: RetryConfig;
    statusCodes: string[];
  },
): Promise<Response> {
  switch (options.config.strategy) {
    case "backoff":
      return retryBackoff(
        wrapFetcher(fetchFn, {
          statusCodes: options.statusCodes,
          retryConnectionErrors: !!options.config.retryConnectionErrors,
        }),
        options.config.backoff ?? defaultBackoff,
      );
    default:
      return await fetchFn();
  }
}

function wrapFetcher(
  fn: () => Promise<Response>,
  options: {
    statusCodes: string[];
    retryConnectionErrors: boolean;
  },
): () => Promise<Response> {
  return async () => {
    try {
      const res = await fn();
      if (isRetryableResponse(res, options.statusCodes)) {
        throw new TemporaryError(res);
      }

      return res;
    } catch (err) {
      if (err instanceof TemporaryError) {
        throw err;
      }

      if (
        options.retryConnectionErrors &&
        (isTimeoutError(err) || isConnectionError(err))
      ) {
        throw err;
      }

      throw new PermanentError(err);
    }
  };
}

const codeRangeRE = new RegExp("^[0-9]xx$", "i");

function isRetryableResponse(res: Response, statusCodes: string[]): boolean {
  const actual = `${res.status}`;

  return statusCodes.some((code) => {
    if (!codeRangeRE.test(code)) {
      return code === actual;
    }

    const expectFamily = code.charAt(0);
    if (!expectFamily) {
      throw new Error("Invalid status code range");
    }

    const actualFamily = actual.charAt(0);
    if (!actualFamily) {
      throw new Error(`Invalid response status code: ${actual}`);
    }

    return actualFamily === expectFamily;
  });
}

async function retryBackoff(
  fn: () => Promise<Response>,
  strategy: BackoffStrategy,
): Promise<Response> {
  const { maxElapsedTime, initialInterval, exponent, maxInterval } = strategy;

  const start = Date.now();
  let x = 0;

  // eslint-disable-next-line no-constant-condition
  while (true) {
    try {
      const res = await fn();
      return res;
    } catch (err) {
      if (err instanceof PermanentError) {
        throw err.inner;
      }
      const elapsed = Date.now() - start;
      if (elapsed > maxElapsedTime) {
        if (err instanceof TemporaryError) {
          return err.res;
        }

        throw err;
      }

      let retryInterval = 0;
      if (err instanceof TemporaryError && err.res && err.res.headers) {
        const retryVal = err.res.headers.get("retry-after") || "";
        if (retryVal != "") {
          const parsedNumber = Number(retryVal);
          if (!isNaN(parsedNumber) && Number.isInteger(parsedNumber)) {
            retryInterval = parsedNumber * 1000;
          } else {
            const parsedDate = Date.parse(retryVal);
            if (!isNaN(parsedDate)) {
              const deltaMS = parsedDate - Date.now();
              retryInterval = deltaMS > 0 ? Math.ceil(deltaMS) : 0;
            }
          }
        }
      }

      if (retryInterval == 0) {
        retryInterval =
          initialInterval * Math.pow(x, exponent) + Math.random() * 1000;
      }

      const d = Math.min(retryInterval, maxInterval);

      await delay(d);
      x++;
    }
  }
}

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