// engine/token-optimizer/response-cache.ts — LRU cache with content-hash keys (SPEC-082 AC-02, AC-10)
import { createHash } from 'node:crypto';
import { countTokens } from './counter.js';
const DEFAULT_CACHE_CONFIG = {
    defaultTtlMs: 5 * 60 * 1000, // 5 minutes
    maxEntries: 100,
    maxPersistentSizeMb: 5,
    persistMinTtlMs: 60 * 60 * 1000, // 1 hour
};
/**
 * In-memory LRU response cache with content-hash keys and TTL support.
 */
export class ResponseCache {
    entries = new Map();
    config;
    hits = 0;
    misses = 0;
    totalTokensSaved = 0;
    constructor(config) {
        this.config = { ...DEFAULT_CACHE_CONFIG, ...config };
    }
    /**
     * Generate a content-hash key from input text.
     */
    static hashKey(input) {
        return createHash('sha256').update(input).digest('hex').slice(0, 16);
    }
    /**
     * Get a cached response by key. Returns undefined on miss.
     */
    get(key) {
        const entry = this.entries.get(key);
        if (!entry) {
            this.misses++;
            return undefined;
        }
        // Check TTL expiration
        if (Date.now() > entry.expiresAt) {
            this.entries.delete(key);
            this.misses++;
            return undefined;
        }
        // LRU: move to end
        this.entries.delete(key);
        entry.hitCount++;
        const savedTokens = countTokens(entry.value).tokens;
        entry.tokensSaved += savedTokens;
        this.totalTokensSaved += savedTokens;
        this.entries.set(key, entry);
        this.hits++;
        return entry.value;
    }
    /**
     * Cache a response with optional TTL override.
     */
    set(key, value, ttlMs) {
        // Evict LRU if at capacity
        if (this.entries.size >= this.config.maxEntries && !this.entries.has(key)) {
            const firstKey = this.entries.keys().next().value;
            if (firstKey !== undefined) {
                this.entries.delete(firstKey);
            }
        }
        const now = Date.now();
        const entry = {
            key,
            value,
            hash: ResponseCache.hashKey(value),
            createdAt: now,
            expiresAt: now + (ttlMs ?? this.config.defaultTtlMs),
            hitCount: 0,
            tokensSaved: 0,
        };
        this.entries.set(key, entry);
    }
    /**
     * Invalidate a specific cache entry or entries matching a pattern.
     */
    invalidate(keyOrPattern) {
        // Direct key match
        if (this.entries.has(keyOrPattern)) {
            this.entries.delete(keyOrPattern);
            return 1;
        }
        // Glob-like pattern: escape regex metacharacters, then restore glob wildcards.
        // This prevents ReDoS from user-supplied patterns with special regex chars.
        const escaped = RegExp.escape(keyOrPattern).replace(/\\\*/g, '.*').replace(/\\\?/g, '.');
        const regex = new RegExp(`^${escaped}$`);
        let count = 0;
        for (const key of [...this.entries.keys()]) {
            if (regex.test(key)) {
                this.entries.delete(key);
                count++;
            }
        }
        return count;
    }
    /**
     * Get cache statistics.
     */
    stats() {
        const total = this.hits + this.misses;
        return {
            hits: this.hits,
            misses: this.misses,
            entries: this.entries.size,
            totalTokensSaved: this.totalTokensSaved,
            hitRate: total > 0 ? this.hits / total : 0,
        };
    }
    /**
     * Remove all expired entries.
     */
    cleanup() {
        const now = Date.now();
        let removed = 0;
        for (const [key, entry] of this.entries) {
            if (now > entry.expiresAt) {
                this.entries.delete(key);
                removed++;
            }
        }
        return removed;
    }
    /**
     * Clear all entries and reset stats.
     */
    clear() {
        this.entries.clear();
        this.hits = 0;
        this.misses = 0;
        this.totalTokensSaved = 0;
    }
    /**
     * Get entries suitable for persistence (TTL > persistMinTtlMs).
     */
    getPersistableEntries() {
        const now = Date.now();
        const result = [];
        for (const entry of this.entries.values()) {
            const remainingTtl = entry.expiresAt - now;
            if (remainingTtl > this.config.persistMinTtlMs) {
                result.push({ ...entry });
            }
        }
        return result;
    }
    /**
     * Load entries from persistence.
     */
    loadEntries(entries) {
        const now = Date.now();
        for (const entry of entries) {
            if (now < entry.expiresAt) {
                this.entries.set(entry.key, entry);
            }
        }
    }
}
//# sourceMappingURL=response-cache.js.map