"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.agentTriggerRegistry = void 0;
const crypto_1 = require("crypto");
const logger_1 = require("../../lib/logger");
const publish_1 = require("../../lib/events/publish");
const subscriptions_1 = require("./subscriptions");
const input_mapper_1 = require("./input-mapper");
const index_1 = require("./index");
/**
 * Agent Trigger Registry — arms event subscriptions for active agents at startup.
 *
 * Analogous to LTCronRegistry but for event-driven reactions. When an event
 * matches a subscription's topic pattern (and optional filter), the registry
 * invokes the configured workflow with a deterministic ID for distributed dedup.
 */
class AgentTriggerRegistry {
    constructor() {
        this.adapter = null;
        this.unsubs = new Map();
        this.connected = false;
    }
    /**
     * Load all active subscriptions from DB and arm event listeners.
     * Also subscribes to `agent.triggers_changed` so other containers
     * can signal this one to re-arm after subscription CRUD.
     */
    async connect(adapter) {
        this.adapter = adapter;
        const subs = await (0, subscriptions_1.listActiveSubscriptions)();
        for (const sub of subs) {
            logger_1.loggerRegistry.info(`[long-tail] arming trigger: ${sub.agent_name}/${sub.topic} → ${sub.reaction_type}:${sub.tool_name || sub.workflow_type || sub.mcp_prompt?.slice(0, 20) || '?'}`);
            this.armSubscription(sub);
        }
        // Listen for cross-container trigger sync signals
        adapter.on('agent.triggers_changed', (event) => {
            const agentId = event.data?.agentId;
            const action = event.data?.action;
            if (!agentId)
                return;
            if (action === 'deleted') {
                this.stopAgent(agentId);
                logger_1.loggerRegistry.info(`[long-tail] agent triggers stopped (remote): ${agentId}`);
            }
            else {
                this.restartAgent(agentId).then(() => {
                    logger_1.loggerRegistry.info(`[long-tail] agent triggers synced (remote): ${agentId}`);
                }).catch(() => { });
            }
        });
        this.connected = true;
        logger_1.loggerRegistry.info(`[long-tail] agent trigger registry: ${subs.length} subscription(s) armed`);
    }
    /**
     * Re-arm all subscriptions for a specific agent (after config change).
     */
    async restartAgent(agentId) {
        if (!this.adapter)
            return;
        // Stop existing subs for this agent
        for (const [key, unsub] of this.unsubs) {
            if (key.startsWith(`${agentId}:`)) {
                unsub();
                this.unsubs.delete(key);
            }
        }
        // Reload from DB
        const subs = await (0, subscriptions_1.listActiveSubscriptions)();
        const agentSubs = subs.filter((s) => s.agent_id === agentId);
        for (const sub of agentSubs) {
            this.armSubscription(sub);
        }
        logger_1.loggerRegistry.info(`[long-tail] agent triggers restarted for ${agentId}: ${agentSubs.length} sub(s)`);
    }
    /**
     * Stop all subscriptions for a specific agent.
     */
    stopAgent(agentId) {
        for (const [key, unsub] of this.unsubs) {
            if (key.startsWith(`${agentId}:`)) {
                unsub();
                this.unsubs.delete(key);
            }
        }
    }
    /**
     * Disconnect all subscriptions.
     */
    disconnect() {
        for (const unsub of this.unsubs.values()) {
            unsub();
        }
        this.unsubs.clear();
        this.connected = false;
        logger_1.loggerRegistry.info('[long-tail] agent trigger registry disconnected');
    }
    armSubscription(sub) {
        if (!this.adapter)
            return;
        const key = `${sub.agent_id}:${sub.id}`;
        const handler = this.buildHandler(sub);
        // Normalize topic: strip the 'lt.events.' prefix if present, since
        // CallbackEventAdapter matches against raw event.type (e.g., 'workflow.failed',
        // not 'lt.events.workflow.failed'). Users may store either form in the DB.
        const rawTopic = sub.topic.replace(/^lt\.events\./, '');
        const unsub = this.adapter.on(rawTopic, handler);
        this.unsubs.set(key, unsub);
    }
    buildHandler(sub) {
        return async (event) => {
            try {
                logger_1.loggerRegistry.info(`[long-tail] trigger matched: ${sub.agent_name}/${sub.topic} for event ${event.type}`);
                // 1. Evaluate optional filter
                if (sub.filter && !this.matchesFilter(event, sub.filter)) {
                    logger_1.loggerRegistry.info(`[long-tail] trigger filtered out: ${sub.agent_name}/${sub.topic}`);
                    return;
                }
                // 2. Derive deterministic workflow ID for distributed dedup
                const deterministicId = this.deriveWorkflowId(sub, event);
                // 3. Apply input mapping to transform event → envelope data
                const mapped = Object.keys(sub.input_mapping).length > 0
                    ? (0, input_mapper_1.applyInputMapping)(sub.input_mapping, event)
                    : { data: event.data ?? {}, metadata: { source: 'agent', agentName: sub.agent_name } };
                // 4. Emit agent.started
                (0, publish_1.publishAgentEvent)({
                    type: 'agent.started',
                    agentId: sub.agent_id,
                    agentName: sub.agent_name,
                    data: { topic: sub.topic, eventType: event.type, deterministicId },
                });
                // 5. Invoke the reaction workflow
                await this.executeReaction(sub, mapped, deterministicId);
                // 6. Emit agent.completed + update last_run_at
                (0, publish_1.publishAgentEvent)({
                    type: 'agent.completed',
                    agentId: sub.agent_id,
                    agentName: sub.agent_name,
                    data: { topic: sub.topic, eventType: event.type },
                });
                (0, index_1.updateAgent)(sub.agent_id, {
                    last_run_at: new Date().toISOString(),
                }).catch(() => { }); // best-effort
            }
            catch (err) {
                // Duplicate job errors are expected in multi-container — another
                // container won the race. Log as info and continue.
                const isDuplicate = err.message?.includes('already exists');
                if (isDuplicate) {
                    logger_1.loggerRegistry.info(`[long-tail] agent trigger dedup (another container handled): ${sub.agent_name}/${sub.topic}`);
                }
                else {
                    (0, publish_1.publishAgentEvent)({
                        type: 'agent.failed',
                        agentId: sub.agent_id,
                        agentName: sub.agent_name,
                        status: 'error',
                        data: { topic: sub.topic, error: err.message },
                    });
                    logger_1.loggerRegistry.warn(`[long-tail] agent trigger failed: ${sub.agent_name}/${sub.topic}: ${err.message}`);
                }
            }
        };
    }
    /**
     * Shallow key-value match: every key in filter must match the corresponding
     * key in event.data. Missing keys in event.data fail the match.
     */
    /**
     * Match event.data against a filter object.
     *
     * Supports:
     * - Equality:  { "extension": "png" }
     * - Negation:  { "extension": { "$ne": "png" } }
     * - In-list:   { "extension": { "$in": ["png", "jpg"] } }
     * - Exists:    { "trace_id": { "$exists": true } }
     */
    matchesFilter(event, filter) {
        const data = event.data ?? {};
        for (const [key, expected] of Object.entries(filter)) {
            const actual = data[key];
            if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
                if ('$ne' in expected && actual === expected.$ne)
                    return false;
                if ('$in' in expected && !expected.$in.includes(actual))
                    return false;
                if ('$exists' in expected) {
                    const exists = actual !== undefined && actual !== null;
                    if (expected.$exists !== exists)
                        return false;
                }
            }
            else {
                if (actual !== expected)
                    return false;
            }
        }
        return true;
    }
    /**
     * Deterministic workflow ID derived from the subscription + event.
     * Multiple containers receiving the same event produce the same ID,
     * so HotMesh's idempotent workflow.start() prevents duplicate execution.
     */
    deriveWorkflowId(sub, event) {
        const eventKey = event.workflowId || event.taskId || event.escalationId || '';
        const uniquePart = eventKey || (0, crypto_1.createHash)('sha256')
            .update(`${event.timestamp}:${event.type}:${JSON.stringify(event.data ?? {})}`)
            .digest('hex')
            .slice(0, 12);
        return `agent-${sub.agent_id}-${sub.id.slice(0, 8)}-${uniquePart}`;
    }
    async executeReaction(sub, mapped, deterministicId) {
        switch (sub.reaction_type) {
            case 'durable': {
                const { invokeWorkflow } = await Promise.resolve().then(() => __importStar(require('../workflow-invocation')));
                await invokeWorkflow({
                    workflowType: sub.workflow_type,
                    data: mapped.data ?? mapped,
                    metadata: mapped.metadata ?? { source: 'agent', certified: true },
                    executeAs: sub.execute_as ?? sub.agent_user_id ?? undefined,
                    options: { workflowId: deterministicId },
                    auth: { userId: sub.agent_user_id || 'lt-system', role: 'admin' },
                });
                break;
            }
            case 'pipeline': {
                const { invokeYamlWorkflow } = await Promise.resolve().then(() => __importStar(require('../yaml-workflow/invoke')));
                const { getYamlWorkflow } = await Promise.resolve().then(() => __importStar(require('../yaml-workflow/db')));
                const wf = await getYamlWorkflow(sub.pipeline_id);
                if (!wf)
                    throw new Error(`Pipeline ${sub.pipeline_id} not found`);
                await invokeYamlWorkflow(wf, {
                    data: mapped.data ?? mapped,
                    execute_as: sub.execute_as ?? sub.agent_user_id ?? undefined,
                    source: 'agent',
                    jobId: deterministicId,
                });
                break;
            }
            case 'mcp_query': {
                const { startMcpQuery } = await Promise.resolve().then(() => __importStar(require('../insight')));
                await startMcpQuery({
                    prompt: sub.mcp_prompt,
                    wait: false,
                    userId: sub.agent_user_id ?? undefined,
                });
                break;
            }
            case 'capability': {
                logger_1.loggerRegistry.info(`[long-tail] agent capability: ${sub.agent_name} → ${sub.tool_name} on ${sub.server_id} (id=${deterministicId})`);
                const { invokeWorkflow } = await Promise.resolve().then(() => __importStar(require('../workflow-invocation')));
                await invokeWorkflow({
                    workflowType: 'capabilityInvoke',
                    data: {
                        serverId: sub.server_id,
                        toolName: sub.tool_name,
                        arguments: mapped,
                    },
                    metadata: { source: 'agent' },
                    executeAs: sub.execute_as ?? sub.agent_user_id ?? undefined,
                    options: { workflowId: deterministicId },
                    auth: { userId: sub.agent_user_id || 'lt-system', role: 'admin' },
                });
                break;
            }
        }
    }
}
exports.agentTriggerRegistry = new AgentTriggerRegistry();
