#!/usr/bin/env node
import { createHash } from "node:crypto";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { lstat, open, readFile, readdir, realpath, rename, rm } from "node:fs/promises";
import { verifyRunrailDistribution } from "./scripts/verify-runrail-distribution.mjs";

const sourceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
const homeRoot = path.resolve(os.homedir());
const product = "Runrail Local Audit Runner";
const desktopProduct = "Runrail Desktop";
const distributionVersion = "2.4.14";
const installationStateName = "RUNRAIL_INSTALLATION.json";
const uninstallJournalName = "RUNRAIL_UNINSTALL.json";
const uninstallFaultPhaseEnvironmentName = "RUNRAIL_UNINSTALL_TEST_INTERRUPT_AFTER";
const uninstallFaultExitCode = 86;

function sha256(value) {
  return createHash("sha256").update(value).digest("hex");
}

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (value && typeof value === "object") {
    return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
  }
  return value;
}

function stableJson(value) {
  return `${JSON.stringify(stableValue(value), null, 2)}\n`;
}

function compareUtf8Names(left, right) {
  return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
}

function refuse(code, message) {
  const error = new Error(message);
  error.code = code;
  throw error;
}

function interruptAfterPhase(phase) {
  if (String(process.env[uninstallFaultPhaseEnvironmentName] || "") === phase) {
    process.exit(uninstallFaultExitCode);
  }
}

function defaultInstallBase() {
  if (process.platform === "win32") {
    return path.join(process.env.LOCALAPPDATA || path.join(homeRoot, "AppData", "Local"), "Runrail");
  }
  if (process.platform === "darwin") {
    return path.join(homeRoot, "Library", "Application Support", "Runrail");
  }
  return path.join(process.env.XDG_DATA_HOME || path.join(homeRoot, ".local", "share"), "runrail");
}

function defaultBinDir(installBase) {
  if (process.platform === "win32") return path.join(installBase, "bin");
  return path.join(homeRoot, ".local", "bin");
}

function defaultDesktopEntryDir() {
  if (process.platform === "win32") {
    return path.join(
      process.env.APPDATA || path.join(homeRoot, "AppData", "Roaming"),
      "Microsoft",
      "Windows",
      "Start Menu",
      "Programs"
    );
  }
  if (process.platform === "darwin") return path.join(homeRoot, "Applications");
  return path.join(process.env.XDG_DATA_HOME || path.join(homeRoot, ".local", "share"), "applications");
}

async function canonicalTarget(value) {
  const requested = path.resolve(value);
  const missingParts = [];
  let cursor = requested;
  while (true) {
    try {
      return path.join(await realpath(cursor), ...missingParts.reverse());
    } catch (error) {
      if (error?.code !== "ENOENT") throw error;
      const parent = path.dirname(cursor);
      if (parent === cursor) throw error;
      missingParts.push(path.basename(cursor));
      cursor = parent;
    }
  }
}

function pathIdentity(value) {
  if (typeof value !== "string" || !value.trim()) return "";
  const resolved = path.resolve(value);
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}

function samePath(left, right) {
  return pathIdentity(left) === pathIdentity(right);
}

async function exists(target) {
  try {
    return await lstat(target);
  } catch (error) {
    if (error?.code === "ENOENT") return null;
    throw error;
  }
}

async function safeInstallRoot(value, canonicalSourceRoot, canonicalHomeRoot, canonicalCwd) {
  const resolved = await canonicalTarget(value);
  const filesystemRoot = path.parse(resolved).root;
  const relativeToSource = path.relative(canonicalSourceRoot, resolved);
  const insideSource = relativeToSource === "" || (
    relativeToSource !== ".." &&
    !relativeToSource.startsWith(`..${path.sep}`) &&
    !path.isAbsolute(relativeToSource)
  );
  if (
    resolved === filesystemRoot ||
    resolved === canonicalHomeRoot ||
    resolved === canonicalSourceRoot ||
    resolved === canonicalCwd ||
    resolved.length <= filesystemRoot.length + 2 ||
    insideSource
  ) {
    refuse("runrail-uninstall-unsafe-path", `Refusing unsafe Runrail uninstall root: ${resolved}`);
  }
  return resolved;
}

