import { FAILED_PLUGIN_PREFIX } from "./sdk/constants.js";
import { existsSync, readdirSync, readFileSync } from "fs";
import { join, resolve, sep } from "path";
import { pathToFileURL } from "url";
import { PLUGINS_NODE_MODULES } from "../utils/paths.js";
import { validateManifest, validateChecks } from "./validate.js";
import { extractReason } from "../utils/errors.js";
import { registerPlugin, registerFailedPlugin, clearPluginRegistry, mapRegistryPlugins, savePluginCache, } from "./registry.js";
export async function loadPlugins(options) {
    const importer = options?.importer ?? ((p) => import(p));
    const loaded = [];
    const errors = [];
    clearPluginRegistry();
    if (!existsSync(PLUGINS_NODE_MODULES)) {
        return { loaded, errors };
    }
    const entries = readdirSync(PLUGINS_NODE_MODULES, { withFileTypes: true });
    const pluginDirs = entries.filter((e) => e.isDirectory() && e.name.startsWith("kastell-plugin-"));
    if (pluginDirs.length === 0) {
        return { loaded, errors };
    }
    const results = await Promise.allSettled(pluginDirs.map(async (dir) => {
        const pluginDir = join(PLUGINS_NODE_MODULES, dir.name);
        const manifestPath = join(pluginDir, "kastell-plugin.json");
        const failedManifest = () => ({
            name: dir.name,
            version: "0.0.0",
            apiVersion: "1",
            kastell: "*",
            capabilities: ["audit"],
            checkPrefix: FAILED_PLUGIN_PREFIX,
            entry: "",
        });
        let manifestRaw;
        try {
            manifestRaw = readFileSync(manifestPath, "utf-8");
        }
        catch {
            registerFailedPlugin(failedManifest(), `cannot read kastell-plugin.json`);
            throw new Error(`${dir.name}: cannot read kastell-plugin.json`);
        }
        let manifestParsed;
        try {
            manifestParsed = JSON.parse(manifestRaw);
        }
        catch {
            registerFailedPlugin(failedManifest(), `invalid JSON in kastell-plugin.json`);
            throw new Error(`${dir.name}: invalid JSON in kastell-plugin.json`);
        }
        const manifest = validateManifest(manifestParsed);
        const resolvedDir = resolve(pluginDir);
        const resolvedEntry = resolve(resolvedDir, manifest.entry);
        if (!resolvedEntry.startsWith(resolvedDir + sep) && resolvedEntry !== resolvedDir) {
            registerFailedPlugin(manifest, `entry escapes plugin directory: ${manifest.entry}`);
            throw new Error(`${dir.name}: entry escapes plugin directory: ${manifest.entry}`);
        }
        const entryUrl = pathToFileURL(resolvedEntry).href;
        let mod;
        try {
            mod = await importer(entryUrl);
        }
        catch (err) {
            const msg = extractReason(err);
            registerFailedPlugin(manifest, msg);
            throw new Error(`${dir.name}: import failed — ${msg}`, { cause: err });
        }
        const ns = mod;
        // ESM CJS interop: namespace has .default = module.exports, plus .module.exports = module.exports
        const moduleObj = (ns.default ?? ns["module.exports"] ?? ns);
        if (!Array.isArray(moduleObj.checks)) {
            registerFailedPlugin(manifest, "module does not export checks array");
            throw new Error(`${dir.name}: module does not export checks array`);
        }
        let checks;
        try {
            checks = validateChecks(moduleObj.checks, manifest.checkPrefix);
        }
        catch (err) {
            const msg = extractReason(err);
            registerFailedPlugin(manifest, msg);
            throw new Error(`${dir.name}: check validation failed — ${msg}`, { cause: err });
        }
        const enrichedManifest = {
            ...manifest,
            commands: moduleObj.commands ?? manifest.commands,
            mcpTools: moduleObj.mcpTools ?? manifest.mcpTools,
            fixes: moduleObj.fixes ?? manifest.fixes,
        };
        function guardHandlerPath(pluginDir, handler, type) {
            const resolved = resolve(pluginDir, handler);
            if (!resolved.startsWith(pluginDir + sep) && resolved !== pluginDir) {
                throw new Error(`${type} handler escapes plugin directory: ${handler}`);
            }
        }
        if (enrichedManifest.fixes) {
            for (const fix of enrichedManifest.fixes) {
                guardHandlerPath(resolvedDir, fix.handler, "fix");
            }
        }
        if (enrichedManifest.commands) {
            for (const cmd of enrichedManifest.commands) {
                guardHandlerPath(resolvedDir, cmd.handler, "command");
            }
        }
        if (enrichedManifest.mcpTools) {
            for (const tool of enrichedManifest.mcpTools) {
                guardHandlerPath(resolvedDir, tool.handler, "mcpTool");
            }
        }
        registerPlugin(enrichedManifest, checks);
        return manifest.name;
    }));
    for (const result of results) {
        if (result.status === "fulfilled") {
            loaded.push(result.value);
        }
        else {
            errors.push(extractReason(result.reason));
        }
    }
    const manifests = mapRegistryPlugins((_, entry) => entry.status === "loaded" ? entry.manifest : null).filter((m) => m !== null);
    savePluginCache(manifests);
    return { loaded, errors };
}
//# sourceMappingURL=loader.js.map