/**
 * Tool Cache - Caches tool results and server responses
 *
 * Provides intelligent caching for MCP tool calls to improve performance
 * and reduce redundant operations. Supports multiple eviction strategies:
 * - LRU (Least Recently Used)
 * - FIFO (First In, First Out)
 * - LFU (Least Frequently Used)
 */
import { createHash } from "crypto";
import { EventEmitter } from "events";
import { withTimeout } from "../../utils/async/withTimeout.js";
/**
 * Tool Cache - High-performance caching for MCP tool results
 *
 * @example
 * ```typescript
 * const cache = new ToolCache({
 *   ttl: 60000, // 1 minute
 *   maxSize: 500,
 *   strategy: 'lru',
 * });
 *
 * // Cache a tool result
 * cache.set('getUserById:123', { id: 123, name: 'John' });
 *
 * // Retrieve from cache
 * const user = cache.get('getUserById:123');
 *
 * // Invalidate by pattern
 * cache.invalidate('getUserById:*');
 * ```
 */
export class ToolCache extends EventEmitter {
    cache = new Map();
    config;
    stats;
    cleanupTimer;
    constructor(config) {
        super();
        this.config = {
            ttl: config.ttl,
            maxSize: config.maxSize,
            strategy: config.strategy,
            enableAutoCleanup: config.enableAutoCleanup ?? true,
            cleanupInterval: config.cleanupInterval ?? 60000,
            namespace: config.namespace ?? "",
        };
        this.stats = {
            hits: 0,
            misses: 0,
            evictions: 0,
            size: 0,
            maxSize: this.config.maxSize,
            hitRate: 0,
        };
        if (this.config.enableAutoCleanup) {
            this.startAutoCleanup();
        }
    }
    /**
     * Get a value from the cache
     */
    get(key) {
        const fullKey = this.getFullKey(key);
        const entry = this.cache.get(fullKey);
        if (!entry) {
            this.stats.misses++;
            this.updateHitRate();
            this.emit("miss", { key: fullKey });
            return undefined;
        }
        // Check expiration
        if (this.isExpired(entry)) {
            this.deleteWithReason(fullKey, "expired");
            this.stats.misses++;
            this.updateHitRate();
            this.emit("miss", { key: fullKey });
            return undefined;
        }
        // Update access metadata
        entry.accessedAt = Date.now();
        entry.accessCount++;
        this.stats.hits++;
        this.updateHitRate();
        this.emit("hit", { key: fullKey, value: entry.value });
        return entry.value;
    }
    /**
     * Set a value in the cache
     */
    set(key, value, ttl) {
        const fullKey = this.getFullKey(key);
        const effectiveTtl = ttl ?? this.config.ttl;
        const now = Date.now();
        // Evict if at capacity
        if (this.cache.size >= this.config.maxSize && !this.cache.has(fullKey)) {
            this.evictOne();
        }
        const entry = {
            value,
            expires: now + effectiveTtl,
            createdAt: now,
            accessedAt: now,
            accessCount: 1,
            key: fullKey,
        };
        this.cache.set(fullKey, entry);
        this.stats.size = this.cache.size;
        this.emit("set", { key: fullKey, value, ttl: effectiveTtl });
    }
    /**
     * Check if a key exists and is not expired
     */
    has(key) {
        const fullKey = this.getFullKey(key);
        const entry = this.cache.get(fullKey);
        if (!entry) {
            return false;
        }
        if (this.isExpired(entry)) {
            this.deleteWithReason(fullKey, "expired");
            return false;
        }
        return true;
    }
    /**
     * Delete a specific key from the cache
     */
    delete(key) {
        const fullKey = this.getFullKey(key);
        const deleted = this.cache.delete(fullKey);
        if (deleted) {
            this.stats.size = this.cache.size;
            this.emit("evict", { key: fullKey, reason: "manual" });
        }
        return deleted;
    }
    /**
     * Invalidate entries matching a pattern
     * Supports glob-style patterns with * wildcard
     */
    invalidate(pattern) {
        const fullPattern = this.getFullKey(pattern);
        const regex = this.patternToRegex(fullPattern);
        let invalidated = 0;
        for (const key of this.cache.keys()) {
            if (regex.test(key)) {
                this.cache.delete(key);
                invalidated++;
                this.emit("evict", { key, reason: "manual" });
            }
        }
        this.stats.size = this.cache.size;
        return invalidated;
    }
    /**
     * Clear all entries from the cache
     */
    clear() {
        const entriesRemoved = this.cache.size;
        this.cache.clear();
        this.stats.size = 0;
        this.emit("clear", { entriesRemoved });
    }
    /**
     * Get or set a value (cache-aside pattern)
     */
    async getOrSet(key, factory, ttl) {
        const existing = this.get(key);
        if (existing !== undefined) {
            return existing;
        }
        const factoryTimeoutMs = 30_000;
        const value = await withTimeout(Promise.resolve(factory()), factoryTimeoutMs, `ToolCache getOrSet factory timed out after ${factoryTimeoutMs}ms for key "${key}"`);
        if (value === undefined) {
            return value;
        }
        this.set(key, value, ttl);
        return value;
    }
    /**
     * Get cache statistics
     */
    getStats() {
        return { ...this.stats };
    }
    /**
     * Reset statistics
     */
    resetStats() {
        this.stats.hits = 0;
        this.stats.misses = 0;
        this.stats.evictions = 0;
        this.updateHitRate();
    }
    /**
     * Get all keys in the cache
     */
    keys() {
        return Array.from(this.cache.keys());
    }
    /**
     * Get the number of entries in the cache
     */
    get size() {
        return this.cache.size;
    }
    /**
     * Generate a cache key from tool name and arguments
     */
    static generateKey(toolName, args) {
        const stableStringify = (value, seen = new WeakSet()) => {
            if (value === null || typeof value !== "object") {
                return JSON.stringify(value);
            }
            if (value instanceof Date) {
                return `{"$date":${JSON.stringify(value.toISOString())}}`;
            }
            if (seen.has(value)) {
                throw new TypeError("Circular structures are not supported in cache keys");
            }
            seen.add(value);
            if (Array.isArray(value)) {
                const result = "[" + value.map((v) => stableStringify(v, seen)).join(",") + "]";
                seen.delete(value);
                return result;
            }
            const sortedKeys = Object.keys(value).sort();
            const entries = sortedKeys.map((k) => JSON.stringify(k) +
                ":" +
                stableStringify(value[k], seen));
            seen.delete(value);
            return "{" + entries.join(",") + "}";
        };
        const argsHash = createHash("sha256")
            .update(stableStringify(args))
            .digest("hex")
            .substring(0, 16);
        return `${toolName}:${argsHash}`;
    }
    /**
     * Stop the auto-cleanup timer
     */
    destroy() {
        if (this.cleanupTimer) {
            clearInterval(this.cleanupTimer);
            this.cleanupTimer = undefined;
        }
        this.clear();
    }
    // ==================== Private Methods ====================
    getFullKey(key) {
        return this.config.namespace ? `${this.config.namespace}:${key}` : key;
    }
    isExpired(entry) {
        return Date.now() > entry.expires;
    }
    /**
     * Delete a cache entry by its full key with a specific eviction reason.
     */
    deleteWithReason(fullKey, reason) {
        const deleted = this.cache.delete(fullKey);
        if (deleted) {
            this.stats.evictions++;
            this.stats.size = this.cache.size;
            this.emit("evict", { key: fullKey, reason });
        }
        return deleted;
    }
    evictOne() {
        const entryToEvict = this.selectEvictionCandidate();
        if (entryToEvict) {
            this.cache.delete(entryToEvict.key);
            this.stats.evictions++;
            this.stats.size = this.cache.size;
            this.emit("evict", { key: entryToEvict.key, reason: "capacity" });
        }
    }
    selectEvictionCandidate() {
        if (this.cache.size === 0) {
            return undefined;
        }
        switch (this.config.strategy) {
            case "lru":
                return this.findLRU();
            case "fifo":
                return this.findFIFO();
            case "lfu":
                return this.findLFU();
            default:
                return this.findLRU();
        }
    }
    findLRU() {
        let oldest;
        let oldestTime = Infinity;
        for (const entry of this.cache.values()) {
            if (entry.accessedAt < oldestTime) {
                oldestTime = entry.accessedAt;
                oldest = entry;
            }
        }
        return oldest;
    }
    findFIFO() {
        let oldest;
        let oldestTime = Infinity;
        for (const entry of this.cache.values()) {
            if (entry.createdAt < oldestTime) {
                oldestTime = entry.createdAt;
                oldest = entry;
            }
        }
        return oldest;
    }
    findLFU() {
        let leastFrequent;
        let lowestCount = Infinity;
        for (const entry of this.cache.values()) {
            if (entry.accessCount < lowestCount) {
                lowestCount = entry.accessCount;
                leastFrequent = entry;
            }
        }
        return leastFrequent;
    }
    patternToRegex(pattern) {
        const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
        const regexPattern = escaped.replace(/\*/g, "[^:]*");
        return new RegExp(`^${regexPattern}$`);
    }
    updateHitRate() {
        const total = this.stats.hits + this.stats.misses;
        this.stats.hitRate = total > 0 ? this.stats.hits / total : 0;
    }
    startAutoCleanup() {
        this.cleanupTimer = setInterval(() => {
            this.cleanupExpired();
        }, this.config.cleanupInterval);
        // Don't keep the process alive just for cleanup
        if (this.cleanupTimer.unref) {
            this.cleanupTimer.unref();
        }
    }
    cleanupExpired() {
        const now = Date.now();
        for (const [key, entry] of this.cache.entries()) {
            if (now > entry.expires) {
                this.cache.delete(key);
                this.stats.evictions++;
                this.emit("evict", { key, reason: "expired" });
            }
        }
        this.stats.size = this.cache.size;
    }
}
/**
 * Factory function to create a ToolCache instance
 */