async function ownedPathDigest(target, relative = ".") {
  let info;
  try {
    info = await lstat(target);
  } catch (error) {
    if (error?.code === "ENOENT") {
      refuse("runrail-uninstall-component-missing", `Runrail-owned path is missing: ${target}`);
    }
    throw error;
  }
  if (info.isSymbolicLink()) {
    refuse("runrail-uninstall-symlink-refused", `Refusing a symlinked Runrail-owned path: ${target}`);
  }
  const digest = createHash("sha256");
  if (info.isDirectory()) {
    digest.update(`${relative}\0directory\0`);
    const entries = (await readdir(target)).sort((left, right) => compareUtf8Names(left, right));
    for (const name of entries) {
      digest.update(await ownedPathDigest(path.join(target, name), path.posix.join(relative, name)));
    }
  } else if (info.isFile()) {
    digest.update(`${relative}\0file\0`);
    digest.update(await readFile(target));
  } else {
    refuse("runrail-uninstall-component-type-invalid", `Unsupported Runrail-owned path type: ${target}`);
  }
  return digest.digest();
}

async function ownedPathSha256(target) {
  return (await ownedPathDigest(target)).toString("hex");
}

function launcherPaths(binDir) {
  if (process.platform === "win32") {
    return [
      path.join(binDir, "runrail.cmd"),
      path.join(binDir, "runrail.ps1"),
      path.join(binDir, "runrail-desktop.cmd"),
      path.join(binDir, "runrail-desktop.ps1")
    ];
  }
  return [path.join(binDir, "runrail"), path.join(binDir, "runrail-desktop")];
}

function desktopEntryPaths(entryDir) {
  if (process.platform === "win32") {
    return {
      kind: "windows-start-menu",
      target: path.join(entryDir, "Runrail Desktop.lnk"),
      marker: path.join(entryDir, "Runrail Desktop.runrail.json")
    };
  }
  if (process.platform === "darwin") {
    const target = path.join(entryDir, "Runrail Desktop.app");
    return {
      kind: "macos-application",
      target,
      marker: path.join(target, "Contents", "Resources", "RUNRAIL_DESKTOP_APP.json")
    };
  }
  return {
    kind: "linux-desktop-entry",
    target: path.join(entryDir, "runrail-desktop.desktop"),
    marker: path.join(entryDir, "runrail-desktop.runrail.json")
  };
}

function desktopOwnershipMarker(installBase, currentPath) {
  return {
    schemaVersion: 1,
    artifactType: "runrail-native-desktop-entry",
    product: desktopProduct,
    managed: true,
    version: distributionVersion,
    installBase,
    currentPath
  };
}

function pathIsInside(parent, child) {
  const relative = path.relative(parent, child);
  return relative === "" || (
    relative !== ".." &&
    !relative.startsWith(`..${path.sep}`) &&
    !path.isAbsolute(relative)
  );
}

async function presentManagedPaths(paths) {
  const present = [];
  for (const candidate of paths) {
    if (await exists(candidate)) present.push(candidate);
  }
  return present;
}

async function readOwnershipState(statePath) {
  const stateInfo = await exists(statePath);
  if (!stateInfo) return null;
  if (!stateInfo.isFile() || stateInfo.isSymbolicLink()) {
    refuse("runrail-uninstall-ownership-state-unsafe", `Unsafe Runrail ownership state: ${statePath}`);
  }
  try {
    return JSON.parse(await readFile(statePath, "utf8"));
  } catch {
    refuse("runrail-uninstall-ownership-state-invalid", `Invalid Runrail ownership state: ${statePath}`);
  }
}

async function requireCurrentPointer(currentPath, releaseDir) {
  const currentInfo = await exists(currentPath);
  if (!currentInfo) {
    refuse("runrail-uninstall-component-missing", `Runrail current pointer is missing: ${currentPath}`);
  }
  if (!currentInfo.isSymbolicLink()) {
    refuse("runrail-uninstall-component-type-invalid", `Runrail current pointer is not a managed link: ${currentPath}`);
  }
  const [currentTarget, releaseTarget] = await Promise.all([realpath(currentPath), realpath(releaseDir)]);
  if (!samePath(currentTarget, releaseTarget)) {
    refuse("runrail-uninstall-component-tampered", `Runrail current pointer has an unexpected target: ${currentPath}`);
  }
}

