import { spawnSync, execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, appendFileSync, cpSync, lstatSync, readlinkSync, unlinkSync } from "node:fs";
import { resolve, join, dirname } from "node:path";
import { homedir } from "node:os";
import { createInterface } from "node:readline";
import { removeBrandStanza, hasAnyBrandStanza } from "./samba-provision.js";
const HOME = homedir();
const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
// Brand manifest — read from payload to derive brand-specific installation paths.
// No fallback: if brand.json is missing, the package is corrupted.
const BRAND_PATH = join(PAYLOAD_DIR, "platform", "config", "brand.json");
if (!existsSync(BRAND_PATH)) {
    console.error("FATAL: brand.json not found in payload. Package may be corrupted.");
    process.exit(1);
}
let BRAND;
try {
    BRAND = JSON.parse(readFileSync(BRAND_PATH, "utf-8"));
}
catch (err) {
    console.error(`FATAL: Failed to parse brand.json: ${err.message}`);
    process.exit(1);
}
const INSTALL_DIR = resolve(HOME, BRAND.installDir);
const CONFIG_DIR = resolve(HOME, BRAND.configDir);
const LOG_FILE = join("/tmp", `${BRAND.productName.toLowerCase().replace(/\s+/g, "-")}-uninstall-${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
const TOTAL = "11";
// ---------------------------------------------------------------------------
// Logging — timestamped to console AND persistent log file in /tmp
// (Log lives in /tmp because the uninstall deletes the config directory)
// ---------------------------------------------------------------------------
function logFile(msg) {
    try {
        appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`);
    }
    catch { /* fs full */ }
}
const _log = console.log;
const _err = console.error;
console.log = (...args) => { _log(...args); logFile(args.map(String).join(" ")); };
console.error = (...args) => { _err(...args); logFile(`[ERROR] ${args.map(String).join(" ")}`); };
function log(step, message) {
    console.log(`[${step}/${TOTAL}] ${message}`);
}
// ---------------------------------------------------------------------------
// Shell helpers (duplicated from index.ts — intentionally not shared)
// ---------------------------------------------------------------------------
function shell(command, args, options) {
    const cmd = options?.sudo ? "sudo" : command;
    const cmdArgs = options?.sudo ? [command, ...args] : args;
    const start = Date.now();
    logFile(`> ${cmd} ${cmdArgs.join(" ")}`);
    const result = spawnSync(cmd, cmdArgs, {
        stdio: "inherit",
        timeout: options?.timeout ?? 120_000,
    });
    const dur = ((Date.now() - start) / 1000).toFixed(1);
    if (result.status !== 0) {
        const reason = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
        logFile(`  FAILED (${reason}) after ${dur}s`);
        throw new Error(`${cmd} ${cmdArgs.slice(0, 3).join(" ")} failed (${reason})`);
    }
    logFile(`  OK in ${dur}s`);
}
function isLinux() {
    return process.platform === "linux";
}
function isDarwin() {
    return process.platform === "darwin";
}
// mirror the launchd identifiers from index.ts so the uninstall
// can locate the LaunchAgent without reaching across modules. Per the
// `uninstall.ts:` "Shell helpers (duplicated from index.ts ...)" policy,
// these are intentionally duplicated rather than shared.
function launchdLabel() {
    return `com.rubytech.${BRAND.hostname}`;
}
function plistPath() {
    return resolve(HOME, "Library/LaunchAgents", `${launchdLabel()}.plist`);
}
function gui() {
    const uid = typeof process.getuid === "function" ? process.getuid() : 0;
    return `gui/${uid}`;
}
function commandExists(cmd) {
    try {
        execFileSync("which", [cmd], { stdio: "pipe" });
        return true;
    }
    catch {
        return false;
    }
}
// ---------------------------------------------------------------------------
// Detection
// ---------------------------------------------------------------------------
export function isMaxyInstalled() {
    return existsSync(INSTALL_DIR);
}
/** Known brand hostnames in the Maxy ecosystem. Each brand ships a main unit
 *  (`<hostname>.service`) and a per-brand edge unit
 *  (`<hostname>-edge.service`). Peer detection matches only these filenames, not any stray
 *  `.service` file — stale units, gnome-keyring disable markers, and unrelated
 *  user services are not peer evidence. When a third brand is
 *  added under `brands/`, append its hostname here. */
const KNOWN_BRAND_HOSTNAMES = ["maxy", "maxy-code", "realagent", "realagent-code", "maxy-2", "maxy-3", "maxy-4"];
/** Detect whether another brand is installed on this device.
 *  device-wide steps (apt package purge, Ollama binary removal, apt
 *  repo cleanup, ~/.claude / ~/.ollama wipes) must skip when a peer brand is
 *  present — its runtime still depends on those singletons.
 *
 *  Returns the matched peer service filename (so callers can cite it in logs)
 *  or `undefined` when no peer brand is detected. Truthiness maps cleanly to
 *  "peer present" for existing `if (peerBrandPresent())` callers.
 *
 *  narrowed from "any `.service` file that isn't ours" to an
 *  allowlist keyed on `KNOWN_BRAND_HOSTNAMES`. The previous rule counted
 *  a `gnome-keyring-daemon.service -> /dev/null` disable-marker symlink (and
 *  any other unrelated user unit) as peer evidence, silently preserving
 *  `~/.claude` OAuth state on single-brand re-installs. */
