import { createHash } from 'node:crypto';
import { compareCodeUnits } from '../utils.js';
export const DEFAULT_FILE_TOKEN_COUNT_CACHE_ENTRIES = 10_000;
const cachesByIndex = new WeakMap();
function rowKey(row) {
    return `${row.tokenizer}:${row.sha256}`;
}
export function isFileTokenCountRow(value) {
    if (typeof value !== 'object' || value === null || Array.isArray(value))
        return false;
    const row = value;
    return (Object.keys(row).length === 3 &&
        typeof row.tokenizer === 'string' &&
        row.tokenizer.length > 0 &&
        typeof row.sha256 === 'string' &&
        /^[0-9a-f]{64}$/u.test(row.sha256) &&
        typeof row.count === 'number' &&
        Number.isSafeInteger(row.count) &&
        row.count >= 0);
}
export function attachFileTokenCountCache(index, cache) {
    cachesByIndex.set(index, cache);
}
export function getFileTokenCountCache(index) {
    return cachesByIndex.get(index);
}
export async function flushFileTokenCountCache(index) {
    await cachesByIndex.get(index)?.flush();
}
/**
 * Cache whole-file token counts by content rather than by path.
 *
 * A rename stays hot, two identical generated files share one count, and an
 * edit cannot reuse a stale value because it necessarily changes the digest.
 * The tokenizer is part of the key because BPE encodings do not agree.
 */
export function createFileTokenCountCache(options = DEFAULT_FILE_TOKEN_COUNT_CACHE_ENTRIES) {
    const config = typeof options === 'number' ? { maxEntries: options } : options;
    const requestedMaxEntries = config.maxEntries ?? DEFAULT_FILE_TOKEN_COUNT_CACHE_ENTRIES;
    const maxEntries = Number.isFinite(requestedMaxEntries)
        ? Math.max(1, Math.floor(requestedMaxEntries))
        : DEFAULT_FILE_TOKEN_COUNT_CACHE_ENTRIES;
    const counts = new Map();
    let persistQueue = Promise.resolve();
    function admit(row) {
        const key = rowKey(row);
        counts.delete(key);
        counts.set(key, { ...row });
        while (counts.size > maxEntries) {
            const oldest = counts.keys().next().value;
            if (oldest == null)
                break;
            counts.delete(oldest);
        }
    }
    function queuePersist(row) {
        if (!config.persist)
            return;
        persistQueue = persistQueue.then(() => config.persist?.(row)).catch(() => undefined);
    }
    for (const row of config.initialRows ?? []) {
        if (isFileTokenCountRow(row))
            admit(row);
    }
    return {
        count(content, tokenizer, countTokens) {
            const digest = createHash('sha256').update(content, 'utf8').digest('hex');
            const key = rowKey({ tokenizer, sha256: digest });
            const cached = counts.get(key);
            if (cached) {
                // Refresh insertion order so the cap behaves as a small LRU.
                counts.delete(key);
                counts.set(key, cached);
                return cached.count;
            }
            const count = countTokens(content);
            const row = { tokenizer, sha256: digest, count };
            admit(row);
            if (isFileTokenCountRow(row))
                queuePersist(row);
            return count;
        },
        load(rows) {
            for (const row of rows) {
                if (isFileTokenCountRow(row))
                    admit(row);
            }
        },
        entries() {
            return [...counts.values()]
                .map(row => ({ ...row }))
                .sort((left, right) => compareCodeUnits(left.tokenizer, right.tokenizer) || compareCodeUnits(left.sha256, right.sha256));
        },
        async flush() {
            await persistQueue;
        },
        clear() {
            counts.clear();
        },
        get size() {
            return counts.size;
        },
    };
}
//# sourceMappingURL=token-count-cache.js.map