function verifyOwnershipStateRecord(state, expected) {
  if (!state || typeof state !== "object" || Array.isArray(state)) {
    refuse("runrail-uninstall-ownership-state-tampered", "Runrail installation ownership state is invalid or tampered.");
  }
  const { checksumSha256, ...body } = state;
  const stateLauncherPaths = Array.isArray(state.launchers)
    ? state.launchers.map((entry) => String(entry?.path || ""))
    : [];
  if (
    state.schemaVersion !== 1 ||
    state.artifactType !== "runrail-installation-ownership" ||
    state.product !== product ||
    state.managed !== true ||
    state.version !== distributionVersion ||
    state.platform !== process.platform ||
    !samePath(state.installBase, expected.installBase) ||
    !samePath(state.releaseDir, expected.releaseDir) ||
    !samePath(state.currentPath, expected.currentPath) ||
    !samePath(state.binDir, expected.binDir) ||
    !samePath(state.desktopEntryDir, expected.desktopEntryDir) ||
    JSON.stringify(stateLauncherPaths.map(pathIdentity)) !==
      JSON.stringify(expected.launchers.map(pathIdentity)) ||
    state.desktopLauncher?.kind !== expected.desktopEntry.kind ||
    !samePath(state.desktopLauncher?.path || "", expected.desktopEntry.target) ||
    !samePath(state.desktopLauncher?.markerPath || "", expected.desktopEntry.marker) ||
    !Array.isArray(state.launchers) ||
    !state.launchers.every((launcher) => /^[a-f0-9]{64}$/.test(String(launcher?.sha256 || ""))) ||
    !/^[a-f0-9]{64}$/.test(String(state.desktopLauncher?.targetSha256 || "")) ||
    !/^[a-f0-9]{64}$/.test(String(state.desktopLauncher?.markerSha256 || "")) ||
    checksumSha256 !== sha256(stableJson(body))
  ) {
    refuse("runrail-uninstall-ownership-state-tampered", "Runrail installation ownership state is invalid or tampered.");
  }
}

async function verifyOwnershipState(state, expected) {
  verifyOwnershipStateRecord(state, expected);
  const releaseInfo = await exists(expected.releaseDir);
  if (!releaseInfo) {
    refuse("runrail-uninstall-component-missing", `Runrail release is missing: ${expected.releaseDir}`);
  }
  if (!releaseInfo.isDirectory() || releaseInfo.isSymbolicLink()) {
    refuse("runrail-uninstall-symlink-refused", `Refusing an unsafe Runrail release: ${expected.releaseDir}`);
  }
  try {
    await verifyRunrailDistribution(expected.releaseDir);
  } catch {
    refuse("runrail-uninstall-component-tampered", `Runrail release verification failed: ${expected.releaseDir}`);
  }
  await requireCurrentPointer(expected.currentPath, expected.releaseDir);

  for (const launcher of state.launchers) {
    if ((await ownedPathSha256(launcher.path)) !== launcher.sha256) {
      refuse("runrail-uninstall-component-tampered", `Runrail launcher verification failed: ${launcher.path}`);
    }
  }
  if (
    (await ownedPathSha256(expected.desktopEntry.target)) !== state.desktopLauncher.targetSha256 ||
    (await ownedPathSha256(expected.desktopEntry.marker)) !== state.desktopLauncher.markerSha256
  ) {
    refuse(
      "runrail-uninstall-component-tampered",
      `Runrail native desktop entry verification failed: ${expected.desktopEntry.target}`
    );
  }
  let nativeMarker;
  try {
    nativeMarker = JSON.parse(await readFile(expected.desktopEntry.marker, "utf8"));
  } catch {
    refuse(
      "runrail-uninstall-component-tampered",
      `Runrail native desktop ownership marker is invalid: ${expected.desktopEntry.marker}`
    );
  }
  if (stableJson(nativeMarker) !== stableJson(desktopOwnershipMarker(expected.installBase, expected.currentPath))) {
    refuse(
      "runrail-uninstall-component-tampered",
      `Runrail native desktop ownership marker is tampered: ${expected.desktopEntry.marker}`
    );
  }
}

