// Server-local lifecycle for Issue #183's ephemeral context-pack micro-index. The workflow
// package owns the cache implementation; this module scopes instances to a connected chat/session
// and provides deterministic cleanup hooks for chat/project lifecycle changes. Nothing persists.
import { createHash } from "node:crypto";
import { createMicroIndex, DEFAULT_MICRO_INDEX, } from "@oscharko-dev/keiko-workflows";
const DEFAULT_MAX_SCOPES = 128;
function assertPositiveInteger(name, value) {
    if (!Number.isInteger(value) || value <= 0) {
        throw new RangeError(`${name} must be a positive integer`);
    }
}
function resolveOptions(options) {
    const resolved = {
        ttlMs: options?.ttlMs ?? DEFAULT_MICRO_INDEX.ttlMs,
        maxEntriesPerScope: options?.maxEntriesPerScope ?? DEFAULT_MICRO_INDEX.maxEntries,
        maxScopes: options?.maxScopes ?? DEFAULT_MAX_SCOPES,
    };
    assertPositiveInteger("ttlMs", resolved.ttlMs);
    assertPositiveInteger("maxEntriesPerScope", resolved.maxEntriesPerScope);
    assertPositiveInteger("maxScopes", resolved.maxScopes);
    return resolved;
}
function sha256Hex(value) {
    return createHash("sha256").update(value).digest("hex");
}
function scopeKey(scope) {
    const source = JSON.stringify({
        workspaceRoot: scope.workspaceRoot,
        scopeId: scope.scopeId,
        kind: scope.kind,
        relativePaths: [...scope.relativePaths].sort(),
        conversationId: scope.conversationId,
        connectedAtMs: scope.connectedAtMs,
    });
    return `gci-${sha256Hex(source).slice(0, 16)}`;
}
function evictExpired(entries, nowMs) {
    for (const [key, entry] of entries) {
        if (entry.expiresAtMs <= nowMs) {
            entry.index.clear();
            entries.delete(key);
        }
    }
}
function evictOldest(entries, maxScopes) {
    while (entries.size >= maxScopes) {
        let oldest;
        for (const entry of entries.values()) {
            if (oldest === undefined || entry.touchedAtMs < oldest.touchedAtMs) {
                oldest = entry;
            }
        }
        if (oldest === undefined) {
            return;
        }
        oldest.index.clear();
        entries.delete(oldest.key);
    }
}
function createEntry(key, scope, nowMs, options) {
    const clock = { nowMs };
    const now = nowMs();
    return {
        key,
        index: createMicroIndex({
            ttlMs: options.ttlMs,
            maxEntries: options.maxEntriesPerScope,
            nowMs: () => clock.nowMs(),
        }),
        clock,
        conversationId: scope.conversationId,
        workspaceRoot: scope.workspaceRoot,
        touchedAtMs: now,
        expiresAtMs: now + options.ttlMs,
    };
}
function touchEntry(entry, nowMs, ttlMs) {
    const now = nowMs();
    entry.clock.nowMs = nowMs;
    entry.touchedAtMs = now;
    entry.expiresAtMs = now + ttlMs;
}
function getOrCreateScopeIndex(entries, options, scope, nowMs) {
    evictExpired(entries, nowMs());
    const key = scopeKey(scope);
    const existing = entries.get(key);
    if (existing !== undefined) {
        touchEntry(existing, nowMs, options.ttlMs);
        return existing.index;
    }
    evictOldest(entries, options.maxScopes);
    const created = createEntry(key, scope, nowMs, options);
    entries.set(key, created);
    return created.index;
}
function clearMatching(entries, predicate) {
    for (const [key, entry] of entries) {
        if (predicate(entry)) {
            entry.index.clear();
            entries.delete(key);
        }
    }
}
function clearEntries(entries) {
    for (const entry of entries.values()) {
        entry.index.clear();
    }
    entries.clear();
}
export function createGroundedContextIndexRegistry(options) {
    const resolved = resolveOptions(options);
    const entries = new Map();
    return {
        forScope(scope, nowMs) {
            return getOrCreateScopeIndex(entries, resolved, scope, nowMs);
        },
        clearConversation(conversationId) {
            clearMatching(entries, (entry) => entry.conversationId === conversationId);
        },
        clearWorkspace(workspaceRoot) {
            clearMatching(entries, (entry) => entry.workspaceRoot === workspaceRoot);
        },
        clearAll() {
            clearEntries(entries);
        },
        sweep(nowMs) {
            evictExpired(entries, nowMs());
        },
        size() {
            return entries.size;
        },
    };
}
export const groundedContextIndexRegistry = createGroundedContextIndexRegistry();
export function microIndexForGroundedScope(scope, nowMs) {
    return groundedContextIndexRegistry.forScope(scope, nowMs);
}
export function clearGroundedContextIndexesForConversation(conversationId) {
    groundedContextIndexRegistry.clearConversation(conversationId);
}
export function clearGroundedContextIndexesForWorkspace(workspaceRoot) {
    groundedContextIndexRegistry.clearWorkspace(workspaceRoot);
}
export function clearAllGroundedContextIndexes() {
    groundedContextIndexRegistry.clearAll();
}
