/**
 * Plugin Loader
 *
 * Discovers and loads scanner plugins from:
 * 1. Local directory: `.vaspera/plugins/{name}/manifest.json`
 * 2. npm packages: `vaspera-scanner-{name}`
 *
 * Plugins run in child processes for isolation (sandbox mode).
 *
 * @module plugins/loader
 */
import { readFile, readdir, access } from "fs/promises";
import { join, dirname } from "path";
import { fork } from "child_process";
import { logger } from "../logger.js";
import { PluginManifestSchema, } from "./types.js";
/**
 * Plugin registry storing all discovered plugins
 */
class PluginRegistry {
    plugins = new Map();
    projectPath;
    constructor(projectPath) {
        this.projectPath = projectPath;
    }
    /**
     * Add a plugin to the registry
     */
    add(plugin) {
        this.plugins.set(plugin.manifest.name, plugin);
    }
    /**
     * Get a plugin by name
     */
    get(name) {
        return this.plugins.get(name);
    }
    /**
     * Get all loaded plugins
     */
    getAll() {
        return Array.from(this.plugins.values());
    }
    /**
     * Get all enabled plugins
     */
    getEnabled() {
        return this.getAll().filter((p) => p.enabled && !p.loadError);
    }
    /**
     * Check if a plugin is loaded
     */
    has(name) {
        return this.plugins.has(name);
    }
    /**
     * Remove a plugin
     */
    remove(name) {
        return this.plugins.delete(name);
    }
    /**
     * Get registry entries for status reporting
     */
    getEntries() {
        return this.getAll().map((p) => ({
            name: p.manifest.name,
            version: p.manifest.version,
            source: p.source,
            loaded: !p.loadError,
            error: p.loadError,
            loadedAt: new Date().toISOString(),
        }));
    }
    /**
     * Clear all plugins
     */
    clear() {
        this.plugins.clear();
    }
}
/**
 * Load plugins for a project
 */
export async function loadPlugins(projectPath, options = {}) {
    const registry = new PluginRegistry(projectPath);
    const { loadLocal = true, loadNpm = true, only, skip = [], configs = {}, } = options;
    logger.info("plugins.loading", {
        projectPath,
        loadLocal,
        loadNpm,
        only,
        skip,
    });
    // Load local plugins
    if (loadLocal) {
        const localPlugins = await discoverLocalPlugins(projectPath);
        for (const pluginPath of localPlugins) {
            const name = pluginPath.split("/").pop() || "";
            // Check filters
            if (only && !only.includes(name))
                continue;
            if (skip.includes(name))
                continue;
            const plugin = await loadLocalPlugin(pluginPath, configs[name]);
            if (plugin) {
                registry.add(plugin);
            }
        }
    }
    // Load npm plugins
    if (loadNpm) {
        const npmPlugins = await discoverNpmPlugins(projectPath);
        for (const packageName of npmPlugins) {
            const name = packageName.replace("vaspera-scanner-", "");
            // Check filters
            if (only && !only.includes(name))
                continue;
            if (skip.includes(name))
                continue;
            const plugin = await loadNpmPlugin(projectPath, packageName, configs[name]);
            if (plugin) {
                registry.add(plugin);
            }
        }
    }
    logger.info("plugins.loaded", {
        total: registry.getAll().length,
        enabled: registry.getEnabled().length,
        plugins: registry.getEntries().map((e) => ({ name: e.name, source: e.source })),
    });
    return registry;
}
/**
 * Discover local plugins in .vaspera/plugins
 */
async function discoverLocalPlugins(projectPath) {
    const pluginsDir = join(projectPath, ".vaspera", "plugins");
    const plugins = [];
    try {
        await access(pluginsDir);
        const entries = await readdir(pluginsDir, { withFileTypes: true });
        for (const entry of entries) {
            if (entry.isDirectory()) {
                const manifestPath = join(pluginsDir, entry.name, "manifest.json");
                try {
                    await access(manifestPath);
                    plugins.push(join(pluginsDir, entry.name));
                }
                catch {
                    // No manifest, skip
                }
            }
        }
    }
    catch {
        // plugins directory doesn't exist
    }
    return plugins;
}
/**
 * Discover npm plugins (vaspera-scanner-* packages)
 */