function uninstallQuarantinePath(target, componentId, ownershipChecksum) {
  const token = sha256(
    `${ownershipChecksum}\0${componentId}\0${pathIdentity(target)}`
  ).slice(0, 24);
  return path.join(path.dirname(target), `.runrail-rm-${token}`);
}

function buildUninstallComponents(state, expected, statePath) {
  const markerIsInsideTarget = pathIsInside(expected.desktopEntry.target, expected.desktopEntry.marker);
  const components = [
    {
      id: "desktop-launcher",
      path: expected.desktopEntry.target,
      kind: process.platform === "darwin" ? "directory" : "file",
      sha256: state.desktopLauncher.targetSha256
    }
  ];
  if (!markerIsInsideTarget) {
    components.push({
      id: "desktop-marker",
      path: expected.desktopEntry.marker,
      kind: "file",
      sha256: state.desktopLauncher.markerSha256
    });
  }
  state.launchers.forEach((launcher, index) => {
    components.push({
      id: `launcher-${index}`,
      path: launcher.path,
      kind: "file",
      sha256: launcher.sha256
    });
  });
  components.push(
    {
      id: "current-pointer",
      path: expected.currentPath,
      kind: "symlink",
      targetPath: expected.releaseDir
    },
    {
      id: "release",
      path: expected.releaseDir,
      kind: "directory",
      verifiedDistribution: true
    },
    {
      id: "installation-state",
      path: statePath,
      kind: "file",
      sha256: sha256(stableJson(state))
    }
  );
  return components.map((component) => ({
    ...component,
    quarantinePath: uninstallQuarantinePath(
      component.path,
      component.id,
      state.checksumSha256
    )
  }));
}

function buildUninstallJournal(state, expected, statePath, componentPhases = null) {
  const workspacePath = path.join(expected.installBase, "workspace");
  const components = buildUninstallComponents(state, expected, statePath).map((component, index) => ({
    ...component,
    phase: componentPhases?.[index] || "pending"
  }));
  const body = {
    schemaVersion: 1,
    artifactType: "runrail-uninstall-journal",
    product,
    managed: true,
    version: distributionVersion,
    platform: process.platform,
    installBase: expected.installBase,
    installationStatePath: statePath,
    ownershipStateSha256: sha256(stableJson(state)),
    ownershipState: state,
    components,
    preservedByDefault: [workspacePath]
  };
  return { ...body, checksumSha256: sha256(stableJson(body)) };
}

async function readUninstallJournal(journalPath) {
  const journalInfo = await exists(journalPath);
  if (!journalInfo) return null;
  if (!journalInfo.isFile() || journalInfo.isSymbolicLink()) {
    refuse("runrail-uninstall-journal-unsafe", `Unsafe Runrail uninstall journal: ${journalPath}`);
  }
  try {
    return JSON.parse(await readFile(journalPath, "utf8"));
  } catch {
    refuse("runrail-uninstall-journal-invalid", `Invalid Runrail uninstall journal: ${journalPath}`);
  }
}

