/**
 * AsyncGate — lightweight async MCP handler wrapper.
 *
 * Long-running MCP tool calls timeout in Claude Code (~60s). This module lets
 * any handler opt into an async contract: if work exceeds maxSyncMs, return
 * {queued:true, runId} immediately and finish in the background. Callers poll
 * nexus_run_status(runId) for completion.
 *
 * Design:
 *   - In-memory ring buffer (cap 200 jobs). No SQLite dep — keep hot path fast.
 *   - withAsyncGate wraps a handler fn; uses Promise.race against a deadline.
 *   - getAsyncGate() returns the process-singleton instance.
 */
import { randomUUID } from 'crypto';
import { nexusEventBus } from '../../../engines/event-bus.js';
// ─── Ring buffer ──────────────────────────────────────────────────────────────
const RING_CAP = 200;
class AsyncGate {
    jobs = new Map();
    insertOrder = [];
    /** Run handler; if it takes >maxSyncMs, queue it and return runId immediately. */
    async run(fn, opts) {
        const maxSyncMs = opts.maxSyncMs ?? 2000;
        const runId = randomUUID();
        const job = {
            runId,
            tool: opts.tool,
            args: opts.args,
            status: 'pending',
            stage: 'queued',
            progress: 0,
            queuedAt: Date.now(),
            etaMs: opts.etaMs,
        };
        this.enqueueJob(job);
        const startWork = () => (async () => {
            job.status = 'running';
            job.stage = 'executing';
            job.startedAt = Date.now();
            try {
                const result = await fn();
                job.status = 'completed';
                job.stage = 'done';
                job.progress = 100;
                job.result = result;
                job.completedAt = Date.now();
                nexusEventBus.emit('async_gate.completed', { runId, tool: opts.tool });
                return result;
            }
            catch (err) {
                job.status = 'failed';
                job.stage = 'error';
                job.error = err instanceof Error ? err.message : String(err);
                job.completedAt = Date.now();
                nexusEventBus.emit('async_gate.failed', { runId, tool: opts.tool, error: job.error });
                return {
                    content: [{
                            type: 'text',
                            text: `❌ Run ${runId} failed\nTool: ${opts.tool}\nError: ${job.error ?? 'unknown'}`,
                        }],
                };
            }
        })();
        if (opts.alwaysQueue) {
            setImmediate(() => {
                startWork().catch(() => { });
            });
            return { queued: true, runId, etaMs: opts.etaMs };
        }
        // Race: handler vs deadline. Note: this only protects paths that yield
        // quickly. Cold synchronous work should set alwaysQueue so the receipt is
        // returned before the heavy section starts.
        let syncResolved = false;
        const deadline = new Promise((resolve) => setTimeout(() => resolve(null), maxSyncMs));
        const work = startWork();
        // Suppress unhandled rejection: deadline may win while work is still in flight.
        work.catch(() => { });
        const winner = await Promise.race([
            work.then((r) => { syncResolved = true; return r; }),
            deadline,
        ]);
        if (syncResolved && winner !== null) {
            // Completed within deadline — return inline
            return winner;
        }
        // Still running — return async receipt
        return { queued: true, runId, etaMs: opts.etaMs };
    }
    getJob(runId) {
        return this.jobs.get(runId);
    }
    /** Format job status as MCP text response */
    formatStatus(runId) {
        const job = this.jobs.get(runId);
        if (!job) {
            return { content: [{ type: 'text', text: `❌ Async run not found: ${runId}` }] };
        }
        if (job.status === 'completed' && job.result) {
            return job.result;
        }
        if (job.status === 'failed') {
            return {
                content: [{
                        type: 'text',
                        text: `❌ Run ${runId} failed\nTool: ${job.tool}\nError: ${job.error ?? 'unknown'}`,
                    }],
            };
        }
        const elapsed = Date.now() - job.queuedAt;
        const etaLine = job.etaMs ? `  ETA: ~${Math.ceil((job.etaMs - elapsed) / 1000)}s remaining` : '';
        return {
            content: [{
                    type: 'text',
                    text: [
                        `⏳ Run ${runId}`,
                        `  Tool:     ${job.tool}`,
                        `  Status:   ${job.status}`,
                        `  Stage:    ${job.stage}`,
                        `  Progress: ${job.progress}%`,
                        `  Elapsed:  ${Math.round(elapsed / 100) / 10}s`,
                        etaLine,
                        '',
                        'Poll again with nexus_run_status(runId) in a few seconds.',
                    ].filter(l => l !== undefined).join('\n'),
                }],
        };
    }
    updateStage(runId, stage, progress) {
        const job = this.jobs.get(runId);
        if (job && job.status === 'running') {
            job.stage = stage;
            job.progress = Math.min(99, progress);
        }
    }
    markLifecycleReported(runId) {
        const job = this.jobs.get(runId);
        if (job)
            job.lifecycleReportedAt = Date.now();
    }
    /** Return jobs that have been running longer than their stale threshold. */
    getStaleJobs(now, defaultStaleMs, etaMultiplier) {
        const stale = [];
        for (const job of this.jobs.values()) {
            if (job.status !== 'running')
                continue;
            const startedAt = job.startedAt ?? job.queuedAt;
            const threshold = job.etaMs != null
                ? job.etaMs * etaMultiplier
                : defaultStaleMs;
            if (now - startedAt > threshold)
                stale.push(job);
        }
        return stale;
    }
    /** Mark a running job as failed (watchdog reap). */
    markFailed(runId, reason) {
        const job = this.jobs.get(runId);
        if (job && (job.status === 'running' || job.status === 'pending')) {
            job.status = 'failed';
            job.stage = 'error';
            job.error = reason;
            job.completedAt = Date.now();
        }
    }
    enqueueJob(job) {
        if (this.insertOrder.length >= RING_CAP) {
            // Skip eviction if oldest job is still running — losing it would make polls fail.
            const oldestId = this.insertOrder[0];
            const oldest = oldestId ? this.jobs.get(oldestId) : undefined;
            if (oldest && (oldest.status === 'running' || oldest.status === 'pending')) {
                // Buffer full with live jobs — evict second-oldest completed/failed instead
                const evictIdx = this.insertOrder.findIndex(id => {
                    const j = this.jobs.get(id);
                    return j && j.status !== 'running' && j.status !== 'pending';
                });
                if (evictIdx !== -1) {
                    const evictId = this.insertOrder.splice(evictIdx, 1)[0];
                    if (evictId)
                        this.jobs.delete(evictId);
                }
                // If all live, just append without eviction (buffer temporarily oversized).
            }
            else {
                this.insertOrder.shift();
                if (oldestId)
                    this.jobs.delete(oldestId);
            }
        }
        this.jobs.set(job.runId, job);
        this.insertOrder.push(job.runId);
    }
}
// ─── Singleton ────────────────────────────────────────────────────────────────
let _instance = null;
export function getAsyncGate() {
    if (!_instance)
        _instance = new AsyncGate();
    return _instance;
}
// ─── withAsyncGate helper ─────────────────────────────────────────────────────
/**
 * Wrap an MCP handler function in the async gate.
 *
 * Usage:
 *   return withAsyncGate(() => doSlowWork(), { tool: 'nexus_orchestrate', maxSyncMs: 2000, etaMs: 30_000 });
 *
 * If work finishes within maxSyncMs → inline result (transparent).
 * If not → { queued: true, runId } immediately; work continues in bg.
 */
export function withAsyncGate(fn, opts) {
    return getAsyncGate().run(fn, opts);
}
