import fs from "node:fs/promises";
import { resolveUserPath } from "../utils.js";
import { discoverClawdbotPlugins } from "./discovery.js";
import { installPluginFromNpmSpec, resolvePluginInstallDir } from "./install.js";
import { recordPluginInstall } from "./installs.js";
import { loadPluginManifest } from "./manifest.js";
async function readInstalledPackageVersion(dir) {
    try {
        const raw = await fs.readFile(`${dir}/package.json`, "utf-8");
        const parsed = JSON.parse(raw);
        return typeof parsed.version === "string" ? parsed.version : undefined;
    }
    catch {
        return undefined;
    }
}
function resolveBundledPluginSources(params) {
    const discovery = discoverClawdbotPlugins({ workspaceDir: params.workspaceDir });
    const bundled = new Map();
    for (const candidate of discovery.candidates) {
        if (candidate.origin !== "bundled")
            continue;
        const manifest = loadPluginManifest(candidate.rootDir);
        if (!manifest.ok)
            continue;
        const pluginId = manifest.manifest.id;
        if (bundled.has(pluginId))
            continue;
        const npmSpec = candidate.packageOpenclaw?.install?.npmSpec?.trim() ||
            candidate.packageName?.trim() ||
            undefined;
        bundled.set(pluginId, {
            pluginId,
            localPath: candidate.rootDir,
            npmSpec,
        });
    }
    return bundled;
}
function pathsEqual(left, right) {
    if (!left || !right)
        return false;
    return resolveUserPath(left) === resolveUserPath(right);
}
function buildLoadPathHelpers(existing) {
    let paths = [...existing];
    const resolveSet = () => new Set(paths.map((entry) => resolveUserPath(entry)));
    let resolved = resolveSet();
    let changed = false;
    const addPath = (value) => {
        const normalized = resolveUserPath(value);
        if (resolved.has(normalized))
            return;
        paths.push(value);
        resolved.add(normalized);
        changed = true;
    };
    const removePath = (value) => {
        const normalized = resolveUserPath(value);
        if (!resolved.has(normalized))
            return;
        paths = paths.filter((entry) => resolveUserPath(entry) !== normalized);
        resolved = resolveSet();
        changed = true;
    };
    return {
        addPath,
        removePath,
        get changed() {
            return changed;
        },
        get paths() {
            return paths;
        },
    };
}
export async function updateNpmInstalledPlugins(params) {
    const logger = params.logger ?? {};
    const installs = params.config.plugins?.installs ?? {};
    const targets = params.pluginIds?.length ? params.pluginIds : Object.keys(installs);
    const outcomes = [];
    let next = params.config;
    let changed = false;
    for (const pluginId of targets) {
        if (params.skipIds?.has(pluginId)) {
            outcomes.push({
                pluginId,
                status: "skipped",
                message: `Skipping "${pluginId}" (already updated).`,
            });
            continue;
        }
        const record = installs[pluginId];
        if (!record) {
            outcomes.push({
                pluginId,
                status: "skipped",
                message: `No install record for "${pluginId}".`,
            });
            continue;
        }
        if (record.source !== "npm") {
            outcomes.push({
                pluginId,
                status: "skipped",
                message: `Skipping "${pluginId}" (source: ${record.source}).`,
            });
            continue;
        }
        if (!record.spec) {
            outcomes.push({
                pluginId,
                status: "skipped",
                message: `Skipping "${pluginId}" (missing npm spec).`,
            });
            continue;
        }
        const installPath = record.installPath ?? resolvePluginInstallDir(pluginId);
        const currentVersion = await readInstalledPackageVersion(installPath);
        if (params.dryRun) {
            let probe;
            try {
                probe = await installPluginFromNpmSpec({
                    spec: record.spec,
                    mode: "update",
                    dryRun: true,
                    expectedPluginId: pluginId,
                    logger,
                });
            }
            catch (err) {
                outcomes.push({
                    pluginId,
                    status: "error",
                    message: `Failed to check ${pluginId}: ${String(err)}`,
                });
                continue;
            }
            if (!probe.ok) {
                outcomes.push({
                    pluginId,
                    status: "error",
                    message: `Failed to check ${pluginId}: ${probe.error}`,
                });
                continue;
            }
            const nextVersion = probe.version ?? "unknown";
            const currentLabel = currentVersion ?? "unknown";
            if (currentVersion && probe.version && currentVersion === probe.version) {
                outcomes.push({
                    pluginId,
                    status: "unchanged",
                    currentVersion: currentVersion ?? undefined,
                    nextVersion: probe.version ?? undefined,
                    message: `${pluginId} is up to date (${currentLabel}).`,
                });
            }
            else {
                outcomes.push({
                    pluginId,
                    status: "updated",
                    currentVersion: currentVersion ?? undefined,
                    nextVersion: probe.version ?? undefined,
                    message: `Would update ${pluginId}: ${currentLabel} -> ${nextVersion}.`,
                });
            }
            continue;
        }
        let result;
        try {
            result = await installPluginFromNpmSpec({
                spec: record.spec,
                mode: "update",
                expectedPluginId: pluginId,
                logger,
            });
        }
        catch (err) {
            outcomes.push({
                pluginId,
                status: "error",
                message: `Failed to update ${pluginId}: ${String(err)}`,
            });
            continue;
        }
        if (!result.ok) {
            outcomes.push({
                pluginId,
                status: "error",
                message: `Failed to update ${pluginId}: ${result.error}`,
            });
            continue;
        }
        const nextVersion = result.version ?? (await readInstalledPackageVersion(result.targetDir));
        next = recordPluginInstall(next, {
            pluginId,
            source: "npm",
            spec: record.spec,
            installPath: result.targetDir,
            version: nextVersion,
        });
        changed = true;
        const currentLabel = currentVersion ?? "unknown";
        const nextLabel = nextVersion ?? "unknown";
        if (currentVersion && nextVersion && currentVersion === nextVersion) {
            outcomes.push({
                pluginId,
                status: "unchanged",
                currentVersion: currentVersion ?? undefined,
                nextVersion: nextVersion ?? undefined,
                message: `${pluginId} already at ${currentLabel}.`,
            });
        }
        else {
            outcomes.push({
                pluginId,
                status: "updated",
                currentVersion: currentVersion ?? undefined,
                nextVersion: nextVersion ?? undefined,
                message: `Updated ${pluginId}: ${currentLabel} -> ${nextLabel}.`,
            });
        }
    }
    return { config: next, changed, outcomes };
}
export async function syncPluginsForUpdateChannel(params) {
    const summary = {
        switchedToBundled: [],
        switchedToNpm: [],
        warnings: [],
        errors: [],
    };
    const bundled = resolveBundledPluginSources({ workspaceDir: params.workspaceDir });
    if (bundled.size === 0) {
        return { config: params.config, changed: false, summary };
    }
    let next = params.config;
    const loadHelpers = buildLoadPathHelpers(next.plugins?.load?.paths ?? []);
    const installs = next.plugins?.installs ?? {};
    let changed = false;
    if (params.channel === "dev") {
        for (const [pluginId, record] of Object.entries(installs)) {
            const bundledInfo = bundled.get(pluginId);
            if (!bundledInfo)
                continue;
            loadHelpers.addPath(bundledInfo.localPath);
            const alreadyBundled = record.source === "path" && pathsEqual(record.sourcePath, bundledInfo.localPath);
            if (alreadyBundled)
                continue;
            next = recordPluginInstall(next, {
                pluginId,
                source: "path",
                sourcePath: bundledInfo.localPath,
                installPath: bundledInfo.localPath,
                spec: record.spec ?? bundledInfo.npmSpec,
                version: record.version,
            });
            summary.switchedToBundled.push(pluginId);
            changed = true;
        }
    }
    else {
        for (const [pluginId, record] of Object.entries(installs)) {
            const bundledInfo = bundled.get(pluginId);
            if (!bundledInfo)
                continue;
            if (record.source === "npm") {
                loadHelpers.removePath(bundledInfo.localPath);
                continue;
            }
            if (record.source !== "path")
                continue;
            if (!pathsEqual(record.sourcePath, bundledInfo.localPath))
                continue;
            const spec = record.spec ?? bundledInfo.npmSpec;
            if (!spec) {
                summary.warnings.push(`Missing npm spec for ${pluginId}; keeping local path.`);
                continue;
            }
            let result;
            try {
                result = await installPluginFromNpmSpec({
                    spec,
                    mode: "update",
                    expectedPluginId: pluginId,
                    logger: params.logger,
                });
            }
            catch (err) {
                summary.errors.push(`Failed to install ${pluginId}: ${String(err)}`);
                continue;
            }
            if (!result.ok) {
                summary.errors.push(`Failed to install ${pluginId}: ${result.error}`);
                continue;
            }
            next = recordPluginInstall(next, {
                pluginId,
                source: "npm",
                spec,
                installPath: result.targetDir,
                version: result.version,
                sourcePath: undefined,
            });
            summary.switchedToNpm.push(pluginId);
            changed = true;
            loadHelpers.removePath(bundledInfo.localPath);
        }
    }
    if (loadHelpers.changed) {
        next = {
            ...next,
            plugins: {
                ...next.plugins,
                load: {
                    ...next.plugins?.load,
                    paths: loadHelpers.paths,
                },
            },
        };
        changed = true;
    }
    return { config: next, changed, summary };
}
