/**
 * Per-alias event watchers.
 *
 * Each drawlatch key alias gets its own independent polling loop.
 * On startup, all agents are scanned for unique mcpKeyAlias values and a
 * watcher is started for each. Events are stored in the shared event log
 * and dispatched to the trigger system as before.
 *
 * Configuration via environment variables:
 *   EVENT_WATCHER_POLL_INTERVAL — poll interval in ms (default: 3000)
 */
import { appendEvent } from "./event-log.js";
import { dispatchEvent } from "./trigger-dispatcher.js";
import { getProxy, resetClient } from "./proxy-singleton.js";
import { listAgents } from "./agent-file-service.js";
import { resolveAgentKeyAlias } from "./agent-settings.js";
import { createLogger } from "../utils/logger.js";
const log = createLogger("event-watcher");
// ── Configuration ───────────────────────────────────────────────────
const BASE_POLL_INTERVAL = parseInt(process.env.EVENT_WATCHER_POLL_INTERVAL || "3000", 10);
const MAX_BACKOFF = 60_000; // 60 seconds
const watchers = new Map();
// ── Public API ──────────────────────────────────────────────────────
/**
 * Initialize event watchers for all agents that have an mcpKeyAlias.
 * Collects unique aliases and starts one watcher per alias.
 */
export function initEventWatchers() {
    const agents = listAgents();
    const aliases = new Set();
    for (const agent of agents) {
        const resolved = resolveAgentKeyAlias(agent);
        if (resolved.mcpKeyAlias) {
            aliases.add(resolved.mcpKeyAlias);
        }
    }
    if (aliases.size === 0) {
        log.info("No agents with mcpKeyAlias found — no event watchers started");
        return;
    }
    log.info(`Starting event watchers for ${aliases.size} alias(es): ${[...aliases].join(", ")}`);
    for (const alias of aliases) {
        startWatcherForAlias(alias);
    }
}
/**
 * Graceful shutdown: stop all watchers.
 */
export function shutdownEventWatchers() {
    for (const [alias, state] of watchers) {
        if (state.pollTimer) {
            clearTimeout(state.pollTimer);
            state.pollTimer = null;
        }
        log.info(`Event watcher stopped for alias "${alias}"`);
    }
    watchers.clear();
    log.info("All event watchers shut down");
}
/**
 * Start (or restart) a watcher for a specific alias.
 */
export function startWatcherForAlias(alias) {
    // Stop existing watcher if running
    stopWatcherForAlias(alias);
    const client = getProxy(alias);
    if (!client) {
        log.info(`Event watcher not started for alias "${alias}" — proxy unavailable`);
        return;
    }
    const state = {
        alias,
        pollTimer: null,
        cursors: new Map(),
        currentBackoff: BASE_POLL_INTERVAL,
        consecutiveFailures: 0,
    };
    watchers.set(alias, state);
    schedulePoll(state);
    log.info(`Event watcher started for alias "${alias}" — interval=${BASE_POLL_INTERVAL}ms`);
}
/**
 * Reset all per-connection cursors for a specific alias.
 *
 * Called when the proxy is reinitialized (e.g., after connection config changes
 * or mode switches). Ingestor restarts reset their event counters, so the
 * watcher's cursors must also be reset to avoid missing events whose IDs
 * fall below the previous cursor position.
 */
export function resetCursorsForAlias(alias) {
    const state = watchers.get(alias);
    if (!state)
        return;
    const count = state.cursors.size;
    state.cursors.clear();
    if (count > 0) {
        log.info(`Reset ${count} cursor(s) for alias "${alias}"`);
    }
}
/**
 * Stop a watcher for a specific alias.
 */
export function stopWatcherForAlias(alias) {
    const state = watchers.get(alias);
    if (!state)
        return;
    if (state.pollTimer) {
        clearTimeout(state.pollTimer);
        state.pollTimer = null;
    }
    watchers.delete(alias);
    log.info(`Event watcher stopped for alias "${alias}"`);
}
// ── Internal ────────────────────────────────────────────────────────
/**
 * Schedule the next poll for a watcher.
 */
