/**
 * Retry Policy Module
 *
 * Provides exponential backoff retry logic, circuit breaker pattern,
 * and comprehensive error recovery for streaming operations.
 */
import { EventEmitter } from 'events';
export const DEFAULT_RETRY_CONFIG = {
    maxAttempts: 3,
    initialDelayMs: 1000,
    maxDelayMs: 30000,
    backoffMultiplier: 2,
    jitter: true,
    jitterFactor: 0.3,
    attemptTimeoutMs: 60000,
    retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'RATE_LIMIT', 'SERVICE_UNAVAILABLE'],
    nonRetryableErrors: ['AUTH_FAILED', 'INVALID_REQUEST', 'NOT_FOUND'],
};
export const DEFAULT_CIRCUIT_BREAKER_CONFIG = {
    failureThreshold: 5,
    failureWindowMs: 60000,
    resetTimeoutMs: 30000,
    successThreshold: 3,
    probeTimeoutMs: 10000,
};
// ============================================================================
// Retry Utilities
// ============================================================================
/**
 * Calculate delay for a given attempt with exponential backoff
 */
export function calculateDelay(attempt, config) {
    const baseDelay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1);
    const delay = Math.min(baseDelay, config.maxDelayMs);
    if (config.jitter) {
        const jitterRange = delay * config.jitterFactor;
        const jitter = (Math.random() - 0.5) * 2 * jitterRange;
        return Math.max(0, Math.round(delay + jitter));
    }
    return Math.round(delay);
}
/**
 * Check if an error is retryable based on config
 */
export function isRetryable(error, config) {
    const errorCode = error.code || error.name || '';
    const errorMessage = error.message || '';
    // Check non-retryable first
    if (config.nonRetryableErrors) {
        for (const pattern of config.nonRetryableErrors) {
            if (errorCode.includes(pattern) || errorMessage.includes(pattern)) {
                return false;
            }
        }
    }
    // Check retryable
    if (config.retryableErrors) {
        for (const pattern of config.retryableErrors) {
            if (errorCode.includes(pattern) || errorMessage.includes(pattern)) {
                return true;
            }
        }
    }
    // Default: retry network errors
    return errorCode.startsWith('E') || errorMessage.includes('timeout');
}
/**
 * Sleep for a given duration
 */
export function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
/**
 * Create a timeout promise
 */
export function withTimeout(promise, timeoutMs, message = 'Operation timed out') {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            reject(new Error(`${message} after ${timeoutMs}ms`));
        }, timeoutMs);
        promise
            .then(result => {
            clearTimeout(timer);
            resolve(result);
        })
            .catch(error => {
            clearTimeout(timer);
            reject(error);
        });
    });
}
// ============================================================================
// Retry Executor
// ============================================================================
/**
 * Execute an operation with retry logic
 */
export async function retry(operation, config = {}) {
    const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
    const attempts = [];
    const startTime = Date.now();
    for (let attempt = 1; attempt <= fullConfig.maxAttempts; attempt++) {
        const attemptRecord = {
            attempt,
            startTime: Date.now(),
            success: false,
        };
        try {
            let result;
            if (fullConfig.attemptTimeoutMs) {
                result = await withTimeout(operation(), fullConfig.attemptTimeoutMs);
            }
            else {
                result = await operation();
            }
            attemptRecord.success = true;
            attemptRecord.endTime = Date.now();
            attempts.push(attemptRecord);
            return {
                success: true,
                value: result,
                attempts,
                totalDurationMs: Date.now() - startTime,
            };
        }
        catch (error) {
            const err = error instanceof Error ? error : new Error(String(error));
            attemptRecord.error = err;
            attemptRecord.endTime = Date.now();
            attempts.push(attemptRecord);
            // Check if we should retry
            if (attempt < fullConfig.maxAttempts && isRetryable(err, fullConfig)) {
                const delay = calculateDelay(attempt, fullConfig);
                attemptRecord.delayMs = delay;
                await sleep(delay);
            }
            else {
                // No more retries
                return {
                    success: false,
                    error: err,
                    attempts,
                    totalDurationMs: Date.now() - startTime,
                };
            }
        }
    }
    // Should not reach here
    return {
        success: false,
        error: new Error('Max retry attempts exceeded'),
        attempts,
        totalDurationMs: Date.now() - startTime,
    };
}
/**
 * Execute with retry and return value or throw
 */
