"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.OperationCache = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const OperationFingerprint_1 = require("./OperationFingerprint");
/**
 * OperationCache Service
 * Provides caching and replay capabilities for operations
 */
class OperationCache {
    constructor(baseDir, fingerprintService) {
        // Configuration
        this.maxCacheSize = 100 * 1024 * 1024; // 100MB
        this.defaultTTL = 24 * 60 * 60 * 1000; // 24 hours
        this.maxEntries = 10000;
        this.baseDir = baseDir || process.cwd();
        this.cachePath = path.join(this.baseDir, ".mycontext", "operation-cache.json");
        this.cache = new Map();
        this.fingerprintService =
            fingerprintService || new OperationFingerprint_1.OperationFingerprintService(this.baseDir);
        this.stats = { hits: 0, misses: 0 };
    }
    /**
     * Initialize: Load cache from disk
     */
    async init() {
        await this.fingerprintService.init();
        if (await fs.pathExists(this.cachePath)) {
            try {
                const data = await fs.readJson(this.cachePath);
                this.cache = new Map(Object.entries(data.cache || {}));
                this.stats = data.stats || { hits: 0, misses: 0 };
            }
            catch (error) {
                console.warn("Failed to load operation cache, starting fresh:", error);
                this.cache = new Map();
            }
        }
        else {
            await fs.ensureDir(path.dirname(this.cachePath));
            await this.persist();
        }
        // Clean expired entries on init
        await this.cleanExpired();
    }
    /**
     * Get cached result for operation
     */
    async get(op) {
        const fingerprint = this.fingerprintService.generateFingerprint(op);
        const entry = this.cache.get(fingerprint);
        if (!entry) {
            this.stats.misses++;
            await this.persist();
            return null;
        }
        // Check if expired
        if (entry.metadata.expiresAt) {
            const expired = new Date(entry.metadata.expiresAt) < new Date();
            if (expired) {
                await this.invalidate(fingerprint, "expired");
                this.stats.misses++;
                await this.persist();
                return null;
            }
        }
        // Check if files have changed
        const filesChanged = await this.checkFileChanges(entry);
        if (filesChanged) {
            await this.invalidate(fingerprint, "file-changed");
            this.stats.misses++;
            await this.persist();
            return null;
        }
        // Cache hit!
        entry.metadata.hits++;
        entry.metadata.lastAccessed = new Date().toISOString();
        this.stats.hits++;
        await this.persist();
        return entry.result;
    }
    /**
     * Set cache entry for operation
     */
    async set(op, result, durationMs) {
        const fingerprint = this.fingerprintService.generateFingerprint(op);
        // Capture file hashes for affected files
        const fileHashes = await this.captureFileHashes(op, result);
        const entry = {
            fingerprint,
            operationId: op.id,
            operationType: op.type,
            result,
            context: {
                fileHashes,
                timestamp: new Date().toISOString(),
                durationMs,
            },
            metadata: {
                hits: 0,
                lastAccessed: new Date().toISOString(),
                createdAt: new Date().toISOString(),
                expiresAt: new Date(Date.now() + this.defaultTTL).toISOString(),
            },
        };
        this.cache.set(fingerprint, entry);
        // Enforce max entries
        if (this.cache.size > this.maxEntries) {
            await this.evictOldest();
        }
        await this.persist();
    }
    /**
     * Check if operation result is cached
     */
    async has(op) {
        const fingerprint = this.fingerprintService.generateFingerprint(op);
        const entry = this.cache.get(fingerprint);
        if (!entry)
            return false;
        // Check expiration
        if (entry.metadata.expiresAt) {
            const expired = new Date(entry.metadata.expiresAt) < new Date();
            if (expired) {
                await this.invalidate(fingerprint, "expired");
                return false;
            }
        }
        // Check file changes
        const filesChanged = await this.checkFileChanges(entry);
        if (filesChanged) {
            await this.invalidate(fingerprint, "file-changed");
            return false;
        }
        return true;
    }
    /**
     * Invalidate cache entry
     */
    async invalidate(fingerprint, reason) {
        this.cache.delete(fingerprint);
        await this.persist();
    }
    /**
     * Invalidate all entries for a specific operation type
     */
    async invalidateByType(operationType) {
        let count = 0;
        for (const [fingerprint, entry] of this.cache.entries()) {
            if (entry.operationType === operationType) {
                this.cache.delete(fingerprint);
                count++;
            }
        }
        await this.persist();
        return count;
    }
    /**
     * Invalidate all entries that touch a specific file
     */
    async invalidateByFile(filePath) {
        let count = 0;
        const absPath = path.resolve(this.baseDir, filePath);
        for (const [fingerprint, entry] of this.cache.entries()) {
            if (absPath in entry.context.fileHashes) {
                this.cache.delete(fingerprint);
                count++;
            }
        }
        await this.persist();
        return count;
    }
    /**
     * Clear all cache entries
     */
    async clear() {
        this.cache.clear();
        this.stats = { hits: 0, misses: 0 };
        await this.persist();
    }
    /**
     * Clean expired entries
     */
    async cleanExpired() {
        let count = 0;
        const now = new Date();
        for (const [fingerprint, entry] of this.cache.entries()) {
            if (entry.metadata.expiresAt) {
                const expiresAt = new Date(entry.metadata.expiresAt);
                if (expiresAt < now) {
                    this.cache.delete(fingerprint);
                    count++;
                }
            }
        }
        if (count > 0) {
            await this.persist();
        }
        return count;
    }
    /**
     * Get cache statistics
     */
    async getStats() {
        const entries = Array.from(this.cache.values());
        const totalHits = entries.reduce((sum, e) => sum + e.metadata.hits, 0);
        const totalRequests = this.stats.hits + this.stats.misses;
        const hitRate = totalRequests > 0 ? this.stats.hits / totalRequests : 0;
        // Calculate cache size
        const cacheSize = await this.getCacheSize();
        // Find oldest and newest
        const oldest = entries.reduce((old, e) => !old || new Date(e.metadata.createdAt) < new Date(old.metadata.createdAt)
            ? e
            : old, entries[0]);
        const newest = entries.reduce((neu, e) => !neu || new Date(e.metadata.createdAt) > new Date(neu.metadata.createdAt)
            ? e
            : neu, entries[0]);
        return {
            totalEntries: this.cache.size,
            totalHits: this.stats.hits,
            totalMisses: this.stats.misses,
            hitRate: Math.round(hitRate * 100) / 100,
            cacheSize,
            oldestEntry: oldest?.metadata.createdAt,
            newestEntry: newest?.metadata.createdAt,
        };
    }
    /**
     * Get all cache entries (for debugging)
     */
    getAll() {
        return Array.from(this.cache.values());
    }
    /**
     * Capture file hashes for cache validation
     */
    async captureFileHashes(op, result) {
        const hashes = {};
        const files = this.getAffectedFiles(op, result);
        for (const file of files) {
            const absPath = path.resolve(this.baseDir, file);
            if (await fs.pathExists(absPath)) {
                const content = await fs.readFile(absPath);
                const hash = crypto.createHash("sha256").update(content).digest("hex");
                hashes[absPath] = hash;
            }
        }
        return hashes;
    }
    /**
     * Check if files have changed since cache entry was created
     */
    async checkFileChanges(entry) {
        for (const [filePath, cachedHash] of Object.entries(entry.context.fileHashes)) {
            if (!(await fs.pathExists(filePath))) {
                // File was deleted
                return true;
            }
            const content = await fs.readFile(filePath);
            const currentHash = crypto
                .createHash("sha256")
                .update(content)
                .digest("hex");
            if (currentHash !== cachedHash) {
                // File was modified
                return true;
            }
        }
        return false;
    }
    /**
     * Get files affected by operation
     */
    getAffectedFiles(op, result) {
        const files = [];
        // Add output path if present
        if (result.outputPath) {
            files.push(result.outputPath);
        }
        // Add paths from operation params
        switch (op.type) {
            case "create-file":
            case "patch-file":
            case "delete-file":
                if (op.params.path)
                    files.push(op.params.path);
                break;
            case "rename-file":
                if (op.params.from)
                    files.push(op.params.from);
                if (op.params.to)
                    files.push(op.params.to);
                break;
            case "add-import":
            case "add-export":
            case "remove-import":
            case "add-classname":
            case "add-server-action":
            case "add-test":
                if (op.params.file)
                    files.push(op.params.file);
                break;
        }
        return files;
    }
    /**
     * Evict oldest cache entry
     */
    async evictOldest() {
        const entries = Array.from(this.cache.entries());
        // Sort by last accessed (oldest first)
        entries.sort((a, b) => {
            const aTime = new Date(a[1].metadata.lastAccessed).getTime();
            const bTime = new Date(b[1].metadata.lastAccessed).getTime();
            return aTime - bTime;
        });
        if (entries.length > 0) {
            const oldestEntry = entries[0];
            if (oldestEntry) {
                const [oldestKey] = oldestEntry;
                this.cache.delete(oldestKey);
            }
        }
    }
    /**
     * Get cache file size
     */
    async getCacheSize() {
        if (await fs.pathExists(this.cachePath)) {
            const stat = await fs.stat(this.cachePath);
            return stat.size;
        }
        return 0;
    }
    /**
     * Persist cache to disk
     */
    async persist() {
        const data = {
            version: "1.0.0",
            cache: Object.fromEntries(this.cache),
            stats: this.stats,
            updatedAt: new Date().toISOString(),
        };
        await fs.writeJson(this.cachePath, data, { spaces: 2 });
    }
    /**
     * Configure cache settings
     */
    configure(options) {
        if (options.maxCacheSize !== undefined) {
            this.maxCacheSize = options.maxCacheSize;
        }
        if (options.defaultTTL !== undefined) {
            this.defaultTTL = options.defaultTTL;
        }
        if (options.maxEntries !== undefined) {
            this.maxEntries = options.maxEntries;
        }
    }
}
exports.OperationCache = OperationCache;
//# sourceMappingURL=OperationCache.js.map