import { spawn } from 'child_process';
import { readFileSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function readCliPkgVersion() {
    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';
}
import { acquireDaemonLock, getDaemonLockPath, isProcessAlive, readDaemonLock, removeDaemonLock, } from './lock.js';
export const DEFAULT_DAEMON_READY_TIMEOUT_MS = 60_000;
const DAEMON_HEALTH_PROBE_TIMEOUT_MS = 5_000;
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchJson(url, init, timeoutMs) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const response = await fetch(url, {
            ...init,
            signal: controller.signal,
        });
        if (!response.ok) {
            throw new Error(`HTTP ${response.status} ${response.statusText}`);
        }
        return await response.json();
    }
    catch (error) {
        if (error?.name === 'AbortError') {
            throw new Error(`daemon health probe timed out after ${timeoutMs}ms`);
        }
        throw error;
    }
    finally {
        clearTimeout(timer);
    }
}
function buildDaemonUrl(record, pathname) {
    if (!record.port) {
        throw new Error('Daemon lock does not include an RPC port.');
    }
    return `http://127.0.0.1:${record.port}${pathname}`;
}
function daemonHeaders(record) {
    if (!record.token) {
        throw new Error('Daemon lock does not include an auth token.');
    }
    return {
        'content-type': 'application/json',
        'x-nexus-daemon-token': record.token,
    };
}
export async function pingDaemonHealth(record, timeoutMs = 1_500) {
    return fetchJson(buildDaemonUrl(record, '/health'), {
        method: 'GET',
        headers: daemonHeaders(record),
    }, timeoutMs);
}
export async function daemonRequest(record, pathname, body, timeoutMs = 5_000) {
    return fetchJson(buildDaemonUrl(record, pathname), {
        method: body === undefined ? 'GET' : 'POST',
        headers: daemonHeaders(record),
        body: body === undefined ? undefined : JSON.stringify(body),
    }, timeoutMs);
}
async function waitForDaemonReady(workspace, timeoutMs) {
    const deadline = Date.now() + timeoutMs;
    const lockPath = getDaemonLockPath(workspace);
    let lastError = 'daemon lock not found';
    while (Date.now() < deadline) {
        const record = readDaemonLock(lockPath);
        if (!record) {
            lastError = 'daemon lock not found';
            await sleep(100);
            continue;
        }
        if (!isProcessAlive(record.pid)) {
            lastError = `daemon pid ${record.pid} is not alive`;
            removeDaemonLock(lockPath, { pid: record.pid });
            await sleep(100);
            continue;
        }
        if (!record.port || !record.token) {
            lastError = 'daemon lock is waiting for rpc metadata';
            await sleep(100);
            continue;
        }
        try {
            await pingDaemonHealth(record, Math.min(timeoutMs, DAEMON_HEALTH_PROBE_TIMEOUT_MS));
            return record;
        }
        catch (error) {
            const message = error?.message || String(error);
            lastError = message.includes('HTTP 503')
                ? 'daemon runtime is still initializing'
                : message;
            await sleep(100);
        }
    }
    throw new Error(`Timed out waiting for daemon readiness: ${lastError}`);
}
function spawnDaemonProcess(workspace, entrypoint) {
    const cliEntrypoint = path.resolve(entrypoint);
    const child = spawn(process.execPath, [cliEntrypoint, 'daemon', '__serve'], {
        cwd: workspace.repoRoot,
        env: {
            ...process.env,
            NEXUS_WORKSPACE_ROOT: workspace.workspaceRoot,
        },
        detached: true,
        stdio: 'ignore',
    });
    child.unref();
}
export async function ensureDaemonReady(workspace, options = {}) {
    const timeoutMs = options.timeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
    const existing = acquireDaemonLock(workspace);
    if (existing.status === 'running') {
        const currentVersion = readCliPkgVersion();
        const daemonVersion = existing.record.pkgVersion;
        if (currentVersion !== 'unknown' && daemonVersion && daemonVersion !== currentVersion) {
            // Version mismatch — stop the stale daemon before spawning the new one
            await stopDaemon(workspace).catch(() => { });
        }
        else {
            return waitForDaemonReady(workspace, timeoutMs);
        }
    }
    removeDaemonLock(existing.lockPath, { pid: existing.record.pid });
    if (options.spawnIfMissing === false) {
        throw new Error(`Nexus Prime daemon is not running for ${workspace.workspaceRoot}`);
    }
    const entrypoint = options.entrypoint ?? process.argv[1];
    if (!entrypoint) {
        throw new Error('Unable to resolve the Nexus Prime CLI entrypoint for daemon spawn.');
    }
    spawnDaemonProcess(workspace, entrypoint);
    return waitForDaemonReady(workspace, timeoutMs);
}
export async function getDaemonStatus(workspace) {
    const lockPath = getDaemonLockPath(workspace);
    const record = readDaemonLock(lockPath);
    if (!record) {
        return { running: false, healthy: false, stale: false };
    }
    if (!isProcessAlive(record.pid)) {
        removeDaemonLock(lockPath, { pid: record.pid });
        return {
            running: false,
            healthy: false,
            stale: true,
            record,
        };
    }
    try {
        const health = record.port && record.token ? await pingDaemonHealth(record, 1_500) : undefined;
        return {
            running: true,
            healthy: Boolean(health?.ok),
            stale: false,
            record,
            health,
        };
    }
    catch {
        return {
            running: true,
            healthy: false,
            stale: false,
            record,
        };
    }
}
export async function stopDaemon(workspace, options = {}) {
    const lockPath = getDaemonLockPath(workspace);
    const record = readDaemonLock(lockPath);
    if (!record) {
        return {
            stopped: false,
            stale: false,
            forced: Boolean(options.force),
        };
    }
    if (!isProcessAlive(record.pid)) {
        removeDaemonLock(lockPath, { pid: record.pid });
        return {
            stopped: true,
            stale: true,
            forced: Boolean(options.force),
            record,
        };
    }
    try {
        if (record.port && record.token) {
            await daemonRequest(record, '/shutdown', { force: Boolean(options.force) }, 2_000);
        }
        else {
            process.kill(record.pid, 'SIGTERM');
        }
    }
    catch {
        process.kill(record.pid, options.force ? 'SIGKILL' : 'SIGTERM');
    }
    const deadline = Date.now() + (options.force ? 8_000 : 5_000);
    while (Date.now() < deadline) {
        if (!isProcessAlive(record.pid)) {
            removeDaemonLock(lockPath, { pid: record.pid });
            return {
                stopped: true,
                stale: false,
                forced: Boolean(options.force),
                record,
            };
        }
        await sleep(100);
    }
    if (options.force) {
        process.kill(record.pid, 'SIGKILL');
        const killDeadline = Date.now() + 2_000;
        while (Date.now() < killDeadline) {
            if (!isProcessAlive(record.pid)) {
                removeDaemonLock(lockPath, { pid: record.pid });
                return {
                    stopped: true,
                    stale: false,
                    forced: true,
                    record,
                };
            }
            await sleep(100);
        }
    }
    throw new Error(`Failed to stop daemon pid ${record.pid}`);
}