function verifyUninstallJournal(journal, expected, statePath) {
  if (!journal || typeof journal !== "object" || Array.isArray(journal)) {
    refuse("runrail-uninstall-journal-invalid", `Invalid Runrail uninstall journal: ${path.join(expected.installBase, uninstallJournalName)}`);
  }
  const { checksumSha256, ...body } = journal;
  if (
    journal.schemaVersion !== 1 ||
    journal.artifactType !== "runrail-uninstall-journal" ||
    journal.product !== product ||
    journal.managed !== true ||
    journal.version !== distributionVersion ||
    journal.platform !== process.platform ||
    !samePath(journal.installBase, expected.installBase) ||
    !samePath(journal.installationStatePath, statePath) ||
    !journal.ownershipState ||
    !Array.isArray(journal.components) ||
    checksumSha256 !== sha256(stableJson(body))
  ) {
    refuse("runrail-uninstall-journal-invalid", `Invalid Runrail uninstall journal: ${path.join(expected.installBase, uninstallJournalName)}`);
  }
  try {
    verifyOwnershipStateRecord(journal.ownershipState, expected);
  } catch {
    refuse("runrail-uninstall-journal-invalid", `Invalid Runrail uninstall journal: ${path.join(expected.installBase, uninstallJournalName)}`);
  }
  const componentPhases = journal.components.map((component) => String(component?.phase || ""));
  let phaseState = "removed";
  for (const phase of componentPhases) {
    if (!["pending", "staged", "removed"].includes(phase)) {
      refuse("runrail-uninstall-journal-invalid", `Invalid Runrail uninstall journal: ${path.join(expected.installBase, uninstallJournalName)}`);
    }
    if (phaseState === "removed") {
      if (phase === "staged") phaseState = "staged";
      else if (phase === "pending") phaseState = "pending";
    } else if (phaseState === "staged") {
      if (phase !== "pending") {
        refuse("runrail-uninstall-journal-invalid", `Invalid Runrail uninstall journal: ${path.join(expected.installBase, uninstallJournalName)}`);
      }
      phaseState = "pending";
    } else if (phase !== "pending") {
      refuse("runrail-uninstall-journal-invalid", `Invalid Runrail uninstall journal: ${path.join(expected.installBase, uninstallJournalName)}`);
    }
  }
  const expectedJournal = buildUninstallJournal(
    journal.ownershipState,
    expected,
    statePath,
    componentPhases
  );
  if (
    journal.ownershipStateSha256 !== sha256(stableJson(journal.ownershipState)) ||
    stableJson(journal) !== stableJson(expectedJournal)
  ) {
    refuse("runrail-uninstall-journal-invalid", `Invalid Runrail uninstall journal: ${path.join(expected.installBase, uninstallJournalName)}`);
  }
  return expectedJournal;
}

async function writeUninstallJournal(journalPath, journal) {
  const temporaryPath = `${journalPath}.tmp`;
  if (await exists(temporaryPath)) {
    refuse(
      "runrail-uninstall-journal-temp-conflict",
      `Refusing uninstall while an incomplete journal write exists: ${temporaryPath}`
    );
  }
  let handle = null;
  let temporaryCreated = false;
  try {
    handle = await open(temporaryPath, "wx", 0o600);
    temporaryCreated = true;
    await handle.writeFile(stableJson(journal), "utf8");
    await handle.sync();
    await handle.close();
    handle = null;
    await rename(temporaryPath, journalPath);
  } catch (error) {
    if (handle) await handle.close().catch(() => {});
    if (temporaryCreated) await rm(temporaryPath, { force: true }).catch(() => {});
    if (error?.code === "EEXIST") {
      refuse(
        "runrail-uninstall-journal-temp-conflict",
        `Refusing uninstall while another journal update owns: ${temporaryPath}`
      );
    }
    throw error;
  }
}

function journalIsSameOrNextTransition(current, candidate) {
  const phaseRank = new Map([
    ["pending", 0],
    ["staged", 1],
    ["removed", 2]
  ]);
  let advanced = 0;
  for (let index = 0; index < current.components.length; index += 1) {
    const currentRank = phaseRank.get(current.components[index].phase);
    const candidateRank = phaseRank.get(candidate.components[index].phase);
    if (candidateRank === currentRank) continue;
    if (candidateRank !== currentRank + 1) return false;
    advanced += 1;
  }
  return advanced <= 1;
}

async function recoverUninstallJournalTemporary(journalPath, journal, expected, statePath) {
  const temporaryPath = `${journalPath}.tmp`;
  const temporaryInfo = await exists(temporaryPath);
  if (!temporaryInfo) return journal;
  if (!temporaryInfo.isFile() || temporaryInfo.isSymbolicLink()) {
    refuse(
      "runrail-uninstall-journal-temp-conflict",
      `Unsafe Runrail uninstall journal temporary path: ${temporaryPath}`
    );
  }
  let candidate = null;
  try {
    candidate = verifyUninstallJournal(
      await readUninstallJournal(temporaryPath),
      expected,
      statePath
    );
  } catch {
    await rm(temporaryPath);
    return journal;
  }
  if (!journalIsSameOrNextTransition(journal, candidate)) {
    refuse(
      "runrail-uninstall-journal-temp-conflict",
      `Runrail uninstall journal temporary state conflicts with the durable journal: ${temporaryPath}`
    );
  }
  await rename(temporaryPath, journalPath);
  return candidate;
}

