import fs from 'fs';
import path from 'path';
import { spawnSync } from 'child_process';
import { resolveNexusStateDir } from '../engines/runtime-registry.js';
/** Write a heartbeat to the lockfile every this many ms. */
export const HEARTBEAT_INTERVAL_MS = 10_000;
/** A lock whose heartbeat is older than this is considered stale and acquirable. */
export const STALE_HEARTBEAT_MS = 30_000;
function isDaemonLockRecord(value) {
    if (!value || typeof value !== 'object')
        return false;
    const record = value;
    return (record.version === 1
        && typeof record.pid === 'number'
        && typeof record.startedAt === 'number'
        && typeof record.updatedAt === 'number'
        && typeof record.stateKey === 'string'
        && typeof record.stateRoot === 'string'
        && typeof record.workspaceRoot === 'string'
        && typeof record.repoRoot === 'string');
}
export function getDaemonLockPath(workspace) {
    return path.join(workspace.stateRoot, 'nexus-daemon.lock.json');
}
export function isProcessAlive(pid) {
    if (!Number.isInteger(pid) || pid <= 0) {
        return false;
    }
    if (process.platform === 'win32') {
        try {
            const result = spawnSync('tasklist', ['/fi', `PID eq ${pid}`, '/nh', '/fo', 'csv'], {
                encoding: 'utf8',
                windowsHide: true,
                timeout: 2_000,
            });
            // tasklist CSV output contains the quoted PID string when the process exists.
            return result.status === 0 && (result.stdout ?? '').includes(`"${pid}"`);
        }
        catch {
            return false;
        }
    }
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (error) {
        return error?.code === 'EPERM';
    }
}
/**
 * A lock is considered stale when its heartbeat has not been updated within
 * STALE_HEARTBEAT_MS (30 s).  A SIGKILL'd daemon cannot release its lock but
 * will stop writing heartbeats, so the next starter can safely acquire after
 * the TTL expires.
 */
export function isLockStale(lock) {
    if (lock.heartbeatAt == null) {
        // Legacy lock without a heartbeat field — fall back to pid check only.
        return !isProcessAlive(lock.pid);
    }
    if (Date.now() - lock.heartbeatAt > STALE_HEARTBEAT_MS) {
        return true;
    }
    return !isProcessAlive(lock.pid);
}
export function readDaemonLock(lockPath) {
    if (!fs.existsSync(lockPath)) {
        return null;
    }
    try {
        const parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
        if (!isDaemonLockRecord(parsed)) {
            return null;
        }
        return {
            ...parsed,
            stateRoot: path.resolve(parsed.stateRoot),
            workspaceRoot: path.resolve(parsed.workspaceRoot),
            repoRoot: path.resolve(parsed.repoRoot),
        };
    }
    catch {
        return null;
    }
}
export function writeDaemonLock(lockPath, record) {
    const nextRecord = {
        ...record,
        updatedAt: Date.now(),
        stateRoot: path.resolve(record.stateRoot),
        workspaceRoot: path.resolve(record.workspaceRoot),
        repoRoot: path.resolve(record.repoRoot),
    };
    fs.mkdirSync(path.dirname(lockPath), { recursive: true });
    fs.writeFileSync(lockPath, JSON.stringify(nextRecord, null, 2), 'utf8');
    return nextRecord;
}
function buildLockRecord(workspace, overrides = {}) {
    const now = Date.now();
    return {
        version: 1,
        pid: overrides.pid ?? process.pid,
        port: overrides.port,
        token: overrides.token,
        startedAt: overrides.startedAt ?? now,
        updatedAt: overrides.updatedAt ?? now,
        stateKey: overrides.stateKey ?? workspace.stateKey,
        stateRoot: overrides.stateRoot ?? workspace.stateRoot,
        workspaceRoot: overrides.workspaceRoot ?? workspace.workspaceRoot,
        repoRoot: overrides.repoRoot ?? workspace.repoRoot,
    };
}
export function acquireDaemonLock(workspace, overrides = {}) {
    const lockPath = getDaemonLockPath(workspace);
    let staleRemoved = false;
    for (let attempt = 0; attempt < 3; attempt += 1) {
        const candidate = buildLockRecord(workspace, overrides);
        try {
            fs.mkdirSync(path.dirname(lockPath), { recursive: true });
            fs.writeFileSync(lockPath, JSON.stringify(candidate, null, 2), {
                encoding: 'utf8',
                flag: 'wx',
            });
            return {
                status: 'acquired',
                lockPath,
                record: candidate,
                staleRemoved,
            };
        }
        catch (error) {
            if (error?.code !== 'EEXIST') {
                throw error;
            }
            const existing = readDaemonLock(lockPath);
            if (existing && !isLockStale(existing)) {
                return {
                    status: 'running',
                    lockPath,
                    record: existing,
                    staleRemoved,
                };
            }
            try {
                fs.unlinkSync(lockPath);
                staleRemoved = true;
            }
            catch (unlinkError) {
                if (unlinkError?.code !== 'ENOENT') {
                    throw unlinkError;
                }
            }
        }
    }
    throw new Error(`Unable to acquire daemon lock at ${lockPath}`);
}
export function removeDaemonLock(lockPath, options = {}) {
    const existing = readDaemonLock(lockPath);
    if (options.pid && existing?.pid && existing.pid !== options.pid) {
        return false;
    }
    try {
        fs.unlinkSync(lockPath);
        return true;
    }
    catch (error) {
        if (error?.code === 'ENOENT') {
            return false;
        }
        throw error;
    }
}
export function scanAllLockfiles(rootDir = resolveNexusStateDir()) {
    const locks = [];
    const visit = (dirPath) => {
        let entries;
        try {
            entries = fs.readdirSync(dirPath, { withFileTypes: true });
        }
        catch {
            return;
        }
        for (const entry of entries) {
            const entryPath = path.join(dirPath, entry.name);
            if (entry.isDirectory()) {
                visit(entryPath);
                continue;
            }
            if (!entry.isFile() || entry.name !== 'nexus-daemon.lock.json') {
                continue;
            }
            const lock = readDaemonLock(entryPath);
            if (!lock) {
                continue;
            }
            locks.push({
                lockPath: entryPath,
                stateRoot: path.dirname(entryPath),
                lock,
            });
        }
    };
    visit(rootDir);
    return locks;
}
