import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import path from 'node:path';
import { buildCodebaseIndex } from './build.js';
import { mapWithConcurrency } from '../utils.js';
const CACHE_VERSION = '1';
function hashContent(content) {
    return createHash('sha256').update(content).digest('hex').slice(0, 16);
}
function defaultCachePath(repoRoot) {
    return path.join(repoRoot, '.occ-cache', 'index.json');
}
export async function loadIndexCache(cachePath) {
    try {
        const raw = await readFile(cachePath, 'utf8');
        const parsed = JSON.parse(raw);
        if (parsed.cacheVersion !== CACHE_VERSION)
            return undefined;
        return await rehydrateIndexCache(parsed);
    }
    catch {
        return undefined;
    }
}
/**
 * Full-mode caches strip `content`/`lines` on write, so a chunk-capable index
 * has to re-read the source to restore them. Re-reading also validates
 * freshness: each file's current bytes are hashed against the stored
 * `fileHashes`, and any mismatch (modified or deleted file) invalidates the
 * whole cache so the caller rebuilds. Excerpt/none caches carry no rehydratable
 * content and are returned unchanged.
 */
async function rehydrateIndexCache(cached) {
    if (cached.contentMode !== 'full')
        return cached;
    const rehydrated = await mapWithConcurrency(cached.files, 32, async (file) => {
        let content;
        try {
            content = await readFile(file.path, 'utf8');
        }
        catch {
            return null; // Removed since caching — treat the cache as stale.
        }
        const expected = cached.fileHashes[file.relativePath];
        if (expected != null && hashContent(content) !== expected)
            return null; // Changed since caching.
        return { ...file, content, lines: content.split(/\r?\n/) };
    });
    if (rehydrated.includes(null))
        return undefined;
    return { ...cached, files: rehydrated.filter((file) => file !== null) };
}
export async function saveIndexCache(index, cachePath) {
    await mkdir(path.dirname(cachePath), { recursive: true });
    // Strip file content from cache to reduce size - content can be re-read
    const slimFiles = index.files.map(f => ({
        ...f,
        content: undefined,
        lines: undefined,
    }));
    const slimIndex = { ...index, files: slimFiles };
    await writeFile(cachePath, JSON.stringify(slimIndex), 'utf8');
}
export function invalidateCache(cachePath) {
    return writeFile(cachePath, '', 'utf8').catch(() => {
        /* ignore if missing */
    });
}
export async function buildCodebaseIndexCached(options) {
    const repoRoot = path.resolve(options.repoRoot);
    const cachePath = options.cachePath ?? defaultCachePath(repoRoot);
    // Always do a full build (incremental rebuild would require re-reading all files
    // to compute hashes anyway, and the build is fast enough for most repos)
    const index = await buildCodebaseIndex({
        ...options,
        contentMode: options.contentMode ?? 'full',
    });
    const fileHashes = {};
    for (const file of index.files) {
        fileHashes[file.relativePath] = hashContent(file.content ?? file.excerpt?.content ?? '');
    }
    const cached = {
        ...index,
        cacheVersion: CACHE_VERSION,
        cachedAt: new Date().toISOString(),
        fileHashes,
    };
    await saveIndexCache(cached, cachePath);
    return cached;
}
//# sourceMappingURL=cache.js.map