/**
 * Process-level cache for knowledge index snapshots.
 *
 * Knowledge sources are constructor-level static content. Multiple NeuroLink
 * instances in the same server process can therefore share the same immutable
 * lexical index instead of rebuilding it for every conversation/session.
 */
import { createHash } from "crypto";
import { buildIndexSnapshot } from "./knowledgeIndex.js";
import { normalizeAndValidate, resolveEntry } from "./resolve.js";
const CACHE_SCHEMA_VERSION = "knowledge-index-cache:v1";
export const KNOWLEDGE_INDEX_CACHE_LIMIT = 32;
const indexBuildCache = new Map();
const rememberCacheEntry = (cacheKey, buildPromise) => {
    if (indexBuildCache.has(cacheKey)) {
        indexBuildCache.delete(cacheKey);
    }
    indexBuildCache.set(cacheKey, buildPromise);
    while (indexBuildCache.size > KNOWLEDGE_INDEX_CACHE_LIMIT) {
        const oldestKey = indexBuildCache.keys().next().value;
        if (oldestKey === undefined) {
            return;
        }
        indexBuildCache.delete(oldestKey);
    }
};
const escapePart = (value) => encodeURIComponent(value);
const stableStringify = (value) => {
    if (value === null || typeof value !== "object") {
        return JSON.stringify(value);
    }
    if (Array.isArray(value)) {
        return `[${value.map((item) => stableStringify(item)).join(",")}]`;
    }
    const record = value;
    const entries = Object.keys(record)
        .sort()
        .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`);
    return `{${entries.join(",")}}`;
};
const compareEntries = (left, right) => left.id.localeCompare(right.id) ||
    stableStringify(left).localeCompare(stableStringify(right));
const hashSourceContent = (source, fallbackVersion) => {
    const version = source.version ?? fallbackVersion;
    const effectiveContent = {
        id: source.id,
        version,
        entries: source.entries
            .map((entry) => resolveEntry(entry, version))
            .sort(compareEntries),
    };
    return createHash("sha256")
        .update(stableStringify(effectiveContent))
        .digest("hex");
};
const formatFieldWeights = (weights) => [
    `title=${weights.title}`,
    `aliases=${weights.aliases}`,
    `keywords=${weights.keywords}`,
    `summary=${weights.summary}`,
    `body=${weights.body}`,
].join(",");
const formatSource = (source, fallbackVersion) => [
    escapePart(source.id),
    escapePart(source.version ?? fallbackVersion),
    source.entries.length,
    source.entries
        .map((entry) => escapePart(entry.id))
        .sort()
        .join(","),
    hashSourceContent(source, fallbackVersion),
].join(":");
/**
 * Cache key intentionally uses source/version metadata, not conversation scope.
 * The source content hash prevents stale reuse even when a host forgets to bump
 * a source version after changing entries.
 */
export const buildKnowledgeIndexCacheKey = (sources, fallbackVersion, weights) => [
    CACHE_SCHEMA_VERSION,
    `weights=${formatFieldWeights(weights)}`,
    ...sources.map((source) => formatSource(source, fallbackVersion)),
].join("|");
export const getOrBuildKnowledgeIndexSnapshot = async (sources, fallbackVersion, weights) => {
    const cacheKey = buildKnowledgeIndexCacheKey(sources, fallbackVersion, weights);
    const cached = indexBuildCache.get(cacheKey);
    if (cached) {
        rememberCacheEntry(cacheKey, cached);
        return cached;
    }
    const buildPromise = (async () => {
        const { entries, validation } = await normalizeAndValidate(sources, {
            manifestVersion: fallbackVersion,
        });
        if (!validation.ok) {
            return { snapshot: null, validation };
        }
        return {
            snapshot: buildIndexSnapshot(entries, weights),
            validation,
        };
    })();
    rememberCacheEntry(cacheKey, buildPromise);
    void buildPromise.catch(() => {
        if (indexBuildCache.get(cacheKey) === buildPromise) {
            indexBuildCache.delete(cacheKey);
        }
    });
    return buildPromise;
};
