/** Error categories for retry logic. */
type ErrorKind = "transient" | "permanent" | "validation" | "budget";

/** Classify an error for retry decisions. */
export function classifyError(error: unknown): ErrorKind {
  if (error instanceof Error) {
    const msg = error.message.toLowerCase();

    // Budget exhausted — stop immediately
    if (msg.includes("402") || msg.includes("payment required")) return "budget";

    // Permanent — don't retry
    if (msg.includes("400") || msg.includes("422") || msg.includes("bad request"))
      return "permanent";

    // Validation failure (Zod parse)
    if (msg.includes("validation") || msg.includes("parse")) return "validation";

    // Transient — retry with backoff
    if (
      msg.includes("429") ||
      msg.includes("500") ||
      msg.includes("502") ||
      msg.includes("503") ||
      msg.includes("econnreset") ||
      msg.includes("timeout") ||
      msg.includes("rate")
    )
      return "transient";
  }

  // Default to transient (safer — we'll retry)
  return "transient";
}

/**
 * Retry a function with exponential backoff.
 * Returns the result or throws after maxRetries.
 */
export async function withRetry<T>(
  fn: () => Promise<T>,
  opts: { maxRetries?: number; baseDelayMs?: number; label?: string } = {},
): Promise<T> {
  const { maxRetries = 5, baseDelayMs = 1000, label = "operation" } = opts;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const kind = classifyError(error);

      // Never retry budget or permanent errors
      if (kind === "budget" || kind === "permanent") throw error;

      // Validation gets 3 retries (models like GLM-5 intermittently produce unparseable output)
      if (kind === "validation" && attempt >= 3) throw error;

      // Transient: exponential backoff
      if (attempt === maxRetries) throw error;

      const delay = baseDelayMs * 2 ** attempt;
      process.stderr.write(
        `  ⟳ ${label} attempt ${attempt + 1}/${maxRetries} failed (${kind}), retrying in ${delay}ms\n`,
      );
      await new Promise((r) => setTimeout(r, delay));
    }
  }

  throw new Error("unreachable");
}
