"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerWorkersForWorkflow = registerWorkersForWorkflow;
exports.registerAllActiveWorkers = registerAllActiveWorkers;
const hotmesh_1 = require("@hotmeshio/hotmesh");
const db_1 = require("../../../lib/db");
const logger_1 = require("../../../lib/logger");
const ephemeral_1 = require("../../iam/ephemeral");
const mcpClient = __importStar(require("../../mcp/client"));
const connection_1 = require("../../mcp/client/connection");
const yamlDb = __importStar(require("../db"));
const scope_1 = require("./scope");
const callbacks_1 = require("./callbacks");
const events_1 = require("./events");
/**
 * HotMesh YAML maps serialize arrays as objects with numeric keys
 * (e.g., {0: "a", 1: "b"}). Recursively coerce these back to arrays
 * before passing args to MCP tools that expect real arrays.
 */
function coerceNumericObjects(obj) {
    if (Array.isArray(obj))
        return obj.map(coerceNumericObjects);
    if (obj === null || typeof obj !== 'object')
        return obj;
    const record = obj;
    const keys = Object.keys(record);
    if (keys.length > 0 && keys.every((k, i) => k === String(i))) {
        return keys.map((k) => coerceNumericObjects(record[k]));
    }
    const result = {};
    for (const [k, v] of Object.entries(record)) {
        result[k] = coerceNumericObjects(v);
    }
    return result;
}
/** Track which topics already have registered workers */
const registeredTopics = new Set();
/**
 * Register HotMesh workers for all activities in a YAML workflow.
 * Each worker delegates based on tool_source:
 * - 'db'  -> callDbTool() (internal DB MCP server)
 * - 'mcp' -> mcpClient.callServerTool() (external MCP servers)
 * - 'llm' -> callLLM() with prompt template interpolation
 */
