/**
 * olam kg watch <workspace> — opt-in pristine watcher.
 *
 * Phase C2 of olam-kg-service (plan ~/.claude/plans/olam-kg-service.md).
 *
 * Runs `graphify <workspacePath> --watch --update --graph <pristinePath>/graph.json`
 * as a managed subprocess against the operator's workspace clone so the
 * pristine KG stays fresh as host-side commits land. Off by default;
 * operator opts in per workspace.
 *
 * Single-instance enforcement via PID file at
 * `~/.olam/kg/<workspace>/.watch.pid`. Reclaim stale PIDs (process gone
 * but file remains) silently. Reject when an active PID still owns the
 * file.
 *
 * Lifecycle:
 *   1. CLI process spawns graphify in the FOREGROUND (`detached: false`)
 *      so the operator's terminal owns it; Ctrl-C kills both cleanly.
 *   2. SIGINT/SIGTERM on this process → forwarded to graphify; subprocess
 *      exits; CLI removes PID file; CLI exits.
 *   3. graphify exiting on its own → CLI removes PID file + exits with
 *      graphify's status code.
 *
 * Risk T7 mitigation: workspace name regex-validated via @olam/core's
 * `validateWorkspaceName`; `--graph` path resolved via `kgPristinePath`
 * (which asserts the resolved path stays under `~/.olam/kg/`). All
 * subprocess invocations use argv arrays (no shell interpolation).
 */
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { kgPristinePath } from '@olam/core/src/kg/storage-paths.js';
import { validateWorkspaceName } from '@olam/core/src/world/workspace-name.js';
import { printError, printInfo, printSuccess } from '../output.js';
function pidFilePath(workspace) {
    return path.join(kgPristinePath(workspace), '.watch.pid');
}
/**
 * Check whether a given PID is currently a live process.
 *
 * `process.kill(pid, 0)` returns truthy AND doesn't actually send a
 * signal when called with signal=0 — it's the standard POSIX liveness
 * check. Throws ESRCH when the PID is dead. EPERM means "exists but
 * I can't signal it" (different user); we treat EPERM as "alive" because
 * the same operator owning olam should own the graphify subprocess.
 */
function isPidAlive(pid) {
    if (!Number.isInteger(pid) || pid <= 0)
        return false;
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (err) {
        const code = err.code;
        if (code === 'EPERM')
            return true; // exists, different user
        return false; // ESRCH or other → dead
    }
}
function readAndClassifyPid(workspace) {
    const file = pidFilePath(workspace);
    if (!fs.existsSync(file))
        return { status: 'no-pidfile', pid: null };
    let pid;
    try {
        const raw = fs.readFileSync(file, 'utf-8').trim();
        pid = Number.parseInt(raw, 10);
    }
    catch {
        // Unreadable → treat as stale; the writer must have been killed mid-flush
        fs.rmSync(file, { force: true });
        return { status: 'stale-reclaimed', pid: null };
    }
    if (!Number.isInteger(pid) || pid <= 0) {
        fs.rmSync(file, { force: true });
        return { status: 'stale-reclaimed', pid: null };
    }
    if (isPidAlive(pid))
        return { status: 'active', pid };
    fs.rmSync(file, { force: true });
    return { status: 'stale-reclaimed', pid: null };
}
function writePidFile(workspace, pid) {
    const file = pidFilePath(workspace);
    const dir = path.dirname(file);
    fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(file, String(pid), { encoding: 'utf-8' });
}
function removePidFile(workspace) {
    const file = pidFilePath(workspace);
    try {
        fs.rmSync(file, { force: true });
    }
    catch {
        // best-effort cleanup
    }
}
export async function runKgWatch(workspaceArg, opts, deps = {}) {
    const cwd = deps.cwd ?? opts.cwd ?? process.cwd();
    const name = workspaceArg ?? path.basename(cwd).toLowerCase();
    try {
        validateWorkspaceName(name);
    }
    catch (err) {
        printError(err.message);
        return { exitCode: 1, pidWritten: false };
    }
    const pristinePath = kgPristinePath(name);
    const graphPath = path.join(pristinePath, 'graphify-out', 'graph.json');
    // Single-instance gate. PRE-spawn so we never start a duplicate watcher.
    const pidState = readAndClassifyPid(name);
    if (pidState.status === 'active') {
        printError(`olam kg watch ${name} — already running (PID ${pidState.pid}). `
            + `Send SIGINT to that process to stop the existing watcher first.`);
        return { exitCode: 1, pidWritten: false };
    }
    if (pidState.status === 'stale-reclaimed') {
        printInfo('stale-pid', `reclaimed dead PID file at ${pidFilePath(name)}`);
    }
    const spawnFn = deps.spawnImpl ?? spawn;
    const child = spawnFn('graphify', [cwd, '--watch', '--update', '--graph', graphPath], {
        cwd,
        env: process.env,
        detached: false,
        stdio: 'inherit',
    });
    if (typeof child.pid !== 'number') {
        printError('graphify failed to launch (no PID assigned)');
        return { exitCode: 1, pidWritten: false };
    }
    writePidFile(name, child.pid);
    printSuccess(`olam kg watch ${name} — graphify PID ${child.pid} (Ctrl-C to stop)`);
    if (deps.registerSignalHandlers !== false) {
        const forward = (sig) => {
            if (child.pid && !child.killed) {
                try {
                    process.kill(child.pid, sig);
                }
                catch {
                    // child already dead; fall through to wait
                }
            }
        };
        process.on('SIGINT', () => forward('SIGINT'));
        process.on('SIGTERM', () => forward('SIGTERM'));
    }
    return new Promise((resolve) => {
        child.on('exit', (code, signal) => {
            removePidFile(name);
            const exitCode = typeof code === 'number' ? code : signal === 'SIGINT' || signal === 'SIGTERM' ? 0 : 1;
            resolve({ exitCode, pidWritten: true });
        });
        child.on('error', (err) => {
            removePidFile(name);
            printError(`graphify subprocess error: ${err.message}`);
            resolve({ exitCode: 1, pidWritten: true });
        });
    });
}
export function registerKgWatchCommand(kg) {
    kg.command('watch')
        .description('Run graphify --watch against a workspace, keeping its pristine KG fresh '
        + 'as host-side commits land. Single-instance via PID file; Ctrl-C cleanly stops.')
        .argument('[workspace]', 'workspace name (lowercase alphanumeric + hyphens/underscores)')
        .action(async (workspaceArg) => {
        const r = await runKgWatch(workspaceArg, {});
        if (r.exitCode !== 0)
            process.exitCode = r.exitCode;
    });
}
//# sourceMappingURL=kg-watch.js.map