import { exec, execFile } from "node:child_process";
import { promisify } from "node:util";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { z } from "zod";
import { tool } from "@anthropic-ai/claude-agent-sdk";
import { getCurrentVersion, getNpmVersionByTag, compareSemver, fetchChangelogFromNpm, getInstallationInfo, } from "../utils/version-check.js";
import { detectProcessManager, isElectronChild, scheduleRestart } from "../utils/restart.js";
import { runGracefulShutdown } from "../agent/state.js";
import { writePendingUpgrade } from "../config/index.js";
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
const PACKAGE_NAME = "visionclaw";
const RESTART_DELAY = 10_000;
const EXEC_OPTIONS = process.platform === "win32" ? { windowsHide: true } : {};
const EXTERNAL_AGENT_DIR = path.join(os.homedir(), ".visionclaw", "agent");
const EXTERNAL_BUNDLE = path.join(EXTERNAL_AGENT_DIR, "bundle.cjs");
async function checkPnpmCandidate(command, source) {
    try {
        const { stdout } = await execFileAsync(command, ["--version"], EXEC_OPTIONS);
        const version = stdout.trim();
        if (!version)
            return null;
        return { command, version, source };
    }
    catch {
        return null;
    }
}
function homebrewCellarPnpmCandidates() {
    const candidates = [];
    for (const cellar of ["/opt/homebrew/Cellar", "/usr/local/Cellar"]) {
        let packages;
        try {
            packages = fs.readdirSync(cellar).filter((entry) => entry === "pnpm" || entry.startsWith("pnpm@"));
        }
        catch {
            continue;
        }
        for (const pkg of packages) {
            const pkgDir = path.join(cellar, pkg);
            let versions;
            try {
                versions = fs.readdirSync(pkgDir);
            }
            catch {
                continue;
            }
            for (const version of versions) {
                candidates.push(path.join(pkgDir, version, "bin", "pnpm"));
            }
        }
    }
    return candidates;
}
function uniqueCandidates(candidates) {
    return [...new Set(candidates.filter(Boolean))];
}
export async function resolvePnpmCommand() {
    const explicitCandidates = [
        { command: "pnpm", source: "path" },
        ...(process.env.PNPM_HOME ? [{ command: path.join(process.env.PNPM_HOME, "pnpm"), source: "pnpm_home" }] : []),
        { command: path.join(os.homedir(), "Library", "pnpm", "pnpm"), source: "home_pnpm" },
        { command: "/opt/homebrew/bin/pnpm", source: "homebrew_bin" },
        { command: "/usr/local/bin/pnpm", source: "homebrew_bin" },
    ];
    const candidates = [
        ...explicitCandidates,
        ...homebrewCellarPnpmCandidates().map((command) => ({ command, source: "homebrew_cellar" })),
    ];
    for (const candidate of uniqueCandidates(candidates.map(({ command }) => command))) {
        const source = candidates.find((entry) => entry.command === candidate)?.source ?? "path";
        const resolved = await checkPnpmCandidate(candidate, source);
        if (resolved)
            return resolved;
    }
    throw new Error("pnpm is required for upgrades but was not found in PATH or common installation locations");
}
/**
 * Download the new agent bundle from npm for Electron-based installations.
 * Instead of `pnpm add -g`, we fetch the tarball, extract bundle.cjs, and
 * replace the external copy at ~/.visionclaw/agent/bundle.cjs.
 */
