import { cp, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { assertExpectedHash, hashDirectory, hashManifest } from "./plugin-integrity.js";
import { loadPluginManifest, validatePluginManifest } from "./plugin-manifest.js";
import { pluginMarketplaceEntries } from "./plugin-marketplace.js";
import { diffPluginManifestPermissions } from "./plugin-permission-diff.js";
import { verifyPluginManifestSignature } from "./plugin-signatures.js";
import { PluginStore } from "./plugin-store.js";
import { formatShellCommand, resolveNpmSpawnCommand } from "../support/operations.js";
import { PLUGIN_MANIFEST_FILE, } from "./plugin-types.js";
export class PluginInstaller {
    store;
    constructor(store) {
        this.store = store;
    }
    async analyze(request) {
        const prepared = await this.prepareInstall(request);
        await prepared.resolved.cleanup?.();
        return prepared.analysis;
    }
    async install(request) {
        const prepared = await this.prepareInstall(request);
        const { resolved, analysis } = prepared;
        try {
            const manifest = analysis.manifest;
            const destination = this.store.installVersionPath(manifest.id, manifest.version);
            const existing = await this.store.get(manifest.id);
            if (existing && !request.force && existing.version === manifest.version) {
                throw new Error(`Plugin ${manifest.id}@${manifest.version} is already installed. Use --force to reinstall.`);
            }
            if (existing && analysis.permissionDiff.hasEscalation && !request.approvePermissionDiff && !request.approvePermissions) {
                throw new Error(`Plugin update requires permission approval: ${analysis.permissionDiff.riskyChanges.join("; ") || analysis.permissionDiff.addedPermissions.join(", ")}`);
            }
            await rm(destination, { recursive: true, force: true });
            await mkdir(path.dirname(destination), { recursive: true });
            await cp(resolved.pluginDir, destination, {
                recursive: true,
                filter: (source) => !shouldSkipPath(source, resolved.type),
            });
            const now = new Date().toISOString();
            const settings = buildDefaultSettings(manifest.settings ?? [], existing?.settings ?? {});
            const record = {
                id: manifest.id,
                name: manifest.name,
                version: manifest.version,
                description: manifest.description,
                author: manifest.author,
                homepage: manifest.homepage,
                repository: manifest.repository,
                license: manifest.license,
                nordrelay: manifest.nordrelay,
                entry: manifest.entry,
                installPath: destination,
                manifestPath: path.join(destination, PLUGIN_MANIFEST_FILE),
                source: {
                    type: resolved.type,
                    value: resolved.value,
                    ref: resolved.ref,
                    revision: resolved.revision,
                    packageName: resolved.packageName,
                    resolvedRef: resolved.resolvedRef,
                    integrity: resolved.packageHash,
                },
                manifestHash: analysis.manifestHash,
                packageHash: analysis.packageHash,
                trustLevel: analysis.trustLevel,
                signature: analysis.signature,
                signaturePublicKey: request.signaturePublicKey,
                permissionDiff: analysis.permissionDiff,
                enabled: Boolean(request.enable),
                status: request.enable ? "enabled" : "installed",
                permissions: manifest.permissions ?? [],
                approvedPermissions: request.approvePermissions
                    ? manifest.permissions ?? []
                    : (existing?.approvedPermissions ?? []).filter((permission) => (manifest.permissions ?? []).includes(permission)),
                capabilities: manifest.capabilities ?? {},
                settingsSchema: manifest.settings ?? [],
                settings,
                installedAt: existing?.installedAt ?? now,
                updatedAt: now,
            };
            await this.store.save(record);
            await this.store.saveLock({
                id: record.id,
                version: record.version,
                source: record.source,
                manifestHash: record.manifestHash,
                packageHash: record.packageHash,
                permissions: record.permissions,
                approvedPermissions: record.approvedPermissions,
                capabilities: record.capabilities,
                trustLevel: record.trustLevel,
                signature: record.signature,
                signaturePublicKey: record.signaturePublicKey,
                installedAt: record.installedAt,
                updatedAt: record.updatedAt,
            });
            return record;
        }
        finally {
            await resolved.cleanup?.();
        }
    }
    async validatePath(pluginDir) {
        const validation = await loadPluginManifest(path.resolve(pluginDir));
        if (validation.manifest) {
            const normalized = validatePluginManifest(validation.manifest);
            return normalized;
        }
        return validation;
    }
    async scaffold(request) {
        const targetDir = path.resolve(request.targetDir);
        const id = request.id.trim();
        const name = request.name?.trim() || id;
        const description = request.description?.trim() || "NordRelay plugin";
        const validation = validatePluginManifest({
            id,
            name,
            version: "0.1.0",
            description,
            entry: "index.js",
        });
        if (!validation.ok) {
            throw new Error(validation.issues.map((issue) => issue.message).join("; "));
        }
        await mkdir(targetDir, { recursive: true });
        await writeFile(path.join(targetDir, PLUGIN_MANIFEST_FILE), `${JSON.stringify({
            id,
            name,
            version: "0.1.0",
            description,
            author: "",
            nordrelay: ">=0.9.8",
            entry: "index.js",
            permissions: [],
            capabilities: {
                workflowActions: [
                    {
                        id: `${id}.example`,
                        title: "Example action",
                        description: "Returns a small example payload.",
                    },
                ],
            },
            settings: [],
        }, null, 2)}\n`, "utf8");
        await writeFile(path.join(targetDir, "index.js"), [
            "#!/usr/bin/env node",
            "",
            "process.stdin.setEncoding('utf8');",
            "let input = '';",
            "process.stdin.on('data', (chunk) => { input += chunk; });",
            "process.stdin.on('end', () => {",
            "  const request = input ? JSON.parse(input) : {};",
            "  process.stdout.write(JSON.stringify({ ok: true, request }) + '\\n');",
            "});",
            "",
        ].join("\n"), "utf8");
        await writeFile(path.join(targetDir, "README.md"), `# ${name}\n\n${description}\n\nInstall locally with:\n\n\`\`\`sh\nnordrelay plugin install ${targetDir}\n\`\`\`\n`, "utf8");
        return targetDir;
    }
    async resolveSource(request) {
        const source = request.source.trim();
        if (!source) {
            throw new Error("Plugin source is required.");
        }
        if (isGitHubSource(source)) {
            return this.cloneGitHubSource(source, request.ref);
        }
        if (isNpmSource(source)) {
            return this.unpackNpmSource(source);
        }
        const pluginDir = path.resolve(source);
        return {
            type: "local",
            value: pluginDir,
            pluginDir,
            packageHash: await hashDirectory(pluginDir),
        };
    }
    async prepareInstall(request) {
        const resolved = await this.resolveSource(request);
        try {
            const validation = await loadPluginManifest(resolved.pluginDir);
            if (!validation.ok || !validation.manifest) {
                throw new Error(validation.issues.map((issue) => issue.message).join("; ") || "Invalid plugin manifest.");
            }
            const manifest = validation.manifest;
            const manifestHash = hashManifest(manifest);
            const packageHash = resolved.packageHash;
            const marketplaceEntry = marketplaceEntryForResolvedSource(resolved, request.source);
            assertOfficialTrustRequest(request, resolved, manifest, marketplaceEntry);
            assertExpectedHash(manifestHash, request.expectedManifestHash ?? marketplaceEntry?.expectedManifestHash, "Plugin manifest");
            assertExpectedHash(packageHash, request.expectedPackageHash ?? marketplaceEntry?.expectedPackageHash, "Plugin package");
            const requireSignature = request.requireSignature ?? marketplaceEntry?.signatureRequired;
            const signature = verifyPluginManifestSignature(manifest, request.signaturePublicKey, requireSignature);
            if (requireSignature && signature.status !== "verified") {
                throw new Error(signature.message || "Plugin manifest signature verification failed.");
            }
            const existing = await this.store.get(manifest.id);
            const permissionDiff = diffPluginManifestPermissions(existing, manifest);
            const trustLevel = request.trustLevel ?? defaultTrustLevel(resolved);
            return {
                resolved,
                analysis: {
                    manifest,
                    source: {
                        type: resolved.type,
                        value: resolved.value,
                        ref: resolved.ref,
                        revision: resolved.revision,
                        packageName: resolved.packageName,
                        resolvedRef: resolved.resolvedRef,
                        packageHash: resolved.packageHash,
                    },
                    manifestHash,
                    packageHash,
                    trustLevel,
                    signature,
                    permissionDiff,
                },
            };
        }
        catch (error) {
            await resolved.cleanup?.();
            throw error;
        }
    }
    async cloneGitHubSource(source, ref) {
        const parsed = parseGitHubSource(source, ref);
        const tmp = await mkdtemp(path.join(os.tmpdir(), "nordrelay-plugin-"));
        const clone = spawnSync("git", ["clone", "--depth", "1", parsed.repoUrl, tmp], {
            encoding: "utf8",
            stdio: "pipe",
        });
        if (clone.status !== 0) {
            await rm(tmp, { recursive: true, force: true });
            throw new Error(`git clone failed: ${clone.stderr || clone.stdout || clone.error?.message || "unknown error"}`);
        }
        if (parsed.ref) {
            const checkout = spawnSync("git", ["checkout", parsed.ref], { cwd: tmp, encoding: "utf8", stdio: "pipe" });
            if (checkout.status !== 0) {
                await rm(tmp, { recursive: true, force: true });
                throw new Error(`git checkout failed: ${checkout.stderr || checkout.stdout || "unknown error"}`);
            }
        }
        const revision = spawnSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8", stdio: "pipe" });
        const resolvedRef = revision.status === 0 ? revision.stdout.trim() : undefined;
        return {
            type: "github",
            value: parsed.repoUrl,
            ref: parsed.ref,
            revision: resolvedRef,
            resolvedRef,
            packageHash: await hashDirectory(tmp),
            pluginDir: tmp,
            cleanup: () => rm(tmp, { recursive: true, force: true }),
        };
    }
    async unpackNpmSource(source) {
        const spec = source.slice("npm:".length).trim();
        if (!spec) {
            throw new Error("npm plugin source must look like npm:@scope/package[@version].");
        }
        const tmp = await mkdtemp(path.join(os.tmpdir(), "nordrelay-plugin-npm-"));
        const extractDir = path.join(tmp, "package");
        await mkdir(extractDir, { recursive: true });
        const pack = spawnNpmSync(["pack", spec, "--json", "--pack-destination", tmp]);
        if (pack.status !== 0) {
            await rm(tmp, { recursive: true, force: true });
            throw new Error(`npm pack failed: ${pack.stderr || pack.stdout || pack.error?.message || "unknown error"}`);
        }
        const packed = parseNpmPackResult(pack.stdout);
        const tarball = path.join(tmp, packed.filename);
        const unpack = spawnSync("tar", ["-xzf", tarball, "-C", extractDir, "--strip-components", "1"], {
            encoding: "utf8",
            stdio: "pipe",
        });
        if (unpack.status !== 0) {
            await rm(tmp, { recursive: true, force: true });
            throw new Error(`npm package extract failed: ${unpack.stderr || unpack.stdout || "unknown error"}`);
        }
        const install = spawnNpmSync(["install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--install-links=true"], { cwd: extractDir });
        if (install.status !== 0) {
            await rm(tmp, { recursive: true, force: true });
            throw new Error(`npm dependency install failed: ${install.stderr || install.stdout || install.error?.message || "unknown error"}`);
        }
        return {
            type: "npm",
            value: spec,
            packageName: packed.name,
            revision: packed.version,
            resolvedRef: `${packed.name}@${packed.version}`,
            packageHash: await hashDirectory(extractDir),
            pluginDir: extractDir,
            cleanup: () => rm(tmp, { recursive: true, force: true }),
        };
    }
}
function spawnNpmSync(args, options = {}) {
    const npm = resolveNpmSpawnCommand();
    if (!npm) {
        return {
            status: 1,
            stdout: "",
            stderr: "npm was not found. Install Node.js/npm or add npm to PATH.",
            error: undefined,
        };
    }
    const fullArgs = [...npm.argsPrefix, ...args];
    return spawnSync(npm.shell ? formatShellCommand(npm.command, fullArgs) : npm.command, npm.shell ? [] : fullArgs, {
        cwd: options.cwd,
        encoding: "utf8",
        shell: npm.shell,
        stdio: "pipe",
        windowsHide: true,
    });
}
function isGitHubSource(source) {
    return source.startsWith("github:") || /^https:\/\/github\.com\/[^/]+\/[^/#]+/i.test(source);
}
function isNpmSource(source) {
    return source.startsWith("npm:");
}
function parseGitHubSource(source, ref) {
    if (source.startsWith("github:")) {
        const value = source.slice("github:".length);
        const [repo, inlineRef] = value.split("#", 2);
        if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) {
            throw new Error("GitHub plugin source must look like github:owner/repo or github:owner/repo#ref.");
        }
        return { repoUrl: `https://github.com/${repo}.git`, ref: ref ?? inlineRef };
    }
    const [url, inlineRef] = source.split("#", 2);
    const match = /^https:\/\/github\.com\/([^/]+)\/([^/#]+?)(?:\.git)?$/i.exec(url);
    if (!match) {
        throw new Error("GitHub plugin source must be a github.com repository URL.");
    }
    return { repoUrl: `https://github.com/${match[1]}/${match[2]}.git`, ref: ref ?? inlineRef };
}
function shouldSkipPath(source, sourceType) {
    const parts = source.split(path.sep);
    return parts.includes(".git") || (sourceType !== "npm" && parts.includes("node_modules")) || parts.includes(".DS_Store");
}
function buildDefaultSettings(schema, previous) {
    const settings = {};
    for (const item of schema) {
        if (previous[item.key] !== undefined) {
            settings[item.key] = previous[item.key];
        }
        else if (item.default !== undefined) {
            settings[item.key] = item.default;
        }
        else {
            settings[item.key] = item.type === "boolean" ? false : "";
        }
    }
    return settings;
}
function parseNpmPackResult(raw) {
    const parsed = JSON.parse(raw || "[]");
    const first = Array.isArray(parsed) ? parsed[0] : parsed;
    if (!first || typeof first !== "object") {
        throw new Error("npm pack did not return package metadata.");
    }
    const record = first;
    const filename = typeof record.filename === "string" ? record.filename : "";
    const name = typeof record.name === "string" ? record.name : "";
    const version = typeof record.version === "string" ? record.version : "";
    if (!filename || !name || !version) {
        throw new Error("npm pack metadata is missing filename, name, or version.");
    }
    return { filename, name, version };
}
function defaultTrustLevel(source) {
    if (source.type === "local")
        return "local";
    if (/github\.com\/nordbyte\//i.test(source.value) || String(source.packageName ?? "").startsWith("@nordbyte/")) {
        return "verified";
    }
    return "community";
}
function marketplaceEntryForResolvedSource(resolved, requestedSource) {
    const requested = normalizePluginSourceSpecifier(requestedSource);
    const resolvedValue = normalizePluginSourceSpecifier(resolvedSourceSpecifier(resolved));
    return pluginMarketplaceEntries().find((entry) => {
        const sources = [entry.source, entry.verifiedSource, entry.repository, entry.packageName ? `npm:${entry.packageName}` : ""]
            .map(normalizePluginSourceSpecifier)
            .filter(Boolean);
        return sources.includes(requested) || sources.includes(resolvedValue);
    });
}
function assertOfficialTrustRequest(request, resolved, manifest, marketplaceEntry) {
    if (request.trustLevel !== "official") {
        return;
    }
    if (!marketplaceEntry?.official) {
        throw new Error("Official plugin trust is only allowed for official Marketplace sources.");
    }
    if (marketplaceEntry.id !== manifest.id) {
        throw new Error(`Official Marketplace plugin id mismatch: expected ${marketplaceEntry.id}, got ${manifest.id}.`);
    }
    if (marketplaceEntry.packageName && resolved.type === "npm" && resolved.packageName !== marketplaceEntry.packageName) {
        throw new Error(`Official Marketplace package mismatch: expected ${marketplaceEntry.packageName}, got ${resolved.packageName ?? "unknown"}.`);
    }
}
function resolvedSourceSpecifier(resolved) {
    if (resolved.type === "npm")
        return `npm:${resolved.packageName ?? resolved.value}`;
    if (resolved.type === "github")
        return resolved.value;
    return resolved.value;
}
function normalizePluginSourceSpecifier(value) {
    const input = String(value ?? "").trim();
    if (!input)
        return "";
    if (input.startsWith("npm:")) {
        return `npm:${input.slice("npm:".length).trim().replace(/@latest$/i, "")}`;
    }
    if (input.startsWith("github:")) {
        const source = input.slice("github:".length).split("#", 1)[0].replace(/\.git$/i, "");
        return `github:${source.toLowerCase()}`;
    }
    const github = /^https:\/\/github\.com\/([^/]+)\/([^/#]+?)(?:\.git)?(?:#.*)?$/i.exec(input);
    if (github) {
        return `github:${github[1].toLowerCase()}/${github[2].toLowerCase()}`;
    }
    return input.toLowerCase();
}
