/**
 * Dev Lock Manager
 *
 * Manages a lockfile to track the dev server process and its children.
 * On startup, detects and cleans up stale processes from previous sessions.
 * Ensures proper cleanup on all exit paths.
 */
import { join, dirname } from 'node:path';
import { randomUUID } from 'node:crypto';
import { existsSync, unlinkSync } from 'node:fs';
import { promises as fs } from 'node:fs';
function getLockPath(rootDir) {
    return join(rootDir, '.agentuity', 'devserver.lock');
}
/**
 * Check if a process with the given PID exists
 */
function pidExists(pid) {
    if (!Number.isInteger(pid) || pid <= 0)
        return false;
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (err) {
        const error = err;
        if (error.code === 'ESRCH' || error.code === 'EINVAL')
            return false;
        // EPERM means it exists but we can't signal it
        return error.code === 'EPERM';
    }
}
/**
 * Send a signal to a process group (negative PID) with fallback to direct PID.
 * Returns true if the signal was sent successfully.
 */
function killProcessTree(pid, signal, logger) {
    // Safety: never send signals to PID 0 (own process group), PID 1 (init/systemd),
    // or other dangerously low PIDs. process.kill(-1) would signal every process
    // the user owns, which would crash the entire desktop session.
    if (pid <= 1) {
        logger.debug('Refusing to kill dangerous pid %d, skipping process tree kill', pid);
        return false;
    }
    // Try process group kill first (kills all children too)
    try {
        process.kill(-pid, signal);
        logger.debug('Sent %s to process group -%d', signal, pid);
        return true;
    }
    catch (err) {
        const error = err;
        if (error.code !== 'ESRCH') {
            logger.debug('Process group kill failed for pid %d (%s), falling back to direct', pid, error.code);
        }
    }
    // Fall back to direct PID kill
    try {
        process.kill(pid, signal);
        logger.debug('Sent %s to pid %d (direct)', signal, pid);
        return true;
    }
    catch (err) {
        const error = err;
        if (error.code !== 'ESRCH') {
            logger.debug('Direct kill failed for pid %d: %s', pid, error.code);
        }
        return false;
    }
}
/**
 * Kill a process by PID with SIGTERM, then SIGKILL if still alive.
 * Targets the entire process tree to prevent orphaned child processes.
 */
async function killPid(pid, logger) {
    if (!pidExists(pid))
        return;
    // Send SIGTERM to process tree
    const sent = killProcessTree(pid, 'SIGTERM', logger);
    if (!sent)
        return;
    // Give it a moment to exit gracefully
    await new Promise((r) => setTimeout(r, 500));
    // Always attempt SIGKILL on the process tree even if the leader has exited.
    // On Unix, process groups persist after the leader exits and signaling via
    // negative PGID still reaches remaining members. killProcessTree() handles
    // ESRCH gracefully if the group no longer exists.
    // Force kill the entire process tree
    killProcessTree(pid, 'SIGKILL', logger);
    // Wait for process to fully terminate
    await new Promise((r) => setTimeout(r, 100));
}
/**
 * Read an existing lockfile (if any)
 */
async function readLock(lockPath, logger) {
    if (!existsSync(lockPath))
        return null;
    try {
        const raw = await fs.readFile(lockPath, 'utf8');
        const parsed = JSON.parse(raw);
        if (parsed && parsed.version === 1)
            return parsed;
    }
    catch (err) {
        logger.warn('Failed to read/parse devserver.lock: %s', err);
    }
    return null;
}
/**
 * Remove lockfile if it exists
 */
async function removeLock(lockPath, logger) {
    try {
        await fs.unlink(lockPath);
        logger.debug('Removed devserver.lock');
    }
    catch (err) {
        const error = err;
        if (error.code !== 'ENOENT') {
            logger.debug('Failed to remove devserver.lock: %s', error.message);
        }
    }
}
/**
 * Check if a port is in use by attempting to connect to it.
 * Uses GET instead of HEAD since some servers return 405 for HEAD requests.
 * Any response (including errors like 404, 500) means the port is in use.
 */
async function isPortResponding(port) {
    try {
        const response = await fetch(`http://127.0.0.1:${port}/`, {
            method: 'GET',
            signal: AbortSignal.timeout(500),
        });
        // Consume body to avoid memory leaks
        await response.text().catch(() => { });
        return true;
    }
    catch (err) {
        // Connection refused (ECONNREFUSED) means nothing is listening
        // Other errors (timeout, reset) might indicate a busy port
        const error = err;
        const code = error.cause?.code;
        if (code === 'ECONNREFUSED' || code === 'ECONNRESET') {
            return false;
        }
        // For other errors (like timeout), assume port might be in use but unresponsive
        return false;
    }
}
/**
 * Kill processes referenced by a stale lock, then remove the lock
 */
