"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAppsHostClient = getAppsHostClient;
exports.waitForAppsHostClient = waitForAppsHostClient;
exports.shutdownAppsRuntime = shutdownAppsRuntime;
exports.registerEnabledAppsToolProxies = registerEnabledAppsToolProxies;
exports.listDynamicAppToolsetsForMember = listDynamicAppToolsetsForMember;
exports.resolveDynamicAppToolAvailabilityForMember = resolveDynamicAppToolAvailabilityForMember;
exports.initAppsRuntime = initAppsRuntime;
const crypto_1 = require("crypto");
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const dialog_1 = require("../dialog");
const dialog_global_registry_1 = require("../dialog-global-registry");
const dialog_instance_registry_1 = require("../dialog-instance-registry");
const log_1 = require("../log");
const tool_availability_updates_1 = require("../tool-availability-updates");
const app_reminders_1 = require("../tools/app-reminders");
const registry_1 = require("../tools/registry");
const client_1 = require("../apps-host/client");
const dialog_run_controls_1 = require("./dialog-run-controls");
const enabled_apps_1 = require("./enabled-apps");
const problems_1 = require("./problems");
const log = (0, log_1.createLogger)('apps-runtime');
let appsHostClient = null;
let appsHostTransition = null;
let hostedAppsSignature = null;
let appsRuntimeConfig = null;
let refreshQueue = Promise.resolve();
const registeredAppArtifactsById = new Map();
function asError(error) {
    return error instanceof Error ? error : new Error(String(error));
}
function formatAppCapabilityUnavailableMessage(params) {
    return [
        `App capability unavailable: ${params.capability}`,
        `appId=${params.appId}`,
        `reason=${params.detail}`,
        'This app-specific capability is unavailable for now. Continue with other available tools unless this app is required.',
    ].join('\n');
}
function resolveMainDialogFor(dlg) {
    if (dlg instanceof dialog_1.MainDialog) {
        return dlg;
    }
    if (dlg instanceof dialog_1.SideDialog) {
        return dlg.mainDialog;
    }
    throw new Error(`Unsupported dialog type for app runtime: ${dlg.constructor.name}`);
}
async function ensureMainDialogLoaded(rootId) {
    const existing = dialog_global_registry_1.globalDialogRegistry.get(rootId);
    if (existing) {
        await existing.loadSideDialogRegistry();
        await existing.loadActiveCalleeDispatchesFromPersistence();
        return existing;
    }
    return await (0, dialog_instance_registry_1.getOrRestoreMainDialog)(rootId, 'running');
}
async function ensureDialogLoadedBySelfId(mainDialog, dialogSelfId) {
    if (dialogSelfId === mainDialog.id.rootId) {
        return mainDialog;
    }
    const existing = mainDialog.lookupDialog(dialogSelfId);
    if (existing) {
        return existing;
    }
    return await (0, dialog_instance_registry_1.ensureDialogLoaded)(mainDialog, new dialog_1.DialogID(dialogSelfId, mainDialog.id.rootId), 'running');
}
function describeTarget(target) {
    if ('dialogId' in target) {
        return `dialogId=${target.dialogId}`;
    }
    const parts = [`agentId=${target.agentId}`];
    if (target.mainDialogId) {
        parts.push(`mainDialogId=${target.mainDialogId}`);
    }
    if (target.sessionSlug) {
        parts.push(`sessionSlug=${target.sessionSlug}`);
    }
    return parts.join(' ');
}
async function resolveTargetDialog(sourceDlg, target) {
    const sourceRoot = resolveMainDialogFor(sourceDlg);
    if ('dialogId' in target) {
        const matches = [];
        const seenDialogKeys = new Set();
        const pushMatch = (dialog) => {
            if (!dialog)
                return;
            const key = dialog.id.valueOf();
            if (seenDialogKeys.has(key))
                return;
            seenDialogKeys.add(key);
            matches.push(dialog);
        };
        pushMatch(await ensureDialogLoadedBySelfId(sourceRoot, target.dialogId));
        if (target.dialogId !== sourceRoot.id.rootId) {
            pushMatch(await ensureMainDialogLoaded(target.dialogId));
        }
        if (matches.length === 1) {
            return matches[0];
        }
        if (matches.length === 0) {
            throw new Error(`dialog reminder target not found (${describeTarget(target)})`);
        }
        throw new Error(`dialog reminder target is ambiguous (${describeTarget(target)})`);
    }
    const targetRootId = target.mainDialogId ?? sourceRoot.id.rootId;
    const targetRoot = targetRootId === sourceRoot.id.rootId ? sourceRoot : await ensureMainDialogLoaded(targetRootId);
    if (!targetRoot) {
        throw new Error(`dialog reminder target root not found (${describeTarget(target)})`);
    }
    await targetRoot.loadSideDialogRegistry();
    await targetRoot.loadActiveCalleeDispatchesFromPersistence();
    if (target.sessionSlug) {
        const sideDialog = targetRoot.lookupSideDialog(target.agentId, target.sessionSlug);
        if (!sideDialog) {
            throw new Error(`dialog reminder target not found (${describeTarget(target)})`);
        }
        return sideDialog;
    }
    const matches = targetRoot
        .getLoadedDialogTreeSnapshot()
        .filter((dialog) => dialog.agentId === target.agentId);
    if (matches.length === 1) {
        return matches[0];
    }
    if (matches.length === 0) {
        throw new Error(`dialog reminder target not found (${describeTarget(target)})`);
    }
    throw new Error(`dialog reminder target is ambiguous (${describeTarget(target)})`);
}
async function applyDialogReminderRequestBatches(sourceDlg, appId, dialogReminderRequests) {
    for (const batch of dialogReminderRequests) {
        if (batch.reminderRequests.length === 0) {
            continue;
        }
        const targetDlg = await resolveTargetDialog(sourceDlg, batch.target);
        await (0, app_reminders_1.applyAppReminderRequests)(targetDlg, {
            appId,
            reminderRequests: batch.reminderRequests,
            resolveHostClient: waitForAppsHostClient,
        });
    }
}
function getAppsHostClient() {
    if (!appsHostClient) {
        throw new Error('Apps host is not initialized');
    }
    return appsHostClient;
}
async function waitForAppsHostClient() {
    if (appsHostClient) {
        return appsHostClient;
    }
    if (appsHostTransition) {
        return await appsHostTransition;
    }
    throw new Error('Apps host is not initialized');
}
async function stopAppsHost() {
    const client = appsHostClient;
    if (!client)
        return;
    appsHostClient = null;
    hostedAppsSignature = null;
    await client.shutdown();
}
async function shutdownAppsRuntime() {
    await stopAppsHost();
    const transition = appsHostTransition;
    if (!transition) {
        return;
    }
    const client = await transition;
    if (appsHostClient !== client) {
        return;
    }
    appsHostClient = null;
    hostedAppsSignature = null;
    await client.shutdown();
}
function ensureNoDuplicateTool(toolName, appId) {
    const existing = (0, registry_1.getTool)(toolName);
    if (existing) {
        throw new Error(`App '${appId}' attempted to register duplicate tool '${toolName}'`);
    }
}
function ensureNoDuplicateToolset(toolsetId, appId) {
    const existing = (0, registry_1.getToolset)(toolsetId);
    if (existing) {
        throw new Error(`App '${appId}' attempted to register duplicate toolset '${toolsetId}'`);
    }
}
function registerAppToolset(params) {
    ensureNoDuplicateToolset(params.toolsetId, params.appId);
    for (const t of params.tools)
        ensureNoDuplicateTool(t.name, params.appId);
    for (const t of params.tools)
        (0, registry_1.registerTool)(t);
    (0, registry_1.registerToolset)(params.toolsetId, params.tools);
    (0, registry_1.setToolsetMeta)(params.toolsetId, { source: 'app', descriptionI18n: params.descriptionI18n });
}
function computeAppSignature(app) {
    return JSON.stringify({
        appId: app.appId,
        runtimePort: app.runtimePort,
        installJson: app.installJson,
        hostSourceVersion: app.hostSourceVersion,
    });
}
function computeHostedAppsSignature(enabledApps) {
    return JSON.stringify(enabledApps.map((app) => ({
        appId: app.appId,
        runtimePort: app.runtimePort,
        installJson: app.installJson,
        hostSourceVersion: app.hostSourceVersion,
    })));
}
async function computeHostSourceVersion(params) {
    if (params.source.kind !== 'local') {
        return null;
    }
    const host = params.installJson.host;
    if (host.kind !== 'node_module') {
        return null;
    }
    const moduleAbs = path_1.default.resolve(params.source.pathAbs, host.moduleRelPath);
    try {
        const content = await promises_1.default.readFile(moduleAbs);
        const digest = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
        return `sha256:${digest}`;
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        return `unreadable:${moduleAbs}:${message}`;
    }
}
function unregisterRegisteredAppArtifacts(appId) {
    const registered = registeredAppArtifactsById.get(appId);
    if (!registered) {
        (0, app_reminders_1.unregisterAppReminderOwnersForApps)({ appIds: [appId] });
        return;
    }
    for (const toolName of registered.toolNames) {
        (0, registry_1.unregisterTool)(toolName);
    }
    for (const toolsetId of registered.toolsetIds) {
        (0, registry_1.unregisterToolset)(toolsetId);
    }
    for (const controlId of registered.dialogRunControlIds) {
        (0, dialog_run_controls_1.unregisterAppDialogRunControl)(controlId);
    }
    (0, app_reminders_1.unregisterAppReminderOwnersForApps)({ appIds: [appId] });
    registeredAppArtifactsById.delete(appId);
}
async function ensureAppsHostReadyForToolCalls() {
    const config = appsRuntimeConfig;
    if (!config && !appsHostClient) {
        throw new Error('Apps host is unavailable in the current runtime; start an interactive Dominds runtime first');
    }
    if (config) {
        await registerEnabledAppsToolProxies({ rtwsRootAbs: config.rtwsRootAbs });
    }
    return await waitForAppsHostClient();
}
function registerAppArtifacts(app) {
    const toolNames = [];
    const toolsetIds = [];
    const dialogRunControlIds = [];
    const toolsets = app.installJson.contributes?.toolsets ?? [];
    for (const ts of toolsets) {
        const tools = ts.tools.map((t) => ({
            type: 'func',
            name: t.name,
            description: t.description,
            descriptionI18n: t.descriptionI18n,
            parameters: t.parameters,
            call: async (dlg, caller, args) => {
                try {
                    const host = await ensureAppsHostReadyForToolCalls();
                    const result = await host.callTool(t.name, args, {
                        dialogId: dlg.id.selfId,
                        mainDialogId: dlg.id.rootId,
                        agentId: dlg.agentId,
                        taskDocPath: dlg.taskDocPath,
                        sessionSlug: dlg instanceof dialog_1.SideDialog ? dlg.sessionSlug : undefined,
                        callerId: caller.id,
                    });
                    if (Array.isArray(result.reminderRequests) && result.reminderRequests.length > 0) {
                        await (0, app_reminders_1.applyAppReminderRequests)(dlg, {
                            appId: app.appId,
                            reminderRequests: result.reminderRequests,
                            resolveHostClient: waitForAppsHostClient,
                        });
                    }
                    if (Array.isArray(result.dialogReminderRequests) &&
                        result.dialogReminderRequests.length > 0) {
                        await applyDialogReminderRequestBatches(dlg, app.appId, result.dialogReminderRequests);
                    }
                    return result.output;
                }
                catch (error) {
                    const err = asError(error);
                    log.warn('App tool call failed', err, {
                        appId: app.appId,
                        toolName: t.name,
                        dialogId: dlg.id.valueOf(),
                        rootId: dlg.id.rootId,
                        selfId: dlg.id.selfId,
                        agentId: dlg.agentId,
                        callerId: caller.id,
                    });
                    throw new Error(formatAppCapabilityUnavailableMessage({
                        appId: app.appId,
                        capability: `tool:${t.name}`,
                        detail: err.message,
                    }));
                }
            },
        }));
        registerAppToolset({
            appId: app.appId,
            toolsetId: ts.id,
            tools,
            descriptionI18n: ts.descriptionI18n,
        });
        toolsetIds.push(ts.id);
        toolNames.push(...tools.map((tool) => tool.name));
    }
    const controls = app.installJson.contributes?.dialogRunControls ?? [];
    for (const control of controls) {
        (0, dialog_run_controls_1.registerAppDialogRunControl)({
            id: control.id,
            appId: app.appId,
            descriptionI18n: control.descriptionI18n,
        });
        dialogRunControlIds.push(control.id);
    }
    (0, app_reminders_1.ensureAppReminderOwnersRegistered)({
        enabledApps: [app],
        resolveHostClient: waitForAppsHostClient,
    });
    registeredAppArtifactsById.set(app.appId, {
        signature: computeAppSignature(app),
        toolNames,
        toolsetIds,
        dialogRunControlIds,
    });
}
function syncRegisteredAppArtifacts(params) {
    let changed = false;
    const nextAppIds = new Set(params.enabledApps.map((app) => app.appId));
    for (const appId of registeredAppArtifactsById.keys()) {
        if (!nextAppIds.has(appId)) {
            unregisterRegisteredAppArtifacts(appId);
            changed = true;
        }
    }
    for (const app of params.enabledApps) {
        const nextSignature = computeAppSignature(app);
        const existing = registeredAppArtifactsById.get(app.appId);
        if (existing?.signature === nextSignature) {
            continue;
        }
        if (existing) {
            unregisterRegisteredAppArtifacts(app.appId);
        }
        registerAppArtifacts(app);
        changed = true;
    }
    return changed;
}
async function syncAppsHostToEnabledApps(params) {
    const config = appsRuntimeConfig;
    if (!config) {
        return;
    }
    const nextSignature = computeHostedAppsSignature(params.enabledApps);
    if (params.enabledApps.length === 0) {
        await stopAppsHost();
        return;
    }
    if (appsHostClient && hostedAppsSignature === nextSignature) {
        return;
    }
    log.info(`Starting apps-host (${params.enabledApps.length} enabled apps)`);
    const transition = (async () => {
        await stopAppsHost();
        const { client } = await (0, client_1.startAppsHost)({
            rtwsRootAbs: config.rtwsRootAbs,
            kernel: config.kernel,
            apps: params.enabledApps,
        });
        return client;
    })();
    appsHostTransition = transition;
    try {
        const client = await transition;
        appsHostClient = client;
        hostedAppsSignature = nextSignature;
    }
    finally {
        if (appsHostTransition === transition) {
            appsHostTransition = null;
        }
    }
}
async function refreshEnabledAppsRuntimeNow(params) {
    const snapshot = await (0, enabled_apps_1.loadEnabledAppsSnapshot)({ rtwsRootAbs: params.rtwsRootAbs });
    (0, problems_1.reconcileAppsResolutionIssuesToProblems)({ issues: snapshot.issues });
    const enabledApps = await Promise.all(snapshot.enabledApps.map(async (app) => ({
        appId: app.id,
        runtimePort: app.runtimePort,
        installJson: app.installJson,
        hostSourceVersion: await computeHostSourceVersion({
            source: app.source,
            installJson: app.installJson,
        }),
    })));
    const appArtifactsChanged = syncRegisteredAppArtifacts({ enabledApps });
    if (appArtifactsChanged) {
        (0, tool_availability_updates_1.notifyToolAvailabilityRegistryMaybeChanged)({
            reason: 'registry_changed',
            trigger: 'apps-runtime:tool-proxies-refreshed',
        });
    }
    if (params.ensureHost) {
        await syncAppsHostToEnabledApps({ enabledApps });
    }
}
async function registerEnabledAppsToolProxies(params) {
    const run = refreshQueue.then(() => refreshEnabledAppsRuntimeNow({
        rtwsRootAbs: params.rtwsRootAbs,
        ensureHost: appsRuntimeConfig !== null,
    }));
    refreshQueue = run.catch(() => undefined);
    await run;
}
async function listDynamicAppToolsetsForMember(_params) {
    const result = await resolveDynamicAppToolAvailabilityForMember(_params);
    return result.status === 'ready' ? result.toolsetIds : [];
}
async function resolveDynamicAppToolAvailabilityForMember(_params) {
    if (_params.taskDocPath.trim() === '') {
        return { status: 'not_applicable', toolsetIds: [] };
    }
    try {
        await registerEnabledAppsToolProxies({ rtwsRootAbs: _params.rtwsRootAbs });
    }
    catch (error) {
        const err = asError(error);
        log.warn(`Failed to refresh enabled app tool proxies while resolving dynamic toolsets for member '${_params.memberId}'.`, err);
        return {
            status: 'error',
            toolsetIds: [],
            errorText: err.message,
        };
    }
    if (!appsRuntimeConfig && !appsHostClient && !appsHostTransition) {
        return { status: 'not_applicable', toolsetIds: [] };
    }
    if (!appsHostClient && !appsHostTransition) {
        return { status: 'not_applicable', toolsetIds: [] };
    }
    try {
        const host = await ensureAppsHostReadyForToolCalls();
        const toolsetIds = await host.listDynamicToolsets({
            memberId: _params.memberId,
            taskDocPath: _params.taskDocPath,
            ...(typeof _params.dialogId === 'string' && _params.dialogId.trim() !== ''
                ? { dialogId: _params.dialogId.trim() }
                : {}),
            ...(typeof _params.mainDialogId === 'string' && _params.mainDialogId.trim() !== ''
                ? { mainDialogId: _params.mainDialogId.trim() }
                : {}),
            ...(typeof _params.agentId === 'string' && _params.agentId.trim() !== ''
                ? { agentId: _params.agentId.trim() }
                : {}),
            ...(typeof _params.sessionSlug === 'string' && _params.sessionSlug.trim() !== ''
                ? { sessionSlug: _params.sessionSlug.trim() }
                : {}),
        });
        return { status: 'ready', toolsetIds };
    }
    catch (error) {
        const err = asError(error);
        log.warn(`Failed to load dynamic app toolsets for member '${_params.memberId}'.`, err);
        return {
            status: 'error',
            toolsetIds: [],
            errorText: err.message,
        };
    }
}
async function initAppsRuntime(params) {
    appsRuntimeConfig = {
        rtwsRootAbs: params.rtwsRootAbs,
        kernel: params.kernel,
    };
    const run = refreshQueue.then(() => refreshEnabledAppsRuntimeNow({
        rtwsRootAbs: params.rtwsRootAbs,
        ensureHost: true,
    }));
    refreshQueue = run.catch(() => undefined);
    await run;
}