export async function retryOrThrow(operation, config = {}) {
    const result = await retry(operation, config);
    if (!result.success) {
        const error = result.error || new Error('Operation failed');
        error.retryAttempts = result.attempts;
        throw error;
    }
    return result.value;
}
// ============================================================================
// Circuit Breaker
// ============================================================================
export class CircuitBreaker extends EventEmitter {
    config;
    state = 'closed';
    failures = [];
    successes = 0;
    openedAt;
    stats = {
        totalRequests: 0,
        successfulRequests: 0,
        failedRequests: 0,
        rejectedRequests: 0,
    };
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config };
    }
    /**
     * Execute an operation through the circuit breaker
     */
    async execute(operation) {
        this.stats.totalRequests++;
        // Check circuit state
        if (this.state === 'open') {
            // Check if we should transition to half-open
            if (this.shouldProbe()) {
                this.transitionTo('half-open');
            }
            else {
                this.stats.rejectedRequests++;
                this.emit('rejected');
                throw new CircuitOpenError('Circuit breaker is open');
            }
        }
        try {
            let result;
            if (this.state === 'half-open') {
                this.emit('probe');
                result = await withTimeout(operation(), this.config.probeTimeoutMs);
            }
            else {
                result = await operation();
            }
            this.recordSuccess();
            return result;
        }
        catch (error) {
            const err = error instanceof Error ? error : new Error(String(error));
            this.recordFailure(err);
            throw err;
        }
    }
    /**
     * Record a successful operation
     */
    recordSuccess() {
        this.stats.successfulRequests++;
        this.emit('success');
        if (this.state === 'half-open') {
            this.successes++;
            if (this.successes >= this.config.successThreshold) {
                this.transitionTo('closed');
            }
        }
        // Clear old failures
        this.cleanupFailures();
    }
    /**
     * Record a failed operation
     */
    recordFailure(error) {
        this.stats.failedRequests++;
        this.emit('failure', error);
        const now = Date.now();
        this.failures.push({ time: now, error });
        this.cleanupFailures();
        if (this.state === 'half-open') {
            // Any failure in half-open goes back to open
            this.transitionTo('open');
        }
        else if (this.state === 'closed') {
            // Check if we need to open
            const recentFailures = this.failures.filter(f => f.time > now - this.config.failureWindowMs);
            if (recentFailures.length >= this.config.failureThreshold) {
                this.transitionTo('open');
            }
        }
    }
    /**
     * Check if we should probe (transition from open to half-open)
     */
    shouldProbe() {
        if (!this.openedAt)
            return false;
        return Date.now() - this.openedAt >= this.config.resetTimeoutMs;
    }
    /**
     * Transition to a new state
     */
    transitionTo(newState) {
        const oldState = this.state;
        this.state = newState;
        if (newState === 'open') {
            this.openedAt = Date.now();
            this.successes = 0;
        }
        else if (newState === 'closed') {
            this.failures = [];
            this.successes = 0;
            this.openedAt = undefined;
        }
        else if (newState === 'half-open') {
            this.successes = 0;
        }
        this.emit('state-change', oldState, newState);
    }
    /**
     * Remove old failures outside the window
     */
    cleanupFailures() {
        const cutoff = Date.now() - this.config.failureWindowMs;
        this.failures = this.failures.filter(f => f.time > cutoff);
    }
    /**
     * Get current state
     */
    getState() {
        // Check if we should auto-transition to half-open
        if (this.state === 'open' && this.shouldProbe()) {
            this.transitionTo('half-open');
        }
        return this.state;
    }
    /**
     * Get statistics
     */
    getStats() {
        return {
            state: this.getState(),
            failures: this.failures.length,
            successes: this.successes,
            lastFailureTime: this.failures[this.failures.length - 1]?.time,
            lastSuccessTime: undefined, // Not tracked
            openedAt: this.openedAt,
            ...this.stats,
        };
    }
    /**
     * Manually reset the circuit breaker
     */
    reset() {
        this.transitionTo('closed');
        this.stats = {
            totalRequests: 0,
            successfulRequests: 0,
            failedRequests: 0,
            rejectedRequests: 0,
        };
    }
    /**
     * Force open the circuit
     */
    trip() {
        if (this.state !== 'open') {
            this.transitionTo('open');
        }
    }
}
/**
 * Error thrown when circuit is open
 */