async function registerWorkersForWorkflow(workflow) {
    const defaultRetry = {
        maximumAttempts: 3,
        backoffCoefficient: 2,
        maximumInterval: 30,
    };
    const workerConfigs = [];
    // Clear previously registered workers for this workflow so redeployments
    // pick up updated manifests (e.g., new hook_topic bindings).
    for (const a of workflow.activity_manifest) {
        if (a.type === 'worker') {
            const key = a.workflow_name ? `${a.topic}:${a.workflow_name}` : a.topic;
            registeredTopics.delete(key);
        }
    }
    // Build lookup: for escalate_and_wait workers, find the following hook's topic
    const hookTopicByEscalationTool = new Map();
    for (const hookAct of workflow.activity_manifest.filter(a => a.type === 'hook' && a.hook_topic)) {
        // Find the worker that transitions TO this hook — it's the escalation tool
        // Convention: the escalation worker's activity_id precedes the hook in sequence
        const hookIdx = workflow.activity_manifest.indexOf(hookAct);
        if (hookIdx > 0) {
            const preceding = workflow.activity_manifest[hookIdx - 1];
            if (preceding.mcp_tool_name === 'escalate_and_wait') {
                hookTopicByEscalationTool.set(preceding.activity_id, hookAct.hook_topic);
            }
        }
    }
    const workerActivities = workflow.activity_manifest.filter((a) => {
        if (a.type !== 'worker')
            return false;
        const key = a.workflow_name ? `${a.topic}:${a.workflow_name}` : a.topic;
        return !registeredTopics.has(key);
    });
    const totalSteps = workerActivities.length;
    let stepIndex = 0;
    for (const activity of workerActivities) {
        const currentStep = stepIndex++;
        const toolSource = activity.tool_source || (activity.mcp_server_id === 'db' ? 'db' : 'mcp');
        const wrap = (cb) => (0, events_1.wrapWithEvents)(activity, workflow.app_id, currentStep, totalSteps, (0, scope_1.wrapWithScope)(cb));
        if (toolSource === 'transform') {
            // Transform/reshape step — deterministic data mapping between steps
            workerConfigs.push({
                topic: activity.topic,
                workflowName: activity.workflow_name,
                connection: (0, db_1.getConnection)(),
                retry: defaultRetry,
                callback: wrap((0, callbacks_1.buildTransformCallback)(activity)),
            });
        }
        else if (toolSource === 'llm') {
            // LLM interpretation step
            workerConfigs.push({
                topic: activity.topic,
                workflowName: activity.workflow_name,
                connection: (0, db_1.getConnection)(),
                retry: defaultRetry,
                callback: wrap((0, callbacks_1.buildLlmCallback)(activity)),
            });
        }
        else if (toolSource === 'db') {
            if (!activity.mcp_tool_name)
                continue;
            const rawToolName = activity.mcp_tool_name;
            const toolName = rawToolName.includes('__') ? rawToolName.slice(rawToolName.indexOf('__') + 2) : rawToolName;
            const dbServerId = activity.mcp_server_id || 'long-tail-db';
            const toolArgs = activity.tool_arguments;
            workerConfigs.push({
                topic: activity.topic,
                workflowName: activity.workflow_name,
                connection: (0, db_1.getConnection)(),
                retry: defaultRetry,
                callback: wrap(async (data) => {
                    const wfName = data.data?.workflowName || activity.workflow_name || toolName;
                    logger_1.loggerRegistry.debug(`[yaml-workflow:worker] entering db/${toolName} wf=${wfName} argKeys=[${Object.keys(data.data || {}).join(',')}]`);
                    const args = (data.data || {});
                    let mergedArgs = toolArgs ? { ...toolArgs, ...args } : args;
                    delete mergedArgs._scope;
                    delete mergedArgs.workflowName;
                    mergedArgs = await (0, ephemeral_1.exchangeTokensInArgs)(mergedArgs);
                    const result = await mcpClient.callServerTool(dbServerId, toolName, mergedArgs);
                    logger_1.loggerRegistry.debug(`[yaml-workflow:worker] leaving db/${toolName} wf=${wfName} resultKeys=[${Object.keys(result || {}).join(',')}]`);
                    return { metadata: { ...data.metadata }, data: result };
                }),
            });
        }
        else {
            if (!activity.mcp_tool_name)
                continue;
            const rawToolName = activity.mcp_tool_name;
            const toolName = rawToolName.includes('__') ? rawToolName.slice(rawToolName.indexOf('__') + 2) : rawToolName;
            const serverId = activity.mcp_server_id;
            if (!serverId)
                continue;
            const storedArgs = activity.tool_arguments;
            // For escalate_and_wait, resolve hookTopic at runtime from the activity
            // context — multiple escalation workers share a single registered callback,
            // so we look up by activity_id from the incoming metadata, not the static
            // activity captured at registration time.
            const staticHookTopic = hookTopicByEscalationTool.get(activity.activity_id);
            // Identify keys that are wired via input_mappings. When a wired key
            // resolves to nothing (upstream step failed/returned null), we must
            // NOT fall back to stored tool_arguments — that would leak hardcoded
            // values from the original execution trace.
            const wiredKeys = new Set(Object.keys(activity.input_mappings || {}).filter(k => k !== '_scope' && k !== 'workflowName'));
            if (toolName === 'escalate_and_wait') {
                logger_1.loggerRegistry.info(`[yaml-workflow] escalate_and_wait worker: activityId=${activity.activity_id}, hookTopic=${staticHookTopic || 'NONE'}, mapKeys=[${[...hookTopicByEscalationTool.keys()].join(',')}]`);
            }
            workerConfigs.push({
                topic: activity.topic,
                workflowName: activity.workflow_name,
                connection: (0, db_1.getConnection)(),
                retry: defaultRetry,
                callback: wrap(async (data) => {
                    const wfName = data.data?.workflowName || activity.workflow_name || toolName;
                    logger_1.loggerRegistry.debug(`[yaml-workflow:worker] entering mcp/${toolName} wf=${wfName} server=${serverId} argKeys=[${Object.keys(data.data || {}).join(',')}]`);
                    const args = (data.data || {});
                    // Start from stored defaults, then strip any wired keys that
                    // didn't arrive (upstream failure) so stale defaults don't leak.
                    const mergedArgs = storedArgs ? { ...storedArgs } : {};
                    for (const wk of wiredKeys) {
                        if (!(wk in args))
                            delete mergedArgs[wk];
                    }
                    for (const [key, value] of Object.entries(args)) {
                        if (key === '_scope' || key === 'workflowName')
                            continue;
                        if (value !== undefined) {
                            mergedArgs[key] = value;
                        }
                    }
                    logger_1.loggerRegistry.debug(`[yaml-workflow:worker] merged mcp/${toolName} wf=${wfName} mergedKeys=[${Object.keys(mergedArgs).join(',')}]`);
                    // For escalate_and_wait: inject YAML signal routing so the MCP tool
                    // stores engine:'yaml' + hookTopic + jobId in the escalation metadata.
                    // Resolve hookTopic at runtime — multiple escalation workers share
                    // this callback, so we look up by the current activity_id from metadata.
                    const runtimeActivityId = data.metadata?.aid;
                    const yamlHookTopic = runtimeActivityId
                        ? hookTopicByEscalationTool.get(runtimeActivityId) || staticHookTopic
                        : staticHookTopic;
                    if (yamlHookTopic) {
                        const jid = data.metadata?.jid;
                        mergedArgs._yaml_signal_routing = {
                            engine: 'yaml',
                            appId: workflow.app_id,
                            hookTopic: yamlHookTopic,
                            jobId: jid,
                            workflowType: workflow.graph_topic,
                            workflowId: jid,
                            taskQueue: workflow.app_id,
                        };
                    }
                    const exchangedArgs = await (0, ephemeral_1.exchangeTokensInArgs)(mergedArgs);
                    const coercedArgs = coerceNumericObjects(exchangedArgs);
                    // Try direct dispatch for built-in servers (bypasses MCP transport).
                    // Falls through to mcpClient.callServerTool() for external servers.
                    let result;
                    const builtin = await (0, connection_1.dispatchBuiltinTool)(serverId, toolName, coercedArgs);
                    if (builtin) {
                        result = builtin.result;
                    }
                    else {
                        result = await mcpClient.callServerTool(serverId, toolName, coercedArgs);
                    }
                    if (result && typeof result === 'object' && 'error' in result) {
                        logger_1.loggerRegistry.error(`[yaml-workflow:worker] ${toolName} error: ${JSON.stringify(result).slice(0, 200)}`);
                    }
                    if (result == null) {
                        logger_1.loggerRegistry.warn(`[yaml-workflow:worker] ${toolName} returned null/undefined`);
                    }
                    logger_1.loggerRegistry.debug(`[yaml-workflow:worker] leaving mcp/${toolName} wf=${wfName} resultKeys=[${Object.keys(result || {}).join(',')}]`);
                    return { metadata: { ...data.metadata }, data: result };
                }),
            });
        }
        const regKey = activity.workflow_name ? `${activity.topic}:${activity.workflow_name}` : activity.topic;
        registeredTopics.add(regKey);
    }
    if (workerConfigs.length === 0)
        return;
    await hotmesh_1.HotMesh.init({
        appId: workflow.app_id,
        guid: `compiled::${workflow.graph_topic}-${hotmesh_1.HotMesh.guid()}`,
        engine: {
            connection: (0, db_1.getConnection)(),
            retry: {
                maximumAttempts: 3,
                backoffCoefficient: 2,
                maximumInterval: '30s',
            },
        },
        workers: workerConfigs,
    });
    logger_1.loggerRegistry.info(`[yaml-workflow] registered ${workerConfigs.length} workers for ${workflow.app_id}`);
}
/**
 * Register workers for all active YAML workflows.
 * Groups by app_id so flows sharing a namespace share a single engine/connection pool.
 * Called at startup in workers/index.ts.
 */
