import { accessSync, constants, existsSync, realpathSync } from "fs";
import { homedir } from "os";
import { basename, dirname, join, resolve, sep, win32 } from "path";
import { shouldUseWindowsShell, spawnProcessSync } from "./utils/child-process.js";
function makeSelfUpdateCommand(installStep, uninstallStep) {
    if (!uninstallStep)
        return installStep;
    return {
        ...installStep,
        display: `${uninstallStep.display} && ${installStep.display}`,
        steps: [uninstallStep, installStep],
    };
}
function makeSelfUpdateCommandStep(command, args) {
    return {
        command,
        args,
        display: [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "),
    };
}
export function detectInstallMethodForRuntime(runtime) {
    if (runtime.isBunBinary) {
        return "bun-binary";
    }
    const resolvedPath = `${runtime.moduleDir}\0${process.execPath || ""}`.toLowerCase().replace(/\\/g, "/");
    if (resolvedPath.includes("/pnpm/") || resolvedPath.includes("/.pnpm/")) {
        return "pnpm";
    }
    if (resolvedPath.includes("/yarn/") || resolvedPath.includes("/.yarn/")) {
        return "yarn";
    }
    if (resolvedPath.includes("/install/global/node_modules/")) {
        return "bun";
    }
    if (resolvedPath.includes("/npm/") || resolvedPath.includes("/node_modules/")) {
        return "npm";
    }
    return "unknown";
}
function getInferredNpmInstall(runtime) {
    const packageDir = runtime.getPackageDir();
    const path = process.platform === "win32" || packageDir.includes("\\") ? win32 : { basename, dirname };
    const parent = path.dirname(packageDir);
    let root;
    if (path.basename(parent).startsWith("@") && path.basename(path.dirname(parent)) === "node_modules") {
        root = path.dirname(parent);
    }
    else if (path.basename(parent) === "node_modules") {
        root = parent;
    }
    if (!root)
        return undefined;
    const rootParent = path.dirname(root);
    if (path.basename(rootParent) === "lib")
        return { root, prefix: path.dirname(rootParent) };
    // Windows global npm prefixes use `<prefix>\\node_modules`, which is
    // indistinguishable from local project installs by path shape alone. Do not
    // infer unsupported Windows custom prefixes without `npm root -g` evidence.
    return undefined;
}
function getSelfUpdateCommandForMethod(runtime, method, installedPackageName, updatePackageName = installedPackageName, npmCommand) {
    switch (method) {
        case "bun-binary":
            return undefined;
        case "pnpm": {
            const match = readCommandOutput("pnpm", ["root", "-g"])
                ? undefined
                : /^(.*[\\/]global[\\/][^\\/]+)[\\/]\.pnpm[\\/]/.exec(runtime.getPackageDir());
            const binDirArgs = match
                ? [`--config.global-bin-dir=${process.env.PNPM_HOME || dirname(dirname(match[1]))}`]
                : [];
            return makeSelfUpdateCommand(makeSelfUpdateCommandStep("pnpm", [
                "install",
                "-g",
                "--ignore-scripts",
                "--config.minimumReleaseAge=0",
                ...binDirArgs,
                updatePackageName,
            ]), updatePackageName === installedPackageName
                ? undefined
                : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", ...binDirArgs, installedPackageName]));
        }
        case "yarn":
            return makeSelfUpdateCommand(makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]), updatePackageName === installedPackageName
                ? undefined
                : makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]));
        case "bun":
            return makeSelfUpdateCommand(makeSelfUpdateCommandStep("bun", [
                "install",
                "-g",
                "--ignore-scripts",
                "--minimum-release-age=0",
                updatePackageName,
            ]), updatePackageName === installedPackageName
                ? undefined
                : makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]));
        case "npm": {
            const [command = "npm", ...npmArgs] = npmCommand ?? [];
            const inferred = npmCommand?.length ? undefined : getInferredNpmInstall(runtime);
            const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])];
            const installStep = makeSelfUpdateCommandStep(command, [
                ...prefixArgs,
                "install",
                "-g",
                "--ignore-scripts",
                "--min-release-age=0",
                updatePackageName,
            ]);
            const uninstallStep = updatePackageName === installedPackageName
                ? undefined
                : makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]);
            return makeSelfUpdateCommand(installStep, uninstallStep);
        }
        case "unknown":
            return undefined;
    }
}
function readCommandOutput(command, args, options = {}) {
    const result = spawnProcessSync(command, args, {
        encoding: "utf-8",
        stdio: ["ignore", "pipe", "pipe"],
        env: process.env,
        shell: shouldUseWindowsShell(command),
    });
    if (result.status === 0)
        return result.stdout.trim() || undefined;
    if (options.requireSuccess) {
        const reason = result.error?.message || result.stderr.trim() || `exit code ${result.status ?? "unknown"}`;
        throw new Error(`Failed to run ${[command, ...args].join(" ")}: ${reason}`);
    }
    return undefined;
}
function getGlobalPackageRoots(runtime, method, _packageName, npmCommand) {
    switch (method) {
        case "npm": {
            const configured = !!npmCommand?.length;
            const [command = "npm", ...npmArgs] = npmCommand ?? [];
            if (configured && command === "bun") {
                const bunBin = readCommandOutput(command, [...npmArgs, "pm", "bin", "-g"], {
                    requireSuccess: true,
                });
                const roots = [join(homedir(), ".bun", "install", "global", "node_modules")];
                if (bunBin) {
                    roots.push(join(dirname(bunBin), "install", "global", "node_modules"));
                }
                return roots;
            }
            const root = readCommandOutput(command, [...npmArgs, "root", "-g"], {
                requireSuccess: configured,
            });
            const inferred = configured ? undefined : getInferredNpmInstall(runtime);
            return [root, inferred?.root].filter((x) => !!x);
        }
        case "pnpm": {
            const root = readCommandOutput("pnpm", ["root", "-g"]);
            if (root)
                return [root, dirname(root)];
            const match = /^(.*[\\/]global[\\/][^\\/]+)[\\/]\.pnpm[\\/]/.exec(runtime.getPackageDir());
            return match ? [match[1]] : [];
        }
        case "yarn": {
            const dir = readCommandOutput("yarn", ["global", "dir"]);
            return dir ? [dir, join(dir, "node_modules")] : [];
        }
        case "bun": {
            const bunBin = readCommandOutput("bun", ["pm", "bin", "-g"]);
            const roots = [join(homedir(), ".bun", "install", "global", "node_modules")];
            if (bunBin) {
                roots.push(join(dirname(bunBin), "install", "global", "node_modules"));
            }
            return roots;
        }
        case "bun-binary":
        case "unknown":
            return [];
    }
}
function normalizeExistingPathForComparison(path, resolveSymlinks) {
    const resolvedPath = resolve(path);
    if (!existsSync(resolvedPath)) {
        return undefined;
    }
    let normalizedPath = resolvedPath;
    if (resolveSymlinks) {
        try {
            normalizedPath = realpathSync(resolvedPath);
        }
        catch {
            return undefined;
        }
    }
    if (process.platform === "win32") {
        normalizedPath = normalizedPath.toLowerCase();
    }
    return normalizedPath;
}
function getPathComparisonCandidates(path) {
    return Array.from(new Set([normalizeExistingPathForComparison(path, false), normalizeExistingPathForComparison(path, true)].filter((candidate) => !!candidate)));
}
function getEntrypointPackageDir() {
    const entrypoint = process.argv[1];
    if (!entrypoint)
        return undefined;
    let dir = dirname(entrypoint);
    while (dir !== dirname(dir)) {
        if (existsSync(join(dir, "package.json"))) {
            return dir;
        }
        dir = dirname(dir);
    }
    return undefined;
}
function isSelfUpdatePathWritable(runtime) {
    const packageDir = runtime.getPackageDir();
    try {
        accessSync(packageDir, constants.W_OK);
        accessSync(dirname(packageDir), constants.W_OK);
        return true;
    }
    catch {
        return false;
    }
}
function isManagedByGlobalPackageManager(runtime, method, packageName, npmCommand) {
    const packageDirs = [runtime.getPackageDir(), getEntrypointPackageDir()].filter((dir) => !!dir);
    const packageDirCandidates = packageDirs.flatMap((dir) => getPathComparisonCandidates(dir));
    return getGlobalPackageRoots(runtime, method, packageName, npmCommand).some((root) => {
        return getPathComparisonCandidates(root).some((normalizedRoot) => {
            const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
            return packageDirCandidates.some((packageDir) => packageDir.startsWith(rootPrefix));
        });
    });
}
export function getSelfUpdateCommandForRuntime(runtime, packageName, npmCommand, updatePackageName = packageName) {
    const method = detectInstallMethodForRuntime(runtime);
    const command = getSelfUpdateCommandForMethod(runtime, method, packageName, updatePackageName, npmCommand);
    if (!command || !isManagedByGlobalPackageManager(runtime, method, packageName, npmCommand) || !isSelfUpdatePathWritable(runtime)) {
        return undefined;
    }
    return command;
}
export function getSelfUpdateUnavailableInstructionForRuntime(runtime, packageName, npmCommand, updatePackageName = packageName) {
    const method = detectInstallMethodForRuntime(runtime);
    if (method === "bun-binary") {
        return `Download from: https://github.com/earendil-works/pi-mono/releases/latest`;
    }
    const command = getSelfUpdateCommandForMethod(runtime, method, packageName, updatePackageName, npmCommand);
    if (command) {
        if (isManagedByGlobalPackageManager(runtime, method, packageName, npmCommand) && !isSelfUpdatePathWritable(runtime)) {
            return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`;
        }
        return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`;
    }
    return `Update ${updatePackageName} using the package manager, wrapper, or source checkout that provides this installation.`;
}
export function getUpdateInstructionForRuntime(runtime, packageName) {
    const method = detectInstallMethodForRuntime(runtime);
    const command = getSelfUpdateCommandForMethod(runtime, method, packageName);
    if (command) {
        return `Run: ${command.display}`;
    }
    return getSelfUpdateUnavailableInstructionForRuntime(runtime, packageName);
}
//# sourceMappingURL=config-self-update.js.map