import { randomUUID } from "node:crypto";
import { dirname } from "node:path";
import { parse, stringify } from "yaml";
import { hasOwnErrorCode } from "./error-codes.js";
export async function readRegistry(registryFile, fs) {
    try {
        await assertPathHasNoSymbolicLinks(registryFile, fs);
        const content = await fs.readFile(registryFile, "utf8");
        const parsed = parse(content);
        if (!isWorktreeRegistry(parsed)) {
            throw new Error(`Invalid worktree registry: ${registryFile}`);
        }
        return parsed;
    }
    catch (error) {
        if (isNotFound(error)) {
            return { worktrees: [] };
        }
        throw error;
    }
}
export async function writeRegistry(registryFile, registry, fs) {
    await assertPathHasNoSymbolicLinks(registryFile, fs);
    await fs.mkdir(dirname(registryFile), { recursive: true });
    await assertPathHasNoSymbolicLinks(registryFile, fs);
    const yaml = stringify(registry, { lineWidth: 0 });
    const temporaryFile = `${registryFile}.tmp-${randomUUID()}`;
    let temporaryCreated = false;
    try {
        await assertPathHasNoSymbolicLinks(temporaryFile, fs);
        await fs.writeFile(temporaryFile, yaml, { encoding: "utf8", flag: "wx" });
        temporaryCreated = true;
        await assertPathHasNoSymbolicLinks(registryFile, fs);
        await fs.rename(temporaryFile, registryFile);
    }
    catch (error) {
        if (temporaryCreated || !isAlreadyExists(error)) {
            await fs.unlink(temporaryFile).catch(() => undefined);
        }
        throw error;
    }
}
function isNotFound(error) {
    return hasOwnErrorCode(error, "ENOENT");
}
function isAlreadyExists(error) {
    return hasOwnErrorCode(error, "EEXIST");
}
async function assertPathHasNoSymbolicLinks(targetPath, fs) {
    const segments = targetPath.split("/").filter(Boolean);
    let currentPath = targetPath.startsWith("/") ? "" : ".";
    for (const segment of segments) {
        currentPath = `${currentPath}/${segment}`;
        try {
            if ((await fs.lstat(currentPath)).isSymbolicLink()) {
                if (isAllowedSystemAlias(currentPath)) {
                    continue;
                }
                throw new Error(`Refusing worktree registry path containing symbolic link: ${currentPath}`);
            }
        }
        catch (error) {
            if (isNotFound(error)) {
                return;
            }
            throw error;
        }
    }
}
function isAllowedSystemAlias(path) {
    return path === "/var";
}
function isWorktreeRegistry(value) {
    if (typeof value !== "object" || value === null) {
        return false;
    }
    const worktrees = getOwnEntry(value, "worktrees");
    if (!Array.isArray(worktrees)) {
        return false;
    }
    return worktrees.every(isWorktree);
}
function isWorktree(value) {
    if (typeof value !== "object" || value === null) {
        return false;
    }
    const name = getOwnEntry(value, "name");
    const path = getOwnEntry(value, "path");
    const branch = getOwnEntry(value, "branch");
    const baseBranch = getOwnEntry(value, "baseBranch");
    const createdAt = getOwnEntry(value, "createdAt");
    const source = getOwnEntry(value, "source");
    const agent = getOwnEntry(value, "agent");
    const status = getOwnEntry(value, "status");
    const storyId = getOwnEntry(value, "storyId");
    const planPath = getOwnEntry(value, "planPath");
    const prompt = getOwnEntry(value, "prompt");
    return (typeof name === "string" &&
        typeof path === "string" &&
        typeof branch === "string" &&
        typeof baseBranch === "string" &&
        typeof createdAt === "string" &&
        typeof source === "string" &&
        typeof agent === "string" &&
        (status === "active" || status === "done" || status === "failed" || status === "removing") &&
        (storyId === undefined || typeof storyId === "string") &&
        (planPath === undefined || typeof planPath === "string") &&
        (prompt === undefined || typeof prompt === "string"));
}
function getOwnEntry(record, key) {
    return Object.prototype.hasOwnProperty.call(record, key)
        ? record[key]
        : undefined;
}
export async function addWorktreeEntry(registryFile, entry, fs) {
    const registry = await readRegistry(registryFile, fs);
    registry.worktrees.push(entry);
    await writeRegistry(registryFile, registry, fs);
}
export async function removeWorktreeEntry(registryFile, name, fs) {
    const registry = await readRegistry(registryFile, fs);
    registry.worktrees = registry.worktrees.filter((w) => w.name !== name);
    await writeRegistry(registryFile, registry, fs);
}
export async function updateWorktreeStatus(registryFile, name, status, deps) {
    const { fs } = deps;
    const registry = await readRegistry(registryFile, fs);
    const entry = registry.worktrees.find((w) => w.name === name);
    if (!entry) {
        throw new Error(`Worktree "${name}" not found in registry`);
    }
    entry.status = status;
    await writeRegistry(registryFile, registry, fs);
}
