/**
 * Generic retry wrapper for ConnectWise API calls.
 *
 * Retries on transient errors (ECONNABORTED, ECONNRESET, ETIMEDOUT,
 * ECONNREFUSED, ERR_NETWORK) with exponential backoff.  Non-transient
 * errors (e.g. 404, 400) are re-thrown immediately.
 */

const TRANSIENT_CODES = new Set([
  "ECONNABORTED",
  "ECONNRESET",
  "ETIMEDOUT",
  "ECONNREFUSED",
  "ERR_NETWORK",
  "ENETUNREACH",
]);

export interface CwRetryOptions {
  /** Maximum number of attempts (including the first). Default: 3 */
  maxAttempts?: number;
  /** Base delay in ms before the first retry. Doubles each retry. Default: 1000 */
  baseDelayMs?: number;
  /** Optional label for log messages. */
  label?: string;
}

/**
 * Execute `fn` and retry up to `maxAttempts - 1` times on transient
 * Axios / network errors.
 */
export async function withCwRetry<T>(
  fn: () => Promise<T>,
  opts: CwRetryOptions = {},
): Promise<T> {
  const { maxAttempts = 3, baseDelayMs = 1_000, label } = opts;

  let lastError: unknown;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (err) {
      lastError = err;

      if (!isRetryable(err) || attempt === maxAttempts) throw err;

      const delay = baseDelayMs * 2 ** (attempt - 1); // 1s, 2s, 4s …
      const tag = label ? `[${label}] ` : "";
      console.warn(
        `${tag}CW transient error (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms — ${describeErr(err)}`,
      );
      await sleep(delay);
    }
  }

  // Should never reach here, but satisfy TS:
  throw lastError;
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function isRetryable(err: unknown): boolean {
  if (typeof err !== "object" || err === null) return false;
  const e = err as Record<string, any>;
  if (!e.isAxiosError) return false;

  // Retry on known transient codes
  if (TRANSIENT_CODES.has(e.code)) return true;

  // Also retry on 5xx server errors from CW
  const status = e.response?.status;
  if (typeof status === "number" && status >= 500) return true;

  return false;
}

function describeErr(err: unknown): string {
  if (typeof err !== "object" || err === null) return String(err);
  const e = err as Record<string, any>;
  if (e.isAxiosError) {
    const method = (e.config?.method ?? "?").toUpperCase();
    const url = e.config?.url ?? "unknown";
    return `${method} ${url} → ${e.code ?? `HTTP ${e.response?.status}`}`;
  }
  return (e as Error).message ?? String(err);
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