export const createToolCache = (config) => new ToolCache(config);
/**
 * Default cache configuration
 */
export const DEFAULT_CACHE_CONFIG = {
    ttl: 5 * 60 * 1000, // 5 minutes
    maxSize: 500,
    strategy: "lru",
    enableAutoCleanup: true,
    cleanupInterval: 60000, // 1 minute
};
/**
 * Tool-specific cache wrapper with automatic key generation
 */
export class ToolResultCache {
    cache;
    constructor(config) {
        this.cache = new ToolCache({
            ...DEFAULT_CACHE_CONFIG,
            ...config,
            namespace: config?.namespace ?? "tool-results",
        });
    }
    /**
     * Cache a tool result
     */
    cacheResult(toolName, args, result, ttl) {
        const key = ToolCache.generateKey(toolName, args);
        this.cache.set(key, result, ttl);
    }
    /**
     * Get a cached tool result
     */
    getCachedResult(toolName, args) {
        const key = ToolCache.generateKey(toolName, args);
        return this.cache.get(key);
    }
    /**
     * Check if a result is cached
     */
    hasCachedResult(toolName, args) {
        const key = ToolCache.generateKey(toolName, args);
        return this.cache.has(key);
    }
    /**
     * Invalidate all cached results for a tool
     */
    invalidateTool(toolName) {
        return this.cache.invalidate(`${toolName}:*`);
    }
    /**
     * Get cache statistics
     */
    getStats() {
        return this.cache.getStats();
    }
    /**
     * Clear all cached results
     */
    clear() {
        this.cache.clear();
    }
    /**
     * Destroy the cache
     */
    destroy() {
        this.cache.destroy();
    }
}
/**
 * Create a tool result cache instance
 */
export const createToolResultCache = (config) => new ToolResultCache(config);
