/**
 * Retry and Circuit Breaker Utilities
 *
 * Provides retry logic with exponential backoff and circuit breaker pattern
 * for handling transient failures in scanner and LLM operations.
 *
 * @module util/retry
 */
/**
 * Classify an error as transient or permanent
 *
 * Transient errors (should retry):
 * - Timeout errors (ETIMEDOUT, ESOCKETTIMEDOUT)
 * - Rate limiting (429)
 * - Server errors (5xx)
 * - Network errors (ECONNRESET, ECONNREFUSED, ENOTFOUND)
 *
 * Permanent errors (should not retry):
 * - File not found (ENOENT)
 * - Permission denied (EACCES, EPERM)
 * - Invalid input (4xx except 429)
 * - Authentication errors (401, 403)
 */
export function classifyError(error) {
    const message = error.message.toLowerCase();
    const code = error.code;
    // Check for transient errors
    if (code === "ETIMEDOUT" ||
        code === "ESOCKETTIMEDOUT" ||
        code === "ECONNRESET" ||
        code === "ECONNREFUSED" ||
        code === "ENOTFOUND" ||
        code === "ENETUNREACH" ||
        code === "EAI_AGAIN") {
        return "transient";
    }
    // Check for permanent errors
    if (code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "EEXIST") {
        return "permanent";
    }
    // Check message for HTTP status codes
    if (message.includes("429") || message.includes("rate limit")) {
        return "transient";
    }
    if (message.includes("500") ||
        message.includes("502") ||
        message.includes("503") ||
        message.includes("504") ||
        message.includes("internal server error") ||
        message.includes("bad gateway") ||
        message.includes("service unavailable") ||
        message.includes("gateway timeout")) {
        return "transient";
    }
    if (message.includes("401") ||
        message.includes("403") ||
        message.includes("404") ||
        message.includes("unauthorized") ||
        message.includes("forbidden") ||
        message.includes("not found")) {
        return "permanent";
    }
    // Check for timeout in message
    if (message.includes("timeout") || message.includes("timed out")) {
        return "transient";
    }
    return "unknown";
}
/**
 * Calculate delay with exponential backoff and optional jitter
 */
function calculateDelay(attempt, initialDelayMs, maxDelayMs, backoffMultiplier, jitter) {
    // Exponential backoff: initialDelay * multiplier^(attempt-1)
    let delay = initialDelayMs * Math.pow(backoffMultiplier, attempt - 1);
    // Cap at max delay
    delay = Math.min(delay, maxDelayMs);
    // Add jitter (±25%)
    if (jitter) {
        const jitterRange = delay * 0.25;
        delay = delay + (Math.random() * 2 - 1) * jitterRange;
    }
    return Math.round(delay);
}
/**
 * Sleep for a given number of milliseconds
 */
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
 * Retry error thrown when all attempts fail
 */
export class RetryError extends Error {
    attempts;
    lastError;
    errors;
    constructor(message, attempts, lastError, errors) {
        super(message);
        this.attempts = attempts;
        this.lastError = lastError;
        this.errors = errors;
        this.name = "RetryError";
    }
}
/**
 * Circuit breaker error thrown when circuit is open
 */
export class CircuitOpenError extends Error {
    circuitName;
    openedAt;
    resetAt;
    constructor(circuitName, openedAt, resetAt) {
        super(`Circuit '${circuitName}' is open. Will reset at ${resetAt.toISOString()}`);
        this.circuitName = circuitName;
        this.openedAt = openedAt;
        this.resetAt = resetAt;
        this.name = "CircuitOpenError";
    }
}
/**
 * Execute a function with retry logic
 *
 * @param fn - The async function to execute
 * @param options - Retry options
 * @returns The result of the function
 * @throws RetryError if all attempts fail
 *
 * @example
 * ```typescript
 * const result = await withRetry(
 *   () => fetchData(),
 *   { maxAttempts: 3, onRetry: (attempt, err) => console.log(`Retry ${attempt}: ${err.message}`) }
 * );
 * ```
 */
