/**
 * McpRegistry — manages all connected MCP servers.
 *
 * Progressive loading strategy:
 *   - On connect: load tool names + descriptions only (F2)
 *   - On first callTool: fetch full JSON schema for that tool and cache it
 *
 * Tool namespace: mcp:<server-name>:<tool-name>
 */
import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
import { loadMcpConfig } from '@pellux/goodvibes-sdk/platform/mcp/config';
import { McpClient } from './client.js';
import { McpPermissionManager } from '@pellux/goodvibes-sdk/platform/runtime/mcp/permissions';
import { McpSchemaFreshnessTracker } from '@pellux/goodvibes-sdk/platform/runtime/mcp/schema-freshness';
import { emitMcpConfigured, emitMcpPolicyUpdated, emitMcpSchemaQuarantineApproved, emitMcpSchemaQuarantined, } from '../runtime/emitters/mcp.js';
import { getSandboxConfigSnapshot } from '../runtime/sandbox/manager.js';
import {} from '../runtime/sandbox/session-registry.js';
import { resolveSandboxCommandPlan } from '../runtime/sandbox/backend.js';
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
function compactEnv(env) {
    return Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === 'string'));
}
export class McpRegistry {
    clients = new Map();
    permissions = new McpPermissionManager();
    freshness = new McpSchemaFreshnessTracker();
    runtimeBus = null;
    sandboxConfigManager = null;
    sandboxSessions;
    sandboxSessionByServer = new Map();
    hookDispatcher;
    constructor(options) {
        this.hookDispatcher = options.hookDispatcher;
        this.sandboxSessions = options.sandboxSessions;
    }
    setRuntimeBus(runtimeBus) {
        this.runtimeBus = runtimeBus;
    }
    setSandboxRuntime(configManager, sessions) {
        this.sandboxConfigManager = configManager;
        this.sandboxSessions = sessions;
    }
    /**
     * connectAll — Load config from .goodvibes/mcp.json and connect to all servers.
     * Errors on individual servers are logged but do not abort the whole startup.
     */
    async connectAll(roots) {
        const mcpConfig = loadMcpConfig(roots);
        await Promise.allSettled(mcpConfig.servers.map((serverConfig) => this._connectServer(serverConfig)));
    }
    /**
     * connectServer — Connect a single MCP server by config.
     * Exposed for programmatic use (testing, dynamic registration).
     */
    async connectServer(serverConfig) {
        await this._connectServer(serverConfig);
    }
    /**
     * listAllTools — Return all registered tools (name + description) from all connected servers.
     * Only loads tool names and descriptions — full schemas are NOT fetched here.
     */
    async listAllTools() {
        const results = [];
        for (const [serverName, client] of this.clients) {
            if (!client.isConnected)
                continue;
            try {
                const tools = await client.listTools();
                for (const tool of tools) {
                    results.push({
                        qualifiedName: `mcp:${serverName}:${tool.name}`,
                        serverName,
                        toolName: tool.name,
                        description: tool.description,
                    });
                }
            }
            catch (err) {
                logger.info('McpRegistry: failed to list tools from server', { server: serverName, err: summarizeError(err) });
            }
        }
        return results;
    }
    /**
     * getToolSchema — Fetch full JSON schema for a qualified tool name.
     * Triggers lazy schema load and caches within McpClient.
     */
    async getToolSchema(qualifiedName) {
        const parsed = this._parseQualifiedName(qualifiedName);
        if (!parsed)
            return null;
        const client = this.clients.get(parsed.serverName);
        if (!client || !client.isConnected)
            return null;
        return client.getToolSchema(parsed.toolName);
    }
    /**
     * callTool — Execute a tool by its qualified name.
     * Fetches full schema on first use (progressive loading — F2).
     */
    async callTool(qualifiedName, args) {
        const parsed = this._parseQualifiedName(qualifiedName);
        if (!parsed) {
            throw new Error(`McpRegistry: invalid qualified tool name '${qualifiedName}'`);
        }
        const client = this.clients.get(parsed.serverName);
        if (!client) {
            throw new Error(`McpRegistry: no server named '${parsed.serverName}'`);
        }
        if (!client.isConnected) {
            throw new Error(`McpRegistry: server '${parsed.serverName}' is not connected`);
        }
        if (this.freshness.isQuarantined(parsed.serverName)) {
            const record = this.freshness.getRecord(parsed.serverName);
            throw new Error(`MCP call '${qualifiedName}' blocked: schema quarantined (${record?.quarantine?.reason ?? 'unknown'})${record?.quarantine?.detail ? ` — ${record.quarantine.detail}` : ''}`);
        }
        const permission = this.permissions.evaluateToolCall(parsed.serverName, parsed.toolName, args);
        if (permission.verdict === 'deny') {
            throw new Error(`MCP call '${qualifiedName}' denied: ${permission.reason}`);
        }
        if (permission.verdict === 'ask') {
            throw new Error(`MCP call '${qualifiedName}' requires approval: ${permission.reason}`);
        }
        // Pre:mcp:call hook
        const dispatcher = this.hookDispatcher;
        const preEvent = {
            path: 'Pre:mcp:call',
            phase: 'Pre',
            category: 'mcp',
            specific: 'call',
            sessionId: '', timestamp: Date.now(),
            payload: { tool: qualifiedName, args },
        };
        const preResult = await dispatcher.fire(preEvent).catch(() => ({ ok: true, decision: undefined }));
        if (preResult.decision === 'deny') {
            throw new Error(`MCP call '${qualifiedName}' denied by hook: ${preResult.reason ?? 'no reason'}`);
        }
        try {
            const result = await client.callTool(parsed.toolName, args);
            this.freshness.markFresh(parsed.serverName);
            // Post:mcp:call hook (fire-and-forget)
            const postEvent = {
                path: 'Post:mcp:call',
                phase: 'Post',
                category: 'mcp',
                specific: 'call',
                sessionId: '', timestamp: Date.now(),
                payload: { tool: qualifiedName, args },
            };
            dispatcher.fire(postEvent).catch((err) => { logger.debug('Post:mcp:call hook error', { error: summarizeError(err) }); });
            return result;
        }
        catch (err) {
            this.freshness.markFailed(parsed.serverName, summarizeError(err));
            // Fail:mcp:call hook (fire-and-forget)
            const failEvent = {
                path: 'Fail:mcp:call',
                phase: 'Fail',
                category: 'mcp',
                specific: 'call',
                sessionId: '', timestamp: Date.now(),
                payload: { tool: qualifiedName, args, error: summarizeError(err) },
            };
            dispatcher.fire(failEvent).catch((hookErr) => { logger.debug('Fail:mcp:call hook error', { error: String(hookErr) }); });
            throw err;
        }
    }
    /**
     * disconnectAll — Stop all connected MCP server processes.
     */
    async disconnectAll() {
        // Lifecycle:mcp:disconnected hooks (fire-and-forget for each server)
        const dispatcher = this.hookDispatcher;
        for (const name of this.clients.keys()) {
            const disconnectedEvent = {
                path: 'Lifecycle:mcp:disconnected',
                phase: 'Lifecycle',
                category: 'mcp',
                specific: 'disconnected',
                sessionId: '', timestamp: Date.now(),
                payload: { server: name },
            };
            dispatcher.fire(disconnectedEvent).catch((err) => { logger.debug('Lifecycle:mcp:disconnected hook error', { error: summarizeError(err) }); });
        }
        await Promise.allSettled(Array.from(this.clients.values()).map((client) => client.disconnect()));
        this.clients.clear();
        for (const sessionId of this.sandboxSessionByServer.values()) {
            this.sandboxSessions.stop(sessionId);
        }
        this.sandboxSessionByServer.clear();
    }
    /**
     * getClient — Get the McpClient for a given server name (for advanced use).
     */
    getClient(serverName) {
        return this.clients.get(serverName);
    }
    /** Connected server names. */
    get serverNames() {
        return Array.from(this.clients.keys());
    }
    /**
     * listServers — Return status info for all known servers (connected or not).
     */
    listServers() {
        return Array.from(this.clients.entries()).map(([name, client]) => ({
            name,
            connected: client.isConnected,
        }));
    }
    listServerSecurity() {
        return this.listServers().map((server) => {
            const permissions = this.permissions.getServerPermissions(server.name);
            const freshnessRecord = this.freshness.getRecord(server.name);
            return {
                name: server.name,
                connected: server.connected,
                role: permissions?.profile.role ?? 'general',
                trustMode: permissions?.profile.mode ?? 'ask-on-risk',
                allowedPaths: permissions?.profile.allowedPaths ?? [],
                allowedHosts: permissions?.profile.allowedHosts ?? [],
                schemaFreshness: this.freshness.getFreshness(server.name),
                quarantineReason: freshnessRecord?.quarantine?.reason,
                quarantineDetail: freshnessRecord?.quarantine?.detail,
                quarantineApprovedBy: freshnessRecord?.quarantine?.overrideAcknowledgedBy,
            };
        });
    }
    listServerSandboxBindings() {
        return this.serverNames.map((name) => {
            const sessionId = this.sandboxSessionByServer.get(name);
            const session = sessionId ? this.sandboxSessions.get(sessionId) : null;
            return {
                name,
                sessionId: sessionId ?? undefined,
                profileId: session?.profileId === 'mcp-shared' || session?.profileId === 'mcp-per-server'
                    ? session.profileId
                    : undefined,
                state: session?.state,
                backend: session?.resolvedBackend ?? session?.backend,
                startupStatus: session?.startupStatus,
            };
        });
    }
    setServerTrustMode(serverName, mode) {
        this.permissions.setTrustMode(serverName, mode);
        this._emitPolicyUpdate(serverName);
    }
    setServerRole(serverName, role) {
        this.permissions.setServerRole(serverName, role);
        this._emitPolicyUpdate(serverName);
    }
    listRecentSecurityDecisions(limit = 8) {
        return this.permissions.listRecentDecisions(limit);
    }
    quarantineSchema(serverName, reason, detail) {
        this.freshness.markQuarantined(serverName, reason, detail);
        if (this.runtimeBus) {
            emitMcpSchemaQuarantined(this.runtimeBus, {
                sessionId: 'mcp-registry',
                traceId: `mcp-registry:${serverName}:schema-quarantined`,
                source: 'mcp-registry',
            }, { serverId: serverName, reason, ...(detail ? { detail } : {}) });
        }
    }
    approveSchemaQuarantine(serverName, operatorId) {
        this.freshness.approveQuarantine(serverName, operatorId);
        if (this.runtimeBus) {
            emitMcpSchemaQuarantineApproved(this.runtimeBus, {
                sessionId: 'mcp-registry',
                traceId: `mcp-registry:${serverName}:schema-approved`,
                source: 'mcp-registry',
            }, { serverId: serverName, operatorId });
        }
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    async _connectServer(serverConfig) {
        const { name } = serverConfig;
        if (this.clients.has(name)) {
            logger.info('McpRegistry: server already registered', { name });
            return;
        }
        let sandboxSessionId = null;
        let processSpec;
        if (this.sandboxConfigManager) {
            const resolved = await this._resolveSandboxProcessSpec(serverConfig);
            sandboxSessionId = resolved?.sessionId ?? null;
            processSpec = resolved?.processSpec;
        }
        const client = new McpClient(serverConfig, processSpec ? { processSpec } : undefined);
        this.freshness.registerServer(name);
        try {
            await client.connect();
            this.permissions.registerServer(name, 'standard', {
                role: serverConfig.role ?? 'general',
                mode: serverConfig.trustMode ?? 'ask-on-risk',
                allowedPaths: serverConfig.allowedPaths ?? [],
                allowedHosts: serverConfig.allowedHosts ?? [],
            });
            this.clients.set(name, client);
            if (sandboxSessionId) {
                this.sandboxSessionByServer.set(name, sandboxSessionId);
            }
            this.freshness.markFresh(name);
            logger.info('McpRegistry: server connected', { name });
            if (this.runtimeBus) {
                emitMcpConfigured(this.runtimeBus, {
                    sessionId: 'mcp-registry',
                    traceId: `mcp-registry:${name}:configured`,
                    source: 'mcp-registry',
                }, {
                    serverId: name,
                    transport: 'stdio',
                    role: serverConfig.role ?? 'general',
                    trustMode: serverConfig.trustMode ?? 'ask-on-risk',
                    allowedPaths: serverConfig.allowedPaths ?? [],
                    allowedHosts: serverConfig.allowedHosts ?? [],
                });
            }
            // Lifecycle:mcp:connected hook (fire-and-forget)
            const connectedEvent = {
                path: 'Lifecycle:mcp:connected',
                phase: 'Lifecycle',
                category: 'mcp',
                specific: 'connected',
                sessionId: '', timestamp: Date.now(),
                payload: { server: name },
            };
            this.hookDispatcher.fire(connectedEvent).catch((err) => { logger.debug('Lifecycle:mcp:connected hook error', { error: summarizeError(err) }); });
        }
        catch (err) {
            if (sandboxSessionId) {
                this.sandboxSessions.stop(sandboxSessionId);
                this.sandboxSessionByServer.delete(name);
            }
            this.freshness.markFailed(name, summarizeError(err));
            logger.error('McpRegistry: failed to connect server', { name, err: summarizeError(err) });
            // Don't register the client — it's not usable
        }
    }
    async _resolveSandboxProcessSpec(serverConfig) {
        const configManager = this.sandboxConfigManager;
        if (!configManager)
            return null;
        const sandbox = getSandboxConfigSnapshot(configManager);
        if (sandbox.mcpIsolation === 'disabled')
            return null;
        const profileId = this._selectSandboxProfile(serverConfig);
        const label = `${serverConfig.name} MCP`;
        const session = await this.sandboxSessions.start(profileId, label, configManager);
        if (!session.launchPlan) {
            throw new Error(`Sandbox session ${session.id} for MCP server '${serverConfig.name}' is missing a launch plan.`);
        }
        const resolvedPlan = resolveSandboxCommandPlan(session.launchPlan, serverConfig.command, serverConfig.args ?? [], configManager);
        return {
            sessionId: session.id,
            processSpec: {
                command: resolvedPlan.command,
                args: [...resolvedPlan.args],
                env: compactEnv({ ...(serverConfig.env ?? {}), ...(resolvedPlan.env ?? {}) }),
                cwd: session.launchPlan.workspaceRoot,
                summary: resolvedPlan.summary,
                sandboxSessionId: session.id,
            },
        };
    }
    _selectSandboxProfile(serverConfig) {
        const configManager = this.sandboxConfigManager;
        if (!configManager)
            return 'mcp-shared';
        const sandbox = getSandboxConfigSnapshot(configManager);
        switch (sandbox.mcpIsolation) {
            case 'per-server-vm':
                return 'mcp-per-server';
            case 'shared-vm':
                return 'mcp-shared';
            case 'hybrid':
                return this._requiresDedicatedMcpSandbox(serverConfig) ? 'mcp-per-server' : 'mcp-shared';
            case 'disabled':
            default:
                return 'mcp-shared';
        }
    }
    _requiresDedicatedMcpSandbox(serverConfig) {
        return Boolean((serverConfig.allowedHosts?.length ?? 0) > 0
            || (serverConfig.allowedPaths?.length ?? 0) > 0
            || serverConfig.role === 'automation'
            || serverConfig.role === 'browser'
            || serverConfig.role === 'ops'
            || serverConfig.role === 'remote');
    }
    /**
     * Parse mcp:<server>:<tool> qualified name.
     * Returns null if the name doesn't match the expected format.
     */
    _parseQualifiedName(qualifiedName) {
        const parts = qualifiedName.split(':');
        if (parts.length < 3 || parts[0] !== 'mcp')
            return null;
        // serverName is parts[1], toolName is the rest joined (tools can have colons)
        const serverName = parts[1];
        const toolName = parts.slice(2).join(':');
        if (!serverName || !toolName)
            return null;
        return { serverName, toolName };
    }
    _emitPolicyUpdate(serverName) {
        if (!this.runtimeBus)
            return;
        const permissions = this.permissions.getServerPermissions(serverName);
        if (!permissions)
            return;
        emitMcpPolicyUpdated(this.runtimeBus, {
            sessionId: 'mcp-registry',
            traceId: `mcp-registry:${serverName}:policy`,
            source: 'mcp-registry',
        }, {
            serverId: serverName,
            role: permissions.profile.role,
            trustMode: permissions.profile.mode,
            allowedPaths: [...permissions.profile.allowedPaths],
            allowedHosts: [...permissions.profile.allowedHosts],
        });
    }
}
