/**
 * Request Optimizer
 *
 * Optimizes API requests through:
 * - Request batching
 * - Deduplication of concurrent requests
 * - Retry with exponential backoff
 * - Request prioritization
 */
import { EventEmitter } from 'events';
// ============================================================================
// Default Configuration
// ============================================================================
const DEFAULT_CONFIG = {
    maxConcurrent: 5,
    batchWindowMs: 50,
    maxRetries: 3,
    retryBaseDelayMs: 1000,
    timeoutMs: 30000,
    deduplicate: true,
};
// ============================================================================
// Request Optimizer Class
// ============================================================================
export class RequestOptimizer extends EventEmitter {
    config;
    queue = [];
    running = new Map();
    pendingDedup = new Map();
    stats;
    latencies = [];
    batchTimeout = null;
    processing = false;
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_CONFIG, ...config };
        this.stats = {
            totalRequests: 0,
            successfulRequests: 0,
            failedRequests: 0,
            retriedRequests: 0,
            deduplicatedRequests: 0,
            averageLatency: 0,
            currentConcurrency: 0,
        };
    }
    /**
     * Execute a request with optimization
     */
    async execute(key, executeFn, options = {}) {
        const { priority = 0, deduplicate = this.config.deduplicate } = options;
        // Check for duplicate pending request
        if (deduplicate && this.pendingDedup.has(key)) {
            this.stats.deduplicatedRequests++;
            this.emit('deduplicated', { key });
            return this.pendingDedup.get(key);
        }
        // Create pending request
        const promise = new Promise((resolve, reject) => {
            const request = {
                id: `${key}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
                key,
                priority,
                execute: executeFn,
                resolve: resolve,
                reject,
                retries: 0,
                createdAt: Date.now(),
            };
            this.queue.push(request);
            this.stats.totalRequests++;
        });
        // Track for deduplication
        if (deduplicate) {
            this.pendingDedup.set(key, promise);
            promise.finally(() => this.pendingDedup.delete(key));
        }
        // Schedule batch processing
        this.scheduleBatchProcess();
        return promise;
    }
    /**
     * Execute immediately without queuing
     */
    async executeImmediate(executeFn, options = {}) {
        const { retries = this.config.maxRetries, timeout = this.config.timeoutMs } = options;
        return this.executeWithRetry(executeFn, retries, timeout);
    }
    /**
     * Get current statistics
     */
    getStats() {
        return {
            ...this.stats,
            currentConcurrency: this.running.size,
        };
    }
    /**
     * Reset statistics
     */
    resetStats() {
        this.stats = {
            totalRequests: 0,
            successfulRequests: 0,
            failedRequests: 0,
            retriedRequests: 0,
            deduplicatedRequests: 0,
            averageLatency: 0,
            currentConcurrency: 0,
        };
        this.latencies = [];
    }
    /**
     * Clear pending requests
     */
    clear() {
        for (const request of this.queue) {
            request.reject(new Error('Request cancelled'));
        }
        this.queue = [];
        this.pendingDedup.clear();
        if (this.batchTimeout) {
            clearTimeout(this.batchTimeout);
            this.batchTimeout = null;
        }
    }
    /**
     * Get queue status
     */
    getQueueStatus() {
        return {
            pending: this.queue.length,
            running: this.running.size,
            maxConcurrent: this.config.maxConcurrent,
        };
    }
    // ============================================================================
    // Private Methods
    // ============================================================================
    scheduleBatchProcess() {
        if (this.batchTimeout)
            return;
        this.batchTimeout = setTimeout(() => {
            this.batchTimeout = null;
            this.processQueue();
        }, this.config.batchWindowMs);
    }
    async processQueue() {
        if (this.processing)
            return;
        this.processing = true;
        try {
            // Sort by priority (higher first)
            this.queue.sort((a, b) => b.priority - a.priority);
            while (this.queue.length > 0 && this.running.size < this.config.maxConcurrent) {
                const request = this.queue.shift();
                if (!request)
                    continue;
                this.running.set(request.id, request);
                this.stats.currentConcurrency = this.running.size;
                this.processRequest(request).finally(() => {
                    this.running.delete(request.id);
                    this.stats.currentConcurrency = this.running.size;
                    // Continue processing if queue has items
                    if (this.queue.length > 0) {
                        this.processQueue();
                    }
                });
            }
        }
        finally {
            this.processing = false;
        }
    }
    async processRequest(request) {
        const startTime = Date.now();
        try {
            const result = await this.executeWithRetry(request.execute, this.config.maxRetries - request.retries, this.config.timeoutMs);
            const latency = Date.now() - startTime;
            this.recordLatency(latency);
            this.stats.successfulRequests++;
            request.resolve(result);
            this.emit('success', {
                key: request.key,
                latency,
                retries: request.retries,
            });
        }
        catch (error) {
            this.stats.failedRequests++;
            request.reject(error);
            this.emit('failure', {
                key: request.key,
                error,
                retries: request.retries,
            });
        }
    }
    async executeWithRetry(executeFn, maxRetries, timeout) {
        let lastError = null;
        for (let attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                // Execute with timeout
                const result = await this.withTimeout(executeFn(), timeout);
                return result;
            }
            catch (error) {
                lastError = error;
                if (attempt < maxRetries) {
                    this.stats.retriedRequests++;
                    // Exponential backoff
                    const delay = this.config.retryBaseDelayMs * Math.pow(2, attempt);
                    await this.delay(delay);
                    this.emit('retry', { attempt: attempt + 1, maxRetries, error });
                }
            }
        }
        throw lastError || new Error('Request failed after retries');
    }
    withTimeout(promise, timeoutMs) {
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new Error(`Request timed out after ${timeoutMs}ms`));
            }, timeoutMs);
            promise
                .then((result) => {
                clearTimeout(timer);
                resolve(result);
            })
                .catch((error) => {
                clearTimeout(timer);
                reject(error);
            });
        });
    }
    delay(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }
    recordLatency(latency) {
        this.latencies.push(latency);
        // Keep only last 100 latencies
        if (this.latencies.length > 100) {
            this.latencies = this.latencies.slice(-100);
        }
        // Update average
        this.stats.averageLatency =
            this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length;
    }
}
// ============================================================================
// Singleton Instance
// ============================================================================
let optimizerInstance = null;
export function getRequestOptimizer(config) {
    if (!optimizerInstance) {
        optimizerInstance = new RequestOptimizer(config);
    }
    return optimizerInstance;
}
export function resetRequestOptimizer() {
    if (optimizerInstance) {
        optimizerInstance.clear();
    }
    optimizerInstance = null;
}
// ============================================================================
// Utility Functions
// ============================================================================
/**
 * Execute multiple requests in parallel with concurrency limit
 */
export async function executeParallel(requests, options = {}) {
    const optimizer = getRequestOptimizer({
        maxConcurrent: options.maxConcurrent || 5,
    });
    const results = new Map();
    await Promise.all(requests.map(async (req) => {
        try {
            const result = await optimizer.execute(req.key, req.execute, {
                priority: req.priority,
            });
            results.set(req.key, result);
        }
        catch (error) {
            results.set(req.key, error);
        }
    }));
    return results;
}
/**
 * Batch requests with automatic deduplication
 */
export function batchRequests(keys, batchFn) {
    const optimizer = getRequestOptimizer();
    const batchKey = keys.sort().join(',');
    return optimizer.execute(batchKey, () => batchFn(keys));
}
//# sourceMappingURL=request-optimizer.js.map