async function verifyOwnedComponent(component, candidatePath, journal, expected) {
  if (component.id === "current-pointer") {
    await requireCurrentPointer(candidatePath, component.targetPath);
    return;
  }
  if (component.id === "release") {
    const releaseInfo = await exists(candidatePath);
    if (!releaseInfo?.isDirectory() || releaseInfo.isSymbolicLink()) {
      refuse("runrail-uninstall-symlink-refused", `Refusing an unsafe Runrail release: ${candidatePath}`);
    }
    try {
      await verifyRunrailDistribution(candidatePath);
    } catch {
      refuse("runrail-uninstall-component-tampered", `Runrail release verification failed: ${candidatePath}`);
    }
    return;
  }
  if (component.id === "installation-state") {
    const state = await readOwnershipState(candidatePath);
    if (!state || stableJson(state) !== stableJson(journal.ownershipState)) {
      refuse(
        "runrail-uninstall-ownership-state-tampered",
        "Runrail installation ownership state changed after uninstall began."
      );
    }
    return;
  }
  if ((await ownedPathSha256(candidatePath)) !== component.sha256) {
    refuse("runrail-uninstall-component-tampered", `Runrail-owned path verification failed: ${candidatePath}`);
  }
  if (component.id === "desktop-marker") {
    let marker;
    try {
      marker = JSON.parse(await readFile(candidatePath, "utf8"));
    } catch {
      refuse("runrail-uninstall-component-tampered", `Runrail native desktop ownership marker is invalid: ${candidatePath}`);
    }
    if (stableJson(marker) !== stableJson(desktopOwnershipMarker(expected.installBase, expected.currentPath))) {
      refuse("runrail-uninstall-component-tampered", `Runrail native desktop ownership marker is tampered: ${candidatePath}`);
    }
  }
}

function verifyQuarantinedComponentRoot(component, info) {
  const valid = component.kind === "directory"
    ? info.isDirectory() && !info.isSymbolicLink()
    : component.kind === "symlink"
      ? info.isSymbolicLink()
      : info.isFile() && !info.isSymbolicLink();
  if (!valid) {
    refuse(
      "runrail-uninstall-quarantine-unsafe",
      `Unsafe Runrail uninstall quarantine path: ${component.quarantinePath}`
    );
  }
}

function journalWithComponentPhase(journal, componentIndex, phase, expected, statePath) {
  const componentPhases = journal.components.map((component) => component.phase);
  componentPhases[componentIndex] = phase;
  return buildUninstallJournal(
    journal.ownershipState,
    expected,
    statePath,
    componentPhases
  );
}

