"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports._setItcHandlersForTest = _setItcHandlersForTest;
exports._resetListChangedForTest = _resetListChangedForTest;
exports._setUserResolverForTest = _setUserResolverForTest;
exports.initListChanged = initListChanged;
exports.seedSessionSnapshot = seedSessionSnapshot;
/**
 * MCP server-push notification dispatcher (#619). Subscribes to Harper's
 * existing role-cache-invalidation + schema-reload event channels and
 * emits per-session `notifications/tools/list_changed` /
 * `notifications/resources/list_changed` frames over the registered
 * SSE streams.
 *
 * Per-session computation only — never broadcast. For each event, we
 * walk the per-worker session registry, re-resolve the session's user
 * (so the diff sees the freshly-mutated permission set, not the snapshot
 * captured at GET-stream open), recompute that session's tools/list (or
 * resources/list) under the fresh user, diff against the snapshot from
 * the prior emission, and push a notification iff the visible set
 * actually changed. Sessions whose visible surface is unchanged see
 * nothing.
 */
const harper_logger_ts_1 = __importDefault(require("../../utility/logging/harper_logger.js"));
const resources_ts_1 = require("./resources.js");
const sessionRegistry_ts_1 = require("./sessionRegistry.js");
const toolRegistry_ts_1 = require("./toolRegistry.js");
const MAX_TOOLS_PAGE = 1000;
const MAX_RESOURCES_PAGE = 1000;
let initialized = false;
let onUserChangeBound;
let onSchemaChangeBound;
// Test seams: avoid importing the real ITC handler from unit tests.
let _itcHandlersOverride;
function _setItcHandlersForTest(h) {
    _itcHandlersOverride = h;
}
function _resetListChangedForTest() {
    initialized = false;
    onUserChangeBound = undefined;
    onSchemaChangeBound = undefined;
}
function loadItcHandlers() {
    if (_itcHandlersOverride)
        return _itcHandlersOverride;
    try {
        return require('../../server/itc/serverHandlers');
    }
    catch (err) {
        harper_logger_ts_1.default.trace(`MCP listChanged: ITC handlers unavailable (${err.message})`);
        return undefined;
    }
}
// Test seam: lets unit tests stub the user re-resolution without pulling in
// security/user.ts (which initializes the system catalogs at module-load).
let _userResolverOverride;
function _setUserResolverForTest(fn) {
    _userResolverOverride = fn;
}
async function resolveUser(username) {
    if (!username)
        return undefined;
    if (_userResolverOverride)
        return _userResolverOverride(username);
    try {
        const { findAndValidateUser } = require('../../security/user');
        const fresh = await findAndValidateUser(username, null, false);
        return fresh;
    }
    catch (err) {
        harper_logger_ts_1.default.trace(`MCP listChanged: user re-resolve failed for ${username}: ${err.message}`);
        return undefined;
    }
}
function toolsListNames(profile, session) {
    const { tools } = (0, toolRegistry_ts_1.listTools)({
        user: session.user,
        profile,
        sessionId: session.sessionId,
        limit: MAX_TOOLS_PAGE,
    });
    return tools.map((t) => ({ name: t.name }));
}
function resourcesListUris(profile, session) {
    const result = (0, resources_ts_1.listResources)({ user: session.user, profile, limit: MAX_RESOURCES_PAGE });
    return result.resources.map((r) => ({ uri: r.uri }));
}
function sameSet(a, b) {
    if (!a)
        return false;
    if (a.length !== b.length)
        return false;
    const aKeys = new Set(a.map((x) => x.name ?? x.uri));
    for (const x of b) {
        if (!aKeys.has(x.name ?? x.uri))
            return false;
    }
    return true;
}
/**
 * Re-emit `notifications/tools/list_changed` for one session iff its
 * visible tools list has changed since the last snapshot. Wrapped in
 * try/catch so a single failing session never breaks the loop.
 */
function maybeNotifyToolsChanged(record) {
    try {
        const current = toolsListNames(record.profile, record);
        if (sameSet(record.lastTools, current))
            return;
        record.lastTools = current;
        record.queue.send({
            event: 'message',
            data: { jsonrpc: '2.0', method: 'notifications/tools/list_changed' },
        });
    }
    catch (err) {
        harper_logger_ts_1.default.trace(`MCP listChanged tools/* for session ${record.sessionId}: ${err.message}`);
    }
}
function maybeNotifyResourcesChanged(record) {
    try {
        const current = resourcesListUris(record.profile, record);
        if (sameSet(record.lastResources, current))
            return;
        record.lastResources = current;
        record.queue.send({
            event: 'message',
            data: { jsonrpc: '2.0', method: 'notifications/resources/list_changed' },
        });
    }
    catch (err) {
        harper_logger_ts_1.default.trace(`MCP listChanged resources/* for session ${record.sessionId}: ${err.message}`);
    }
}
/**
 * Snapshot the current registry as a flat list so re-resolves can happen
 * sequentially without re-walking the live map (which a concurrent
 * registerSession could mutate mid-iteration).
 */
