/**
 * Daemon liveness supervisor.
 *
 * Watches the daemon's `/health` endpoint on a fixed interval. If three
 * consecutive pings time out, the daemon is considered hung — supervisor
 * stops the existing server, constructs a fresh one, and starts it.
 *
 * Architecture: in-process. Holds a direct reference to the live
 * NexusDaemonServer so a restart is a `stop()` + new `NexusDaemonServer().start()`
 * call, not a fork. Cheaper than a child-process supervisor and avoids PID
 * race conditions with the lock file. The tradeoff: a truly deadlocked event
 * loop won't fire setInterval callbacks at all — but a deadlocked Node would
 * also fail any out-of-process probe. The intended fail mode is "hung HTTP
 * handler", which this catches.
 *
 * Opt out with `NEXUS_SUPERVISOR_DISABLED=1`.
 */
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { NexusDaemonServer } from './server.js';
import { pingDaemonHealth } from './client.js';
const DEFAULT_PING_INTERVAL_MS = 30_000;
const DEFAULT_PING_TIMEOUT_MS = 5_000;
const DEFAULT_MAX_TIMEOUTS = 3;
export class DaemonSupervisor {
    daemon;
    workspaceContext;
    getLockRecord;
    incidentsPath;
    pingIntervalMs;
    pingTimeoutMs;
    maxConsecutiveTimeouts;
    consecutiveTimeouts = 0;
    intervalHandle;
    restarting = false;
    stopped = false;
    constructor(options) {
        this.daemon = options.daemon;
        this.workspaceContext = options.workspaceContext;
        this.getLockRecord = options.getLockRecord;
        this.incidentsPath = options.incidentsPath
            ?? path.join(os.homedir(), '.nexus-prime', 'incidents.jsonl');
        this.pingIntervalMs = options.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
        this.pingTimeoutMs = options.pingTimeoutMs ?? DEFAULT_PING_TIMEOUT_MS;
        this.maxConsecutiveTimeouts = options.maxConsecutiveTimeouts ?? DEFAULT_MAX_TIMEOUTS;
    }
    /**
     * Schedule the first tick via setImmediate (no boot-time latency cost),
     * then a periodic interval. Honors NEXUS_SUPERVISOR_DISABLED=1 — returns
     * a no-op start in that case so callers don't need an outer guard.
     */
    start() {
        if (process.env.NEXUS_SUPERVISOR_DISABLED === '1') {
            return;
        }
        if (this.intervalHandle)
            return;
        // Defer so daemon.start() can finish listening before we ping.
        setImmediate(() => {
            if (this.stopped)
                return;
            void this.tick();
        });
        this.intervalHandle = setInterval(() => { void this.tick(); }, this.pingIntervalMs);
        this.intervalHandle.unref();
    }
    /** Stop watching. Safe to call repeatedly. */
    stop() {
        this.stopped = true;
        if (this.intervalHandle) {
            clearInterval(this.intervalHandle);
            this.intervalHandle = undefined;
        }
    }
    /** Single health-check tick. Public for tests. */
    async tick() {
        if (this.stopped || this.restarting)
            return;
        const record = this.getLockRecord();
        if (!record)
            return; // daemon not yet listening — skip silently
        try {
            await pingDaemonHealth(record, this.pingTimeoutMs);
            this.consecutiveTimeouts = 0;
        }
        catch (err) {
            this.consecutiveTimeouts += 1;
            if (this.consecutiveTimeouts >= this.maxConsecutiveTimeouts) {
                await this.handleHung(err);
            }
        }
    }
    async handleHung(error) {
        if (this.restarting)
            return;
        this.restarting = true;
        const record = this.getLockRecord();
        const errMsg = error instanceof Error ? error.message : String(error);
        this.appendIncident({
            ts: Date.now(),
            kind: 'hung',
            consecutiveTimeouts: this.consecutiveTimeouts,
            pid: process.pid,
            port: record?.port,
            error: errMsg,
        });
        try {
            await this.daemon.stop('supervisor-restart');
            const fresh = new NexusDaemonServer(this.workspaceContext);
            await fresh.start();
            this.daemon = fresh;
            this.consecutiveTimeouts = 0;
            this.appendIncident({
                ts: Date.now(),
                kind: 'restart-ok',
                consecutiveTimeouts: 0,
                pid: process.pid,
                port: this.daemon.getLockRecord()?.port,
            });
        }
        catch (restartErr) {
            this.appendIncident({
                ts: Date.now(),
                kind: 'restart-failed',
                consecutiveTimeouts: this.consecutiveTimeouts,
                pid: process.pid,
                port: record?.port,
                error: restartErr instanceof Error ? restartErr.message : String(restartErr),
            });
        }
        finally {
            this.restarting = false;
        }
    }
    appendIncident(incident) {
        try {
            fs.mkdirSync(path.dirname(this.incidentsPath), { recursive: true });
            fs.appendFileSync(this.incidentsPath, JSON.stringify(incident) + '\n', 'utf8');
        }
        catch { /* incident log is best-effort */ }
    }
    /** Test helper: read current consecutive-timeout counter. */
    getConsecutiveTimeouts() {
        return this.consecutiveTimeouts;
    }
    /** Test helper: get the live daemon ref (changes after a restart). */
    getDaemon() {
        return this.daemon;
    }
}
