import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { runCommandWithTimeout } from "../process/exec.js";
import { compareSemverStrings } from "./update-check.js";
import { DEV_BRANCH, isBetaTag, isStableTag } from "./update-channels.js";
import { detectGlobalInstallManagerForRoot, globalInstallArgs } from "./update-global.js";
import { trimLogTail } from "./restart-sentinel.js";
const DEFAULT_TIMEOUT_MS = 20 * 60_000;
const MAX_LOG_CHARS = 8000;
const PREFLIGHT_MAX_COMMITS = 10;
const START_DIRS = ["cwd", "argv1", "process"];
function normalizeDir(value) {
    if (!value)
        return null;
    const trimmed = value.trim();
    if (!trimmed)
        return null;
    return path.resolve(trimmed);
}
function resolveNodeModulesBinPackageRoot(argv1) {
    const normalized = path.resolve(argv1);
    const parts = normalized.split(path.sep);
    const binIndex = parts.lastIndexOf(".bin");
    if (binIndex <= 0)
        return null;
    if (parts[binIndex - 1] !== "node_modules")
        return null;
    const binName = path.basename(normalized);
    const nodeModulesDir = parts.slice(0, binIndex).join(path.sep);
    return path.join(nodeModulesDir, binName);
}
function buildStartDirs(opts) {
    const dirs = [];
    const cwd = normalizeDir(opts.cwd);
    if (cwd)
        dirs.push(cwd);
    const argv1 = normalizeDir(opts.argv1);
    if (argv1) {
        dirs.push(path.dirname(argv1));
        const packageRoot = resolveNodeModulesBinPackageRoot(argv1);
        if (packageRoot)
            dirs.push(packageRoot);
    }
    const proc = normalizeDir(process.cwd());
    if (proc)
        dirs.push(proc);
    return Array.from(new Set(dirs));
}
async function readPackageVersion(root) {
    try {
        const raw = await fs.readFile(path.join(root, "package.json"), "utf-8");
        const parsed = JSON.parse(raw);
        return typeof parsed?.version === "string" ? parsed.version : null;
    }
    catch {
        return null;
    }
}
async function readBranchName(runCommand, root, timeoutMs) {
    const res = await runCommand(["git", "-C", root, "rev-parse", "--abbrev-ref", "HEAD"], {
        timeoutMs,
    }).catch(() => null);
    if (!res || res.code !== 0)
        return null;
    const branch = res.stdout.trim();
    return branch || null;
}
async function listGitTags(runCommand, root, timeoutMs, pattern = "v*") {
    const res = await runCommand(["git", "-C", root, "tag", "--list", pattern, "--sort=-v:refname"], {
        timeoutMs,
    }).catch(() => null);
    if (!res || res.code !== 0)
        return [];
    return res.stdout
        .split("\n")
        .map((line) => line.trim())
        .filter(Boolean);
}
async function resolveChannelTag(runCommand, root, timeoutMs, channel) {
    const tags = await listGitTags(runCommand, root, timeoutMs);
    if (channel === "beta") {
        const betaTag = tags.find((tag) => isBetaTag(tag)) ?? null;
        const stableTag = tags.find((tag) => isStableTag(tag)) ?? null;
        if (!betaTag)
            return stableTag;
        if (!stableTag)
            return betaTag;
        const cmp = compareSemverStrings(betaTag, stableTag);
        if (cmp != null && cmp < 0)
            return stableTag;
        return betaTag;
    }
    return tags.find((tag) => isStableTag(tag)) ?? null;
}
async function resolveGitRoot(runCommand, candidates, timeoutMs) {
    for (const dir of candidates) {
        const res = await runCommand(["git", "-C", dir, "rev-parse", "--show-toplevel"], {
            timeoutMs,
        });
        if (res.code === 0) {
            const root = res.stdout.trim();
            if (root)
                return root;
        }
    }
    return null;
}
async function findPackageRoot(candidates) {
    for (const dir of candidates) {
        let current = dir;
        for (let i = 0; i < 12; i += 1) {
            const pkgPath = path.join(current, "package.json");
            try {
                const raw = await fs.readFile(pkgPath, "utf-8");
                const parsed = JSON.parse(raw);
                if (parsed?.name === "clawdbot")
                    return current;
            }
            catch {
                // ignore
            }
            const parent = path.dirname(current);
            if (parent === current)
                break;
            current = parent;
        }
    }
    return null;
}
async function detectPackageManager(root) {
    try {
        const raw = await fs.readFile(path.join(root, "package.json"), "utf-8");
        const parsed = JSON.parse(raw);
        const pm = parsed?.packageManager?.split("@")[0]?.trim();
        if (pm === "pnpm" || pm === "bun" || pm === "npm")
            return pm;
    }
    catch {
        // ignore
    }
    const files = await fs.readdir(root).catch(() => []);
    if (files.includes("pnpm-lock.yaml"))
        return "pnpm";
    if (files.includes("bun.lockb"))
        return "bun";
    if (files.includes("package-lock.json"))
        return "npm";
    return "npm";
}
async function runStep(opts) {
    const { runCommand, name, argv, cwd, timeoutMs, env, progress, stepIndex, totalSteps } = opts;
    const command = argv.join(" ");
    const stepInfo = {
        name,
        command,
        index: stepIndex,
        total: totalSteps,
    };
    progress?.onStepStart?.(stepInfo);
    const started = Date.now();
    const result = await runCommand(argv, { cwd, timeoutMs, env });
    const durationMs = Date.now() - started;
    const stderrTail = trimLogTail(result.stderr, MAX_LOG_CHARS);
    progress?.onStepComplete?.({
        ...stepInfo,
        durationMs,
        exitCode: result.code,
        stderrTail,
    });
    return {
        name,
        command,
        cwd,
        durationMs,
        exitCode: result.code,
        stdoutTail: trimLogTail(result.stdout, MAX_LOG_CHARS),
        stderrTail: trimLogTail(result.stderr, MAX_LOG_CHARS),
    };
}
function managerScriptArgs(manager, script, args = []) {
    if (manager === "pnpm")
        return ["pnpm", script, ...args];
    if (manager === "bun")
        return ["bun", "run", script, ...args];
    if (args.length > 0)
        return ["npm", "run", script, "--", ...args];
    return ["npm", "run", script];
}
function managerInstallArgs(manager) {
    if (manager === "pnpm")
        return ["pnpm", "install"];
    if (manager === "bun")
        return ["bun", "install"];
    return ["npm", "install"];
}
function normalizeTag(tag) {
    const trimmed = tag?.trim();
    if (!trimmed)
        return "latest";
    return trimmed.startsWith("clawdbot@") ? trimmed.slice("clawdbot@".length) : trimmed;
}
export async function runGatewayUpdate(opts = {}) {
    const startedAt = Date.now();
    const runCommand = opts.runCommand ??
        (async (argv, options) => {
            const res = await runCommandWithTimeout(argv, options);
            return { stdout: res.stdout, stderr: res.stderr, code: res.code };
        });
    const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
    const progress = opts.progress;
    const steps = [];
    const candidates = buildStartDirs(opts);
    let stepIndex = 0;
    let gitTotalSteps = 0;
    const step = (name, argv, cwd, env) => {
        const currentIndex = stepIndex;
        stepIndex += 1;
        return {
            runCommand,
            name,
            argv,
            cwd,
            timeoutMs,
            env,
            progress,
            stepIndex: currentIndex,
            totalSteps: gitTotalSteps,
        };
    };
    const pkgRoot = await findPackageRoot(candidates);
    let gitRoot = await resolveGitRoot(runCommand, candidates, timeoutMs);
    if (gitRoot && pkgRoot && path.resolve(gitRoot) !== path.resolve(pkgRoot)) {
        gitRoot = null;
    }
    if (gitRoot && !pkgRoot) {
        return {
            status: "error",
            mode: "unknown",
            root: gitRoot,
            reason: "not-clawdbot-root",
            steps: [],
            durationMs: Date.now() - startedAt,
        };
    }
    if (gitRoot && pkgRoot && path.resolve(gitRoot) === path.resolve(pkgRoot)) {
        // Get current SHA (not a visible step, no progress)
        const beforeShaResult = await runCommand(["git", "-C", gitRoot, "rev-parse", "HEAD"], {
            cwd: gitRoot,
            timeoutMs,
        });
        const beforeSha = beforeShaResult.stdout.trim() || null;
        const beforeVersion = await readPackageVersion(gitRoot);
        const channel = opts.channel ?? "dev";
        const branch = channel === "dev" ? await readBranchName(runCommand, gitRoot, timeoutMs) : null;
        const needsCheckoutMain = channel === "dev" && branch !== DEV_BRANCH;
        gitTotalSteps = channel === "dev" ? (needsCheckoutMain ? 11 : 10) : 9;
        const statusCheck = await runStep(step("clean check", ["git", "-C", gitRoot, "status", "--porcelain", "--", ":!dist/control-ui/"], gitRoot));
        steps.push(statusCheck);
        const hasUncommittedChanges = statusCheck.stdoutTail && statusCheck.stdoutTail.trim().length > 0;
        if (hasUncommittedChanges) {
            return {
                status: "skipped",
                mode: "git",
                root: gitRoot,
                reason: "dirty",
                before: { sha: beforeSha, version: beforeVersion },
                steps,
                durationMs: Date.now() - startedAt,
            };
        }
        if (channel === "dev") {
            if (needsCheckoutMain) {
                const checkoutStep = await runStep(step(`git checkout ${DEV_BRANCH}`, ["git", "-C", gitRoot, "checkout", DEV_BRANCH], gitRoot));
                steps.push(checkoutStep);
                if (checkoutStep.exitCode !== 0) {
                    return {
                        status: "error",
                        mode: "git",
                        root: gitRoot,
                        reason: "checkout-failed",
                        before: { sha: beforeSha, version: beforeVersion },
                        steps,
                        durationMs: Date.now() - startedAt,
                    };
                }
            }
            const upstreamStep = await runStep(step("upstream check", [
                "git",
                "-C",
                gitRoot,
                "rev-parse",
                "--abbrev-ref",
                "--symbolic-full-name",
                "@{upstream}",
            ], gitRoot));
            steps.push(upstreamStep);
            if (upstreamStep.exitCode !== 0) {
                return {
                    status: "skipped",
                    mode: "git",
                    root: gitRoot,
                    reason: "no-upstream",
                    before: { sha: beforeSha, version: beforeVersion },
                    steps,
                    durationMs: Date.now() - startedAt,
                };
            }
            const fetchStep = await runStep(step("git fetch", ["git", "-C", gitRoot, "fetch", "--all", "--prune", "--tags"], gitRoot));
            steps.push(fetchStep);
            const upstreamShaStep = await runStep(step("git rev-parse @{upstream}", ["git", "-C", gitRoot, "rev-parse", "@{upstream}"], gitRoot));
            steps.push(upstreamShaStep);
            const upstreamSha = upstreamShaStep.stdoutTail?.trim();
            if (!upstreamShaStep.stdoutTail || !upstreamSha) {
                return {
                    status: "error",
                    mode: "git",
                    root: gitRoot,
                    reason: "no-upstream-sha",
                    before: { sha: beforeSha, version: beforeVersion },
                    steps,
                    durationMs: Date.now() - startedAt,
                };
            }
            const revListStep = await runStep(step("git rev-list", ["git", "-C", gitRoot, "rev-list", `--max-count=${PREFLIGHT_MAX_COMMITS}`, upstreamSha], gitRoot));
            steps.push(revListStep);
            if (revListStep.exitCode !== 0) {
                return {
                    status: "error",
                    mode: "git",
                    root: gitRoot,
                    reason: "preflight-revlist-failed",
                    before: { sha: beforeSha, version: beforeVersion },
                    steps,
                    durationMs: Date.now() - startedAt,
                };
            }
            const candidates = (revListStep.stdoutTail ?? "")
                .split("\n")
                .map((line) => line.trim())
                .filter(Boolean);
            if (candidates.length === 0) {
                return {
                    status: "error",
                    mode: "git",
                    root: gitRoot,
                    reason: "preflight-no-candidates",
                    before: { sha: beforeSha, version: beforeVersion },
                    steps,
                    durationMs: Date.now() - startedAt,
                };
            }
            const manager = await detectPackageManager(gitRoot);
            const preflightRoot = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-update-preflight-"));
            const worktreeDir = path.join(preflightRoot, "worktree");
            const worktreeStep = await runStep(step("preflight worktree", ["git", "-C", gitRoot, "worktree", "add", "--detach", worktreeDir, upstreamSha], gitRoot));
            steps.push(worktreeStep);
            if (worktreeStep.exitCode !== 0) {
                await fs.rm(preflightRoot, { recursive: true, force: true }).catch(() => { });
                return {
                    status: "error",
                    mode: "git",
                    root: gitRoot,
                    reason: "preflight-worktree-failed",
                    before: { sha: beforeSha, version: beforeVersion },
                    steps,
                    durationMs: Date.now() - startedAt,
                };
            }
            let selectedSha = null;
            try {
                for (const sha of candidates) {
                    const shortSha = sha.slice(0, 8);
                    const checkoutStep = await runStep(step(`preflight checkout (${shortSha})`, ["git", "-C", worktreeDir, "checkout", "--detach", sha], worktreeDir));
                    steps.push(checkoutStep);
                    if (checkoutStep.exitCode !== 0)
                        continue;
                    const depsStep = await runStep(step(`preflight deps install (${shortSha})`, managerInstallArgs(manager), worktreeDir));
                    steps.push(depsStep);
                    if (depsStep.exitCode !== 0)
                        continue;
                    const lintStep = await runStep(step(`preflight lint (${shortSha})`, managerScriptArgs(manager, "lint"), worktreeDir));
                    steps.push(lintStep);
                    if (lintStep.exitCode !== 0)
                        continue;
                    const buildStep = await runStep(step(`preflight build (${shortSha})`, managerScriptArgs(manager, "build"), worktreeDir));
                    steps.push(buildStep);
                    if (buildStep.exitCode !== 0)
                        continue;
                    selectedSha = sha;
                    break;
                }
            }
            finally {
                const removeStep = await runStep(step("preflight cleanup", ["git", "-C", gitRoot, "worktree", "remove", "--force", worktreeDir], gitRoot));
                steps.push(removeStep);
                await runCommand(["git", "-C", gitRoot, "worktree", "prune"], {
                    cwd: gitRoot,
                    timeoutMs,
                }).catch(() => null);
                await fs.rm(preflightRoot, { recursive: true, force: true }).catch(() => { });
            }
            if (!selectedSha) {
                return {
                    status: "error",
                    mode: "git",
                    root: gitRoot,
                    reason: "preflight-no-good-commit",
                    before: { sha: beforeSha, version: beforeVersion },
                    steps,
                    durationMs: Date.now() - startedAt,
                };
            }
            const rebaseStep = await runStep(step("git rebase", ["git", "-C", gitRoot, "rebase", selectedSha], gitRoot));
            steps.push(rebaseStep);
            if (rebaseStep.exitCode !== 0) {
                const abortResult = await runCommand(["git", "-C", gitRoot, "rebase", "--abort"], {
                    cwd: gitRoot,
                    timeoutMs,
                });
                steps.push({
                    name: "git rebase --abort",
                    command: "git rebase --abort",
                    cwd: gitRoot,
                    durationMs: 0,
                    exitCode: abortResult.code,
                    stdoutTail: trimLogTail(abortResult.stdout, MAX_LOG_CHARS),
                    stderrTail: trimLogTail(abortResult.stderr, MAX_LOG_CHARS),
                });
                return {
                    status: "error",
                    mode: "git",
                    root: gitRoot,
                    reason: "rebase-failed",
                    before: { sha: beforeSha, version: beforeVersion },
                    steps,
                    durationMs: Date.now() - startedAt,
                };
            }
        }
        else {
            const fetchStep = await runStep(step("git fetch", ["git", "-C", gitRoot, "fetch", "--all", "--prune", "--tags"], gitRoot));
            steps.push(fetchStep);
            if (fetchStep.exitCode !== 0) {
                return {
                    status: "error",
                    mode: "git",
                    root: gitRoot,
                    reason: "fetch-failed",
                    before: { sha: beforeSha, version: beforeVersion },
                    steps,
                    durationMs: Date.now() - startedAt,
                };
            }
            const tag = await resolveChannelTag(runCommand, gitRoot, timeoutMs, channel);
            if (!tag) {
                return {
                    status: "error",
                    mode: "git",
                    root: gitRoot,
                    reason: "no-release-tag",
                    before: { sha: beforeSha, version: beforeVersion },
                    steps,
                    durationMs: Date.now() - startedAt,
                };
            }
            const checkoutStep = await runStep(step(`git checkout ${tag}`, ["git", "-C", gitRoot, "checkout", "--detach", tag], gitRoot));
            steps.push(checkoutStep);
            if (checkoutStep.exitCode !== 0) {
                return {
                    status: "error",
                    mode: "git",
                    root: gitRoot,
                    reason: "checkout-failed",
                    before: { sha: beforeSha, version: beforeVersion },
                    steps,
                    durationMs: Date.now() - startedAt,
                };
            }
        }
        const manager = await detectPackageManager(gitRoot);
        const depsStep = await runStep(step("deps install", managerInstallArgs(manager), gitRoot));
        steps.push(depsStep);
        const buildStep = await runStep(step("build", managerScriptArgs(manager, "build"), gitRoot));
        steps.push(buildStep);
        const uiBuildStep = await runStep(step("ui:build", managerScriptArgs(manager, "ui:build"), gitRoot));
        steps.push(uiBuildStep);
        // Restore dist/control-ui/ to committed state to prevent dirty repo after update
        // (ui:build regenerates assets with new hashes, which would block future updates)
        const restoreUiStep = await runStep(step("restore control-ui", ["git", "-C", gitRoot, "checkout", "--", "dist/control-ui/"], gitRoot));
        steps.push(restoreUiStep);
        const doctorStep = await runStep(step("openclaw-cn doctor", managerScriptArgs(manager, "clawdbot", ["doctor", "--non-interactive"]), gitRoot, { OPENCLAW_UPDATE_IN_PROGRESS: "1" }));
        steps.push(doctorStep);
        const failedStep = steps.find((s) => s.exitCode !== 0);
        const afterShaStep = await runStep(step("git rev-parse HEAD (after)", ["git", "-C", gitRoot, "rev-parse", "HEAD"], gitRoot));
        steps.push(afterShaStep);
        const afterVersion = await readPackageVersion(gitRoot);
        return {
            status: failedStep ? "error" : "ok",
            mode: "git",
            root: gitRoot,
            reason: failedStep ? failedStep.name : undefined,
            before: { sha: beforeSha, version: beforeVersion },
            after: {
                sha: afterShaStep.stdoutTail?.trim() ?? null,
                version: afterVersion,
            },
            steps,
            durationMs: Date.now() - startedAt,
        };
    }
    if (!pkgRoot) {
        return {
            status: "error",
            mode: "unknown",
            reason: `no root (${START_DIRS.join(",")})`,
            steps: [],
            durationMs: Date.now() - startedAt,
        };
    }
    const beforeVersion = await readPackageVersion(pkgRoot);
    const globalManager = await detectGlobalInstallManagerForRoot(runCommand, pkgRoot, timeoutMs);
    if (globalManager) {
        const spec = `clawdbot@${normalizeTag(opts.tag)}`;
        const updateStep = await runStep({
            runCommand,
            name: "global update",
            argv: globalInstallArgs(globalManager, spec),
            cwd: pkgRoot,
            timeoutMs,
            progress,
            stepIndex: 0,
            totalSteps: 1,
        });
        const steps = [updateStep];
        const afterVersion = await readPackageVersion(pkgRoot);
        return {
            status: updateStep.exitCode === 0 ? "ok" : "error",
            mode: globalManager,
            root: pkgRoot,
            reason: updateStep.exitCode === 0 ? undefined : updateStep.name,
            before: { version: beforeVersion },
            after: { version: afterVersion },
            steps,
            durationMs: Date.now() - startedAt,
        };
    }
    return {
        status: "skipped",
        mode: "unknown",
        root: pkgRoot,
        reason: "not-git-install",
        before: { version: beforeVersion },
        steps: [],
        durationMs: Date.now() - startedAt,
    };
}