export async function withRetry(fn, options = {}) {
    const { maxAttempts = 3, initialDelayMs = 1000, maxDelayMs = 30000, backoffMultiplier = 2, jitter = true, attemptTimeoutMs, classifyError: classifier = classifyError, onRetry, } = options;
    const errors = [];
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            // Wrap with timeout if specified
            if (attemptTimeoutMs) {
                return await Promise.race([
                    fn(),
                    new Promise((_, reject) => setTimeout(() => reject(new Error(`Attempt ${attempt} timed out after ${attemptTimeoutMs}ms`)), attemptTimeoutMs)),
                ]);
            }
            return await fn();
        }
        catch (error) {
            const err = error;
            errors.push(err);
            // Check if this is the last attempt
            if (attempt >= maxAttempts) {
                throw new RetryError(`All ${maxAttempts} attempts failed. Last error: ${err.message}`, attempt, err, errors);
            }
            // Classify the error
            const errorType = classifier(err);
            // Don't retry permanent errors
            if (errorType === "permanent") {
                throw new RetryError(`Permanent error after ${attempt} attempt(s): ${err.message}`, attempt, err, errors);
            }
            // Calculate delay for next attempt
            const delayMs = calculateDelay(attempt, initialDelayMs, maxDelayMs, backoffMultiplier, jitter);
            // Notify about retry
            if (onRetry) {
                onRetry(attempt, err, delayMs);
            }
            // Wait before retrying
            await sleep(delayMs);
        }
    }
    // This should never be reached, but TypeScript needs it
    throw new Error("Unexpected retry loop exit");
}
/**
 * Circuit breaker implementation
 *
 * States:
 * - CLOSED: Normal operation, requests pass through
 * - OPEN: Circuit tripped, requests fail fast
 * - HALF-OPEN: Testing if service recovered
 */
export class CircuitBreaker {
    state = "closed";
    failures = 0;
    successes = 0;
    lastFailureTime = null;
    name;
    failureThreshold;
    resetTimeoutMs;
    successThreshold;
    onStateChange;
    constructor(name, options = {}) {
        this.name = name;
        this.failureThreshold = options.failureThreshold ?? 5;
        this.resetTimeoutMs = options.resetTimeoutMs ?? 30000;
        this.successThreshold = options.successThreshold ?? 2;
        this.onStateChange = options.onStateChange;
    }
    /**
     * Get the current circuit state
     */
    getState() {
        // Check if we should transition from open to half-open
        if (this.state === "open" && this.lastFailureTime) {
            const elapsed = Date.now() - this.lastFailureTime.getTime();
            if (elapsed >= this.resetTimeoutMs) {
                this.transition("half-open");
            }
        }
        return this.state;
    }
    /**
     * Check if the circuit allows requests
     */
    canExecute() {
        const state = this.getState();
        return state === "closed" || state === "half-open";
    }
    /**
     * Execute a function through the circuit breaker
     *
     * @param fn - The async function to execute
     * @returns The result of the function
     * @throws CircuitOpenError if circuit is open
     */
    async execute(fn) {
        const state = this.getState();
        if (state === "open") {
            const resetAt = new Date(this.lastFailureTime.getTime() + this.resetTimeoutMs);
            throw new CircuitOpenError(this.name, this.lastFailureTime, resetAt);
        }
        try {
            const result = await fn();
            this.onSuccess();
            return result;
        }
        catch (error) {
            this.onFailure();
            throw error;
        }
    }
    /**
     * Record a successful execution
     */
    onSuccess() {
        if (this.state === "half-open") {
            this.successes++;
            if (this.successes >= this.successThreshold) {
                this.transition("closed");
                this.reset();
            }
        }
        else {
            // Reset failure count on success in closed state
            this.failures = 0;
        }
    }
    /**
     * Record a failed execution
     */
    onFailure() {
        this.failures++;
        this.lastFailureTime = new Date();
        if (this.state === "half-open") {
            // Any failure in half-open goes back to open
            this.transition("open");
            this.successes = 0;
        }
        else if (this.state === "closed" && this.failures >= this.failureThreshold) {
            this.transition("open");
        }
    }
    /**
     * Transition to a new state
     */
    transition(newState) {
        if (this.state !== newState) {
            const oldState = this.state;
            this.state = newState;
            if (this.onStateChange) {
                this.onStateChange(oldState, newState);
            }
        }
    }
    /**
     * Reset the circuit breaker
     */
    reset() {
        this.failures = 0;
        this.successes = 0;
        this.lastFailureTime = null;
    }
    /**
     * Force the circuit to a specific state (for testing)
     */
    forceState(state) {
        this.transition(state);
        if (state === "closed") {
            this.reset();
        }
    }
    /**
     * Get circuit breaker stats
     */
    getStats() {
        return {
            name: this.name,
            state: this.getState(),
            failures: this.failures,
            successes: this.successes,
            lastFailureTime: this.lastFailureTime,
        };
    }
}
/**
 * Execute a function with both retry and circuit breaker
 *
 * @param circuitBreaker - The circuit breaker instance
 * @param fn - The async function to execute
 * @param retryOptions - Retry options
 * @returns The result of the function
 */
export async function withRetryAndCircuitBreaker(circuitBreaker, fn, retryOptions = {}) {
    return circuitBreaker.execute(() => withRetry(fn, retryOptions));
}
/**
 * Create a retry wrapper for a function
 *
 * @param fn - The async function to wrap
 * @param options - Retry options
 * @returns A wrapped function that retries on failure
 */
export function createRetryWrapper(fn, options = {}) {
    return (...args) => withRetry(() => fn(...args), options);
}
//# sourceMappingURL=retry.js.map