async function discoverNpmPlugins(projectPath) {
    const plugins = [];
    const nodeModulesDir = join(projectPath, "node_modules");
    try {
        await access(nodeModulesDir);
        const entries = await readdir(nodeModulesDir);
        for (const entry of entries) {
            if (entry.startsWith("vaspera-scanner-")) {
                const packagePath = join(nodeModulesDir, entry);
                const packageJsonPath = join(packagePath, "package.json");
                try {
                    await access(packageJsonPath);
                    plugins.push(entry);
                }
                catch {
                    // Invalid package
                }
            }
        }
    }
    catch {
        // node_modules doesn't exist
    }
    return plugins;
}
/**
 * Load a local plugin from directory
 */
async function loadLocalPlugin(pluginPath, config) {
    const manifestPath = join(pluginPath, "manifest.json");
    try {
        const manifestContent = await readFile(manifestPath, "utf-8");
        const manifestData = JSON.parse(manifestContent);
        // Validate manifest
        const parseResult = PluginManifestSchema.safeParse(manifestData);
        if (!parseResult.success) {
            logger.warn("plugins.invalid_manifest", {
                path: manifestPath,
                errors: parseResult.error.errors.map((e) => e.message),
            });
            return {
                manifest: manifestData,
                path: pluginPath,
                source: "local",
                scan: createErrorScanner(`Invalid manifest: ${parseResult.error.message}`),
                enabled: false,
                loadError: `Invalid manifest: ${parseResult.error.message}`,
            };
        }
        const manifest = parseResult.data;
        // Load the scanner function
        const entryPointPath = join(pluginPath, manifest.entryPoint.module);
        const scan = await loadScannerFunction(entryPointPath, manifest.entryPoint.exportName);
        return {
            manifest,
            path: pluginPath,
            source: "local",
            scan,
            enabled: true,
        };
    }
    catch (error) {
        logger.error("plugins.load_error", {
            path: pluginPath,
            error: String(error),
        });
        return null;
    }
}
/**
 * Load an npm plugin
 */
async function loadNpmPlugin(projectPath, packageName, config) {
    const packagePath = join(projectPath, "node_modules", packageName);
    const packageJsonPath = join(packagePath, "package.json");
    try {
        const packageJsonContent = await readFile(packageJsonPath, "utf-8");
        const packageJson = JSON.parse(packageJsonContent);
        // Check for vaspera plugin manifest in package.json
        const manifestData = packageJson.vaspera || packageJson.vasperaPlugin;
        if (!manifestData) {
            // Try loading manifest.json from package
            const manifestPath = join(packagePath, "manifest.json");
            try {
                const manifestContent = await readFile(manifestPath, "utf-8");
                const manifest = JSON.parse(manifestContent);
                return loadLocalPlugin(packagePath, config);
            }
            catch {
                logger.warn("plugins.no_manifest", { packageName });
                return null;
            }
        }
        // Merge with package.json data
        const fullManifest = {
            name: packageJson.name.replace("vaspera-scanner-", ""),
            version: packageJson.version,
            description: packageJson.description,
            author: packageJson.author,
            homepage: packageJson.homepage,
            license: packageJson.license,
            ...manifestData,
        };
        const parseResult = PluginManifestSchema.safeParse(fullManifest);
        if (!parseResult.success) {
            logger.warn("plugins.invalid_manifest", {
                packageName,
                errors: parseResult.error.errors.map((e) => e.message),
            });
            return null;
        }
        const manifest = parseResult.data;
        // Load the scanner function
        const mainPath = join(packagePath, packageJson.main || "index.js");
        const scan = await loadScannerFunction(mainPath, manifest.entryPoint?.exportName || "default");
        return {
            manifest,
            path: packagePath,
            source: "npm",
            scan,
            enabled: true,
        };
    }
    catch (error) {
        logger.error("plugins.npm_load_error", {
            packageName,
            error: String(error),
        });
        return null;
    }
}
/**
 * Load scanner function from module
 */
async function loadScannerFunction(modulePath, exportName) {
    try {
        // Dynamic import
        const module = await import(modulePath);
        const fn = exportName === "default" ? module.default : module[exportName];
        if (typeof fn !== "function") {
            throw new Error(`Export '${exportName}' is not a function`);
        }
        return fn;
    }
    catch (error) {
        logger.error("plugins.load_function_error", {
            modulePath,
            exportName,
            error: String(error),
        });
        return createErrorScanner(`Failed to load: ${String(error)}`);
    }
}
/**
 * Create an error scanner that returns a failed result
 */