async function downloadBundleFromNpm(version) {
    fs.mkdirSync(EXTERNAL_AGENT_DIR, { recursive: true });
    const backupPath = `${EXTERNAL_BUNDLE}.bak`;
    if (fs.existsSync(EXTERNAL_BUNDLE)) {
        fs.copyFileSync(EXTERNAL_BUNDLE, backupPath);
    }
    // Download the tarball once to a temp file so we can extract multiple
    // pieces from it without re-downloading.
    const tarballUrl = await getNpmTarballUrl(version);
    const tarballPath = path.join(os.tmpdir(), `visionclaw-${version}-${Date.now()}.tgz`);
    try {
        const { stdout: downloadLog } = await execAsync(`curl -sL -o "${tarballPath}" "${tarballUrl}"`, { ...EXEC_OPTIONS, timeout: 120_000 });
        // Extract bundle.cjs from dist-agent/
        await execAsync(`tar -xzf "${tarballPath}" --strip-components=1 -C "${EXTERNAL_AGENT_DIR}" package/dist-agent/bundle.cjs`, { ...EXEC_OPTIONS, timeout: 60_000 });
        // If npm ships the bundle as dist-agent/bundle.cjs inside package/, move it
        const nestedBundle = path.join(EXTERNAL_AGENT_DIR, "dist-agent", "bundle.cjs");
        if (fs.existsSync(nestedBundle)) {
            fs.renameSync(nestedBundle, EXTERNAL_BUNDLE);
            try {
                fs.rmSync(path.join(EXTERNAL_AGENT_DIR, "dist-agent"), { recursive: true });
            }
            catch { /* ok */ }
        }
        if (!fs.existsSync(EXTERNAL_BUNDLE)) {
            throw new Error("bundle.cjs not found after extraction");
        }
        // Also extract supporting directories whose contents are resolved via
        // getPackageRoot() at runtime (e.g. assets/visionclaw_pricing.jpeg,
        // dist/tools/helpers/*.swift, dist/builtin-skills/, scripts/).
        // getPackageRoot() resolves to ~/.visionclaw/ when running from the
        // external bundle, so these dirs must live there.
        // Best-effort: don't let a supporting-dir failure roll back the bundle.
        try {
            await extractSupportingDirsFromTarball(tarballPath);
        }
        catch {
            // Individual dirs already have their own try/catch, so this only
            // catches unexpected errors (e.g. VISIONCLAW_HOME mkdirSync fails).
        }
        if (fs.existsSync(backupPath)) {
            try {
                fs.unlinkSync(backupPath);
            }
            catch { /* ok */ }
        }
        return downloadLog.trim();
    }
    catch (err) {
        if (fs.existsSync(backupPath)) {
            fs.renameSync(backupPath, EXTERNAL_BUNDLE);
        }
        throw err;
    }
    finally {
        try {
            fs.unlinkSync(tarballPath);
        }
        catch { /* ok */ }
    }
}
/**
 * Extract supporting directories from a local npm tarball into
 * ~/.visionclaw/ so that getPackageRoot()-relative paths resolve correctly
 * when running from the external bundle.
 */
async function extractSupportingDirsFromTarball(tarballPath) {
    const VISIONCLAW_HOME = path.join(os.homedir(), ".visionclaw");
    fs.mkdirSync(VISIONCLAW_HOME, { recursive: true });
    // Paths inside the tarball (under package/) that runtime code resolves via
    // getPackageRoot(). Must match what's listed in package.json "files" field.
    // Tar archives always use forward slashes internally — do NOT use path.join
    // here, which would produce backslashes on Windows and fail to match.
    const archivePaths = [
        "assets",
        "scripts",
        "dist/tools",
        "dist/builtin-skills",
    ];
    for (const p of archivePaths) {
        try {
            await execAsync(`tar -xzf "${tarballPath}" --strip-components=1 -C "${VISIONCLAW_HOME}" "package/${p}"`, { ...EXEC_OPTIONS, timeout: 60_000 });
        }
        catch {
            // The directory may not exist in the tarball (e.g. older versions);
            // skip silently.
        }
    }
}
/**
 * Run pnpm/npm install in EXTERNAL_AGENT_DIR to ensure all transitive
 * dependencies (playwright, sqlite-vec, etc.) are available after a
 * bundle-only self-upgrade.
 */