async function registerAllActiveWorkers() {
    logger_1.loggerRegistry.info('[yaml-workflow] checking for active YAML workflows...');
    try {
        const workflows = await yamlDb.getActiveYamlWorkflows();
        logger_1.loggerRegistry.info(`[yaml-workflow] found ${workflows.length} active workflow(s)`);
        if (workflows.length === 0)
            return;
        // Group by app_id to register all workers for the same app together
        const byAppId = new Map();
        for (const wf of workflows) {
            const group = byAppId.get(wf.app_id) || [];
            group.push(wf);
            byAppId.set(wf.app_id, group);
        }
        for (const [appId, group] of byAppId) {
            try {
                for (const wf of group) {
                    await registerWorkersForWorkflow(wf);
                }
                logger_1.loggerRegistry.info(`[yaml-workflow] ${appId}: registered workers for ${group.length} flow(s)`);
            }
            catch (err) {
                logger_1.loggerRegistry.error(`[yaml-workflow] failed to register workers for ${appId}: ${err.message}`);
            }
        }
    }
    catch (err) {
        // Table may not exist yet if migration hasn't run
        if (err.message?.includes('does not exist')) {
            logger_1.loggerRegistry.info('[yaml-workflow] table not yet created, skipping worker registration');
            return;
        }
        logger_1.loggerRegistry.error(`[yaml-workflow] startup worker registration failed: ${err.message}`);
    }
}
