import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
import { join, resolve, isAbsolute } from 'path';
import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
import { createPluginAPI } from './api.js';
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
const PLUGIN_ROOT = 'plugins';
/**
 * Plugin search directories in precedence order.
 * Project-local plugins override global plugins with the same manifest name.
 */
export function getUserPluginDirectory(options) {
    return join(options.homeDir, '.goodvibes', PLUGIN_ROOT);
}
export function getPluginDirectories(options) {
    const dirs = [
        join(options.cwd, '.goodvibes', PLUGIN_ROOT),
        getUserPluginDirectory(options),
    ];
    if (options.additionalDirectories) {
        dirs.push(...options.additionalDirectories);
    }
    return dirs;
}
/**
 * discoverPlugins — Scan the configured plugin directories for valid plugin folders.
 * Each subdirectory with a readable manifest.json is a candidate.
 */
function scanPluginDirectory(rootDir) {
    if (!existsSync(rootDir))
        return [];
    const results = [];
    let entries;
    try {
        entries = readdirSync(rootDir);
    }
    catch (err) {
        logger.warn(`[plugins] Could not read plugins directory '${rootDir}': ${summarizeError(err)}`);
        return [];
    }
    for (const entry of entries) {
        const pluginDir = join(rootDir, entry);
        try {
            if (!statSync(pluginDir).isDirectory())
                continue;
            const manifestPath = join(pluginDir, 'manifest.json');
            if (!existsSync(manifestPath))
                continue;
            const raw = readFileSync(manifestPath, 'utf-8');
            const manifest = JSON.parse(raw);
            if (!manifest.name || !manifest.version) {
                logger.warn(`[plugins] ${entry}: manifest.json missing required fields (name, version)`);
                continue;
            }
            // Validate manifest field types
            if (typeof manifest.name !== 'string' || typeof manifest.version !== 'string') {
                logger.warn(`[plugins] ${entry}: manifest.json 'name' and 'version' must be strings`);
                continue;
            }
            if (manifest.main !== undefined) {
                if (typeof manifest.main !== 'string') {
                    logger.warn(`[plugins] ${entry}: manifest.json 'main' must be a string`);
                    continue;
                }
                if (isAbsolute(manifest.main)) {
                    logger.warn(`[plugins] ${entry}: manifest.json 'main' must be a relative path, not absolute`);
                    continue;
                }
            }
            results.push({ pluginDir, manifest });
        }
        catch (err) {
            logger.warn(`[plugins] ${entry}: failed to parse manifest — ${summarizeError(err)}`);
        }
    }
    return results;
}
export function discoverPlugins(options) {
    const discovered = new Map();
    for (const dir of getPluginDirectories(options)) {
        for (const plugin of scanPluginDirectory(dir)) {
            if (!discovered.has(plugin.manifest.name)) {
                discovered.set(plugin.manifest.name, plugin);
            }
        }
    }
    return [...discovered.values()];
}
/**
 * loadPlugin — Load, init, and activate a single plugin.
 * Returns a LoadedPlugin on success, or null on failure.
 *
 * @param cacheBust - Optional timestamp suffix appended to the import URL to bypass
 *   Bun's module cache. Pass `Date.now()` on reload to force fresh execution.
 */
export async function loadPlugin(discovered, deps, cacheBust, entryDefault) {
    const { manifest, pluginDir } = discovered;
    const entryFile = manifest.main ?? entryDefault ?? 'index.js';
    const entryPath = join(pluginDir, entryFile);
    // Path traversal guard: resolved entry must remain within pluginDir
    const resolvedEntry = resolve(entryPath);
    const resolvedPluginDir = resolve(pluginDir);
    if (!resolvedEntry.startsWith(resolvedPluginDir + '/') && resolvedEntry !== resolvedPluginDir) {
        logger.error(`[plugins] ${manifest.name}: path traversal detected — entry '${entryFile}' resolves outside plugin directory`);
        return null;
    }
    if (!existsSync(entryPath)) {
        logger.warn(`[plugins] ${manifest.name}: entry file not found: ${entryPath}`);
        return null;
    }
    // Trust notice — plugins run as trusted code (like VS Code extensions)
    logger.warn(`[plugins] Loading '${manifest.name}' — plugins are trusted code and run with full application access`);
    const loaded = {
        manifest,
        pluginDir,
        active: false,
        cleanup: [],
    };
    try {
        // Dynamic import — Bun supports TS imports directly.
        // Append cache-bust query param on reload so Bun re-executes the module.
        const importPath = cacheBust !== undefined ? `${entryPath}?t=${cacheBust}` : entryPath;
        const mod = await import(importPath);
        // Validate module shape before casting
        if (!mod || typeof mod !== 'object') {
            logger.warn(`[plugins] ${manifest.name}: entry file did not export a module object`);
            return null;
        }
        const modObj = mod;
        if (typeof modObj['init'] !== 'function') {
            logger.warn(`[plugins] ${manifest.name}: entry file must export an init() function`);
            return null;
        }
        if (modObj['activate'] !== undefined && typeof modObj['activate'] !== 'function') {
            logger.warn(`[plugins] ${manifest.name}: entry file 'activate' export must be a function`);
            return null;
        }
        if (modObj['deactivate'] !== undefined && typeof modObj['deactivate'] !== 'function') {
            logger.warn(`[plugins] ${manifest.name}: entry file 'deactivate' export must be a function`);
            return null;
        }
        const entry = mod;
        loaded.entry = entry;
        const ctx = {
            pluginName: manifest.name,
            runtimeBus: deps.runtimeBus,
            commandRegistry: deps.commandRegistry,
            providerRegistry: deps.providerRegistry,
            toolRegistry: deps.toolRegistry,
            gatewayMethods: deps.gatewayMethods,
            channelRegistry: deps.channelRegistry,
            channelDeliveryRouter: deps.channelDeliveryRouter,
            memoryEmbeddingRegistry: deps.memoryEmbeddingRegistry,
            voiceProviderRegistry: deps.voiceProviderRegistry,
            mediaProviderRegistry: deps.mediaProviderRegistry,
            webSearchProviderRegistry: deps.webSearchProviderRegistry,
            pluginConfig: deps.getPluginConfig(manifest.name),
            cleanup: loaded.cleanup,
        };
        const api = createPluginAPI(ctx);
        // Lifecycle: init
        await entry.init(api);
        // Lifecycle: activate
        if (typeof entry.activate === 'function') {
            await entry.activate();
        }
        loaded.active = true;
        logger.info(`[plugins] ${manifest.name} v${manifest.version} activated`);
        return loaded;
    }
    catch (err) {
        logger.error(`[plugins] ${manifest.name}: load failed — ${summarizeError(err)}`);
        // Run cleanup for anything that was registered before the error
        for (const fn of loaded.cleanup) {
            try {
                fn();
            }
            catch { /* best-effort */ }
        }
        return null;
    }
}
/**
 * unloadPlugin — Deactivate a plugin and run all cleanup callbacks.
 */
export async function unloadPlugin(plugin) {
    if (!plugin.active)
        return;
    try {
        if (typeof plugin.entry?.deactivate === 'function') {
            await plugin.entry.deactivate();
        }
    }
    catch (err) {
        logger.warn(`[plugins] ${plugin.manifest.name}: deactivate threw — ${summarizeError(err)}`);
    }
    for (const fn of plugin.cleanup) {
        try {
            fn();
        }
        catch { /* best-effort */ }
    }
    plugin.cleanup.length = 0;
    plugin.active = false;
    logger.info(`[plugins] ${plugin.manifest.name} deactivated`);
}