async function ensureDepsInstalled() {
    const hostPkg = path.join(EXTERNAL_AGENT_DIR, "package.json");
    if (!fs.existsSync(hostPkg))
        return; // no host package.json → nothing to install
    // Ensure pnpm hoists all transitive deps (e.g. playwright, sqlite-vec)
    // so they are resolvable from bundle.cjs at the top level.
    const npmrcPath = path.join(EXTERNAL_AGENT_DIR, ".npmrc");
    if (!fs.existsSync(npmrcPath)) {
        fs.writeFileSync(npmrcPath, "shamefully-hoist=true\n");
    }
    // Prefer pnpm, fall back to npm
    for (const pm of ["pnpm", "npm"]) {
        try {
            await execAsync(`which ${pm}`, EXEC_OPTIONS);
            await execAsync(`${pm} install`, {
                ...EXEC_OPTIONS,
                cwd: EXTERNAL_AGENT_DIR,
                timeout: 120_000,
            });
            return;
        }
        catch {
            // try next
        }
    }
}
async function getNpmTarballUrl(version) {
    const { stdout } = await execAsync(`npm view ${PACKAGE_NAME}@${version} dist.tarball`, EXEC_OPTIONS);
    const url = stdout.trim();
    if (!url.startsWith("http")) {
        throw new Error(`Invalid tarball URL from npm: ${url}`);
    }
    return url;
}
/**
 * Core upgrade logic shared by the MCP upgrade tool and the remote command handler.
 *
 * Checks npm for the latest version, installs it, writes the pending-upgrade
 * marker, and schedules a restart when a process manager is available.
 *
 * Throws on unexpected errors (callers should catch).
 */