function peerBrandPresent() {
    const systemdUserDir = resolve(HOME, ".config/systemd/user");
    if (!existsSync(systemdUserDir))
        return undefined;
    const peerUnits = KNOWN_BRAND_HOSTNAMES
        .filter((h) => h !== BRAND.hostname)
        .flatMap((h) => [`${h}.service`, `${h}-edge.service`]);
    try {
        const files = new Set(readdirSync(systemdUserDir));
        return peerUnits.find((unit) => files.has(unit));
    }
    catch {
        return undefined;
    }
}
// ---------------------------------------------------------------------------
// Step 1: Stop services
// ---------------------------------------------------------------------------
function stopServices() {
    log("1", "Stopping services...");
    // darwin: bootout the LaunchAgent. bootout exits 0 on success,
    // 113 ("Unknown service") when the agent isn't loaded — both are
    // acceptable end-states for "service stopped". The plist removal in
    // removeSystemdService() finishes the cleanup.
    if (isDarwin()) {
        spawnSync("launchctl", ["bootout", `${gui()}/${launchdLabel()}`], { stdio: "pipe", timeout: 15_000 });
        console.log(`  Booted out ${launchdLabel()}`);
        return;
    }
    // Stop platform user service
    try {
        spawnSync("systemctl", ["--user", "stop", BRAND.serviceName.replace(".service", "")], { stdio: "pipe", timeout: 15_000 });
        console.log(`  Stopped ${BRAND.serviceName}`);
    }
    catch {
        console.log(`  ${BRAND.serviceName} not running`);
    }
    // the edge service owns the public port and VNC stack. Stop it
    // after the main brand service so the edge's ExecStopPost (vnc.sh stop)
    // runs cleanly. The edge unit is per-brand (`<hostname>-edge`).
    const edgeUnitShort = `${BRAND.hostname}-edge`;
    try {
        spawnSync("systemctl", ["--user", "stop", edgeUnitShort], { stdio: "pipe", timeout: 15_000 });
        console.log(`  Stopped ${edgeUnitShort}`);
    }
    catch {
        console.log(`  ${edgeUnitShort} not running`);
    }
    // Stop Neo4j — dedicated branded instance if this brand uses one, else shared.
    // Brand isolation: never stop `neo4j.service` when this brand runs
    // a dedicated `neo4j-<hostname>.service` — stopping the shared instance would
    // break every other brand on the device.
    const neo4jPortForStop = readNeo4jPortFromEnv();
    const neo4jService = neo4jPortForStop !== undefined && neo4jPortForStop !== 7687
        ? `neo4j-${BRAND.hostname}`
        : "neo4j";
    try {
        spawnSync("sudo", ["systemctl", "stop", neo4jService], { stdio: "pipe", timeout: 15_000 });
        console.log(`  Stopped ${neo4jService}`);
    }
    catch {
        console.log(`  ${neo4jService} not running`);
    }
    // Stop the cloudflared scope for this brand. The scope unit name mirrors
    // what resume-tunnel.sh derives: strip leading dot from configDir.
    // BRAND.configDir is ".maxy-code"; strip the dot → "maxy-code".
    // --no-block: uninstall does not wait for graceful shutdown of the tunnel.
    // Exit 5 (no such unit) = scope absent = success.
    const brand = BRAND.configDir.replace(/^\./, "");
    const scopeUnit = `cloudflared-${brand}.scope`;
    const stopResult = spawnSync("systemctl", ["--user", "stop", "--no-block", scopeUnit], { stdio: "pipe" });
    if (stopResult.status === 0) {
        console.log(`  Stopped ${scopeUnit}`);
    }
    else if (stopResult.status === 5) {
        // exit 5 = unit not found — scope already absent, which is expected
        console.log(`  ${scopeUnit} not running (scope absent)`);
    }
    else {
        // Any other non-zero exit means systemctl itself failed (e.g. DBus unavailable)
        console.log(`  systemctl stop ${scopeUnit} exited ${stopResult.status} — scope may still be running`);
    }
    // Brand isolation: the VNC stack and Ollama daemon are
    // device-wide singletons. Killing them during uninstall would interrupt
    // any peer brand still serving requests through them. Skip when a peer
    // is present.
    if (peerBrandPresent()) {
        console.log("  Peer brand present — leaving VNC/Chromium and Ollama running.");
        return;
    }
    // Kill VNC processes (Xtigervnc, websockify, Chromium)
    for (const proc of ["Xtigervnc", "websockify", "chromium"]) {
        try {
            spawnSync("pkill", ["-f", proc], { stdio: "pipe" });
        }
        catch { /* not running */ }
    }
    console.log("  Stopped VNC/browser processes");
    // Kill Ollama
    try {
        spawnSync("sudo", ["systemctl", "stop", "ollama"], { stdio: "pipe", timeout: 10_000 });
    }
    catch { /* not running */ }
    try {
        spawnSync("pkill", ["-f", "ollama"], { stdio: "pipe" });
    }
    catch { /* not running */ }
    console.log("  Stopped Ollama");
}
// ---------------------------------------------------------------------------
// Step 2: Delete Cloudflare tunnel
// ---------------------------------------------------------------------------
function deleteCloudflareTunnel() {
    log("2", "Deleting Cloudflare tunnel...");
    if (!commandExists("cloudflared")) {
        console.log("  cloudflared not installed — skipping tunnel deletion.");
        return;
    }
    // Read only this brand's state/cert (no cross-brand reads).
    const stateFile = join(CONFIG_DIR, "cloudflared/tunnel.state");
    const certFile = join(CONFIG_DIR, "cloudflared/cert.pem");
    // Brand isolation: read the tunnel id + domain from THIS brand's
    // state file and scope `cloudflared` invocations to THIS brand's cert via
    // --origincert. Without --origincert, cloudflared reads the device-wide
    // ~/.cloudflared/cert.pem and would list (and delete) every tunnel in
    // whichever Cloudflare account last logged in on this device.
    let tunnelId;
    let tunnelDomain;
    if (existsSync(stateFile)) {
        try {
            const state = JSON.parse(readFileSync(stateFile, "utf-8"));
            tunnelId = state.tunnelId ?? state.id;
            tunnelDomain = state.domain;
        }
        catch { /* corrupt state file */ }
    }
    if (!existsSync(certFile)) {
        console.log("  cert.pem missing — cannot delete tunnel from Cloudflare.");
        if (tunnelDomain) {
            console.log(`  The tunnel for ${tunnelDomain} may still exist on Cloudflare's edge.`);
            console.log("  Delete it manually at https://one.dash.cloudflare.com → Networks → Tunnels");
        }
        return;
    }
    if (!tunnelId) {
        console.log("  tunnel.state has no tunnel id — skipping delete.");
        console.log("  If a tunnel exists for this brand on Cloudflare's edge, delete it manually.");
        return;
    }
    console.log(`  Deleting tunnel id ${tunnelId} (${tunnelDomain ?? "no domain"})...`);
    const deleteResult = spawnSync("cloudflared", ["--origincert", certFile, "tunnel", "delete", "-f", tunnelId], {
        stdio: "pipe",
        encoding: "utf-8",
        timeout: 30_000,
    });
    if (deleteResult.status === 0) {
        console.log(`  Deleted tunnel ${tunnelId} and its DNS records.`);
    }
    else {
        console.log(`  Failed to delete tunnel ${tunnelId}: ${(deleteResult.stderr || "").trim()}`);
        if (tunnelDomain) {
            console.log(`  Delete it manually at https://one.dash.cloudflare.com → Networks → Tunnels`);
        }
    }
}
// ---------------------------------------------------------------------------
// Step 3: Data export
// ---------------------------------------------------------------------------
export function exportData(exportPath) {
    log("3", `Exporting data to ${exportPath}...`);
    mkdirSync(exportPath, { recursive: true });
    // Export Neo4j via neo4j-admin database dump (database is stopped at this point)
    if (commandExists("neo4j-admin")) {
        console.log("  Exporting Neo4j database...");
        try {
            shell("neo4j-admin", ["database", "dump", "neo4j", `--to-path=${exportPath}`], { sudo: true, timeout: 120_000 });
            console.log("  Neo4j database exported.");
        }
        catch (err) {
            console.log(`  Neo4j export failed: ${err instanceof Error ? err.message : String(err)}`);
            console.log("  Continuing without database export.");
        }
    }
    else {
        console.log("  neo4j-admin not found — skipping database export.");
    }
    // Copy account config
    const accountsDir = resolve(CONFIG_DIR, "accounts");
    if (existsSync(accountsDir)) {
        try {
            const dest = join(exportPath, "accounts");
            cpSync(accountsDir, dest, { recursive: true });
            console.log(`  Account config exported to ${dest}`);
        }
        catch (err) {
            console.log(`  Account config export failed: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    else {
        console.log(`  No account config found at ${accountsDir}`);
    }
    // Copy secrets (password, API key) so a fresh install could reuse them
    for (const file of [".neo4j-password", ".anthropic-api-key", ".admin-pin"]) {
        const src = resolve(CONFIG_DIR, file);
        if (existsSync(src)) {
            try {
                cpSync(src, join(exportPath, file));
                console.log(`  Exported ${file}`);
            }
            catch { /* permission error */ }
        }
    }
    console.log(`  Export complete: ${exportPath}`);
}
// ---------------------------------------------------------------------------
// Step 3b: Remove legacy tunnel shortcut symlinks
//
// Older installer versions created ~/setup-tunnel and ~/reset-tunnel (with
// the ".sh" suffix) as symlinks into INSTALL_DIR's cloudflare scripts
// directory. The wrapper scripts and their installer hook were retired in
// Task 288 (PTY-native cloudflared via Bash). This step removes any leftover
// symlinks from previous installs. Must run BEFORE removeAppDirs removes
// INSTALL_DIR so readlink-then-compare still works.
//
// Only unlink if the symlink's resolved target is inside this brand's
// INSTALL_DIR. A symlink pointing elsewhere came from a different brand install
// on the same home (unsupported but not destructive) — leave it alone.
// ---------------------------------------------------------------------------
const LEGACY_TUNNEL_SHORTCUTS = ["setup-tunnel", "reset-tunnel"].map((n) => `${n}.sh`);
function removeTunnelSymlinks() {
    log("3b", "Removing legacy tunnel shortcut symlinks...");
    for (const linkPath of LEGACY_TUNNEL_SHORTCUTS.map((name) => resolve(HOME, name))) {
        let lstat;
        try {
            lstat = lstatSync(linkPath);
        }
        catch (err) {
            if (err.code === "ENOENT") {
                console.log(`  ${linkPath} not found — skipping`);
                continue;
            }
            console.log(`  Failed to stat ${linkPath}: ${err.message}`);
            continue;
        }
        if (!lstat.isSymbolicLink()) {
            console.log(`  ${linkPath} is not a symlink — leaving untouched`);
            continue;
        }
        let resolvedTarget;
        try {
            const raw = readlinkSync(linkPath);
            resolvedTarget = resolve(dirname(linkPath), raw);
        }
        catch (err) {
            console.log(`  ${linkPath} readlink failed: ${err.message} — leaving untouched`);
            continue;
        }
        const insideInstallDir = resolvedTarget === INSTALL_DIR || resolvedTarget.startsWith(INSTALL_DIR + "/");
        if (!insideInstallDir) {
            console.log(`  ${linkPath} → ${resolvedTarget} (outside ${INSTALL_DIR}) — leaving untouched`);
            continue;
        }
        try {
            unlinkSync(linkPath);
            console.log(`  Unlinked ${linkPath}`);
        }
        catch (err) {
            console.log(`  Failed to unlink ${linkPath}: ${err.message}`);
        }
    }
}
// ---------------------------------------------------------------------------
// Step 4: Remove application directories
// ---------------------------------------------------------------------------
function removeAppDirs() {
    log("4", "Removing application directories...");
    // Brand isolation: ~/.cloudflare and ~/.cloudflared are
    // device-wide singletons (shared across every brand and used by
    // cloudflared's own runtime); per-brand tunnel state lives at
    // ~/{configDir}/cloudflared/ and is removed via CONFIG_DIR. ~/.claude
    // (Claude CLI OAuth state) and ~/.ollama (shared model cache) are also
    // device-wide — skip both when a peer brand is still installed.
    const peer = peerBrandPresent();
    const dirs = [
        { path: INSTALL_DIR, label: `~/${BRAND.installDir}` },
        { path: CONFIG_DIR, label: `~/${BRAND.configDir}` },
        ...(peer ? [] : [
            { path: resolve(HOME, ".claude"), label: "~/.claude" },
            { path: resolve(HOME, ".ollama"), label: "~/.ollama" },
        ]),
    ];
    if (peer) {
        console.log(`  Preserved ~/.claude (peer: ${peer})`);
        console.log(`  Preserved ~/.ollama (peer: ${peer})`);
    }
    for (const { path, label } of dirs) {
        if (existsSync(path)) {
            try {
                rmSync(path, { recursive: true, force: true });
                console.log(`  Removed ${label}`);
            }
            catch (err) {
                console.log(`  Failed to remove ${label}: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
        else {
            console.log(`  ${label} not found — skipping`);
        }
    }
}
// ---------------------------------------------------------------------------
// Step 5: Remove Neo4j data
// ---------------------------------------------------------------------------
function removeNeo4jData() {
    log("5", "Removing Neo4j data...");
    // Brand isolation: remove only THIS brand's Neo4j data. Shared
    // 7687 data is skipped entirely when a peer brand is present. Dedicated
    // branded instances live at /var/lib/neo4j-<hostname>/ and are always
    // this-brand-owned by construction.
    const envPort = readNeo4jPortFromEnv();
    const isDedicated = envPort !== undefined && envPort !== 7687;
    const dataDir = isDedicated ? `/var/lib/neo4j-${BRAND.hostname}` : "/var/lib/neo4j";
    if (!isDedicated && peerBrandPresent()) {
        console.log(`  Shared Neo4j instance on 7687 — peer brand present, skipping data wipe.`);
        return;
    }
    const paths = [`${dataDir}/data`, `${dataDir}/logs`];
    if (isDedicated) {
        // Also clean up the dedicated config dir and systemd unit — install-time
        // code creates both at /etc/neo4j-<hostname>/ and /etc/systemd/system/
        // neo4j-<hostname>.service. Shared instance owns neither.
        paths.push(`/etc/neo4j-${BRAND.hostname}`);
        paths.push(`/etc/systemd/system/neo4j-${BRAND.hostname}.service`);
    }
    for (const p of paths) {
        if (existsSync(p)) {
            try {
                shell("rm", ["-rf", p], { sudo: true });
                console.log(`  Removed ${p}`);
            }
            catch (err) {
                console.log(`  Failed to remove ${p}: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
    }
    if (isDedicated) {
        try {
            spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "pipe" });
        }
        catch { /* ignore */ }
    }
}
/** Read NEO4J_URI port from this brand's .env. Returns undefined when the
 *  file is missing or malformed — caller treats that as "assume shared 7687". */
function readNeo4jPortFromEnv() {
    const envPath = join(CONFIG_DIR, ".env");
    if (!existsSync(envPath))
        return undefined;
    try {
        const match = readFileSync(envPath, "utf-8").match(/^NEO4J_URI=bolt:\/\/localhost:(\d+)$/m);
        if (!match)
            return undefined;
        const port = parseInt(match[1], 10);
        return isNaN(port) ? undefined : port;
    }
    catch {
        return undefined;
    }
}
// ---------------------------------------------------------------------------
// Step 6: Purge system packages
// ---------------------------------------------------------------------------
function purgeSystemPackages() {
    log("6", "Removing system packages...");
    if (!isLinux()) {
        console.log("  Not Linux — skipping package removal.");
        return;
    }
    // Brand isolation: every package in the list below is shared
    // device-wide (neo4j, openjdk, tigervnc, websockify, novnc, cloudflared) —
    // purging them when a peer brand is still installed breaks the peer's
    // runtime. Leave packages in place; the operator can purge manually after
    // uninstalling the last brand on the device.
    if (peerBrandPresent()) {
        console.log("  Peer brand still installed — skipping shared package purge.");
        return;
    }
    const packages = [
        "neo4j",
        "openjdk-17-jre-headless",
        "openjdk-21-jre-headless",
        "tigervnc-standalone-server",
        "python3-websockify",
        "novnc",
        "cloudflared",
    ];
    // Check which are installed before removing
    const installed = packages.filter((pkg) => {
        const result = spawnSync("dpkg", ["-s", pkg], { stdio: "pipe" });
        return result.status === 0;
    });
    if (installed.length === 0) {
        console.log("  No Maxy-specific packages installed.");
        return;
    }
    console.log(`  Removing: ${installed.join(", ")}`);
    try {
        shell("apt-get", ["remove", "--purge", "-y", ...installed], { sudo: true, timeout: 120_000 });
        console.log("  Packages removed.");
    }
    catch (err) {
        console.log(`  Package removal failed: ${err instanceof Error ? err.message : String(err)}`);
        console.log(`  Try manually: sudo apt-get remove --purge -y ${installed.join(" ")}`);
    }
    // Autoremove unused dependencies
    try {
        shell("apt-get", ["autoremove", "-y"], { sudo: true, timeout: 60_000 });
    }
    catch { /* not critical */ }
}
// ---------------------------------------------------------------------------
// Step 7: Remove system config files
// ---------------------------------------------------------------------------
function removeSystemConfig() {
    log("7", "Removing system configuration...");
    if (!isLinux()) {
        console.log("  Not Linux — skipping.");
        return;
    }
    // Brand isolation: the Neo4j apt repo and GPG key are device-wide
    // prerequisites for installing/updating Neo4j packages — leaving them when a
    // peer brand is still installed is the correct posture. Avahi service and
    // NetworkManager config are brand-specific (filenames keyed on BRAND.hostname).
    const peerPresent = peerBrandPresent();
    const files = [
        { path: `/etc/avahi/services/${BRAND.hostname}.service`, label: "Avahi mDNS service" },
        { path: `/etc/NetworkManager/conf.d/${BRAND.hostname}-no-powersave.conf`, label: "WiFi power save config" },
        ...(peerPresent ? [] : [
            { path: "/etc/apt/sources.list.d/neo4j.list", label: "Neo4j apt repository" },
            { path: "/usr/share/keyrings/neo4j.gpg", label: "Neo4j GPG key" },
        ]),
    ];
    if (peerPresent) {
        console.log("  Peer brand still installed — leaving Neo4j apt repo + GPG key in place.");
    }
    for (const { path, label } of files) {
        if (existsSync(path)) {
            try {
                shell("rm", ["-f", path], { sudo: true });
                console.log(`  Removed ${label}`);
            }
            catch (err) {
                console.log(`  Failed to remove ${label}: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
    }
    // Restart avahi to pick up the removed service file
    try {
        spawnSync("sudo", ["systemctl", "restart", "avahi-daemon"], { stdio: "pipe", timeout: 10_000 });
    }
    catch { /* not critical */ }
}
// ---------------------------------------------------------------------------
// Step 8: Remove systemd service
// ---------------------------------------------------------------------------
function removeSystemdService() {
    log("8", "Removing systemd service...");
    // darwin: bootout (idempotent — already done in stopServices,
    // re-running is a no-op exit 113) and delete the plist. The wrapper
    // shell script lives in CONFIG_DIR which step 4 (removeAppDirs) wipes,
    // so no separate cleanup is needed.
    if (isDarwin()) {
        spawnSync("launchctl", ["bootout", `${gui()}/${launchdLabel()}`], { stdio: "pipe", timeout: 15_000 });
        const plist = plistPath();
        if (existsSync(plist)) {
            try {
                rmSync(plist);
                console.log(`  Removed ${plist}`);
            }
            catch (err) {
                console.log(`  Failed to remove ${plist}: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
        else {
            console.log(`  ${plist} not found — skipping`);
        }
        return;
    }
    // Disable the service
    const svcUnit = BRAND.serviceName.replace(".service", "");
    try {
        spawnSync("systemctl", ["--user", "disable", svcUnit], { stdio: "pipe" });
    }
    catch { /* already disabled or missing */ }
    // Remove service file
    const serviceFile = resolve(HOME, `.config/systemd/user/${BRAND.serviceName}`);
    if (existsSync(serviceFile)) {
        try {
            rmSync(serviceFile);
            console.log(`  Removed ${BRAND.serviceName}`);
        }
        catch (err) {
            console.log(`  Failed to remove service file: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    // remove this brand's per-brand edge unit alongside the main brand unit.
    const thisBrandEdge = `${BRAND.hostname}-edge`;
    try {
        spawnSync("systemctl", ["--user", "disable", thisBrandEdge], { stdio: "pipe" });
    }
    catch { /* ignore */ }
    const edgeUnitFile = resolve(HOME, `.config/systemd/user/${thisBrandEdge}.service`);
    if (existsSync(edgeUnitFile)) {
        try {
            rmSync(edgeUnitFile);
            console.log(`  Removed ${thisBrandEdge}.service`);
        }
        catch (err) {
            console.log(`  Failed to remove ${thisBrandEdge}.service: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    // Reload daemon
    try {
        spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
    }
    catch { /* not critical */ }
    // Disable linger
    try {
        const user = execFileSync("whoami", [], { encoding: "utf-8" }).trim();
        spawnSync("sudo", ["loginctl", "disable-linger", user], { stdio: "pipe" });
        console.log("  Disabled user lingering");
    }
    catch { /* not critical */ }
}
// ---------------------------------------------------------------------------
// Step 9: Remove Ollama
// ---------------------------------------------------------------------------
function removeOllama() {
    log("9", "Removing Ollama...");
    // Brand isolation: the Ollama binary and systemd service are
    // device-wide singletons — every brand on this device shares one Ollama
    // process. Leave both in place when a peer brand is still installed;
    // uninstalling Ollama would break the peer brand's embedding pipeline.
    if (peerBrandPresent()) {
        console.log("  Peer brand still installed — leaving Ollama binary and service in place.");
        return;
    }
    // Stop and disable the systemd service if it exists
    try {
        spawnSync("sudo", ["systemctl", "stop", "ollama"], { stdio: "pipe", timeout: 10_000 });
        spawnSync("sudo", ["systemctl", "disable", "ollama"], { stdio: "pipe" });
    }
    catch { /* not running or not installed as service */ }
    // Remove the systemd service file
    const serviceFile = "/etc/systemd/system/ollama.service";
    if (existsSync(serviceFile)) {
        try {
            shell("rm", ["-f", serviceFile], { sudo: true });
            spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "pipe" });
            console.log("  Removed Ollama systemd service");
        }
        catch { /* not critical */ }
    }
    // Remove the binary
    const binary = "/usr/local/bin/ollama";
    if (existsSync(binary)) {
        try {
            shell("rm", ["-f", binary], { sudo: true });
            console.log("  Removed Ollama binary");
        }
        catch (err) {
            console.log(`  Failed to remove Ollama binary: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    else {
        console.log("  Ollama binary not found — already removed");
    }
    // Models directory (~/.ollama/) is removed in step 4 (removeAppDirs)
}
// ---------------------------------------------------------------------------
// Samba teardown. Symmetric to provisionSamba in index.ts.
//
// Peer-brand discipline: smb.conf, the smbpasswd entry, and the samba apt
// package are device-wide singletons shared by every brand that ships an
// SMB share. Drop only this brand's stanza on every uninstall; stop+disable
// units / smbpasswd -x admin / apt-purge samba run only when no brand stanza
// remains in smb.conf AND no peer brand is detected. This mirrors the
// Neo4j / cloudflared / Ollama treatment in steps 5–9 above.
// ---------------------------------------------------------------------------
function removeSamba() {
    log("10", "Removing Samba share...");
    if (!isLinux()) {
        console.log("  Not Linux — skipping.");
        return;
    }
    const SMB_CONF = "/etc/samba/smb.conf";
    if (!existsSync(SMB_CONF)) {
        console.log("  /etc/samba/smb.conf not present — nothing to remove.");
        return;
    }
    let existing = "";
    try {
        const cat = spawnSync("sudo", ["cat", SMB_CONF], { encoding: "utf-8", stdio: "pipe", timeout: 5_000 });
        if (cat.status === 0)
            existing = cat.stdout ?? "";
    }
    catch {
        console.log("  Could not read smb.conf — skipping stanza removal.");
        return;
    }
    const stripped = removeBrandStanza({ existing, brand: BRAND.hostname });
    if (stripped !== existing) {
        const tee = spawnSync("sudo", ["tee", SMB_CONF], { input: stripped, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10_000 });
        if (tee.status === 0) {
            console.log(`  Removed [${BRAND.hostname}] stanza from ${SMB_CONF}`);
        }
        else {
            console.log(`  Failed to write stripped smb.conf: ${(tee.stderr ?? "").trim()}`);
        }
    }
    else {
        console.log(`  No [${BRAND.hostname}] stanza found in ${SMB_CONF}`);
    }
    // Reload smbd so the brand share disappears from the running config without
    // dropping connections to peer brand shares. `reload` is best-effort: the
    // operator has already torn down the brand; failing to reload would leave a
    // dangling share name but nothing routable to its (now-deleted) sharePath.
    spawnSync("sudo", ["systemctl", "reload", "smbd"], { stdio: "pipe", timeout: 10_000 });
    // Per-brand cleanup stops here when a peer brand stanza still references the
    // share — disabling smbd or purging samba would take the peer's share down.
    const peer = peerBrandPresent();
    if (hasAnyBrandStanza(stripped) || peer) {
        const reason = peer ? `peer brand present (${peer})` : "other brand stanza remains";
        console.log(`  Leaving smbd/nmbd + samba package in place — ${reason}`);
        return;
    }
    // No brand stanza, no peer brand — full device-wide teardown.
    try {
        spawnSync("sudo", ["systemctl", "disable", "--now", "smbd", "nmbd"], { stdio: "pipe", timeout: 30_000 });
        console.log("  Stopped + disabled smbd, nmbd");
    }
    catch (err) {
        console.log(`  Failed to disable smbd/nmbd: ${err instanceof Error ? err.message : String(err)}`);
    }
    // Drop the install-owner's smbpasswd entry. `smbpasswd -x` exits 0 on
    // success, non-zero if the user isn't in the passdb — both are acceptable
    // end-states. Owner is read from the persisted file the installer wrote
    // so the cleanup targets the same Unix user the provisioning step keyed
    // on (Task 534): `admin` on a Pi/Hetzner box, `neo` (etc.) on a laptop.
    // Missing file falls back to `admin` because an install that ran before
    // the install-owner persistence existed only ever provisioned `admin`.
    const installOwnerFile = join(CONFIG_DIR, ".install-owner");
    let installOwner = "admin";
    if (existsSync(installOwnerFile)) {
        try {
            const raw = readFileSync(installOwnerFile, "utf-8").trim();
            if (raw.length > 0)
                installOwner = raw;
        }
        catch { /* keep default */ }
    }
    spawnSync("sudo", ["smbpasswd", "-x", installOwner], { stdio: "pipe", timeout: 10_000 });
    console.log(`  smbpasswd -x ${installOwner}`);
    try {
        shell("apt-get", ["remove", "--purge", "-y", "samba", "samba-common-bin"], { sudo: true, timeout: 120_000 });
        console.log("  Purged samba package.");
    }
    catch (err) {
        console.log(`  apt-get purge samba failed: ${err instanceof Error ? err.message : String(err)}`);
        console.log("  Run manually: sudo apt-get remove --purge -y samba samba-common-bin");
    }
    // Drop the smbpasswd sudoers grant once no brand stanza references it.
    spawnSync("sudo", ["rm", "-f", "/etc/sudoers.d/maxy-samba"], { stdio: "pipe", timeout: 5_000 });
    console.log("  Removed /etc/sudoers.d/maxy-samba");
}
// ---------------------------------------------------------------------------
// Step 11: Restore hostname
// ---------------------------------------------------------------------------
function restoreHostname() {
    log("11", "Restoring hostname...");
    if (!isLinux()) {
        console.log("  Not Linux — skipping.");
        return;
    }
    try {
        const hostname = execFileSync("hostname", [], { encoding: "utf-8" }).trim();
        if (hostname !== BRAND.hostname) {
            console.log(`  Hostname is '${hostname}' — not '${BRAND.hostname}', nothing to restore.`);
            return;
        }
        // The installer doesn't save the original hostname, so we use a sensible default
        const fallback = "raspberrypi";
        shell("hostnamectl", ["set-hostname", fallback], { sudo: true });
        shell("sed", ["-i", `s/127\\.0\\.1\\.1.*$/127.0.1.1\\t${fallback}/`, "/etc/hosts"], { sudo: true });
        console.log(`  Hostname restored to '${fallback}'`);
    }
    catch (err) {
        console.log(`  Hostname restore failed: ${err instanceof Error ? err.message : String(err)}`);
    }
}
// ---------------------------------------------------------------------------
// Summary display
// ---------------------------------------------------------------------------
function showRemovalSummary() {
    const peer = peerBrandPresent();
    console.log("");
    console.log("The following will be removed:");
    console.log("");
    console.log(`  Services:   ${BRAND.serviceName}${peer ? "" : ", Neo4j, cloudflared, VNC, Ollama"}`);
    console.log(`  App dirs:   ~/${BRAND.installDir}/, ~/${BRAND.configDir}/${peer ? "" : ", ~/.claude/, ~/.ollama/"}`);
    console.log(`  Database:   Neo4j data + logs for ${BRAND.productName}'s instance`);
    if (!peer) {
        console.log("  Packages:   neo4j, openjdk (17 or 21), tigervnc, websockify, novnc, cloudflared");
        console.log("  Config:     avahi service, WiFi power save, apt repos, GPG keys");
        console.log("  Ollama:     binary and models");
    }
    else {
        console.log("  Config:     avahi service, WiFi power save (apt repo + GPG key left in place)");
    }
    console.log(`  Systemd:    ${BRAND.serviceName} unit file, user lingering`);
    console.log(`  Hostname:   restored from '${BRAND.hostname}' to 'raspberrypi'`);
    console.log(`  Cloudflare: ${BRAND.productName}'s tunnel and DNS records deleted from Cloudflare's edge`);
    console.log("");
    if (peer) {
        console.log("  Peer brand still installed — device-wide singletons (Ollama, cloudflared");
        console.log("  binary, apt packages, ~/.claude, ~/.ollama) are left in place.");
    }
    else {
        console.log("  Node.js and shared system utilities (curl, git, etc.) are NOT removed.");
    }
    console.log("");
}
// ---------------------------------------------------------------------------
// Confirmation prompt
// ---------------------------------------------------------------------------
async function confirmUninstall() {
    const rl = createInterface({ input: process.stdin, output: process.stdout });
    return new Promise((resolve) => {
        rl.question("Type UNINSTALL to confirm: ", (answer) => {
            rl.close();
            resolve(answer.trim() === "UNINSTALL");
        });
    });
}
// ---------------------------------------------------------------------------
// Main entry point
// ---------------------------------------------------------------------------
export async function runUninstall(options) {
    const PKG_VERSION = JSON.parse(readFileSync(resolve(import.meta.dirname, "../package.json"), "utf-8")).version;
    console.log("================================================================");
    console.log(`  ${BRAND.productName} Uninstall  (create-maxy v${PKG_VERSION})`);
    console.log("================================================================");
    console.log(`  Log: ${LOG_FILE}`);
    console.log("");
    // Check if Maxy is installed
    if (!isMaxyInstalled()) {
        console.error(`${BRAND.productName} is not installed (~/${BRAND.installDir} not found). Nothing to uninstall.`);
        process.exit(1);
    }
    // Show what will be removed
    showRemovalSummary();
    // Confirmation gate
    if (!options.skipConfirm) {
        console.log("  This action is permanent. All data will be lost.");
        console.log("");
        const confirmed = await confirmUninstall();
        if (!confirmed) {
            console.log("");
            console.log("Uninstall cancelled.");
            process.exit(0);
        }
    }
    console.log("");
    // Run each step, collecting failures
    const failures = [];
    const steps = [
        { name: "Stop services", fn: stopServices },
        { name: "Delete Cloudflare tunnel", fn: deleteCloudflareTunnel },
        ...(options.exportPath
            ? [{ name: "Export data", fn: () => exportData(options.exportPath) }]
            : []),
        { name: "Remove tunnel script symlinks", fn: removeTunnelSymlinks },
        { name: "Remove application directories", fn: removeAppDirs },
        { name: "Remove Neo4j data", fn: removeNeo4jData },
        { name: "Purge system packages", fn: purgeSystemPackages },
        { name: "Remove system configuration", fn: removeSystemConfig },
        { name: "Remove systemd service", fn: removeSystemdService },
        { name: "Remove Ollama", fn: removeOllama },
        { name: "Remove Samba share", fn: removeSamba },
        { name: "Restore hostname", fn: restoreHostname },
    ];
    for (const step of steps) {
        try {
            step.fn();
        }
        catch (err) {
            const msg = `${step.name}: ${err instanceof Error ? err.message : String(err)}`;
            failures.push(msg);
            console.error(`  FAILED: ${msg}`);
        }
        console.log("");
    }
    // Final summary
    console.log("================================================================");
    if (failures.length === 0) {
        console.log("");
        console.log(`  ${BRAND.productName} has been completely removed.`);
        console.log("");
        console.log("  To reinstall: npx -y @rubytech/create-maxy");
    }
    else {
        console.log("");
        console.log(`  Uninstall completed with ${failures.length} issue(s):`);
        console.log("");
        for (const f of failures) {
            console.log(`    - ${f}`);
        }
        console.log("");
        console.log("  Re-run the uninstall to retry, or fix manually.");
    }
    console.log("");
    console.log(`  Log: ${LOG_FILE}`);
    console.log("================================================================");
    if (failures.length > 0) {
        process.exit(1);
    }
}
