/**
 * Retry Policy
 * Configurable retry strategies for observability exporters
 */
import { logger } from "../utils/logger.js";
/**
 * Base retry policy with common configuration
 */
export class BaseRetryPolicy {
    maxAttempts;
    maxTotalTimeMs;
    retryableErrors;
    nonRetryableErrors;
    constructor(config) {
        this.maxAttempts = config.maxAttempts ?? 3;
        this.maxTotalTimeMs = config.maxTotalTimeMs ?? 60000; // 1 minute
        this.retryableErrors = new Set(config.retryableErrors ?? [
            "ECONNRESET",
            "ETIMEDOUT",
            "ECONNREFUSED",
            "EPIPE",
            "ENOTFOUND",
            "ENETUNREACH",
            "EAI_AGAIN",
            "429", // Rate limit
            "500", // Internal server error
            "502", // Bad gateway
            "503", // Service unavailable
            "504", // Gateway timeout
        ]);
        this.nonRetryableErrors = new Set(config.nonRetryableErrors ?? [
            "400", // Bad request
            "401", // Unauthorized
            "403", // Forbidden
            "404", // Not found
            "422", // Unprocessable entity
        ]);
    }
    isRetryableError(error) {
        const errorCode = this.extractErrorCode(error);
        // Check if explicitly non-retryable
        if (this.nonRetryableErrors.has(errorCode)) {
            return false;
        }
        // Check if explicitly retryable
        if (this.retryableErrors.has(errorCode)) {
            return true;
        }
        // Default: retry on network-like errors
        return (error.message.includes("timeout") ||
            error.message.includes("ECONNRESET") ||
            error.message.includes("network"));
    }
    extractErrorCode(error) {
        // Check for HTTP status code in error
        const httpMatch = error.message.match(/(\d{3})/);
        if (httpMatch) {
            return httpMatch[1];
        }
        // Check for Node.js error code
        const nodeError = error;
        if (nodeError.code) {
            return nodeError.code;
        }
        return "UNKNOWN";
    }
}
/**
 * Exponential backoff retry policy
 * Delay increases exponentially with each attempt
 */
export class ExponentialBackoffPolicy extends BaseRetryPolicy {
    name = "exponential-backoff";
    baseDelayMs;
    maxDelayMs;
    jitterFactor;
    constructor(config) {
        super(config ?? {});
        this.baseDelayMs = config?.baseDelayMs ?? 1000;
        this.maxDelayMs = config?.maxDelayMs ?? 30000;
        this.jitterFactor = config?.jitterFactor ?? 0.1;
    }
    shouldRetry(context) {
        // Check max attempts
        if (context.attempt >= this.maxAttempts) {
            return {
                shouldRetry: false,
                delayMs: 0,
                reason: `Max attempts (${this.maxAttempts}) exceeded`,
            };
        }
        // Check max total time
        if (context.elapsedMs >= this.maxTotalTimeMs) {
            return {
                shouldRetry: false,
                delayMs: 0,
                reason: `Max total time (${this.maxTotalTimeMs}ms) exceeded`,
            };
        }
        // Check if error is retryable
        if (!this.isRetryableError(context.error)) {
            return {
                shouldRetry: false,
                delayMs: 0,
                reason: `Error not retryable: ${context.error.message}`,
            };
        }
        // Calculate delay with exponential backoff
        const exponentialDelay = this.baseDelayMs * 2 ** context.attempt;
        const cappedDelay = Math.min(exponentialDelay, this.maxDelayMs);
        // Add jitter
        const jitter = cappedDelay * this.jitterFactor * (Math.random() * 2 - 1);
        const delayMs = Math.max(0, Math.round(cappedDelay + jitter));
        return {
            shouldRetry: true,
            delayMs,
            reason: `Retrying after ${delayMs}ms (attempt ${context.attempt + 1}/${this.maxAttempts})`,
        };
    }
}
/**
 * Linear backoff retry policy
 * Delay increases linearly with each attempt
 */
export class LinearBackoffPolicy extends BaseRetryPolicy {
    name = "linear-backoff";
    delayIncrementMs;
    initialDelayMs;
    constructor(config) {
        super(config ?? {});
        this.initialDelayMs = config?.initialDelayMs ?? 1000;
        this.delayIncrementMs = config?.delayIncrementMs ?? 1000;
    }
    shouldRetry(context) {
        if (context.attempt >= this.maxAttempts) {
            return {
                shouldRetry: false,
                delayMs: 0,
                reason: `Max attempts (${this.maxAttempts}) exceeded`,
            };
        }
        if (context.elapsedMs >= this.maxTotalTimeMs) {
            return {
                shouldRetry: false,
                delayMs: 0,
                reason: `Max total time (${this.maxTotalTimeMs}ms) exceeded`,
            };
        }
        if (!this.isRetryableError(context.error)) {
            return {
                shouldRetry: false,
                delayMs: 0,
                reason: `Error not retryable: ${context.error.message}`,
            };
        }
        const delayMs = this.initialDelayMs + context.attempt * this.delayIncrementMs;
        return {
            shouldRetry: true,
            delayMs,
            reason: `Retrying after ${delayMs}ms (attempt ${context.attempt + 1}/${this.maxAttempts})`,
        };
    }
}
/**
 * Fixed delay retry policy
 * Same delay for all retry attempts
 */
