import { open, readFile, unlink, mkdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { hostname } from "node:os";
import { z } from "zod";
export const pidLockInfoSchema = z.object({
    pid: z.number(),
    startedAt: z.string(),
    hostname: z.string(),
    uid: z.number(),
    listen: z.string().nullable(),
    desktopManaged: z.boolean().optional(),
});
function parsePidLockInfo(raw) {
    const result = pidLockInfoSchema.safeParse(raw);
    return result.success ? result.data : null;
}
function isErrnoException(err) {
    return err instanceof Error && "code" in err;
}
export class PidLockError extends Error {
    constructor(message, existingLock) {
        super(message);
        this.existingLock = existingLock;
        this.name = "PidLockError";
    }
}
function isPidRunning(pid) {
    try {
        process.kill(pid, 0);
        return true;
    }
    catch {
        return false;
    }
}
function getPidFilePath(paseoHome) {
    return join(paseoHome, "paseo.pid");
}
function resolveOwnerPid(ownerPid) {
    if (typeof ownerPid === "number" && Number.isInteger(ownerPid) && ownerPid > 0) {
        return ownerPid;
    }
    return process.pid;
}
export async function acquirePidLock(paseoHome, listen, options) {
    const pidPath = getPidFilePath(paseoHome);
    // Ensure paseoHome directory exists
    if (!existsSync(paseoHome)) {
        await mkdir(paseoHome, { recursive: true });
    }
    // Try to read existing lock
    let existingLock = null;
    try {
        const content = await readFile(pidPath, "utf-8");
        existingLock = parsePidLockInfo(JSON.parse(content));
    }
    catch {
        // No existing lock or invalid JSON - that's fine
    }
    // Check if existing lock is stale
    const lockOwnerPid = resolveOwnerPid(options?.ownerPid);
    if (existingLock) {
        if (isPidRunning(existingLock.pid)) {
            if (existingLock.pid === lockOwnerPid) {
                return;
            }
            throw new PidLockError(`Another Paseo daemon is already running (PID ${existingLock.pid}, started ${existingLock.startedAt})`, existingLock);
        }
        // Stale lock - remove it
        await unlink(pidPath).catch(() => { });
    }
    // Create new lock with exclusive flag
    const lockInfo = {
        pid: lockOwnerPid,
        startedAt: new Date().toISOString(),
        hostname: hostname(),
        uid: process.getuid?.() ?? 0,
        listen,
        ...(process.env.PASEO_DESKTOP_MANAGED === "1" ? { desktopManaged: true } : {}),
    };
    let fd;
    try {
        fd = await open(pidPath, "wx");
        await fd.write(JSON.stringify(lockInfo));
    }
    catch (err) {
        if (isErrnoException(err) && err.code === "EEXIST") {
            // Race condition - another process created the file
            // Re-read and check
            try {
                const content = await readFile(pidPath, "utf-8");
                const raceLock = parsePidLockInfo(JSON.parse(content));
                if (raceLock) {
                    throw new PidLockError(`Another Paseo daemon is already running (PID ${raceLock.pid})`, raceLock);
                }
                throw new PidLockError("Failed to acquire PID lock due to race condition");
            }
            catch (innerErr) {
                if (innerErr instanceof PidLockError)
                    throw innerErr;
                throw new PidLockError("Failed to acquire PID lock due to race condition");
            }
        }
        throw err;
    }
    finally {
        await fd?.close();
    }
}
export async function updatePidLock(paseoHome, patch, options) {
    const pidPath = getPidFilePath(paseoHome);
    const lockOwnerPid = resolveOwnerPid(options?.ownerPid);
    const content = await readFile(pidPath, "utf-8");
    const existingLock = parsePidLockInfo(JSON.parse(content));
    if (!existingLock) {
        throw new PidLockError("Cannot update PID lock: invalid lock file");
    }
    if (existingLock.pid !== lockOwnerPid) {
        throw new PidLockError(`Cannot update PID lock owned by PID ${existingLock.pid}`, existingLock);
    }
    const updatedLock = {
        ...existingLock,
        ...patch,
    };
    const fd = await open(pidPath, "r+");
    try {
        await fd.truncate(0);
        await fd.writeFile(JSON.stringify(updatedLock));
    }
    finally {
        await fd.close();
    }
}
export async function releasePidLock(paseoHome, options) {
    const pidPath = getPidFilePath(paseoHome);
    const lockOwnerPid = resolveOwnerPid(options?.ownerPid);
    try {
        // Only remove if it's our lock
        const content = await readFile(pidPath, "utf-8");
        const lock = parsePidLockInfo(JSON.parse(content));
        if (lock?.pid === lockOwnerPid) {
            await unlink(pidPath);
        }
    }
    catch {
        // Ignore errors - lock may already be gone
    }
}
export async function getPidLockInfo(paseoHome) {
    const pidPath = getPidFilePath(paseoHome);
    try {
        const content = await readFile(pidPath, "utf-8");
        return parsePidLockInfo(JSON.parse(content));
    }
    catch {
        return null;
    }
}
export async function isLocked(paseoHome) {
    const info = await getPidLockInfo(paseoHome);
    if (!info) {
        return { locked: false };
    }
    if (!isPidRunning(info.pid)) {
        return { locked: false, info };
    }
    return { locked: true, info };
}
//# sourceMappingURL=pid-lock.js.map