/**
 * memory-host-process-migration.ts — idempotent cleanup of the Phase A
 * host-process model's residual state on disk.
 *
 * Phase A spawned `agentmemory` as a host child process and recorded its PID
 * at `~/.olam/memory.pid` + stdout/stderr at `~/.olam/memory-service.log`.
 * Phase B repoints `olam memory start` at the Docker substrate (this skill's
 * `MemoryServiceContainerController`); the legacy pidfile + log become orphan
 * artefacts the moment the new substrate runs.
 *
 * This helper is called from `runMemoryStart` / `runMemoryStop` (B1 / B2) BEFORE
 * the substrate handoff so operators upgrading from the host-process model
 * never have to manually clean up.
 *
 * PID-reuse defence (T2 mitigation). A stale pidfile can record a PID that
 * the OS has since recycled to an unrelated process. SIGTERMing the wrong PID
 * is a foot-gun. Defence:
 *   1. If pidfile is absent → no-op.
 *   2. If recorded PID is dead (kill(0) throws ESRCH) → unlink pidfile only.
 *   3. If recorded PID is alive BUT `ps -p <pid> -o comm=` doesn't return
 *      `node` (the legacy host-process was always a node child) → unlink
 *      pidfile WITHOUT signalling. Logs a warning so support can diagnose.
 *   4. Only when PID is alive AND comm=node → SIGTERM, wait up to 5s,
 *      SIGKILL on timeout, then unlink.
 *
 * Plan reference: docs/plans/memory-service-as-docker-peripheral/phase-b-tasks.md B5
 */
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { MEMORY_PID_PATH, MEMORY_LOG_PATH } from '../commands/memory/_paths.js';
const KILL_TIMEOUT_MS = 5_000;
/**
 * Detect + clean up legacy host-process state. Idempotent — safe to call on
 * every `olam memory start` / `olam memory stop` invocation; no-op when no
 * legacy artefacts present.
 *
 * Side effects (only when artefacts present):
 *   - Unlink `~/.olam/memory.pid` if it exists.
 *   - Send SIGTERM (then SIGKILL on timeout) to the recorded PID iff it's
 *     alive AND its comm name is `node` (the legacy spawn shape).
 *   - Leave `~/.olam/memory-service.log` in place by default — operators
 *     may want to inspect it post-migration. Pass `{ removeLog: true }` to
 *     remove it explicitly (B2's stop path uses this).
 */
export function migrateFromHostProcess(opts = {}) {
    const pidPath = opts.pidPath ?? MEMORY_PID_PATH;
    const logPath = opts.logPath ?? MEMORY_LOG_PATH;
    const pidfileExists = existsSync(pidPath);
    const logExists = existsSync(logPath);
    if (!pidfileExists && !logExists) {
        return { cleaned: false, summary: 'no legacy host-process state to clean' };
    }
    const events = [];
    if (pidfileExists) {
        const pid = readPidFromFile(pidPath);
        if (pid === null) {
            // Pidfile exists but is unparseable — just unlink.
            unlinkSync(pidPath);
            events.push('unlinked unparseable ~/.olam/memory.pid');
        }
        else if (!isProcessAlive(pid)) {
            // Recorded PID is dead — clean unlink with no signalling.
            unlinkSync(pidPath);
            events.push(`unlinked stale ~/.olam/memory.pid (pid ${pid} not alive)`);
        }
        else {
            // PID is alive — check comm name before signalling.
            const comm = processCommName(pid);
            if (comm !== 'node') {
                // PID-reuse case: alive but not our process. Unlink WITHOUT signalling.
                unlinkSync(pidPath);
                events.push(`unlinked ~/.olam/memory.pid without signal (pid ${pid} comm=${comm ?? 'unknown'}, not node — assumed PID-reuse, defensive skip)`);
            }
            else {
                // Our process — terminate gracefully.
                const terminated = terminateProcess(pid);
                unlinkSync(pidPath);
                events.push(terminated
                    ? `terminated legacy host-process pid ${pid} (SIGTERM) + unlinked pidfile`
                    : `terminated legacy host-process pid ${pid} (SIGKILL after timeout) + unlinked pidfile`);
            }
        }
    }
    if (logExists && opts.removeLog) {
        unlinkSync(logPath);
        events.push('removed legacy ~/.olam/memory-service.log');
    }
    else if (logExists) {
        events.push('left ~/.olam/memory-service.log in place (inspectable; pass {removeLog:true} to remove)');
    }
    return { cleaned: true, summary: events.join('; ') };
}
function readPidFromFile(pidPath) {
    try {
        const raw = readFileSync(pidPath, 'utf8').trim();
        const pid = parseInt(raw, 10);
        if (!Number.isFinite(pid) || pid <= 0)
            return null;
        return pid;
    }
    catch {
        return null;
    }
}
function isProcessAlive(pid) {
    try {
        // signal 0 doesn't deliver; just tests existence + permission
        process.kill(pid, 0);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Read the comm name of a process via `ps -p <pid> -o comm=`. Returns null
 * on any error (process gone, ps not available, etc.) — the caller treats
 * null as "don't signal," which is the safe default.
 */
function processCommName(pid) {
    const r = spawnSync('ps', ['-p', String(pid), '-o', 'comm='], { encoding: 'utf-8' });
    if (r.status !== 0)
        return null;
    const comm = r.stdout.trim();
    // `ps` may return a full path (e.g. `/usr/local/bin/node`); take basename.
    return comm.split('/').pop() ?? null;
}
/**
 * SIGTERM the PID; wait up to 5s for it to exit; SIGKILL on timeout.
 * Returns true if SIGTERM was sufficient, false if SIGKILL was needed.
 */
function terminateProcess(pid) {
    try {
        process.kill(pid, 'SIGTERM');
    }
    catch {
        // Already dead between the comm-check and now; nothing to do.
        return true;
    }
    const deadline = Date.now() + KILL_TIMEOUT_MS;
    while (Date.now() < deadline) {
        if (!isProcessAlive(pid))
            return true;
        // Busy-spin sleep — node:fs is synchronous; no async helper here.
        spawnSync('sleep', ['0.1']);
    }
    // Still alive after timeout — escalate.
    try {
        process.kill(pid, 'SIGKILL');
    }
    catch {
        // race: died between probe and SIGKILL
    }
    return false;
}
//# sourceMappingURL=memory-host-process-migration.js.map