function createErrorScanner(errorMessage) {
    return async () => ({
        scanner: "semgrep", // Placeholder - actual scanner type comes from manifest
        findings: [],
        duration: 0,
        success: false,
        error: errorMessage,
    });
}
/**
 * Execute a plugin in sandbox mode (child process)
 */
export async function executePluginSandboxed(plugin, context) {
    const startTime = Date.now();
    return new Promise((resolve) => {
        // Create sandbox worker script path
        const workerPath = join(dirname(import.meta.url.replace("file://", "")), "worker.js");
        const child = fork(workerPath, [], {
            stdio: ["pipe", "pipe", "pipe", "ipc"],
            timeout: context.timeout,
        });
        let result = null;
        let error;
        child.on("message", (message) => {
            if (message.type === "result") {
                result = message.data;
            }
            else if (message.type === "error") {
                error = String(message.data);
            }
        });
        child.on("error", (err) => {
            error = err.message;
        });
        child.on("exit", (code) => {
            const durationMs = Date.now() - startTime;
            if (result) {
                resolve({
                    plugin: plugin.manifest.name,
                    result,
                    durationMs,
                    sandboxed: true,
                });
            }
            else {
                resolve({
                    plugin: plugin.manifest.name,
                    result: {
                        scanner: plugin.manifest.scanner.type,
                        findings: [],
                        duration: durationMs,
                        success: false,
                        error: error || `Process exited with code ${code}`,
                    },
                    durationMs,
                    sandboxed: true,
                    error: error || `Process exited with code ${code}`,
                });
            }
        });
        // Send execution request to child
        child.send({
            type: "execute",
            data: {
                pluginPath: plugin.path,
                manifest: plugin.manifest,
                context: {
                    ...context,
                    // Don't send logger (not serializable)
                    logger: undefined,
                },
            },
        });
        // Timeout handling
        setTimeout(() => {
            child.kill("SIGTERM");
        }, context.timeout + 1000);
    });
}
/**
 * Execute a plugin directly (no sandbox)
 */
export async function executePluginDirect(plugin, context) {
    const startTime = Date.now();
    try {
        const result = await plugin.scan(context);
        return {
            plugin: plugin.manifest.name,
            result,
            durationMs: Date.now() - startTime,
            sandboxed: false,
        };
    }
    catch (error) {
        return {
            plugin: plugin.manifest.name,
            result: {
                scanner: plugin.manifest.scanner.type,
                findings: [],
                duration: Date.now() - startTime,
                success: false,
                error: String(error),
            },
            durationMs: Date.now() - startTime,
            sandboxed: false,
            error: String(error),
        };
    }
}
/**
 * Execute all plugins for a project
 */
export async function executePlugins(registry, projectPath, options = {}) {
    const { files, timeout = 120000, sandbox = true, configs = {}, } = options;
    const plugins = registry.getEnabled();
    const results = [];
    for (const plugin of plugins) {
        const context = {
            projectPath,
            files,
            timeout: plugin.manifest.scanner.defaultTimeout || timeout,
            rulesDir: plugin.manifest.rules?.directory
                ? join(plugin.path, plugin.manifest.rules.directory)
                : undefined,
            config: configs[plugin.manifest.name],
            logger: {
                debug: (msg, data) => logger.debug(`plugin:${plugin.manifest.name}:${msg}`, data),
                info: (msg, data) => logger.info(`plugin:${plugin.manifest.name}:${msg}`, data),
                warn: (msg, data) => logger.warn(`plugin:${plugin.manifest.name}:${msg}`, data),
                error: (msg, data) => logger.error(`plugin:${plugin.manifest.name}:${msg}`, data),
            },
        };
        const result = sandbox && plugin.manifest.capabilities.sandboxable
            ? await executePluginSandboxed(plugin, context)
            : await executePluginDirect(plugin, context);
        results.push(result);
        logger.info("plugins.executed", {
            plugin: plugin.manifest.name,
            success: result.result.success,
            findings: result.result.findings.length,
            durationMs: result.durationMs,
            sandboxed: result.sandboxed,
        });
    }
    return results;
}
/**
 * Get plugin registry for a project
 */
export { PluginRegistry };
//# sourceMappingURL=loader.js.map