/**
 * Per-PID session registry — the headless equivalent of the swarmify VS Code
 * extension's `live-terminals.json`.
 *
 * On a machine with no terminal extension (a bare SSH/tmux host), the only way
 * `ag sessions --active` can attribute a `ps`-discovered agent process to a
 * session is to guess "newest .jsonl in the cwd" — which collapses N agents
 * sharing one repo onto a single session (all rows show the same topic/id).
 *
 * `ag run` closes that gap by recording, at spawn time, one file per launched
 * agent process keyed by its OS pid: the exact session id it was launched with
 * (Claude is started with `--session-id <uuid>`, so the launcher knows it),
 * plus agent/cwd/tmux pane. The active-sessions headless path reads it back for
 * an exact pid -> session match instead of a heuristic.
 *
 * Best-effort throughout: a failed write or a corrupt file degrades to the old
 * heuristic, never throws into the launch or the listing path.
 */
import fs from 'fs';
import path from 'path';
import { execFileSync } from 'child_process';
import { getTerminalsDir } from '../state.js';
/**
 * Pull an explicit `--session-id <uuid>` (or `--session-id=<uuid>`) out of a
 * raw agent arg vector. The transparent shim forwards args untouched, but when
 * a launcher (Claude Code background jobs, IDE harnesses) already names the
 * session, recording it gives the same exact pid -> session mapping `ag run`
 * gets from generating the id itself.
 */
const SESSION_ID_VALUE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export function isSessionIdShape(value) {
    return SESSION_ID_VALUE_RE.test(value);
}
export function extractSessionIdArg(args) {
    for (let i = 0; i < args.length; i++) {
        const a = args[i];
        if (a === '--session-id') {
            const v = args[i + 1];
            if (v && SESSION_ID_VALUE_RE.test(v))
                return v;
        }
        else if (a.startsWith('--session-id=')) {
            const v = a.slice('--session-id='.length);
            if (SESSION_ID_VALUE_RE.test(v))
                return v;
        }
    }
    return undefined;
}
/**
 * Read a live process's original argv (best-effort).
 *
 * Linux: NUL-separated `/proc/<pid>/cmdline`. Darwin: `ps -ww -o args=` then
 * whitespace-split (good enough for Claude's `--session-id <uuid>` tokens;
 * not a full shell-quote parser). Other platforms: undefined.
 *
 * RUSH-2384: the by-pid registry is often empty mid-run (launch pid was a
 * wrapper that exited, prune wiped it, or the agent was not launched via
 * `agents run`). The live process still carries `--session-id` on its argv —
 * the same signal the incident used to prove the session was alive — so the
 * active scan and `agents message` can recover identity without the registry.
 */
export function readProcessArgv(pid) {
    if (!pid || pid < 1)
        return undefined;
    if (process.platform === 'linux') {
        try {
            const buf = fs.readFileSync(`/proc/${pid}/cmdline`);
            const parts = buf.toString('utf8').split('\0').filter(Boolean);
            return parts.length > 0 ? parts : undefined;
        }
        catch {
            return undefined;
        }
    }
    if (process.platform === 'darwin') {
        try {
            const out = execFileSync('ps', ['-ww', '-p', String(pid), '-o', 'args='], {
                encoding: 'utf-8',
                stdio: ['ignore', 'pipe', 'ignore'],
            }).trim();
            if (!out)
                return undefined;
            return out.split(/\s+/);
        }
        catch {
            return undefined;
        }
    }
    return undefined;
}
/**
 * Exact session id a live agent process was launched with, read from its
 * current argv. Undefined when the process is gone, the platform can't expose
 * argv, or the vector carries no `--session-id`.
 */
export function sessionIdFromLivePid(pid) {
    const argv = readProcessArgv(pid);
    if (!argv)
        return undefined;
    return extractSessionIdArg(argv);
}
function pidRegistryDir() {
    return path.join(getTerminalsDir(), 'by-pid');
}
function entryPath(pid) {
    return path.join(pidRegistryDir(), `${pid}.json`);
}
/** Record a launched agent process. Never throws — the registry is an optimization. */
export function writePidSessionEntry(entry) {
    if (!entry.pid || entry.pid < 1)
        return;
    try {
        fs.mkdirSync(pidRegistryDir(), { recursive: true });
        fs.writeFileSync(entryPath(entry.pid), JSON.stringify(entry), 'utf8');
    }
    catch {
        /* degrade to the newest-jsonl heuristic */
    }
}
/** Look up a live pid's recorded session. Returns undefined if absent/corrupt. */
export function readPidSessionEntry(pid) {
    let raw;
    try {
        raw = fs.readFileSync(entryPath(pid), 'utf8');
    }
    catch {
        return undefined;
    }
    try {
        const parsed = JSON.parse(raw);
        if (parsed && typeof parsed === 'object' && typeof parsed.pid === 'number') {
            return parsed;
        }
    }
    catch {
        /* unparseable */
    }
    return undefined;
}
/**
 * Every recorded launch, one entry per live-or-dead pid. Used to index launches
 * by their `tmuxPane` so the authoritative tmux source can attribute a pane it did
 * NOT wrap (an agent bare-spawned into an existing pane) to its exact launch —
 * the caller filters to live pids. Best-effort: unreadable/corrupt files are
 * skipped, a missing dir yields `[]`.
 */
export function listPidSessionEntries() {
    let files;
    try {
        files = fs.readdirSync(pidRegistryDir()).filter(f => f.endsWith('.json'));
    }
    catch {
        return [];
    }
    const out = [];
    for (const f of files) {
        try {
            const parsed = JSON.parse(fs.readFileSync(path.join(pidRegistryDir(), f), 'utf8'));
            if (parsed && typeof parsed === 'object' && typeof parsed.pid === 'number') {
                out.push(parsed);
            }
        }
        catch {
            /* raced with a writer/pruner, or corrupt — skip */
        }
    }
    return out;
}
/**
 * Remove entries whose pid is no longer alive. Best-effort housekeeping.
 *
 * `isAlive` receives the entry's recorded `startedAtMs` so it can reject a pid
 * that a newer process recycled (a "zombie" registry entry), not just one that
 * no longer exists. An unreadable/corrupt entry falls back to the pid-only
 * check.
 */
export function prunePidSessionRegistry(isAlive) {
    let files;
    try {
        files = fs.readdirSync(pidRegistryDir()).filter(f => f.endsWith('.json'));
    }
    catch {
        return;
    }
    for (const f of files) {
        const pid = Number(f.slice(0, -'.json'.length));
        if (!Number.isInteger(pid))
            continue;
        let startedAtMs;
        try {
            const parsed = JSON.parse(fs.readFileSync(path.join(pidRegistryDir(), f), 'utf8'));
            if (typeof parsed?.startedAtMs === 'number')
                startedAtMs = parsed.startedAtMs;
        }
        catch {
            /* unreadable/corrupt — fall back to the pid-only liveness check */
        }
        if (isAlive(pid, startedAtMs))
            continue;
        try {
            fs.unlinkSync(path.join(pidRegistryDir(), f));
        }
        catch {
            /* raced with another writer/pruner */
        }
    }
}