export class FixedDelayPolicy extends BaseRetryPolicy {
    name = "fixed-delay";
    delayMs;
    constructor(config) {
        super(config ?? {});
        this.delayMs = config?.delayMs ?? 1000;
    }
    shouldRetry(context) {
        if (context.attempt >= this.maxAttempts) {
            return {
                shouldRetry: false,
                delayMs: 0,
                reason: `Max attempts (${this.maxAttempts}) exceeded`,
            };
        }
        if (context.elapsedMs >= this.maxTotalTimeMs) {
            return {
                shouldRetry: false,
                delayMs: 0,
                reason: `Max total time (${this.maxTotalTimeMs}ms) exceeded`,
            };
        }
        if (!this.isRetryableError(context.error)) {
            return {
                shouldRetry: false,
                delayMs: 0,
                reason: `Error not retryable: ${context.error.message}`,
            };
        }
        return {
            shouldRetry: true,
            delayMs: this.delayMs,
            reason: `Retrying after ${this.delayMs}ms (attempt ${context.attempt + 1}/${this.maxAttempts})`,
        };
    }
}
/**
 * No retry policy - never retries
 */
export class NoRetryPolicy {
    name = "no-retry";
    maxAttempts = 1;
    maxTotalTimeMs = 0;
    shouldRetry(_context) {
        return {
            shouldRetry: false,
            delayMs: 0,
            reason: "Retry disabled",
        };
    }
}
/**
 * Circuit breaker aware retry policy
 * Works with circuit breaker state to prevent retries when circuit is open
 */
export class CircuitBreakerAwarePolicy extends BaseRetryPolicy {
    name = "circuit-breaker-aware";
    innerPolicy;
    failures = 0;
    lastFailure = 0;
    circuitState = "closed";
    failureThreshold;
    resetTimeoutMs;
    constructor(config) {
        super(config ?? {});
        this.innerPolicy = config?.innerPolicy ?? new ExponentialBackoffPolicy();
        this.failureThreshold = config?.failureThreshold ?? 5;
        this.resetTimeoutMs = config?.resetTimeoutMs ?? 30000;
    }
    shouldRetry(context) {
        // Check circuit state
        if (this.circuitState === "open") {
            if (Date.now() - this.lastFailure > this.resetTimeoutMs) {
                this.circuitState = "half-open";
                logger.info(`[${this.name}] Circuit half-open, allowing probe request`);
            }
            else {
                return {
                    shouldRetry: false,
                    delayMs: 0,
                    reason: "Circuit breaker open",
                };
            }
        }
        // Delegate to inner policy
        const decision = this.innerPolicy.shouldRetry(context);
        // Record result for circuit breaker
        if (!decision.shouldRetry) {
            this.recordFailure();
        }
        return decision;
    }
    recordFailure() {
        this.failures++;
        this.lastFailure = Date.now();
        if (this.failures >= this.failureThreshold) {
            this.circuitState = "open";
            logger.warn(`[${this.name}] Circuit opened after ${this.failures} failures`);
        }
    }
    recordSuccess() {
        if (this.circuitState === "half-open") {
            logger.info(`[${this.name}] Circuit closed after successful request`);
        }
        this.failures = 0;
        this.circuitState = "closed";
    }
    getCircuitState() {
        return this.circuitState;
    }
    reset() {
        this.failures = 0;
        this.circuitState = "closed";
        this.lastFailure = 0;
    }
}
/**
 * Retry executor - executes operations with retry policy
 */
export class RetryExecutor {
    policy;
    constructor(policy) {
        this.policy = policy;
    }
    /**
     * Execute an operation with retry
     * @param operation - The async operation to execute
     * @param operationName - Name for logging
     * @returns The result of the operation
     * @throws The last error if all retries fail
     */
    async execute(operation, operationName) {
        const startTime = Date.now();
        let lastError;
        let attempt = 0;
        while (attempt < this.policy.maxAttempts) {
            try {
                const result = await operation();
                // Record success if policy supports it
                if (this.policy instanceof CircuitBreakerAwarePolicy) {
                    this.policy.recordSuccess();
                }
                return result;
            }
            catch (error) {
                lastError = error;
                const elapsed = Date.now() - startTime;
                const decision = this.policy.shouldRetry({
                    attempt,
                    error: lastError,
                    elapsedMs: elapsed,
                    operationName,
                });
                if (!decision.shouldRetry) {
                    logger.warn(`[RetryExecutor] ${operationName}: ${decision.reason}`);
                    break;
                }
                logger.debug(`[RetryExecutor] ${operationName}: ${decision.reason}`);
                await this.delay(decision.delayMs);
                attempt++;
            }
        }
        throw (lastError ??
            new Error(`${operationName} failed after ${attempt} attempts`));
    }
    delay(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }
}
/**
 * Factory for creating retry policies
 */
export class RetryPolicyFactory {
    /**
     * Create a default policy for production use
     */
    static createDefault() {
        return new ExponentialBackoffPolicy({
            maxAttempts: 3,
            baseDelayMs: 1000,
            maxDelayMs: 30000,
        });
    }
    /**
     * Create an aggressive policy for critical operations
     */
    static createAggressive() {
        return new ExponentialBackoffPolicy({
            maxAttempts: 5,
            maxTotalTimeMs: 120000,
            baseDelayMs: 500,
            maxDelayMs: 60000,
        });
    }
    /**
     * Create a conservative policy for non-critical operations
     */
    static createConservative() {
        return new LinearBackoffPolicy({
            maxAttempts: 2,
            maxTotalTimeMs: 10000,
            initialDelayMs: 500,
            delayIncrementMs: 500,
        });
    }
    /**
     * Create a circuit breaker aware policy
     */
    static createWithCircuitBreaker(config) {
        return new CircuitBreakerAwarePolicy({
            innerPolicy: RetryPolicyFactory.createDefault(),
            failureThreshold: config?.failureThreshold ?? 5,
            resetTimeoutMs: config?.resetTimeoutMs ?? 30000,
        });
    }
}
//# sourceMappingURL=retryPolicy.js.map