import http from 'http';
import { readFileSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { randomBytes, randomUUID } from 'crypto';
import { URL } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function readDaemonPkgVersion() {
    for (const rel of ['../../package.json', '../package.json']) {
        try {
            const parsed = JSON.parse(readFileSync(path.join(__dirname, rel), 'utf8'));
            if (parsed.version)
                return parsed.version;
        }
        catch { /* try next */ }
    }
    return 'unknown';
}
const DAEMON_PKG_VERSION = readDaemonPkgVersion();
import { createNexusPrime } from '../index.js';
import { MCPAdapter } from '../agents/adapters/mcp.js';
import { nexusEventBus } from '../engines/event-bus.js';
import { runStartupHygiene } from '../engines/runtime-hygiene.js';
import { runIntegrityChecks } from '../engines/schema/integrity.js';
import { runPreflight } from './preflight.js';
import { acquireDaemonLock, removeDaemonLock, writeDaemonLock, HEARTBEAT_INTERVAL_MS, } from './lock.js';
import { invokerRegistry } from '../invokers/registry.js';
function listen(server, host) {
    return new Promise((resolve, reject) => {
        server.once('error', reject);
        server.listen(0, host, () => {
            const address = server.address();
            if (!address || typeof address === 'string') {
                reject(new Error('Unable to resolve daemon rpc address.'));
                return;
            }
            resolve(address.port);
        });
    });
}
function parseRequestBody(req) {
    return new Promise((resolve, reject) => {
        const chunks = [];
        req.on('data', (chunk) => {
            chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
            if (chunks.reduce((total, entry) => total + entry.length, 0) > 1_000_000) {
                reject(new Error('Request body too large.'));
            }
        });
        req.on('end', () => {
            if (chunks.length === 0) {
                resolve({});
                return;
            }
            try {
                resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
            }
            catch (error) {
                reject(error);
            }
        });
        req.on('error', reject);
    });
}
function sendJson(res, statusCode, payload) {
    const body = JSON.stringify(payload);
    res.writeHead(statusCode, {
        'content-type': 'application/json',
        'content-length': Buffer.byteLength(body, 'utf8'),
    });
    res.end(body);
}
function formatQueuedToolRun(run) {
    return {
        content: [{
                type: 'text',
                text: JSON.stringify({
                    queued: run.state === 'running',
                    runId: run.runId,
                    state: run.state,
                    toolName: run.toolName,
                    startedAt: run.startedAt,
                    finishedAt: run.finishedAt,
                    result: run.result ?? null,
                    error: run.error ?? null,
                }, null, 2),
            }],
    };
}
export class NexusDaemonServer {
    workspace;
    host = '127.0.0.1';
    httpServer;
    authToken = randomBytes(24).toString('hex');
    sessionAdapters = new Map();
    queuedRuns = new Map();
    cleanupTimer;
    nexus = null;
    lockPath = '';
    lockRecord = null;
    heartbeatTimer = null;
    stopping = false;
    runtimeReady = false;
    constructor(workspace) {
        this.workspace = workspace;
        this.httpServer = http.createServer((req, res) => {
            void this.handleRequest(req, res);
        });
        this.cleanupTimer = setInterval(() => {
            try {
                this.pruneState();
            }
            catch (err) {
                // Never let cleanup crash the daemon.
                console.error('[nexus-daemon] pruneState failed:', err?.message ?? err);
            }
        }, 60_000);
        this.cleanupTimer.unref?.();
    }
    installProcessErrorHandlers() {
        // Prevent a single unhandled rejection / uncaught throw from killing the
        // daemon.  Engines like orchestrator + SSE + synapse emit fire-and-forget
        // promises; without this the first background throw takes the whole
        // control plane down and orchestration starts "failing" silently for the
        // client.
        process.on('uncaughtException', (err) => {
            try {
                console.error('[nexus-daemon] uncaughtException:', err?.stack ?? err?.message ?? err);
                nexusEventBus.emit('nexus.daemon.error', {
                    kind: 'uncaughtException',
                    message: err?.message ?? String(err),
                    stack: err?.stack ?? null,
                });
            }
            catch { /* last-resort logging — swallow */ }
        });
        process.on('unhandledRejection', (reason) => {
            try {
                const message = reason?.message ?? String(reason);
                const stack = reason?.stack ?? null;
                console.error('[nexus-daemon] unhandledRejection:', stack ?? message);
                nexusEventBus.emit('nexus.daemon.error', {
                    kind: 'unhandledRejection',
                    message,
                    stack,
                });
            }
            catch { /* last-resort logging — swallow */ }
        });
    }
    /**
     * Live read of the lock record (port + token) so supervisors and other
     * callers can re-fetch credentials after a stop()/start() cycle without
     * holding stale references.
     */
    getLockRecord() {
        return this.lockRecord;
    }
    startHeartbeat() {
        if (this.heartbeatTimer) {
            return;
        }
        this.heartbeatTimer = setInterval(() => {
            if (this.lockPath && this.lockRecord) {
                try {
                    this.lockRecord = writeDaemonLock(this.lockPath, {
                        ...this.lockRecord,
                        heartbeatAt: Date.now(),
                    });
                }
                catch {
                    // Non-fatal; worst case the next starter will detect us as stale.
                }
            }
        }, HEARTBEAT_INTERVAL_MS);
        this.heartbeatTimer.unref?.();
    }
    async start() {
        // Clear the stopping flag so a second start() after a supervisor-triggered
        // stop() doesn't silently no-op via the SIGINT/SIGTERM handler guard.
        this.stopping = false;
        const lock = acquireDaemonLock(this.workspace, {
            token: this.authToken,
        });
        this.lockPath = lock.lockPath;
        this.lockRecord = lock.record;
        if (lock.status === 'running') {
            return {
                started: false,
                record: lock.record,
            };
        }
        const port = await listen(this.httpServer, this.host);
        this.lockRecord = writeDaemonLock(this.lockPath, {
            ...lock.record,
            port,
            token: this.authToken,
            heartbeatAt: Date.now(),
            pkgVersion: DAEMON_PKG_VERSION,
        });
        this.startHeartbeat();
        this.installSignalHandlers();
        this.installProcessErrorHandlers();
        await runStartupHygiene({
            repoRoot: this.workspace.repoRoot,
            workspaceStateRoot: this.workspace.stateRoot,
            mode: 'stale',
        });
        // Preflight: verify better-sqlite3 native binding loads; auto-rebuild on
        // NODE_MODULE_VERSION mismatch.  Exits daemon startup with ok:false if the
        // binding still fails after rebuild.
        const preflight = await runPreflight(this.workspace.stateRoot);
        if (!preflight.ok) {
            console.error(`[nexus-daemon] startup-error: ${preflight.code} — ${preflight.hint}`);
            console.error(`[nexus-daemon] startup-error.json written to ${this.workspace.stateRoot}`);
            await this.stop('startup-error');
            return { started: false, record: lock.record };
        }
        // Integrity check: detect + auto-repair corrupt sqlite files before
        // engines open them.  Corrupt files are backed up then removed so the
        // engine init path recreates a clean schema on the next open.
        runIntegrityChecks(this.workspace.stateRoot);
        this.nexus = createNexusPrime({
            adapters: [],
            runtime: {
                workspaceRoot: this.workspace.workspaceRoot,
            },
        });
        await this.nexus.start();
        this.runtimeReady = true;
        // Optional repo-search prewarm. It can be CPU-heavy in large workspaces, so
        // keep daemon startup and dashboard responsiveness as the default behavior.
        if (process.env.NEXUS_PREWARM_REPO_SEARCH === '1' && this.workspace.gitRoot) {
            setImmediate(() => {
                void this.nexus?.getOrchestrator().warmRepoSearchIndex().catch(() => { });
            });
        }
        // Warm the invoker availability cache so /api/runtimes returns fast and
        // the dashboard runtime badges render immediately on first load.
        setImmediate(() => { invokerRegistry.warmCache(); });
        return {
            started: true,
            record: this.lockRecord,
        };
    }
    installSignalHandlers() {
        const stop = (signal) => {
            if (this.stopping) {
                return;
            }
            void this.stop(signal);
            setTimeout(() => {
                this.releaseLock();
                process.exit(0);
            }, 3_000).unref();
        };
        process.on('SIGINT', () => stop('SIGINT'));
        process.on('SIGTERM', () => stop('SIGTERM'));
        process.once('exit', () => {
            this.cleanupTimer.unref?.();
            this.releaseLock();
        });
    }
    releaseLock() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
        if (!this.lockPath || !this.lockRecord) {
            return;
        }
        try {
            removeDaemonLock(this.lockPath, { pid: this.lockRecord.pid });
        }
        catch {
            // Best effort during shutdown.
        }
    }
    getSessionAdapter(sessionId) {
        const now = Date.now();
        const existing = this.sessionAdapters.get(sessionId);
        if (existing) {
            existing.lastSeenAt = now;
            return existing.adapter;
        }
        if (!this.nexus) {
            throw new Error('Nexus daemon runtime is not initialized.');
        }
        const adapter = new MCPAdapter();
        adapter.setNexusRef(this.nexus);
        this.sessionAdapters.set(sessionId, {
            adapter,
            lastSeenAt: now,
        });
        return adapter;
    }
    pruneState() {
        const now = Date.now();
        for (const [sessionId, entry] of this.sessionAdapters.entries()) {
            if (now - entry.lastSeenAt > 15 * 60_000) {
                this.sessionAdapters.delete(sessionId);
            }
        }
        for (const [runId, run] of this.queuedRuns.entries()) {
            const finishedAt = run.finishedAt ?? run.startedAt;
            if (now - finishedAt > 15 * 60_000) {
                this.queuedRuns.delete(runId);
            }
        }
    }
    isAuthorized(req) {
        const expected = this.lockRecord?.token;
        if (!expected) {
            return false;
        }
        return req.headers['x-nexus-daemon-token'] === expected;
    }
    async handleToolCall(body) {
        const sessionId = String(body.sessionId ?? 'default');
        const toolName = String(body.name ?? '');
        const args = (body.arguments && typeof body.arguments === 'object')
            ? body.arguments
            : {};
        if (toolName === 'nexus_run_status') {
            const queuedRunId = String(args.runId ?? '');
            const queuedRun = this.queuedRuns.get(queuedRunId);
            if (queuedRun) {
                return {
                    queued: false,
                    result: formatQueuedToolRun(queuedRun),
                };
            }
        }
        const timeoutMs = Math.max(50, Math.min(Number(body.timeoutMs ?? 1_200), 10_000));
        const runId = randomUUID();
        const runRecord = {
            runId,
            sessionId,
            toolName,
            state: 'running',
            startedAt: Date.now(),
        };
        this.queuedRuns.set(runId, runRecord);
        const adapter = this.getSessionAdapter(sessionId);
        const invocation = adapter.invokeTool(toolName, args)
            .then((result) => {
            runRecord.state = 'completed';
            runRecord.finishedAt = Date.now();
            runRecord.result = result;
            return result;
        })
            .catch((error) => {
            runRecord.state = 'failed';
            runRecord.finishedAt = Date.now();
            runRecord.error = error?.message || String(error);
            throw error;
        });
        const outcome = await Promise.race([
            invocation.then((result) => ({ kind: 'result', result })),
            new Promise((resolve) => setTimeout(() => resolve({ kind: 'timeout' }), timeoutMs)),
        ]);
        if (outcome.kind === 'result') {
            this.queuedRuns.delete(runId);
            return {
                queued: false,
                result: outcome.result,
            };
        }
        return {
            queued: true,
            runId,
        };
    }
    async handleRequest(req, res) {
        const url = new URL(req.url ?? '/', `http://${this.host}`);
        try {
            if (!this.isAuthorized(req)) {
                sendJson(res, 401, { error: 'unauthorized' });
                return;
            }
            if (req.method === 'GET' && url.pathname === '/health') {
                const ready = this.runtimeReady && this.nexus !== null;
                sendJson(res, ready ? 200 : 503, {
                    ok: ready,
                    ready,
                    error: ready ? undefined : 'runtime-initializing',
                    pid: process.pid,
                    port: this.lockRecord?.port ?? 0,
                    stateKey: this.workspace.stateKey,
                    workspaceRoot: this.workspace.workspaceRoot,
                    repoRoot: this.workspace.repoRoot,
                    dashboardUrl: this.nexus?.getDashboardAddress?.() ?? null,
                    startedAt: this.lockRecord?.startedAt ?? Date.now(),
                });
                return;
            }
            if (req.method === 'GET' && url.pathname === '/rpc/run-status') {
                const runId = url.searchParams.get('runId') ?? '';
                const run = this.queuedRuns.get(runId);
                sendJson(res, 200, run ?? { runId, state: 'missing' });
                return;
            }
            if (req.method !== 'POST') {
                sendJson(res, 405, { error: 'method-not-allowed' });
                return;
            }
            const body = await parseRequestBody(req);
            if (url.pathname === '/rpc/list-tools') {
                const sessionId = String(body.sessionId ?? 'default');
                const adapter = this.getSessionAdapter(sessionId);
                sendJson(res, 200, {
                    tools: adapter.listTools(),
                });
                return;
            }
            if (url.pathname === '/rpc/call-tool') {
                const result = await this.handleToolCall(body);
                sendJson(res, 200, result);
                return;
            }
            if (url.pathname === '/shutdown') {
                sendJson(res, 200, { stopping: true });
                setImmediate(() => {
                    void this.stop('rpc');
                });
                return;
            }
            sendJson(res, 404, { error: 'not-found' });
        }
        catch (error) {
            sendJson(res, 500, {
                error: error?.message || String(error),
            });
        }
    }
    async stop(signal = 'shutdown') {
        if (this.stopping) {
            return;
        }
        this.stopping = true;
        this.runtimeReady = false;
        nexusEventBus.emit('nexus.shutdown', { signal });
        this.cleanupTimer.unref?.();
        try {
            await new Promise((resolve) => {
                this.httpServer.close(() => resolve());
            });
        }
        catch {
            // Ignore close races during shutdown.
        }
        if (this.nexus) {
            this.nexus.flushMemory();
            await this.nexus.stop();
            this.nexus = null;
        }
        this.releaseLock();
    }
}