function schedulePoll(state) {
    state.pollTimer = setTimeout(() => pollLoop(state), state.currentBackoff);
}
/**
 * Poll a single connection and process its events.
 * Each connection has its own cursor since event IDs are per-ingestor.
 */
async function pollConnection(state, proxyClient, connection) {
    const cursor = state.cursors.get(connection) ?? -1;
    const result = (await proxyClient.callTool("poll_events", {
        after_id: cursor,
        connection,
    }));
    // poll_events may return an array directly or wrapped in { events: [] }
    const events = Array.isArray(result) ? result : result?.events || [];
    if (events.length === 0)
        return;
    log.debug(`[${state.alias}/${connection}] Received ${events.length} events`);
    // Update per-connection cursor to the max event ID
    const maxId = Math.max(...events.map((e) => e.id));
    if (maxId > cursor)
        state.cursors.set(connection, maxId);
    // Store each event in its per-connection log and dispatch to triggers.
    // appendEvent() returns null for duplicates (same idempotency key),
    // so we only dispatch events that were actually stored.
    for (const event of events) {
        const stored = appendEvent({
            id: event.id,
            idempotencyKey: event.idempotencyKey,
            receivedAt: event.receivedAt,
            receivedAtMs: event.receivedAtMs,
            callerAlias: state.alias,
            source: event.source,
            instanceId: event.instanceId,
            eventType: event.eventType,
            data: event.data,
        });
        if (stored) {
            log.debug(`[${state.alias}/${connection}] Stored ${event.source}:${event.eventType} (event ${event.id})`);
            // Dispatch to trigger system (matching is sync, execution is async)
            dispatchEvent(stored);
        }
    }
}
/**
 * The main polling loop for one alias.
 *
 * Discovers active connections via ingestor_status, then polls each
 * connection independently in parallel. This ensures per-ingestor
 * event IDs don't interfere across connections — a high-volume source
 * (e.g. Discord) won't advance the cursor past a lower-volume source
 * (e.g. Slack).
 */
async function pollLoop(state) {
    try {
        const proxyClient = getProxy(state.alias);
        if (!proxyClient)
            return;
        // Discover active connections via ingestor_status
        const statusResult = await proxyClient.callTool("ingestor_status");
        const ingestors = Array.isArray(statusResult) ? statusResult : [];
        if (ingestors.length === 0) {
            state.consecutiveFailures = 0;
            state.currentBackoff = BASE_POLL_INTERVAL;
            schedulePoll(state);
            return;
        }
        // Poll each connection in parallel, each with its own cursor.
        // Individual connection failures are logged but don't fail the whole cycle.
        const results = await Promise.allSettled(ingestors.map((ingestor) => pollConnection(state, proxyClient, ingestor.connection)));
        for (const result of results) {
            if (result.status === "rejected") {
                log.warn(`[${state.alias}] Per-connection poll failed: ${result.reason?.message || result.reason}`);
            }
        }
        // Reset backoff on success
        state.consecutiveFailures = 0;
        state.currentBackoff = BASE_POLL_INTERVAL;
    }
    catch (err) {
        state.consecutiveFailures++;
        state.currentBackoff = Math.min(BASE_POLL_INTERVAL * Math.pow(2, state.consecutiveFailures), MAX_BACKOFF);
        log.warn(`[${state.alias}] Event poll failed (attempt ${state.consecutiveFailures}, next in ${state.currentBackoff}ms): ${err.message}`);
        // Auto-reset session on auth failure
        if (err.message?.includes("401") || err.message?.includes("Session expired")) {
            log.info(`[${state.alias}] Resetting proxy client for rehandshake...`);
            resetClient(state.alias);
        }
    }
    // Schedule next poll (always, even after failure)
    schedulePoll(state);
}
