/**
 * Plugin Manager — discovery, loading, lifecycle, and persistence.
 *
 * Extension directories are scanned for `jait.plugin.json` manifests.
 * Enabled plugins are dynamically imported and their contributions
 * are registered with the tool / provider registries.
 */
import { readdir, readFile, stat } from "node:fs/promises";
import { join, resolve } from "node:path";
import { homedir } from "node:os";
import { validateManifest } from "./manifest.js";
import { discoverOpenClawPlugins, openclawToJaitManifest, createOpenClawPluginModule } from "./openclaw-adapter.js";
/* ------------------------------------------------------------------ */
/*  Constants                                                          */
/* ------------------------------------------------------------------ */
/** Default extensions root: ~/.jait/extensions/ */
export function defaultExtensionsDir() {
    return join(homedir(), ".jait", "extensions");
}
export class PluginManager {
    sqlite;
    toolRegistry;
    gatewayVersion;
    projectRoot;
    extensionsDir;
    openclawExtensionsDirs;
    /** In-memory map of loaded plugins keyed by id. */
    loaded = new Map();
    /** OpenClaw plugin modules keyed by jait id (openclaw:<id>). */
    openclawModules = new Map();
    constructor(deps) {
        this.sqlite = deps.sqlite;
        this.toolRegistry = deps.toolRegistry;
        this.gatewayVersion = deps.gatewayVersion;
        this.projectRoot = deps.projectRoot;
        this.extensionsDir = deps.extensionsDir ?? defaultExtensionsDir();
        this.openclawExtensionsDirs = deps.openclawExtensionsDirs ?? [];
    }
    /* ---------------------------------------------------------------- */
    /*  DB helpers                                                       */
    /* ---------------------------------------------------------------- */
    getInstalled(id) {
        const row = this.sqlite
            .prepare("SELECT * FROM plugins WHERE id = ?")
            .get(id);
        return row ? rowToInstalled(row) : null;
    }
    upsertInstalled(p) {
        this.sqlite
            .prepare(`INSERT INTO plugins (id, display_name, version, description, author, path, status, config, error, installed_at, updated_at)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
         ON CONFLICT(id) DO UPDATE SET
           display_name = excluded.display_name,
           version = excluded.version,
           description = excluded.description,
           author = excluded.author,
           path = excluded.path,
           status = excluded.status,
           config = excluded.config,
           error = excluded.error,
           updated_at = excluded.updated_at`)
            .run(p.id, p.displayName, p.version, p.description ?? null, p.author ?? null, p.path, p.status, JSON.stringify(p.config), p.error ?? null, p.installedAt, p.updatedAt);
    }
    deleteInstalled(id) {
        this.sqlite.prepare("DELETE FROM plugins WHERE id = ?").run(id);
    }
    /* ---------------------------------------------------------------- */
    /*  Discovery — scan extensions directory for manifests              */
    /* ---------------------------------------------------------------- */
    async discover() {
        const manifests = [];
        let entries;
        try {
            entries = await readdir(this.extensionsDir);
        }
        catch {
            // Directory doesn't exist yet — that's fine
            return manifests;
        }
        for (const entry of entries) {
            const dir = join(this.extensionsDir, entry);
            const dirStat = await stat(dir).catch(() => null);
            if (!dirStat?.isDirectory())
                continue;
            const manifestPath = join(dir, "jait.plugin.json");
            try {
                const raw = JSON.parse(await readFile(manifestPath, "utf-8"));
                const err = validateManifest(raw);
                if (err) {
                    console.warn(`Plugin manifest invalid at ${manifestPath}: ${err}`);
                    continue;
                }
                // Attach the resolved directory path for later import
                raw._path = dir;
                manifests.push(raw);
            }
            catch {
                // No manifest or invalid JSON — skip
            }
        }
        return manifests;
    }
    /* ---------------------------------------------------------------- */
    /*  Sync — reconcile discovered manifests with DB state              */
    /* ---------------------------------------------------------------- */
    /**
     * Discover all plugins, sync them with DB, and load enabled ones.
     * Called once at gateway startup.
     */
    async syncAndLoad() {
        const manifests = await this.discover();
        const now = new Date().toISOString();
        for (const manifest of manifests) {
            const dir = manifest._path;
            let existing = this.getInstalled(manifest.id);
            if (!existing) {
                // New plugin discovered — install as disabled by default
                existing = {
                    id: manifest.id,
                    displayName: manifest.displayName,
                    version: manifest.version,
                    description: manifest.description,
                    author: manifest.author,
                    path: dir,
                    status: "installed",
                    config: {},
                    installedAt: now,
                    updatedAt: now,
                };
                this.upsertInstalled(existing);
            }
            else {
                // Update metadata if version/path changed
                if (existing.version !== manifest.version || existing.path !== dir) {
                    existing.version = manifest.version;
                    existing.displayName = manifest.displayName;
                    existing.description = manifest.description;
                    existing.author = manifest.author;
                    existing.path = dir;
                    existing.updatedAt = now;
                    this.upsertInstalled(existing);
                }
            }
            // Load if enabled
            if (existing.status === "enabled") {
                await this.loadPlugin(manifest, existing);
            }
        }
        // ── OpenClaw compatibility: discover openclaw.plugin.json ──────
        // Scan both the Jait extensions dir and any explicit OpenClaw dirs
        const openclawScanDirs = [this.extensionsDir, ...this.openclawExtensionsDirs];
        let openclawCount = 0;
        for (const scanDir of openclawScanDirs) {
            const ocPlugins = await discoverOpenClawPlugins(scanDir);
            for (const { manifest: ocManifest, dir } of ocPlugins) {
                const jaitId = `openclaw:${ocManifest.id}`;
                // Skip if a native Jait plugin with the same base id already exists
                if (manifests.some((m) => m.id === ocManifest.id))
                    continue;
                const jaitManifest = openclawToJaitManifest(ocManifest, dir);
                let existing = this.getInstalled(jaitId);
                if (!existing) {
                    existing = {
                        id: jaitId,
                        displayName: jaitManifest.displayName,
                        version: jaitManifest.version ?? "0.0.0",
                        description: jaitManifest.description,
                        author: "OpenClaw",
                        path: dir,
                        status: "installed",
                        config: {},
                        installedAt: now,
                        updatedAt: now,
                    };
                    this.upsertInstalled(existing);
                }
                else if (existing.path !== dir) {
                    existing.path = dir;
                    existing.updatedAt = now;
                    this.upsertInstalled(existing);
                }
                // Store the module factory for later loading
                const pluginModule = createOpenClawPluginModule(ocManifest, dir);
                this.openclawModules.set(jaitId, pluginModule);
                if (existing.status === "enabled") {
                    await this.loadPluginWithModule(jaitManifest, existing, pluginModule);
                }
                openclawCount++;
            }
        }
        console.log(`Plugins: ${manifests.length} native + ${openclawCount} OpenClaw discovered, ${this.loaded.size} loaded`);
    }
    /* ---------------------------------------------------------------- */
    /*  Load / unload a single plugin                                    */
    /* ---------------------------------------------------------------- */
    buildContext(installed) {
        const self = this;
        return {
            gatewayVersion: this.gatewayVersion,
            projectRoot: this.projectRoot,
            getConfig() {
                // Re-read from DB to get latest
                const current = self.getInstalled(installed.id);
                return (current?.config ?? {});
            },
            async setConfig(value) {
                const current = self.getInstalled(installed.id);
                if (current) {
                    current.config = value;
                    current.updatedAt = new Date().toISOString();
                    self.upsertInstalled(current);
                }
            },
            log: {
                info: (msg, ...args) => console.log(`[plugin:${installed.id}]`, msg, ...args),
                warn: (msg, ...args) => console.warn(`[plugin:${installed.id}]`, msg, ...args),
                error: (msg, ...args) => console.error(`[plugin:${installed.id}]`, msg, ...args),
            },
        };
    }
    async loadPlugin(manifest, installed) {
        try {
            const entryFile = manifest.main ?? "index.js";
            const entryPath = resolve(installed.path, entryFile);
            const mod = await import(entryPath);
            // Support both default export and named `plugin` export
            const pluginModule = mod.default ?? mod.plugin;
            if (!pluginModule || typeof pluginModule.setup !== "function") {
                throw new Error(`Plugin entry at ${entryPath} must export a PluginModule (use definePlugin())`);
            }
            const ctx = this.buildContext(installed);
            const contribution = (await pluginModule.setup(ctx)) ?? null;
            // Register contributed tools
            if (contribution?.tools?.length) {
                this.toolRegistry.registerPluginTools({ id: manifest.id, displayName: manifest.displayName }, contribution.tools);
            }
            this.loaded.set(manifest.id, {
                manifest,
                installed,
                module: pluginModule,
                contribution,
            });
            // Clear any previous error
            if (installed.error) {
                installed.error = undefined;
                installed.updatedAt = new Date().toISOString();
                this.upsertInstalled(installed);
            }
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            console.error(`Failed to load plugin ${manifest.id}:`, message);
            installed.status = "error";
            installed.error = message;
            installed.updatedAt = new Date().toISOString();
            this.upsertInstalled(installed);
        }
    }
    /** Load a plugin using a pre-built module (used for OpenClaw adapter). */
    async loadPluginWithModule(manifest, installed, pluginModule) {
        try {
            const ctx = this.buildContext(installed);
            const contribution = (await pluginModule.setup(ctx)) ?? null;
            if (contribution?.tools?.length) {
                this.toolRegistry.registerPluginTools({ id: installed.id, displayName: installed.displayName }, contribution.tools);
            }
            this.loaded.set(installed.id, {
                manifest,
                installed,
                module: pluginModule,
                contribution,
            });
            if (installed.error) {
                installed.error = undefined;
                installed.updatedAt = new Date().toISOString();
                this.upsertInstalled(installed);
            }
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            console.error(`Failed to load OpenClaw plugin ${installed.id}:`, message);
            installed.status = "error";
            installed.error = message;
            installed.updatedAt = new Date().toISOString();
            this.upsertInstalled(installed);
        }
    }
    async unloadPlugin(id) {
        const loaded = this.loaded.get(id);
        if (!loaded)
            return;
        try {
            await loaded.module?.dispose();
        }
        catch (err) {
            console.error(`Error disposing plugin ${id}:`, err);
        }
        this.loaded.delete(id);
    }
    /* ---------------------------------------------------------------- */
    /*  Public API — used by REST routes                                 */
    /* ---------------------------------------------------------------- */
    /** List all installed plugins (from DB). */
    listInstalled() {
        const rows = this.sqlite
            .prepare("SELECT * FROM plugins ORDER BY display_name")
            .all();
        return rows.map(rowToInstalled);
    }
    /** Get a single installed plugin. */
    getPlugin(id) {
        return this.getInstalled(id);
    }
    /** Enable a plugin and load it. */
    async enable(id) {
        const installed = this.getInstalled(id);
        if (!installed)
            throw new Error(`Plugin '${id}' not found`);
        installed.status = "enabled";
        installed.error = undefined;
        installed.updatedAt = new Date().toISOString();
        this.upsertInstalled(installed);
        // Re-discover manifest so we can load it
        const manifests = await this.discover();
        const manifest = manifests.find((m) => m.id === id);
        if (manifest) {
            await this.loadPlugin(manifest, installed);
        }
        else {
            // Check for OpenClaw adapter module
            const ocModule = this.openclawModules.get(id);
            if (ocModule) {
                const jaitManifest = openclawToJaitManifest({ id: id.replace(/^openclaw:/, ""), name: installed.displayName, version: installed.version, description: installed.description }, installed.path);
                await this.loadPluginWithModule(jaitManifest, installed, ocModule);
            }
        }
        return this.getInstalled(id) ?? installed;
    }
    /** Disable a plugin and unload it. */
    async disable(id) {
        const installed = this.getInstalled(id);
        if (!installed)
            throw new Error(`Plugin '${id}' not found`);
        await this.unloadPlugin(id);
        installed.status = "disabled";
        installed.error = undefined;
        installed.updatedAt = new Date().toISOString();
        this.upsertInstalled(installed);
        return installed;
    }
    /** Uninstall a plugin (unload + remove from DB). Does NOT delete files. */
    async uninstall(id) {
        await this.unloadPlugin(id);
        this.deleteInstalled(id);
    }
    /** Get / set plugin config. */
    getPluginConfig(id) {
        return this.getInstalled(id)?.config ?? {};
    }
    async setPluginConfig(id, config) {
        const installed = this.getInstalled(id);
        if (!installed)
            throw new Error(`Plugin '${id}' not found`);
        installed.config = config;
        installed.updatedAt = new Date().toISOString();
        this.upsertInstalled(installed);
    }
    /** Gracefully dispose of all loaded plugins. */
    async disposeAll() {
        for (const [id] of this.loaded) {
            await this.unloadPlugin(id);
        }
    }
}
/* ------------------------------------------------------------------ */
/*  Row conversion                                                     */
/* ------------------------------------------------------------------ */
function rowToInstalled(row) {
    return {
        id: row.id,
        displayName: row.display_name,
        version: row.version,
        description: row.description ?? undefined,
        author: row.author ?? undefined,
        path: row.path,
        status: row.status,
        config: JSON.parse(row.config || "{}"),
        error: row.error ?? undefined,
        installedAt: row.installed_at,
        updatedAt: row.updated_at,
    };
}
//# sourceMappingURL=manager.js.map