// Ephemeral, session-scoped micro-index for assembled ConnectedContextPacks (Epic #177,
// Issue #183). Pure in-memory cache backed by a Map; entries are TTL-bounded and capped
// by maxEntries (insertion order = LRU). NEVER written to disk — the audit ledger (#187)
// owns persistence. makeIndexKey uses SHA-256 with a fixed prefix so identical inputs
// reuse the same cached pack regardless of caller order on atomStableIds.
import { createHash } from "node:crypto";
export const DEFAULT_MICRO_INDEX = {
    ttlMs: 5 * 60 * 1000,
    maxEntries: 32,
};
// ─── Key derivation ───────────────────────────────────────────────────────────
function canonicalKeyInput(input) {
    const sorted = [...input.atomStableIds].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
    return JSON.stringify([input.scopeId, input.queryKind, input.queryText, sorted]);
}
export function makeIndexKey(input) {
    const hash = createHash("sha256").update(canonicalKeyInput(input)).digest("hex");
    return `ix-${hash.slice(0, 16)}`;
}
// ─── Micro-index factory ──────────────────────────────────────────────────────
function validateOptions(options) {
    if (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
        throw new RangeError("createMicroIndex: ttlMs must be a positive finite number");
    }
    if (!Number.isInteger(options.maxEntries) || options.maxEntries <= 0) {
        throw new RangeError("createMicroIndex: maxEntries must be a positive integer");
    }
    // Validate eagerly so the failure mode is a TypeError at construction rather than a
    // less actionable "options.nowMs is not a function" the next time get/set is called.
    if (typeof options.nowMs !== "function") {
        throw new TypeError("createMicroIndex: nowMs must be a function");
    }
}
function evictExpired(store, nowMs) {
    for (const [key, entry] of store) {
        if (entry.expiresAtMs <= nowMs) {
            store.delete(key);
        }
    }
}
function evictOldest(store, capacity) {
    while (store.size >= capacity) {
        const iterator = store.keys();
        const next = iterator.next();
        if (next.done === true) {
            return;
        }
        store.delete(next.value);
    }
}
export function createMicroIndex(options) {
    validateOptions(options);
    // Insertion order on a Map gives natural LRU semantics: `set` re-inserts at the tail,
    // and `evictOldest` removes from the head.
    const store = new Map();
    function get(key) {
        const entry = store.get(key);
        if (entry === undefined) {
            return undefined;
        }
        const now = options.nowMs();
        if (entry.expiresAtMs <= now) {
            store.delete(key);
            return undefined;
        }
        // Refresh insertion order without changing expiry — recent reads stay warm.
        store.delete(key);
        store.set(key, entry);
        return entry.pack;
    }
    function set(key, pack) {
        const now = options.nowMs();
        evictExpired(store, now);
        if (store.has(key)) {
            store.delete(key);
        }
        else {
            evictOldest(store, options.maxEntries);
        }
        const entry = {
            key,
            pack,
            insertedAtMs: now,
            expiresAtMs: now + options.ttlMs,
        };
        store.set(key, entry);
    }
    function deleteKey(key) {
        store.delete(key);
    }
    function clear() {
        store.clear();
    }
    function size() {
        return store.size;
    }
    return { get, set, delete: deleteKey, clear, size };
}