function snapshotSessions(profile) {
    const out = [];
    (0, sessionRegistry_ts_1.forEachSessionByProfile)(profile, (r) => out.push(r));
    return out;
}
async function refreshSessionUser(record) {
    const fresh = await resolveUser(record.user?.username);
    if (fresh)
        record.user = fresh;
}
/**
 * Fan out a user/role change: for each session on either profile,
 * re-resolve the user (so a role-perm mutation is visible to the diff —
 * the captured `record.user` at GET-stream open is otherwise frozen),
 * recompute the tools list and notify if changed. Resources may also
 * change visibility (table-perm-gated schema URIs), so we re-check
 * those too.
 */
async function onUserChange() {
    for (const r of snapshotSessions('operations')) {
        await refreshSessionUser(r);
        maybeNotifyToolsChanged(r);
        maybeNotifyResourcesChanged(r);
    }
    for (const r of snapshotSessions('application')) {
        await refreshSessionUser(r);
        maybeNotifyToolsChanged(r);
        maybeNotifyResourcesChanged(r);
    }
}
/**
 * Schema changes touch both surfaces — application-profile tools are
 * generated from the Resources registry, and operations-profile
 * resources include the OPERATION list (unchanged) plus harper://schema
 * URIs (changed). Application sessions need the bigger refresh. We also
 * re-resolve the user here in case the schema change coincided with a
 * role mutation (Harper sometimes fires both channels on database-level
 * grants).
 */
async function onSchemaChange() {
    for (const r of snapshotSessions('application')) {
        await refreshSessionUser(r);
        maybeNotifyToolsChanged(r);
        maybeNotifyResourcesChanged(r);
    }
    // Operations sessions only need resources/list refresh — there are no
    // schema-derived operations tools, but `harper://schema/...` URIs may
    // shift if a new table appears under a database the user can describe.
    for (const r of snapshotSessions('operations')) {
        await refreshSessionUser(r);
        maybeNotifyResourcesChanged(r);
    }
}
/**
 * Idempotent: subscribe once at component boot. Repeated calls are
 * no-ops. Returns true if subscriptions were actually installed (false
 * if Harper's ITC handlers aren't available in this process).
 */
function initListChanged() {
    if (initialized)
        return true;
    const handlers = loadItcHandlers();
    if (!handlers)
        return false;
    let installed = 0;
    if (handlers.userHandler?.addListener) {
        // Harper's ITC handler treats listeners as `() => void`. Our handler is
        // async (re-resolves users); fire-and-forget with a swallow so a rejection
        // can never escape the event emitter as an UnhandledPromiseRejection.
        onUserChangeBound = () => {
            onUserChange().catch((err) => harper_logger_ts_1.default.trace(`MCP listChanged onUserChange: ${err.message}`));
        };
        handlers.userHandler.addListener(onUserChangeBound);
        installed++;
    }
    if (handlers.schemaHandler?.addListener) {
        onSchemaChangeBound = () => {
            onSchemaChange().catch((err) => harper_logger_ts_1.default.trace(`MCP listChanged onSchemaChange: ${err.message}`));
        };
        handlers.schemaHandler.addListener(onSchemaChangeBound);
        installed++;
    }
    initialized = installed > 0;
    if (initialized) {
        harper_logger_ts_1.default.info(`MCP listChanged: subscribed to ${installed} event channel(s)`);
    }
    else {
        harper_logger_ts_1.default.warn('MCP listChanged: ITC handlers do not expose addListener; list_changed notifications will not fire');
    }
    return initialized;
}
/**
 * Compute and stash the initial tools/resources snapshot for a session
 * that just opened its GET stream. Without this seed, the first event
 * would always be a "changed" because `lastTools === undefined !==
 * current`. Call right after registerSession.
 */
function seedSessionSnapshot(sessionId) {
    const record = (0, sessionRegistry_ts_1.getRegisteredSession)(sessionId);
    if (!record)
        return;
    try {
        record.lastTools = toolsListNames(record.profile, record);
    }
    catch (err) {
        harper_logger_ts_1.default.trace(`MCP seed tools list for ${sessionId}: ${err.message}`);
    }
    try {
        record.lastResources = resourcesListUris(record.profile, record);
    }
    catch (err) {
        harper_logger_ts_1.default.trace(`MCP seed resources list for ${sessionId}: ${err.message}`);
    }
}
//# sourceMappingURL=listChanged.js.map