export async function performUpgrade(options) {
    const customTag = options?.tag;
    const beta = !customTag && (options?.beta ?? false);
    const tag = customTag ?? (beta ? "beta" : "latest");
    const tagLabel = tag !== "latest" ? ` (${tag})` : "";
    const currentVersion = getCurrentVersion();
    const installation = getInstallationInfo();
    let targetVersion;
    try {
        targetVersion = await getNpmVersionByTag(tag);
    }
    catch {
        return { ok: false, message: `Could not check for updates. Failed to query npm registry (tag: ${tag}).` };
    }
    if (!options?.force && compareSemver(targetVersion, currentVersion) <= 0) {
        return { ok: true, message: `Already up to date. Current version: ${currentVersion} (checked ${tag} tag: ${targetVersion})` };
    }
    const changelog = await fetchChangelogFromNpm(targetVersion, currentVersion);
    const changelogSection = changelog
        ? `\n\nChanges in ${targetVersion}:\n${changelog}`
        : "";
    // ── Electron mode: download bundle to external path ──
    if (isElectronChild()) {
        const installLog = await downloadBundleFromNpm(targetVersion);
        // Ensure external dependencies (playwright, sqlite-vec, etc.) are installed.
        // The host package.json declares visionclaw as a dependency; running
        // pnpm install ensures all transitive deps are present in node_modules.
        await ensureDepsInstalled();
        writePendingUpgrade({
            fromVersion: currentVersion,
            toVersion: targetVersion,
            upgradedAt: new Date().toISOString(),
        });
        scheduleRestart(RESTART_DELAY, runGracefulShutdown, { pm2Version: targetVersion });
        return {
            ok: true,
            message: [
                `Downloaded ${PACKAGE_NAME} ${targetVersion} bundle to ${EXTERNAL_AGENT_DIR}.${tagLabel}`,
                installLog,
                changelogSection,
                `Restarting via Electron in ${RESTART_DELAY / 1000} seconds...`,
            ]
                .filter(Boolean)
                .join("\n"),
        };
    }
    // ── Standard mode: pnpm global install ──
    const pnpm = await resolvePnpmCommand();
    const { stdout: installOut, stderr: installErr } = await execFileAsync(pnpm.command, ["add", "-g", `${PACKAGE_NAME}@${tag}`], EXEC_OPTIONS);
    const installLog = [
        `Using pnpm ${pnpm.version} from ${pnpm.command} (${pnpm.source})`,
        (installOut + installErr).trim(),
    ].filter(Boolean).join("\n");
    if (installation.isLocalCheckout) {
        return {
            ok: true,
            message: [
                `Installed ${PACKAGE_NAME} ${targetVersion} globally via pnpm.${tagLabel}`,
                installLog,
                changelogSection,
                `This VisionClaw instance is running from a local checkout at ${installation.packageRoot}, so its running version remains ${currentVersion} until that checkout is updated and restarted.`,
                "A restart was not scheduled because it would just relaunch the same local checkout.",
            ]
                .filter(Boolean)
                .join("\n"),
        };
    }
    const procManager = detectProcessManager();
    if (procManager) {
        writePendingUpgrade({
            fromVersion: currentVersion,
            toVersion: targetVersion,
            upgradedAt: new Date().toISOString(),
        });
        scheduleRestart(RESTART_DELAY, runGracefulShutdown, { pm2Version: targetVersion });
        return {
            ok: true,
            message: [
                `Installed ${targetVersion} via pnpm; the current process is still running ${currentVersion}.${tagLabel}`,
                installLog,
                changelogSection,
                `Restarting via ${procManager} in ${RESTART_DELAY / 1000} seconds...`,
            ]
                .filter(Boolean)
                .join("\n"),
        };
    }
    return {
        ok: true,
        message: [
            `Installed ${targetVersion} via pnpm; the current process is still running ${currentVersion}.${tagLabel}`,
            installLog,
            changelogSection,
            "Please restart VisionClaw manually to run the new version.",
            "Tip: run VisionClaw with `visionclaw daemon start` to enable automatic restarts after upgrades.",
        ]
            .filter(Boolean)
            .join("\n"),
    };
}
// ---------------------------------------------------------------------------
// Check-only helper (used by the MCP tool)
// ---------------------------------------------------------------------------
async function checkForUpgrade(options) {
    const customTag = options?.tag;
    const beta = !customTag && (options?.beta ?? false);
    const tag = customTag ?? (beta ? "beta" : "latest");
    const tagLabel = tag !== "latest" ? ` (${tag})` : "";
    const currentVersion = getCurrentVersion();
    let targetVersion;
    try {
        targetVersion = await getNpmVersionByTag(tag);
    }
    catch {
        return { ok: false, message: `Could not check for updates. Failed to query npm registry (tag: ${tag}).` };
    }
    if (compareSemver(targetVersion, currentVersion) <= 0) {
        return { ok: true, message: `Already up to date. Current version: ${currentVersion} (checked ${tag} tag: ${targetVersion})` };
    }
    const changelog = await fetchChangelogFromNpm(targetVersion, currentVersion);
    const changelogSection = changelog
        ? `\n\nChanges in ${targetVersion}:\n${changelog}`
        : "";
    return { ok: true, message: `Update available: ${currentVersion} → ${targetVersion}${tagLabel}${changelogSection}` };
}
// ---------------------------------------------------------------------------
// MCP Tool definition
// ---------------------------------------------------------------------------
export const upgradeTool = tool("upgrade", "Check for new versions of VisionClaw and upgrade if available. Can also perform a pure restart without upgrading (restart_only=true).", {
    check_only: z
        .boolean()
        .optional()
        .describe("If true, only check for updates without installing"),
    restart_only: z
        .boolean()
        .optional()
        .describe("If true, skip version check and immediately restart the process"),
    tag: z
        .string()
        .optional()
        .describe("Custom npm dist-tag to install from (e.g. 'beta', 'dev-feat-rdp'). Defaults to 'latest'."),
}, async (args) => {
    try {
        if (args.restart_only) {
            const pm = detectProcessManager();
            if (!pm) {
                return {
                    content: [{ type: "text", text: "Cannot restart: no process manager detected (pm2, systemd, or Electron). Please restart manually." }],
                    isError: true,
                };
            }
            scheduleRestart(RESTART_DELAY, runGracefulShutdown);
            return {
                content: [{ type: "text", text: `Restarting via ${pm} in ${RESTART_DELAY / 1000} seconds...` }],
            };
        }
        const upgradeOpts = {};
        if (args.tag) {
            upgradeOpts.tag = args.tag;
        }
        const result = args.check_only
            ? await checkForUpgrade(upgradeOpts)
            : await performUpgrade(upgradeOpts);
        return {
            content: [{ type: "text", text: result.message }],
            ...(result.ok ? {} : { isError: true }),
        };
    }
    catch (err) {
        return {
            content: [{ type: "text", text: `Upgrade failed: ${err instanceof Error ? err.message : String(err)}` }],
            isError: true,
        };
    }
});
//# sourceMappingURL=upgrade.js.map