import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { dirname } from 'path';
import { logger } from '../utils/logger.js';
import { discoverPlugins, getPluginDirectories, loadPlugin, unloadPlugin, } from './loader.js';
import { PluginTrustStore, } from '../runtime/plugins/trust.js';
import { PluginQuarantineEngine } from '../runtime/plugins/quarantine.js';
import { isHighRiskCapability, resolveCapabilityManifest } from '../runtime/plugins/manifest.js';
import { summarizeError } from '../utils/error-display.js';
const DEFAULT_STATE = { enabled: {}, config: {}, trust: {}, quarantine: {} };
/**
 * PluginManager — orchestrates plugin discovery, loading, and persistence.
 */
export class PluginManager {
    plugins = new Map();
    state = { ...DEFAULT_STATE, enabled: {}, config: {}, trust: {}, quarantine: {} };
    deps;
    /** Trust store — manages tier records for all plugins. */
    trustStore = new PluginTrustStore();
    /** Quarantine engine — manages plugin quarantine state. */
    quarantineEngine = new PluginQuarantineEngine();
    subscribers = new Set();
    pathOptions;
    stateFilePath;
    constructor(options) {
        this.pathOptions = options.pathOptions;
        if (!options.stateFilePath) {
            throw new Error('PluginManager requires an explicit stateFilePath.');
        }
        this.stateFilePath = options.stateFilePath;
    }
    /**
     * init — Must be called once at startup with application dependencies.
     * Loads state from disk, then discovers and loads all enabled plugins.
     */
    async init(deps) {
        this.deps = deps;
        this.loadState();
        await this.loadEnabledPlugins();
    }
    /** Returns status for all discovered plugins (enabled or not). */
    list() {
        const discovered = this.discoverPlugins();
        return discovered.map((d) => {
            const loaded = this.plugins.get(d.manifest.name);
            return {
                name: d.manifest.name,
                version: d.manifest.version,
                description: d.manifest.description,
                author: d.manifest.author,
                enabled: this.isEnabled(d.manifest.name),
                active: loaded?.active ?? false,
                trustTier: this.trustStore.getTier(d.manifest.name),
                quarantined: this.quarantineEngine.isQuarantined(d.manifest.name),
            };
        });
    }
    subscribe(callback) {
        this.subscribers.add(callback);
        return () => this.subscribers.delete(callback);
    }
    getTrustRecord(name) {
        return this.trustStore.getRecord(name);
    }
    getQuarantineRecord(name) {
        return this.quarantineEngine.getRecord(name);
    }
    /**
     * trust — Set the trust tier for a plugin.
     *
     * For the `trusted` tier, prefer `trustSigned()` which also validates the
     * signature. This method is for operator-forced tier assignment.
     */
    trust(name, tier, note) {
        const discovered = this.findDiscoveredPlugin(name);
        if (!discovered) {
            return { ok: false, error: this.notFoundError(name) };
        }
        // Warn if trying to manually set 'trusted' without a signed manifest.
        if (tier === 'trusted') {
            const manifest = discovered.manifest;
            if (!manifest.signature) {
                logger.warn(`[plugins] '${name}' set to 'trusted' tier without a signed manifest — ` +
                    'consider using /plugin verify first');
            }
        }
        const record = this.trustStore.setTier(name, tier, { note });
        this.state.trust[name] = record;
        this.saveState();
        logger.info(`[plugins] ${name}: trust tier set to '${tier}'`);
        this.notifySubscribers();
        return { ok: true };
    }
    /**
     * trustSigned — Elevate a plugin to `trusted` after validating its manifest signature.
     */
    trustSigned(name, publicKey) {
        const discovered = this.findDiscoveredPlugin(name);
        if (!discovered) {
            return { ok: false, error: this.notFoundError(name) };
        }
        const manifest = discovered.manifest;
        const result = this.trustStore.trustSigned(name, manifest, publicKey);
        if (!result.ok) {
            return { ok: false, error: result.reason };
        }
        this.state.trust[name] = result.record;
        this.saveState();
        this.notifySubscribers();
        return { ok: true, fingerprint: result.record.signatureFingerprint };
    }
    /**
     * verify — Inspect a plugin's manifest signature without changing its tier.
     */
    verify(name, publicKey) {
        const discovered = this.findDiscoveredPlugin(name);
        if (!discovered) {
            return { ok: false, valid: false, reason: this.notFoundError(name) };
        }
        const manifest = discovered.manifest;
        const result = this.trustStore.verify(manifest, publicKey);
        return { ok: result.valid, ...result };
    }
    /**
     * capabilities — Return the capability information for a plugin.
     *
     * Returns the full set: requested, granted (based on current trust tier),
     * denied, and which capabilities are high-risk.
     */
    capabilities(name) {
        const discovered = this.findDiscoveredPlugin(name);
        if (!discovered) {
            return null;
        }
        const manifest = discovered.manifest;
        const requested = (manifest.capabilities ?? []);
        const tier = this.trustStore.getTier(name);
        const highRisk = requested.filter((c) => isHighRiskCapability(c));
        const safe = requested.filter((c) => !isHighRiskCapability(c));
        // Capabilities blocked by current trust tier
        const blocked = tier !== 'trusted' ? highRisk : [];
        return { ok: true, requested, highRisk, safe, tier, blocked };
    }
    /**
     * quarantine — Apply quarantine to a plugin.
     *
     * This is the high-level operator path. It resolves the plugin's declared
     * capability manifest using the current trust tier, then applies quarantine
     * immediately to the resolved capability set.
     */
    quarantine(name, reason) {
        const discovered = this.findDiscoveredPlugin(name);
        if (!discovered) {
            return { ok: false, error: this.notFoundError(name) };
        }
        if (this.quarantineEngine.isQuarantined(name)) {
            return { ok: false, error: `Plugin '${name}' is already quarantined` };
        }
        const trustTier = this.trustStore.getTier(name);
        const capabilityManifest = resolveCapabilityManifest(name, discovered.manifest, undefined, trustTier);
        const record = this.quarantineEngine.quarantine(name, capabilityManifest, reason);
        if (!record) {
            return { ok: false, error: `Failed to quarantine '${name}'` };
        }
        this.state.quarantine[name] = { ...record, revokedCapabilities: [...record.revokedCapabilities] };
        this.saveState();
        logger.warn(`[plugins] ${name}: quarantined — ${reason}`);
        this.notifySubscribers();
        return { ok: true };
    }
    /**
     * liftQuarantine — Remove quarantine from a plugin.
     */
    liftQuarantine(name) {
        if (!this.quarantineEngine.isQuarantined(name)) {
            return { ok: false, error: `Plugin '${name}' is not quarantined` };
        }
        this.quarantineEngine.lift(name);
        const record = this.quarantineEngine.getRecord(name);
        if (record) {
            this.state.quarantine[name] = { ...record, revokedCapabilities: [...record.revokedCapabilities] };
        }
        this.saveState();
        logger.info(`[plugins] ${name}: quarantine lifted`);
        this.notifySubscribers();
        return { ok: true };
    }
    /** Enable a plugin by name. Loads it immediately if deps are available. */
    async enable(name) {
        if (this.isEnabled(name)) {
            return { ok: false, error: `Plugin '${name}' is already enabled` };
        }
        const discovered = this.findDiscoveredPlugin(name);
        if (!discovered) {
            return { ok: false, error: this.notFoundError(name) };
        }
        this.state.enabled[name] = true;
        this.saveState();
        if (this.deps) {
            const loaded = await loadPlugin(discovered, this.deps);
            if (loaded) {
                this.plugins.set(name, loaded);
                this.notifySubscribers();
            }
            else {
                // Revert enable on load failure
                delete this.state.enabled[name];
                this.saveState();
                return { ok: false, error: `Plugin '${name}' failed to load — check logs` };
            }
        }
        return { ok: true };
    }
    /** Disable a plugin by name. Deactivates it immediately if active. */
    async disable(name) {
        if (!this.isEnabled(name)) {
            return { ok: false, error: `Plugin '${name}' is not enabled` };
        }
        const loaded = this.plugins.get(name);
        if (loaded) {
            await unloadPlugin(loaded);
            this.plugins.delete(name);
        }
        delete this.state.enabled[name];
        this.saveState();
        this.notifySubscribers();
        return { ok: true };
    }
    /** Reload all currently enabled plugins (deactivate then reactivate). */
    async reload() {
        const names = Object.keys(this.state.enabled).filter((n) => this.state.enabled[n]);
        let reloaded = 0;
        let failed = 0;
        // Deactivate all
        for (const name of names) {
            const loaded = this.plugins.get(name);
            if (loaded) {
                await unloadPlugin(loaded);
                this.plugins.delete(name);
            }
        }
        // Reactivate with cache busting — append timestamp to force fresh import
        if (this.deps) {
            const discovered = this.discoverPlugins();
            const cacheBust = Date.now();
            for (const d of discovered) {
                if (!this.isEnabled(d.manifest.name))
                    continue;
                // Pass cacheBust so loadPlugin appends ?t=<timestamp> to the import URL,
                // forcing Bun to bypass its module cache and re-execute the file.
                const loaded = await loadPlugin(d, this.deps, cacheBust);
                if (loaded) {
                    this.plugins.set(d.manifest.name, loaded);
                    reloaded++;
                }
                else {
                    failed++;
                }
            }
        }
        this.notifySubscribers();
        return { reloaded, failed };
    }
    /** Returns whether a plugin is marked as enabled in persisted state. */
    isEnabled(name) {
        return this.state.enabled[name] === true;
    }
    /** Returns plugin-specific config for a given plugin name. */
    getPluginConfig(name) {
        return this.state.config[name] ?? {};
    }
    // ─── Private helpers ────────────────────────────────────────────────────────
    async loadEnabledPlugins() {
        if (!this.deps)
            return;
        const discovered = this.discoverPlugins();
        for (const d of discovered) {
            if (!this.isEnabled(d.manifest.name))
                continue;
            const loaded = await loadPlugin(d, this.deps);
            if (loaded) {
                this.plugins.set(d.manifest.name, loaded);
            }
        }
        this.notifySubscribers();
    }
    loadState() {
        try {
            if (existsSync(this.stateFilePath)) {
                const raw = readFileSync(this.stateFilePath, 'utf-8');
                const parsed = JSON.parse(raw);
                this.state.enabled = parsed.enabled ?? {};
                this.state.config = parsed.config ?? {};
                this.state.trust = parsed.trust ?? {};
                this.state.quarantine = parsed.quarantine ?? {};
                // Restore trust and quarantine state into their engines.
                if (Object.keys(this.state.trust).length > 0) {
                    this.trustStore.importRecords(this.state.trust);
                }
                // Note: quarantine records are read-only from persistence; active
                // quarantines take effect when plugins are loaded via PluginLifecycleManager.
            }
        }
        catch (err) {
            logger.warn(`[plugins] Could not load state: ${summarizeError(err)}`);
        }
    }
    saveState() {
        try {
            mkdirSync(dirname(this.stateFilePath), { recursive: true });
            writeFileSync(this.stateFilePath, JSON.stringify(this.state, null, 2), 'utf-8');
        }
        catch (err) {
            logger.warn(`[plugins] Could not save state: ${summarizeError(err)}`);
        }
    }
    discoverPlugins() {
        return discoverPlugins(this.pathOptions);
    }
    findDiscoveredPlugin(name) {
        return this.discoverPlugins().find((plugin) => plugin.manifest.name === name);
    }
    notFoundError(name) {
        return `Plugin '${name}' not found in configured plugin search directories (${this.describeSearchDirectories()})`;
    }
    describeSearchDirectories() {
        return getPluginDirectories(this.pathOptions).join(', ');
    }
    notifySubscribers() {
        for (const callback of this.subscribers) {
            try {
                callback();
            }
            catch (err) {
                logger.warn('[plugins] subscriber callback failed', {
                    error: summarizeError(err),
                });
            }
        }
    }
}
