import { appendFile, mkdir, readFile } from "node:fs/promises";
import path from "node:path";
import { spawn, spawnSync } from "node:child_process";
import { EventEmitter } from "node:events";
import { PluginInstaller } from "./plugin-installer.js";
import { hashManifest } from "./plugin-integrity.js";
import { validatePluginManifest } from "./plugin-manifest.js";
import { PluginStore, toPublicPluginRecord } from "./plugin-store.js";
import { PLUGIN_RUNTIME_PERMISSIONS, PLUGIN_MANIFEST_FILE, } from "./plugin-types.js";
const DEFAULT_PLUGIN_TIMEOUT_MS = 120_000;
const DEFAULT_OUTPUT_LIMIT_BYTES = 1024 * 1024;
const DEFAULT_WEB_PANEL_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
const KNOWN_PLUGIN_PERMISSIONS = new Set(PLUGIN_RUNTIME_PERMISSIONS);
const RUNNING_PLUGIN_JOBS = new Map();
export class PluginService {
    home;
    options;
    store;
    installer;
    events = new EventEmitter();
    jobsRecovered = false;
    constructor(home, options = {}) {
        this.home = home;
        this.options = options;
        this.store = new PluginStore(home);
        this.installer = new PluginInstaller(this.store);
    }
    isEnabled() {
        return this.options.enabled !== false;
    }
    async list() {
        const plugins = await this.store.list();
        return plugins.map(toPublicPluginRecord);
    }
    async get(id) {
        const plugin = await this.store.get(id);
        return plugin ? toPublicPluginRecord(plugin) : undefined;
    }
    async install(request) {
        this.assertEnabled();
        const plugin = await this.installer.install(request);
        await this.log(plugin.id, `Installed ${plugin.name} ${plugin.version} from ${plugin.source.value}`);
        return toPublicPluginRecord(plugin);
    }
    async analyzeInstall(request) {
        this.assertEnabled();
        return this.installer.analyze(request);
    }
    async validate(sourcePath) {
        return this.installer.validatePath(sourcePath);
    }
    async scaffold(request) {
        this.assertEnabled();
        return this.installer.scaffold(request);
    }
    async enable(id) {
        this.assertEnabled();
        const plugin = await this.requirePlugin(id);
        plugin.enabled = true;
        plugin.status = "enabled";
        plugin.lastError = undefined;
        plugin.approvedPermissions = Array.from(new Set([...(plugin.approvedPermissions ?? []), ...plugin.permissions])).sort();
        plugin.updatedAt = new Date().toISOString();
        await this.store.save(plugin);
        await this.log(plugin.id, "Enabled plugin.");
        return toPublicPluginRecord(plugin);
    }
    async disable(id) {
        const plugin = await this.requirePlugin(id);
        plugin.enabled = false;
        plugin.status = "disabled";
        plugin.updatedAt = new Date().toISOString();
        await this.store.save(plugin);
        await this.log(plugin.id, "Disabled plugin.");
        return toPublicPluginRecord(plugin);
    }
    async remove(id) {
        this.assertEnabled();
        const plugin = await this.requirePlugin(id);
        await this.log(id, `Removed ${plugin.name} ${plugin.version}.`);
        await this.store.remove(id);
    }
    async updateSettings(id, settings) {
        this.assertEnabled();
        const plugin = await this.requirePlugin(id);
        plugin.settings = mergeSettings(plugin, settings);
        plugin.updatedAt = new Date().toISOString();
        await this.store.save(plugin);
        await this.log(id, "Updated plugin settings.");
        return toPublicPluginRecord(plugin);
    }
    async updateManifest(id) {
        this.assertEnabled();
        const plugin = await this.requirePlugin(id);
        await applyManifestToPlugin(plugin, plugin.manifestPath);
        plugin.manifestHash = hashManifest({
            id: plugin.id,
            name: plugin.name,
            version: plugin.version,
            description: plugin.description,
            author: plugin.author,
            homepage: plugin.homepage,
            repository: plugin.repository,
            license: plugin.license,
            nordrelay: plugin.nordrelay,
            entry: plugin.entry,
            permissions: plugin.permissions,
            capabilities: plugin.capabilities,
            settings: plugin.settingsSchema,
        });
        plugin.updatedAt = new Date().toISOString();
        await this.store.save(plugin);
        await this.log(id, "Reloaded plugin manifest.");
        return toPublicPluginRecord(plugin);
    }
    async catalog() {
        this.assertEnabled();
        const plugins = (await this.store.list()).filter((plugin) => plugin.enabled);
        const catalog = {
            workflowActions: [],
            webPanels: [],
            commands: [],
            agentAdapters: [],
            chatAdapters: [],
            artifactHandlers: [],
            diagnostics: [],
            collectors: [],
        };
        for (const plugin of plugins) {
            for (const action of plugin.capabilities.workflowActions ?? []) {
                catalog.workflowActions.push({
                    pluginId: plugin.id,
                    actionId: action.id,
                    title: action.title,
                    description: action.description,
                    inputSchema: action.inputSchema,
                    outputVariables: action.outputVariables,
                    timeoutMs: action.timeoutMs,
                });
            }
            for (const panel of plugin.capabilities.webPanels ?? []) {
                catalog.webPanels.push({
                    pluginId: plugin.id,
                    panelId: panel.id,
                    title: panel.title,
                    path: panel.path,
                    permission: panel.permission,
                    inputSchema: panel.inputSchema,
                    aggregateCommand: panel.aggregateCommand,
                    allowClientScript: Boolean(panel.allowClientScript),
                    placement: panel.placement,
                    timeoutMs: panel.timeoutMs,
                });
            }
            for (const command of plugin.capabilities.commands ?? []) {
                catalog.commands.push({
                    pluginId: plugin.id,
                    name: command.name,
                    title: command.title,
                    description: command.description,
                    permission: command.permission,
                    inputSchema: command.inputSchema,
                    timeoutMs: command.timeoutMs,
                });
            }
            for (const adapter of plugin.capabilities.agentAdapters ?? []) {
                catalog.agentAdapters.push({ pluginId: plugin.id, id: adapter.id, title: adapter.title, description: adapter.description });
            }
            for (const adapter of plugin.capabilities.chatAdapters ?? []) {
                catalog.chatAdapters.push({ pluginId: plugin.id, id: adapter.id, title: adapter.title, description: adapter.description });
            }
            for (const handler of plugin.capabilities.artifactHandlers ?? []) {
                catalog.artifactHandlers.push({
                    pluginId: plugin.id,
                    id: handler.id,
                    title: handler.title,
                    description: handler.description,
                    inputSchema: handler.inputSchema,
                    timeoutMs: handler.timeoutMs,
                });
            }
            if (plugin.capabilities.diagnostics) {
                catalog.diagnostics.push({ pluginId: plugin.id, title: `${plugin.name} diagnostics` });
            }
            for (const collector of plugin.capabilities.collectors ?? []) {
                catalog.collectors.push({
                    pluginId: plugin.id,
                    collectorId: collector.id,
                    title: collector.title,
                    description: collector.description,
                    intervalMs: collector.intervalMs,
                    runOnStart: collector.runOnStart,
                    inputSchema: collector.inputSchema,
                    timeoutMs: collector.timeoutMs,
                });
            }
        }
        return catalog;
    }
    async readLog(id, maxBytes = 20000) {
        await this.requirePlugin(id);
        const logPath = this.store.logPath(id);
        try {
            const raw = await readFile(logPath, "utf8");
            return raw.length > maxBytes ? raw.slice(raw.length - maxBytes) : raw;
        }
        catch (error) {
            if (error.code === "ENOENT") {
                return "";
            }
            throw error;
        }
    }
    async checkUpdate(id) {
        this.assertEnabled();
        const plugin = await this.requirePlugin(id);
        const checkedAt = new Date().toISOString();
        try {
            if (plugin.source.type === "github") {
                const latestRevision = gitLsRemote(plugin.source.value, plugin.source.ref);
                const analysis = await this.installer.analyze({
                    source: plugin.source.value,
                    ref: plugin.source.ref,
                    trustLevel: plugin.trustLevel,
                    signaturePublicKey: plugin.signaturePublicKey,
                    requireSignature: plugin.signature?.status === "verified",
                });
                return {
                    id,
                    sourceType: plugin.source.type,
                    currentVersion: plugin.version,
                    latestVersion: analysis.manifest.version,
                    currentRevision: plugin.source.revision,
                    latestRevision,
                    permissionDiff: analysis.permissionDiff,
                    manifestHash: analysis.manifestHash,
                    packageHash: analysis.packageHash,
                    trustLevel: analysis.trustLevel,
                    signature: analysis.signature,
                    updateAvailable: Boolean(latestRevision && latestRevision !== plugin.source.revision) || analysis.manifest.version !== plugin.version,
                    checkedAt,
                };
            }
            const analysis = await this.installer.analyze({
                source: plugin.source.type === "npm" ? `npm:${plugin.source.value}` : plugin.source.value,
                ref: plugin.source.ref,
                trustLevel: plugin.trustLevel,
                signaturePublicKey: plugin.signaturePublicKey,
                requireSignature: plugin.signature?.status === "verified",
            });
            const latestVersion = analysis.manifest.version;
            return {
                id,
                sourceType: plugin.source.type,
                currentVersion: plugin.version,
                latestVersion,
                permissionDiff: analysis.permissionDiff,
                manifestHash: analysis.manifestHash,
                packageHash: analysis.packageHash,
                trustLevel: analysis.trustLevel,
                signature: analysis.signature,
                updateAvailable: Boolean(latestVersion && latestVersion !== plugin.version),
                checkedAt,
            };
        }
        catch (error) {
            return {
                id,
                sourceType: plugin.source.type,
                currentVersion: plugin.version,
                updateAvailable: false,
                checkedAt,
                error: error instanceof Error ? error.message : String(error),
            };
        }
    }
    async update(id, options = {}) {
        this.assertEnabled();
        const plugin = await this.requirePlugin(id);
        const updated = await this.installer.install({
            source: plugin.source.type === "npm" ? `npm:${plugin.source.value}` : plugin.source.value,
            ref: plugin.source.ref,
            enable: plugin.enabled,
            approvePermissions: false,
            approvePermissionDiff: options.approvePermissionDiff,
            trustLevel: plugin.trustLevel,
            signaturePublicKey: options.signaturePublicKey ?? plugin.signaturePublicKey,
            requireSignature: options.requireSignature ?? plugin.signature?.status === "verified",
            force: true,
        });
        await this.log(id, `Updated plugin from ${plugin.version} to ${updated.version}.`);
        return toPublicPluginRecord(updated);
    }
    async listJobs(pluginId) {
        await this.recoverInterruptedJobs();
        const jobs = await this.store.listJobs(pluginId);
        return (pluginId ? jobs.filter((job) => job.pluginId === pluginId) : jobs)
            .slice()
            .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
    }
    async getJob(pluginId, jobId) {
        await this.recoverInterruptedJobs();
        return this.store.getJob(pluginId, jobId);
    }
    async startCommandJob(pluginId, command, input) {
        this.assertEnabled();
        await this.requireEnabledPlugin(pluginId);
        const job = {
            id: randomJobId(),
            pluginId,
            title: `${pluginId} ${command}`,
            command,
            status: "queued",
            input,
            logs: [{ timestamp: new Date().toISOString(), level: "info", message: "Job queued." }],
            createdAt: new Date().toISOString(),
        };
        await this.recoverInterruptedJobs();
        await this.store.upsertJob(job);
        this.emitJobs(job.pluginId);
        void this.runCommandJob(job);
        return { ...job, logs: [...job.logs] };
    }
    async cancelJob(pluginId, jobId) {
        const job = await this.getJob(pluginId, jobId);
        if (!job)
            throw new Error(`Plugin job not found: ${jobId}`);
        job.cancelRequested = true;
        const running = RUNNING_PLUGIN_JOBS.get(pluginJobKey(this.home, job.id));
        if (running)
            running.controller.abort();
        if (job.status === "queued" || job.status === "running") {
            job.status = "cancelled";
            job.finishedAt = new Date().toISOString();
            job.logs.push({ timestamp: job.finishedAt, level: "warn", message: "Cancellation requested." });
        }
        const persisted = await this.store.upsertJob(job);
        this.emitJobs(pluginId);
        return { ...persisted, logs: [...persisted.logs] };
    }
    subscribeEvents(pluginId, listener) {
        const eventName = pluginEventName(pluginId);
        const handler = (event) => listener(event);
        this.events.on(eventName, handler);
        void this.emitJobs(pluginId);
        return () => {
            this.events.off(eventName, handler);
        };
    }
    async runCommandJob(job) {
        const latest = await this.store.getJob(job.pluginId, job.id);
        if (latest)
            Object.assign(job, latest);
        if (job.cancelRequested || job.status === "cancelled") {
            job.status = "cancelled";
            job.finishedAt = job.finishedAt ?? new Date().toISOString();
            await this.store.upsertJob(job);
            this.emitJobs(job.pluginId);
            return;
        }
        const controller = new AbortController();
        RUNNING_PLUGIN_JOBS.set(pluginJobKey(this.home, job.id), { controller });
        job.status = "running";
        job.startedAt = new Date().toISOString();
        job.logs.push({ timestamp: job.startedAt, level: "info", message: "Job started." });
        await this.store.upsertJob(job);
        this.emitJobs(job.pluginId);
        try {
            if (job.cancelRequested) {
                job.status = "cancelled";
                job.finishedAt = new Date().toISOString();
                await this.store.upsertJob(job);
                this.emitJobs(job.pluginId);
                return;
            }
            const result = await this.invokeCapability(job.pluginId, "command", job.command ?? "", job.input, { signal: controller.signal });
            job.result = result;
            job.cancelRequested = job.cancelRequested || controller.signal.aborted || result.cancelled || undefined;
            job.status = controller.signal.aborted || result.cancelled || job.cancelRequested ? "cancelled" : result.ok === false ? "failed" : "completed";
            job.finishedAt = new Date().toISOString();
            job.logs.push({
                timestamp: job.finishedAt,
                level: job.status === "cancelled" ? "warn" : result.ok === false ? "error" : "info",
                message: job.status === "cancelled" ? "Job cancelled." : result.ok === false ? result.stderr ?? "Plugin command failed." : "Job completed.",
            });
        }
        catch (error) {
            job.status = controller.signal.aborted || job.cancelRequested ? "cancelled" : "failed";
            job.finishedAt = new Date().toISOString();
            job.logs.push({
                timestamp: job.finishedAt,
                level: job.status === "cancelled" ? "warn" : "error",
                message: job.status === "cancelled" ? "Job cancelled." : error instanceof Error ? error.message : String(error),
            });
        }
        finally {
            RUNNING_PLUGIN_JOBS.delete(pluginJobKey(this.home, job.id));
            await this.store.upsertJob(job);
            this.emitJobs(job.pluginId);
        }
    }
    emitJobs(pluginId) {
        const eventName = pluginEventName(pluginId);
        void this.store.listJobs(pluginId)
            .then((jobs) => {
            this.events.emit(eventName, {
                type: "jobs",
                pluginId,
                jobs: jobs.slice().sort((a, b) => b.createdAt.localeCompare(a.createdAt)),
                at: new Date().toISOString(),
            });
        })
            .catch(() => {
            // Event listeners are best-effort; API reads still expose the durable state.
        });
    }
    async recoverInterruptedJobs() {
        if (this.jobsRecovered)
            return;
        this.jobsRecovered = true;
        const payload = await this.store.readJobs();
        let changed = false;
        const now = new Date().toISOString();
        for (const job of payload.jobs) {
            if ((job.status === "queued" || job.status === "running") && !RUNNING_PLUGIN_JOBS.has(pluginJobKey(this.home, job.id))) {
                job.status = "failed";
                job.finishedAt = job.finishedAt ?? now;
                job.logs = Array.isArray(job.logs) ? job.logs : [];
                job.logs.push({ timestamp: now, level: "error", message: "Job interrupted by NordRelay restart." });
                changed = true;
            }
        }
        if (changed) {
            await this.store.writeJobs(payload);
        }
    }
    async rollback(id, version) {
        this.assertEnabled();
        const plugin = await this.requirePlugin(id);
        const versions = await this.store.installedVersions(id);
        const targetVersion = version || versions.find((candidate) => candidate !== plugin.version);
        if (!targetVersion) {
            throw new Error(`No rollback target found for plugin ${id}.`);
        }
        const installPath = this.store.installVersionPath(id, targetVersion);
        const manifestPath = path.join(installPath, PLUGIN_MANIFEST_FILE);
        await applyManifestToPlugin(plugin, manifestPath);
        plugin.installPath = installPath;
        plugin.manifestPath = manifestPath;
        plugin.updatedAt = new Date().toISOString();
        await this.store.save(plugin);
        await this.log(id, `Rolled back plugin to ${targetVersion}.`);
        return toPublicPluginRecord(plugin);
    }
    async invokeWorkflowAction(pluginId, actionId, input) {
        return this.invokeCapability(pluginId, "workflow-action", actionId, input);
    }
    async invokeCommand(pluginId, command, input) {
        return this.invokeCapability(pluginId, "command", command, input);
    }
    async invokeWebPanel(pluginId, panelId, input) {
        return this.invokeCapability(pluginId, "web-panel", panelId, input);
    }
    async invokeArtifactHandler(pluginId, handlerId, input) {
        return this.invokeCapability(pluginId, "artifact-handler", handlerId, input);
    }
    async invokeDiagnostics(pluginId, input = {}) {
        return this.invokeCapability(pluginId, "diagnostics", "diagnostics", input);
    }
    async invokeCollector(pluginId, collectorId, input = {}) {
        return this.invokeCapability(pluginId, "collector", collectorId, input);
    }
    async invokeCapability(pluginId, type, capabilityId, input, runOptions = {}) {
        this.assertEnabled();
        const plugin = await this.requireEnabledPlugin(pluginId);
        if (!plugin.entry) {
            throw new Error(`Plugin ${pluginId} has no executable entry.`);
        }
        const capability = this.requireCapability(plugin, type, capabilityId);
        this.assertPluginPermissions(plugin);
        const entry = path.resolve(plugin.installPath, plugin.entry);
        if (!entry.startsWith(path.resolve(plugin.installPath))) {
            throw new Error("Plugin entry resolves outside the plugin directory.");
        }
        const dataDir = this.store.dataPath(pluginId);
        await mkdir(dataDir, { recursive: true });
        const permissions = approvedRuntimePermissions(plugin);
        const context = filterHostContext(await this.hostContext(permissions), permissions);
        const request = {
            protocolVersion: 1,
            type,
            pluginId,
            capabilityId,
            actionId: type === "workflow-action" ? capabilityId : undefined,
            command: type === "command" ? capabilityId : undefined,
            panelId: type === "web-panel" ? capabilityId : undefined,
            handlerId: type === "artifact-handler" ? capabilityId : undefined,
            collectorId: type === "collector" ? capabilityId : undefined,
            input,
            settings: plugin.settings,
            dataDir,
            permissions,
            context,
        };
        await this.log(pluginId, `Invoking ${type} ${capabilityId}.`);
        const startedAt = new Date().toISOString();
        const startedMs = Date.now();
        const result = await runPlugin(entry, request, {
            pluginId,
            dataDir,
            timeoutMs: capability.timeoutMs ?? this.options.defaultTimeoutMs ?? DEFAULT_PLUGIN_TIMEOUT_MS,
            outputLimitBytes: this.options.outputLimitBytes ?? (type === "web-panel" ? DEFAULT_WEB_PANEL_OUTPUT_LIMIT_BYTES : DEFAULT_OUTPUT_LIMIT_BYTES),
            ...runOptions,
        });
        const durationMs = Date.now() - startedMs;
        const normalizedResult = normalizePluginResult(result);
        const normalized = {
            ...normalizedResult,
            variables: mergeOutputVariables(normalizedResult, capability.outputVariables),
            durationMs,
        };
        if (!normalized.cancelled) {
            await this.recordInvocation(plugin, normalized, startedAt, new Date().toISOString(), durationMs);
        }
        await this.log(pluginId, `${type} ${capabilityId} completed: ${normalized.cancelled ? "cancelled" : normalized.ok ? "ok" : "failed"} (${durationMs}ms).`);
        return normalized;
    }
    requireCapability(plugin, type, capabilityId) {
        if (type === "workflow-action") {
            const action = (plugin.capabilities.workflowActions ?? []).find((item) => item.id === capabilityId);
            if (!action)
                throw new Error(`Plugin ${plugin.id} does not provide workflow action ${capabilityId}.`);
            return { id: action.id, timeoutMs: action.timeoutMs, outputVariables: action.outputVariables };
        }
        if (type === "command") {
            const command = (plugin.capabilities.commands ?? []).find((item) => item.name === capabilityId);
            if (!command)
                throw new Error(`Plugin ${plugin.id} does not provide command ${capabilityId}.`);
            return { id: command.name, timeoutMs: command.timeoutMs };
        }
        if (type === "web-panel") {
            const panel = (plugin.capabilities.webPanels ?? []).find((item) => item.id === capabilityId);
            if (!panel)
                throw new Error(`Plugin ${plugin.id} does not provide web panel ${capabilityId}.`);
            return { id: panel.id, timeoutMs: panel.timeoutMs };
        }
        if (type === "artifact-handler") {
            const handler = (plugin.capabilities.artifactHandlers ?? []).find((item) => item.id === capabilityId);
            if (!handler)
                throw new Error(`Plugin ${plugin.id} does not provide artifact handler ${capabilityId}.`);
            return { id: handler.id, timeoutMs: handler.timeoutMs };
        }
        if (!plugin.capabilities.diagnostics) {
            if (type === "collector") {
                const collector = (plugin.capabilities.collectors ?? []).find((item) => item.id === capabilityId);
                if (!collector)
                    throw new Error(`Plugin ${plugin.id} does not provide collector ${capabilityId}.`);
                return { id: collector.id, timeoutMs: collector.timeoutMs };
            }
            throw new Error(`Plugin ${plugin.id} does not provide diagnostics.`);
        }
        if (type === "collector") {
            const collector = (plugin.capabilities.collectors ?? []).find((item) => item.id === capabilityId);
            if (!collector)
                throw new Error(`Plugin ${plugin.id} does not provide collector ${capabilityId}.`);
            return { id: collector.id, timeoutMs: collector.timeoutMs };
        }
        return { id: "diagnostics" };
    }
    async hostContext(permissions) {
        const base = {
            runtime: {
                version: this.options.version,
                nodeName: this.options.nodeName,
                nodeId: this.options.nodeId,
                platform: this.options.platform ?? process.platform,
                workspace: this.options.workspace,
            },
        };
        const extra = await this.options.hostContext?.(permissions);
        return { ...base, ...(extra ?? {}) };
    }
    assertEnabled() {
        if (this.options.enabled === false) {
            throw new Error("Plugins are disabled by NORDRELAY_PLUGINS_ENABLED=false.");
        }
    }
    assertPluginPermissions(plugin) {
        for (const permission of plugin.permissions) {
            if (!KNOWN_PLUGIN_PERMISSIONS.has(permission)) {
                throw new Error(`Plugin ${plugin.id} declares unknown permission ${permission}.`);
            }
            if (!plugin.approvedPermissions.includes(permission)) {
                throw new Error(`Plugin ${plugin.id} permission ${permission} is not approved.`);
            }
        }
    }
    async requirePlugin(id) {
        const plugin = await this.store.get(id);
        if (!plugin) {
            throw new Error(`Plugin not found: ${id}`);
        }
        return plugin;
    }
    async requireEnabledPlugin(id) {
        const plugin = await this.requirePlugin(id);
        if (!plugin.enabled) {
            throw new Error(`Plugin is disabled: ${id}`);
        }
        return plugin;
    }
    async recordInvocation(plugin, result, startedAt, finishedAt, durationMs) {
        const latest = await this.requirePlugin(plugin.id);
        const previous = latest.metrics ?? { invocations: 0, failures: 0, totalDurationMs: 0 };
        latest.metrics = {
            invocations: previous.invocations + 1,
            failures: previous.failures + (result.ok ? 0 : 1),
            totalDurationMs: previous.totalDurationMs + durationMs,
            lastStartedAt: startedAt,
            lastFinishedAt: finishedAt,
            lastDurationMs: durationMs,
            lastStatus: result.ok ? "ok" : "failed",
            lastError: result.ok ? undefined : result.stderr ?? stringOutput(result.output) ?? "Plugin failed.",
        };
        latest.status = result.ok ? latest.status : "error";
        latest.lastError = result.ok ? undefined : latest.metrics.lastError;
        latest.updatedAt = finishedAt;
        await this.store.save(latest);
    }
    async log(id, message) {
        await this.store.ensure();
        const line = `${new Date().toISOString()} ${message}\n`;
        await appendFile(this.store.logPath(id), line, "utf8");
    }
}
function mergeSettings(plugin, input) {
    const next = {};
    for (const setting of plugin.settingsSchema) {
        const incoming = input[setting.key];
        if (setting.type === "secret" && incoming === "") {
            next[setting.key] = plugin.settings[setting.key] ?? "";
            continue;
        }
        if (incoming === undefined) {
            next[setting.key] = plugin.settings[setting.key] ?? setting.default ?? (setting.type === "boolean" ? false : "");
            continue;
        }
        next[setting.key] = coerceSettingValue(setting.type, incoming);
    }
    return next;
}
async function applyManifestToPlugin(plugin, manifestPath) {
    const raw = await readFile(manifestPath, "utf8");
    const validation = validatePluginManifest(JSON.parse(raw));
    if (!validation.ok || !validation.manifest) {
        throw new Error(validation.issues.map((issue) => issue.message).join("; ") || "Invalid plugin manifest.");
    }
    const manifest = validation.manifest;
    plugin.name = manifest.name;
    plugin.version = manifest.version;
    plugin.description = manifest.description;
    plugin.author = manifest.author;
    plugin.homepage = manifest.homepage;
    plugin.repository = manifest.repository;
    plugin.license = manifest.license;
    plugin.nordrelay = manifest.nordrelay;
    plugin.entry = manifest.entry;
    plugin.permissions = manifest.permissions ?? [];
    plugin.approvedPermissions = (plugin.approvedPermissions ?? []).filter((permission) => plugin.permissions.includes(permission));
    plugin.capabilities = manifest.capabilities ?? {};
    plugin.settingsSchema = manifest.settings ?? [];
    plugin.settings = mergeSettings(plugin, plugin.settings);
}
function coerceSettingValue(type, value) {
    if (type === "boolean") {
        return value === true || value === "true" || value === "1" || value === 1;
    }
    if (type === "number") {
        const parsed = typeof value === "number" ? value : Number(value);
        return Number.isFinite(parsed) ? parsed : 0;
    }
    return typeof value === "string" ? value : String(value ?? "");
}
function runPlugin(entry, payload, options) {
    return new Promise((resolve) => {
        if (options.signal?.aborted) {
            resolve({ ok: false, stderr: "Plugin job cancelled.", cancelled: true, exitCode: null });
            return;
        }
        let settled = false;
        let stdout = "";
        let stderr = "";
        const child = spawn(process.execPath, [entry], {
            cwd: options.dataDir,
            stdio: ["pipe", "pipe", "pipe"],
            shell: false,
            env: safePluginEnv(options.pluginId, options.dataDir),
        });
        options.onChild?.(child);
        let timeout;
        let killTimer;
        const forceKillSoon = () => {
            killTimer = setTimeout(() => child.kill("SIGKILL"), 1000);
            killTimer.unref?.();
        };
        const terminate = (message, extra = {}) => {
            if (settled)
                return;
            if (!child.killed)
                child.kill("SIGTERM");
            forceKillSoon();
            finish({ ok: false, stderr: `${stderr}\n${message}`.trim(), exitCode: null, ...extra });
        };
        const abortHandler = () => terminate("Plugin job cancelled.", { cancelled: true });
        options.signal?.addEventListener("abort", abortHandler, { once: true });
        const finish = (result) => {
            if (settled)
                return;
            settled = true;
            if (timeout)
                clearTimeout(timeout);
            options.signal?.removeEventListener("abort", abortHandler);
            resolve({
                ...result,
                stdout,
                stderr: result.stderr ?? stderr,
            });
        };
        timeout = setTimeout(() => {
            terminate("Plugin timed out.", { timedOut: true });
        }, Math.max(100, options.timeoutMs));
        child.stdout.setEncoding("utf8");
        child.stderr.setEncoding("utf8");
        child.stdout.on("data", (chunk) => {
            stdout = appendLimited(stdout, chunk, options.outputLimitBytes);
            if (stdout.length >= options.outputLimitBytes) {
                terminate("Plugin stdout exceeded output limit.");
            }
        });
        child.stderr.on("data", (chunk) => {
            stderr = appendLimited(stderr, chunk, options.outputLimitBytes);
            if (stderr.length >= options.outputLimitBytes) {
                terminate("Plugin stderr exceeded output limit.");
            }
        });
        child.on("error", (error) => {
            finish({ ok: false, stderr: error.message, exitCode: null });
        });
        child.on("close", (code) => {
            if (settled)
                return;
            if (code !== 0) {
                finish({ ok: false, stderr: stderr || `Plugin exited with code ${code}.`, exitCode: code });
                return;
            }
            finish(parsePluginOutput(stdout, stderr, code));
        });
        child.stdin.end(`${JSON.stringify(payload)}\n`);
    });
}
function pluginJobKey(home, jobId) {
    return `${home}\0${jobId}`;
}
function pluginEventName(pluginId) {
    return `plugin:${pluginId}`;
}
function parsePluginOutput(stdout, stderr, exitCode) {
    const trimmed = stdout.trim();
    if (!trimmed) {
        return { ok: true, stdout, stderr, exitCode };
    }
    try {
        const parsed = JSON.parse(trimmed);
        if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "ok" in parsed) {
            return { ...parsed, stdout, stderr, exitCode };
        }
        return { ok: true, output: parsed, stdout, stderr, exitCode };
    }
    catch {
        return { ok: true, output: trimmed, stdout, stderr, exitCode };
    }
}
function safePluginEnv(pluginId, dataDir) {
    const env = {
        NORDRELAY_PLUGIN: "1",
        NORDRELAY_PLUGIN_ID: pluginId,
        NORDRELAY_PLUGIN_DATA_DIR: dataDir,
        HOME: dataDir,
        USERPROFILE: dataDir,
        TMPDIR: dataDir,
        TEMP: dataDir,
        TMP: dataDir,
        NODE_ENV: process.env.NODE_ENV ?? "production",
    };
    if (process.env.PATH)
        env.PATH = process.env.PATH;
    if (process.platform === "win32") {
        for (const key of ["SystemRoot", "WINDIR", "ComSpec", "PATHEXT"]) {
            if (process.env[key])
                env[key] = process.env[key];
        }
    }
    return env;
}
function appendLimited(current, chunk, limit) {
    const next = current + chunk;
    return next.length > limit ? next.slice(0, limit) : next;
}
function approvedRuntimePermissions(plugin) {
    return plugin.permissions
        .filter((permission) => plugin.approvedPermissions.includes(permission))
        .filter((permission) => KNOWN_PLUGIN_PERMISSIONS.has(permission));
}
function filterHostContext(context, permissions) {
    const allowed = new Set(permissions);
    const filtered = {};
    if (allowed.has("runtime.read"))
        filtered.runtime = context.runtime;
    if (allowed.has("sessions.read")) {
        filtered.session = context.session;
        filtered.sessions = context.sessions;
    }
    if (allowed.has("usage.read"))
        filtered.usage = context.usage;
    if (allowed.has("activity.read"))
        filtered.activity = context.activity;
    if (allowed.has("artifacts.read") || allowed.has("files.read"))
        filtered.artifacts = context.artifacts;
    if (allowed.has("workflows.read"))
        filtered.workflows = context.workflows;
    if (allowed.has("peers.read"))
        filtered.peers = context.peers;
    if (allowed.has("diagnostics.read"))
        filtered.diagnostics = context.diagnostics;
    if (allowed.has("settings.read"))
        filtered.settings = context.settings;
    return filtered;
}
function normalizePluginResult(result) {
    const output = parsePluginResultRecord(result.output);
    const stdout = parsePluginResultRecord(result.stdout);
    const promoted = pluginResultLike(output) ? output : pluginResultLike(stdout) ? stdout : undefined;
    const fields = promoted ?? output;
    const panel = normalizePluginPanel(result.panel ?? recordField(fields?.panel));
    return {
        ...result,
        ok: typeof promoted?.ok === "boolean" ? promoted.ok : result.ok,
        output: promoted && "output" in promoted ? promoted.output : result.output,
        variables: normalizeVariables(result.variables ?? recordField(fields?.variables)),
        panel,
        html: extractPluginHtml(result.html) ?? extractPluginHtml(fields?.html),
        text: stringField(result.text ?? fields?.text),
        artifacts: Array.isArray(result.artifacts) ? result.artifacts : Array.isArray(fields?.artifacts) ? fields.artifacts : undefined,
        diagnostics: result.diagnostics ?? fields?.diagnostics,
    };
}
function normalizePluginPanel(value) {
    const record = parsePluginResultRecord(value);
    if (!record)
        return undefined;
    const html = extractPluginHtml(record.html);
    const script = stringField(record.script);
    const styles = stringField(record.styles);
    const ui = record.ui;
    return html || script || styles || ui !== undefined ? { html, script, styles, ui } : undefined;
}
function mergeOutputVariables(result, mapping) {
    const existing = normalizeVariables(result.variables);
    if (!mapping || Object.keys(mapping).length === 0) {
        return existing;
    }
    const output = result.output;
    const mapped = { ...(existing ?? {}) };
    for (const [variable, pathExpression] of Object.entries(mapping)) {
        const value = valueAtPath(output, pathExpression);
        if (value !== undefined)
            mapped[variable] = String(value);
    }
    return Object.keys(mapped).length ? mapped : undefined;
}
function recordField(value) {
    return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
}
function parsePluginResultRecord(value, depth = 0) {
    if (value === undefined || value === null || depth > 10)
        return undefined;
    if (typeof value === "string") {
        const text = value.trim();
        if (!text)
            return undefined;
        try {
            return parsePluginResultRecord(JSON.parse(text), depth + 1);
        }
        catch {
            const unescaped = parseEscapedPluginResultString(text);
            return unescaped === undefined ? undefined : parsePluginResultRecord(unescaped, depth + 1);
        }
    }
    return recordField(value);
}
function parseEscapedPluginResultString(text) {
    if (!/^[{\[]/.test(text) || !text.includes("\\\""))
        return undefined;
    const normalized = text.replace(/\\"/g, "\"");
    try {
        JSON.parse(normalized);
        return normalized;
    }
    catch {
        return undefined;
    }
}
function pluginResultLike(value) {
    return Boolean(value && ("ok" in value ||
        "html" in value ||
        "text" in value ||
        "output" in value ||
        "variables" in value ||
        "artifacts" in value ||
        "diagnostics" in value));
}
function extractPluginHtml(value, depth = 0) {
    if (value === undefined || value === null || depth > 10)
        return undefined;
    if (typeof value === "string") {
        const text = value.trim();
        if (!text)
            return undefined;
        if (text.startsWith("<"))
            return value;
        const parsed = parsePluginResultRecord(text);
        if (parsed)
            return extractPluginHtml(parsed.html, depth + 1) ?? extractPluginHtml(parsed.output, depth + 1) ?? extractPluginHtml(parsed.stdout, depth + 1);
        return undefined;
    }
    const record = recordField(value);
    return record ? extractPluginHtml(record.html, depth + 1) ?? extractPluginHtml(record.output, depth + 1) ?? extractPluginHtml(record.stdout, depth + 1) : undefined;
}
function normalizeVariables(value) {
    if (!value || typeof value !== "object" || Array.isArray(value))
        return undefined;
    const variables = {};
    for (const [key, item] of Object.entries(value)) {
        variables[key] = String(item ?? "");
    }
    return Object.keys(variables).length ? variables : undefined;
}
function valueAtPath(value, expression) {
    const parts = expression.split(".").map((part) => part.trim()).filter(Boolean);
    let current = value;
    for (const part of parts) {
        if (!current || typeof current !== "object" || Array.isArray(current))
            return undefined;
        current = current[part];
    }
    return current;
}
function stringField(value) {
    return typeof value === "string" ? value : undefined;
}
function stringOutput(value) {
    if (typeof value === "string")
        return value;
    if (value === undefined)
        return undefined;
    try {
        return JSON.stringify(value);
    }
    catch {
        return String(value);
    }
}
function randomJobId() {
    return `job_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
}
function gitLsRemote(repoUrl, ref) {
    const args = ["ls-remote", repoUrl, ref ?? "HEAD"];
    const result = spawnSync("git", args, { encoding: "utf8", stdio: "pipe" });
    if (result.status !== 0) {
        throw new Error(result.stderr || result.stdout || result.error?.message || "git ls-remote failed");
    }
    const line = result.stdout.split(/\r?\n/).find(Boolean);
    return line?.split(/\s+/)[0];
}