async function removeOwnedComponent(componentIndex, journal, expected, statePath, journalPath) {
  const component = journal.components[componentIndex];
  let [originalInfo, quarantineInfo] = await Promise.all([
    exists(component.path),
    exists(component.quarantinePath)
  ]);
  if (component.phase === "removed") {
    if (originalInfo || quarantineInfo) {
      refuse(
        "runrail-uninstall-component-conflict",
        `Runrail uninstall found a path for a removed component: ${component.path}`
      );
    }
    return journal;
  }
  if (originalInfo && quarantineInfo) {
    refuse(
      "runrail-uninstall-component-conflict",
      `Runrail uninstall found both the managed and quarantined component: ${component.path}`
    );
  }
  if (component.phase === "pending") {
    if (!originalInfo && !quarantineInfo) {
      refuse(
        "runrail-uninstall-component-missing",
        `Runrail uninstall pending component is missing: ${component.path}`
      );
    }
    if (originalInfo) {
      await verifyOwnedComponent(component, component.path, journal, expected);
      await rename(component.path, component.quarantinePath);
      quarantineInfo = await exists(component.quarantinePath);
      if (!quarantineInfo) {
        refuse(
          "runrail-uninstall-component-missing",
          `Runrail uninstall quarantine path is missing: ${component.quarantinePath}`
        );
      }
      interruptAfterPhase(`${component.id}:detached`);
    }
    await verifyOwnedComponent(component, component.quarantinePath, journal, expected);
    // A durable staged phase is the authority to finish a partially removed quarantine on rerun.
    journal = journalWithComponentPhase(journal, componentIndex, "staged", expected, statePath);
    await writeUninstallJournal(journalPath, journal);
    interruptAfterPhase(`${component.id}:staged`);
  }

  [originalInfo, quarantineInfo] = await Promise.all([
    exists(component.path),
    exists(component.quarantinePath)
  ]);
  if (originalInfo) {
    refuse(
      "runrail-uninstall-component-conflict",
      `Runrail uninstall found a replacement at a staged component path: ${component.path}`
    );
  }
  if (quarantineInfo) {
    verifyQuarantinedComponentRoot(component, quarantineInfo);
    await rm(component.quarantinePath, {
      recursive: component.kind === "directory",
      force: true
    });
  }
  interruptAfterPhase(`${component.id}:removed`);
  if (await exists(component.quarantinePath)) {
    refuse(
      "runrail-uninstall-component-conflict",
      `Runrail uninstall could not remove its quarantined component: ${component.quarantinePath}`
    );
  }
  journal = journalWithComponentPhase(journal, componentIndex, "removed", expected, statePath);
  await writeUninstallJournal(journalPath, journal);
  return journal;
}