async function cleanupStaleLock(rootDir, lock, logger) {
    const lockPath = getLockPath(rootDir);
    logger.debug('Cleaning up stale devserver.lock (pid=%d, instance=%s)', lock.mainPid, lock.instanceId);
    // Collect all PIDs to kill (children first, then main)
    const childPids = [];
    for (const child of lock.children ?? []) {
        if (child.pid && child.pid !== lock.mainPid && child.pid !== process.pid) {
            childPids.push(child.pid);
        }
    }
    // Kill children first
    for (const pid of childPids) {
        await killPid(pid, logger);
    }
    // Kill main process if it's not us
    if (lock.mainPid !== process.pid) {
        await killPid(lock.mainPid, logger);
    }
    // Remove the stale lockfile
    await removeLock(lockPath, logger);
}
/**
 * Ensure there is no conflicting dev server for this project
 * Always cleans up any existing lock and kills associated processes
 */
async function ensureNoActiveDevForProject(rootDir, _port, logger) {
    const lockPath = getLockPath(rootDir);
    const existing = await readLock(lockPath, logger);
    if (!existing)
        return;
    const now = Date.now();
    const createdAt = Date.parse(existing.createdAt || '');
    const ageMs = Number.isFinite(createdAt) ? now - createdAt : Infinity;
    const mainAlive = pidExists(existing.mainPid);
    // Check if the recorded Bun port is still responding
    let bunPortInUse = false;
    if (existing.ports?.bun) {
        bunPortInUse = await isPortResponding(existing.ports.bun);
    }
    logger.debug('Found existing lock (pid=%d, mainAlive=%s, bunPortInUse=%s, age=%dms) - cleaning up', existing.mainPid, mainAlive, bunPortInUse, ageMs);
    await cleanupStaleLock(rootDir, existing, logger);
}
/**
 * Initialize a new lock for the current dev run
 * This should be called after ensureNoActiveDevForProject has possibly cleaned stale state
 */
async function initDevLock(rootDir, port, logger) {
    const lockPath = getLockPath(rootDir);
    await fs.mkdir(dirname(lockPath), { recursive: true });
    const state = {
        version: 1,
        projectRoot: rootDir,
        mainPid: process.pid,
        instanceId: randomUUID(),
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        ports: { bun: port },
        children: [],
    };
    const writeLock = async () => {
        state.updatedAt = new Date().toISOString();
        await fs.writeFile(lockPath, JSON.stringify(state, null, 2), { encoding: 'utf8' });
    };
    await writeLock();
    logger.debug('Created devserver.lock (pid=%d, instance=%s)', state.mainPid, state.instanceId);
    const manager = {
        state,
        async registerChild(child) {
            if (!child.pid)
                return;
            // Avoid duplicates
            if (state.children.some((c) => c.pid === child.pid))
                return;
            state.children.push(child);
            await writeLock();
            logger.debug('Registered child process (pid=%d, type=%s)', child.pid, child.type);
        },
        async updatePorts(ports) {
            state.ports = { ...state.ports, ...ports };
            await writeLock();
        },
        async release() {
            await removeLock(lockPath, logger);
        },
    };
    return manager;
}
/**
 * Main entry point for dev lock management
 * Call this early in the dev command to:
 * 1. Clean up any stale processes from previous sessions
 * 2. Create a new lockfile for this session
 */
export async function prepareDevLock(rootDir, port, logger) {
    await ensureNoActiveDevForProject(rootDir, port, logger);
    return initDevLock(rootDir, port, logger);
}
/**
 * Utility to kill all processes in a lockfile by path
 * Useful for emergency cleanup without creating a new lock
 */
export async function cleanupLockfile(rootDir, logger) {
    const lockPath = getLockPath(rootDir);
    const existing = await readLock(lockPath, logger);
    if (existing) {
        await cleanupStaleLock(rootDir, existing, logger);
    }
}
/**
 * Synchronous lockfile removal for use in process.on('exit') handlers
 * Does not kill processes - just removes the file
 */
export function releaseLockSync(rootDir) {
    const lockPath = getLockPath(rootDir);
    try {
        unlinkSync(lockPath);
    }
    catch {
        // Ignore errors - file may already be gone
    }
}
//# sourceMappingURL=dev-lock.js.map