/**
 * runHandler — MCP tool handler wrapper.
 *
 * Provides three invariants every handler needs:
 *   1. Bounded execution time — timeout → structured envelope, not a hang.
 *   2. Labeled failures    — { ok, error: { code, message, retriable, hint } }
 *   3. Transient retries   — SQLite-busy / cold runtime → one retry with backoff.
 *
 * Per-client timeouts (AI Lead refinement):
 *   Codex 90 s · Claude Code 60 s · Cursor 30 s · MCP clients 60 s · default 60 s
 *
 * Types and `envelopeToMcpResponse` are canonical in ./envelope.ts;
 * re-exported from here for backward compatibility.
 */
import { nexusEventBus } from '../../../engines/event-bus.js';
import { recordPerf } from '../../../engines/perf-history.js';
export { envelopeToMcpResponse } from './envelope.js';
// ─── Per-client timeout map ───────────────────────────────────────────────────
const CLIENT_TIMEOUTS = {
    'codex': 90_000,
    'claude-code': 60_000,
    'cursor': 30_000,
    'openclaw': 90_000,
    'hermes': 90_000,
    'nanoclaw': 90_000,
    'picoclaw': 90_000,
    'opencode': 90_000,
    'windsurf': 90_000,
    'antigravity': 90_000,
    'mcp': 60_000,
};
const DEFAULT_TIMEOUT = 60_000;
function resolveTimeout(opts) {
    if (opts.timeoutMs !== undefined)
        return opts.timeoutMs;
    const callerName = String(opts.callerName ?? '').trim().toLowerCase();
    if (callerName && callerName in CLIENT_TIMEOUTS) {
        return CLIENT_TIMEOUTS[callerName];
    }
    return DEFAULT_TIMEOUT;
}
export function resolveHandlerTimeoutForTest(opts = {}) {
    return resolveTimeout(opts);
}
// ─── Error classification ─────────────────────────────────────────────────────
function classifyError(err) {
    const msg = err instanceof Error ? err.message : String(err);
    if (/SQLITE_BUSY|SQLITE_LOCKED/i.test(msg))
        return 'sqlite-busy';
    if (/runtime not yet ready|synapse runtime not yet/i.test(msg))
        return 'runtime-cold';
    if (/invalid.*input|input.*invalid|zod/i.test(msg))
        return 'input-invalid';
    return 'internal';
}
const RETRY_POLICIES = {
    // SQLite lock: 3 retries with exponential backoff (100 → 300 → 900 ms).
    'sqlite-busy': {
        maxRetries: 3,
        backoffMs: (attempt) => 100 * Math.pow(3, attempt - 1),
    },
    // Cold runtime: 1 immediate retry (the runtime may warm up in milliseconds).
    'runtime-cold': {
        maxRetries: 1,
        backoffMs: () => 0,
    },
    // Bad input and timeouts are never retried — retrying won't fix them.
    'input-invalid': { maxRetries: 0, backoffMs: () => 0 },
    'timeout': { maxRetries: 0, backoffMs: () => 0 },
};
const DEFAULT_RETRY_POLICY = {
    maxRetries: 1,
    backoffMs: () => 200,
};
function getRetryPolicy(code) {
    return RETRY_POLICIES[code] ?? DEFAULT_RETRY_POLICY;
}
// ─── Core wrapper ─────────────────────────────────────────────────────────────
/**
 * Run a handler function with timeout, retry, and structured envelope.
 *
 * @example
 * return runHandler('nexus_orchestrate', async () => {
 *     const result = await nexus.orchestrate(goal);
 *     return result;
 * }, { callerName: hctx.adapterName });
 */
export async function runHandler(toolName, fn, opts = {}) {
    const timeoutMs = resolveTimeout(opts);
    const startMs = Date.now();
    let attempts = 0;
    // eslint-disable-next-line no-constant-condition -- retry loop exits via return/break
    while (true) {
        attempts++;
        let timeoutHandle;
        try {
            const timeoutPromise = new Promise((_, reject) => {
                timeoutHandle = setTimeout(() => reject(new Error(`__nexus_timeout__`)), timeoutMs);
            });
            const result = await Promise.race([fn(), timeoutPromise]);
            clearTimeout(timeoutHandle);
            const envelope = {
                ok: true,
                data: result,
                meta: {
                    toolName,
                    durationMs: Date.now() - startMs,
                    attempts,
                    runtimeHotAt: opts.runtimeHotAt ?? null,
                },
            };
            nexusEventBus.emit('mcp.handler.complete', { toolName, attempts, durationMs: envelope.meta.durationMs });
            recordPerf(toolName, envelope.meta.durationMs);
            return envelope;
        }
        catch (err) {
            clearTimeout(timeoutHandle);
            const isTimeout = err instanceof Error && (err.message === '__nexus_timeout__' ||
                err.message === '__nexus_pipeline_timeout__');
            const code = isTimeout ? 'timeout' : classifyError(err);
            const policy = getRetryPolicy(code);
            // opts.retries overrides the policy maxRetries if provided explicitly.
            const maxRetries = opts.retries !== undefined ? opts.retries : policy.maxRetries;
            const canRetry = attempts <= maxRetries;
            if (canRetry) {
                const delay = policy.backoffMs(attempts);
                nexusEventBus.emit('mcp.handler.retry', {
                    toolName,
                    code,
                    attempt: attempts,
                    delayMs: delay,
                });
                if (delay > 0)
                    await new Promise(r => setTimeout(r, delay));
                continue;
            }
            const message = isTimeout
                ? `Handler timed out after ${timeoutMs}ms`
                : (err instanceof Error ? err.message : String(err));
            const retriable = policy.maxRetries > 0 && !isTimeout;
            const envelope = {
                ok: false,
                error: {
                    code,
                    message,
                    retriable,
                    hint: code === 'runtime-cold' ? 'Retry after calling nexus_session_bootstrap' :
                        code === 'sqlite-busy' ? 'SQLite is locked — retry in 1–2 seconds' :
                            undefined,
                },
                meta: {
                    toolName,
                    durationMs: Date.now() - startMs,
                    attempts,
                    runtimeHotAt: opts.runtimeHotAt ?? null,
                },
            };
            nexusEventBus.emit('mcp.handler.failed', { toolName, code, attempts, durationMs: envelope.meta.durationMs });
            return envelope;
        }
    }
}