async function uninstall() {
  await verifyRunrailDistribution(sourceRoot);
  const [canonicalSourceRoot, canonicalHomeRoot, canonicalCwd] = await Promise.all([
    canonicalTarget(sourceRoot),
    canonicalTarget(homeRoot),
    canonicalTarget(process.cwd())
  ]);
  const installBase = await safeInstallRoot(
    process.env.RUNRAIL_INSTALL_BASE || defaultInstallBase(),
    canonicalSourceRoot,
    canonicalHomeRoot,
    canonicalCwd
  );
  const binDir = await safeInstallRoot(
    process.env.RUNRAIL_BIN_DIR || defaultBinDir(installBase),
    canonicalSourceRoot,
    canonicalHomeRoot,
    canonicalCwd
  );
  const desktopEntryDir = await safeInstallRoot(
    process.env.RUNRAIL_DESKTOP_ENTRY_DIR || defaultDesktopEntryDir(),
    canonicalSourceRoot,
    canonicalHomeRoot,
    canonicalCwd
  );
  const releasesRoot = path.join(installBase, "releases");
  const releaseDir = path.join(releasesRoot, "v1");
  const backupDir = path.join(releasesRoot, ".v1-backup");
  const currentPath = path.join(installBase, "current");
  const statePath = path.join(installBase, installationStateName);
  const journalPath = path.join(installBase, uninstallJournalName);
  const journalTemporaryPath = `${journalPath}.tmp`;
  const launchers = launcherPaths(binDir);
  const desktopEntry = desktopEntryPaths(desktopEntryDir);
  const expected = {
    installBase,
    releaseDir,
    currentPath,
    binDir,
    launchers,
    desktopEntryDir,
    desktopEntry
  };

  let journal = await readUninstallJournal(journalPath);
  const state = await readOwnershipState(statePath);
  if (!journal && !state) {
    const present = await presentManagedPaths([
      releaseDir,
      backupDir,
      currentPath,
      ...launchers,
      desktopEntry.target,
      desktopEntry.marker,
      journalTemporaryPath
    ]);
    if (present.length > 0) {
      refuse(
        "runrail-uninstall-ownership-state-missing",
        `Refusing to remove unowned Runrail paths without ${installationStateName}: ${present.join(", ")}`
      );
    }
    const workspacePath = path.join(installBase, "workspace");
    return {
      ok: true,
      product,
      platform: `${process.platform}-${process.arch}`,
      installed: false,
      installBase,
      removed: [],
      preserved: {
        workspacePath,
        workspacePresent: Boolean(await exists(workspacePath)),
        auditEvidencePreserved: true
      }
    };
  }

  if (await exists(backupDir)) {
    refuse(
      "runrail-uninstall-interrupted-update",
      "Refusing uninstall while .v1-backup exists; rerun the verified installer to recover the prior release first."
    );
  }
  if (journal) {
    journal = verifyUninstallJournal(journal, expected, statePath);
    journal = await recoverUninstallJournalTemporary(
      journalPath,
      journal,
      expected,
      statePath
    );
    if (state && stableJson(state) !== stableJson(journal.ownershipState)) {
      refuse(
        "runrail-uninstall-ownership-state-tampered",
        "Runrail installation ownership state changed after uninstall began."
      );
    }
  } else {
    await verifyOwnershipState(state, expected);
    journal = buildUninstallJournal(state, expected, statePath);
    const quarantineConflicts = await presentManagedPaths(
      journal.components.map((component) => component.quarantinePath)
    );
    if (quarantineConflicts.length > 0) {
      refuse(
        "runrail-uninstall-quarantine-conflict",
        `Refusing uninstall because a quarantine path already exists: ${quarantineConflicts.join(", ")}`
      );
    }
    if (await exists(journalTemporaryPath)) {
      const temporaryJournal = verifyUninstallJournal(
        await readUninstallJournal(journalTemporaryPath),
        expected,
        statePath
      );
      if (stableJson(temporaryJournal) !== stableJson(journal)) {
        refuse(
          "runrail-uninstall-journal-temp-conflict",
          `Incomplete Runrail uninstall journal does not match the verified installation: ${journalTemporaryPath}`
        );
      }
      await rename(journalTemporaryPath, journalPath);
      journal = temporaryJournal;
    } else {
      await writeUninstallJournal(journalPath, journal);
    }
    interruptAfterPhase("journal-created");
  }

  const configuredFaultPhase = String(process.env[uninstallFaultPhaseEnvironmentName] || "");
  const validFaultPhases = new Set([
    "journal-created",
    "journal:removed",
    ...journal.components.flatMap((component) => [
      `${component.id}:detached`,
      `${component.id}:staged`,
      `${component.id}:removed`
    ])
  ]);
  if (configuredFaultPhase && !validFaultPhases.has(configuredFaultPhase)) {
    refuse(
      "runrail-uninstall-fault-phase-invalid",
      `Unknown Runrail uninstall interruption phase: ${configuredFaultPhase}`
    );
  }

  for (let componentIndex = 0; componentIndex < journal.components.length; componentIndex += 1) {
    journal = await removeOwnedComponent(
      componentIndex,
      journal,
      expected,
      statePath,
      journalPath
    );
  }
  const remainingManagedPaths = await presentManagedPaths(
    journal.components.flatMap((component) => [component.path, component.quarantinePath])
  );
  if (remainingManagedPaths.length > 0) {
    refuse(
      "runrail-uninstall-component-conflict",
      `Runrail uninstall found a managed path replacement after deletion: ${remainingManagedPaths.join(", ")}`
    );
  }
  await rm(journalPath);
  interruptAfterPhase("journal:removed");

  const workspacePath = path.join(installBase, "workspace");
  const configuredRunrailHome = String(process.env.RUNRAIL_HOME || "").trim();
  const preservedPaths = [workspacePath];
  if (configuredRunrailHome && !preservedPaths.some((entry) => samePath(entry, configuredRunrailHome))) {
    preservedPaths.push(path.resolve(configuredRunrailHome));
  }
  return {
    ok: true,
    product,
    platform: `${process.platform}-${process.arch}`,
    installed: false,
    installBase,
    removed: {
      launchers,
      currentPath,
      releaseDir,
      installationStatePath: statePath,
      desktopLauncher: {
        kind: desktopEntry.kind,
        path: desktopEntry.target,
        markerPath: desktopEntry.marker
      }
    },
    preserved: {
      paths: preservedPaths,
      workspacePath,
      workspacePresent: Boolean(await exists(workspacePath)),
      auditEvidencePreserved: true,
      installBasePreserved: true
    }
  };
}

try {
  const result = await uninstall();
  process.stdout.write(stableJson(result));
} catch (error) {
  process.stderr.write(stableJson({
    ok: false,
    product,
    code: String(error?.code || "runrail-uninstall-failed"),
    error: String(error?.message || error)
  }));
  process.exitCode = 1;
}