export class CircuitOpenError extends Error {
    constructor(message) {
        super(message);
        this.name = 'CircuitOpenError';
    }
}
export const DEFAULT_RETRY_MANAGER_CONFIG = {
    retry: {},
    circuitBreaker: {},
    useCircuitBreaker: true,
};
export class RetryManager extends EventEmitter {
    config;
    circuitBreakers = new Map();
    constructor(config = {}) {
        super();
        this.config = {
            retry: { ...DEFAULT_RETRY_CONFIG, ...config.retry },
            circuitBreaker: { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config.circuitBreaker },
            useCircuitBreaker: config.useCircuitBreaker ?? true,
        };
    }
    /**
     * Get or create a circuit breaker for a service
     */
    getCircuitBreaker(serviceId) {
        let breaker = this.circuitBreakers.get(serviceId);
        if (!breaker) {
            breaker = new CircuitBreaker(this.config.circuitBreaker);
            // Forward events
            breaker.on('state-change', (from, to) => {
                this.emit('circuit-state-change', serviceId, from, to);
            });
            breaker.on('failure', (error) => {
                this.emit('circuit-failure', serviceId, error);
            });
            breaker.on('rejected', () => {
                this.emit('circuit-rejected', serviceId);
            });
            this.circuitBreakers.set(serviceId, breaker);
        }
        return breaker;
    }
    /**
     * Execute an operation with retry and circuit breaker
     */
    async execute(serviceId, operation, config) {
        const retryConfig = { ...this.config.retry, ...config };
        if (!this.config.useCircuitBreaker) {
            return retry(operation, retryConfig);
        }
        const breaker = this.getCircuitBreaker(serviceId);
        return retry(async () => {
            return breaker.execute(operation);
        }, retryConfig);
    }
    /**
     * Execute and return value or throw
     */
    async executeOrThrow(serviceId, operation, config) {
        const result = await this.execute(serviceId, operation, config);
        if (!result.success) {
            throw result.error;
        }
        return result.value;
    }
    /**
     * Get circuit breaker state for a service
     */
    getCircuitState(serviceId) {
        return this.circuitBreakers.get(serviceId)?.getState();
    }
    /**
     * Get circuit breaker stats for a service
     */
    getCircuitStats(serviceId) {
        return this.circuitBreakers.get(serviceId)?.getStats();
    }
    /**
     * Get all circuit breaker stats
     */
    getAllStats() {
        const stats = new Map();
        for (const [id, breaker] of this.circuitBreakers) {
            stats.set(id, breaker.getStats());
        }
        return stats;
    }
    /**
     * Reset a specific circuit breaker
     */
    resetCircuit(serviceId) {
        const breaker = this.circuitBreakers.get(serviceId);
        if (breaker) {
            breaker.reset();
            return true;
        }
        return false;
    }
    /**
     * Reset all circuit breakers
     */
    resetAllCircuits() {
        for (const breaker of this.circuitBreakers.values()) {
            breaker.reset();
        }
    }
    /**
     * Clear a circuit breaker
     */
    clearCircuit(serviceId) {
        return this.circuitBreakers.delete(serviceId);
    }
    /**
     * Clear all circuit breakers
     */
    clearAllCircuits() {
        this.circuitBreakers.clear();
    }
}
// ============================================================================
// Singleton
// ============================================================================
let retryManagerInstance = null;
export function getRetryManager(config) {
    if (!retryManagerInstance) {
        retryManagerInstance = new RetryManager(config);
    }
    return retryManagerInstance;
}
export function resetRetryManager() {
    if (retryManagerInstance) {
        retryManagerInstance.clearAllCircuits();
        retryManagerInstance = null;
    }
}
// ============================================================================
// Convenience Decorators
// ============================================================================
/**
 * Create a retryable version of a function
 */
export function withRetry(fn, config) {
    return (async (...args) => {
        return retryOrThrow(() => fn(...args), config);
    });
}
/**
 * Create a function protected by circuit breaker
 */
export function withCircuitBreaker(fn, serviceId, config) {
    const breaker = new CircuitBreaker(config);
    return (async (...args) => {
        return breaker.execute(() => fn(...args));
    });
}
/**
 * Create a function with both retry and circuit breaker
 */
export function withRetryAndCircuitBreaker(fn, serviceId, config) {
    const manager = new RetryManager(config);
    return (async (...args) => {
        return manager.executeOrThrow(serviceId, () => fn(...args));
    });
}
//# sourceMappingURL=retry-policy.js.map