"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const logging_1 = require("@signalk/streams/logging");
const express_1 = __importDefault(require("express"));
const fs_1 = __importDefault(require("fs"));
const util_1 = require("util");
const lodash_1 = __importDefault(require("lodash"));
const path_1 = __importDefault(require("path"));
const constants_1 = require("../constants");
const debug_1 = require("../debug");
const plugin_paths_1 = require("../plugin-paths");
const serialports_1 = require("../serialports");
const debug = (0, debug_1.createDebug)('signalk-server:interfaces:plugins');
const deltastats_1 = require("../deltastats");
const modules_1 = require("../modules");
const put = require('../put');
const _putPath = put.putPath;
const getModulePublic = require('../config/get').getModulePublic;
const requestResponse_1 = require("../requestResponse");
const signalk_schema_1 = require("@signalk/signalk-schema");
const pluginid_1 = require("../pluginid");
const atomicWrite_1 = require("../atomicWrite");
// #521 Returns path to load plugin-config assets.
const getPluginConfigPublic = getModulePublic('@signalk/plugin-config');
const DEFAULT_ENABLED_PLUGINS = process.env.DEFAULTENABLEDPLUGINS
    ? process.env.DEFAULTENABLEDPLUGINS.split(',')
    : [];
function backwardsCompat(url) {
    return [`${constants_1.SERVERROUTESPREFIX}${url}`, url];
}
module.exports = (theApp) => {
    const onStopHandlers = {};
    const appNodeModules = path_1.default.join(theApp.config.appPath, 'node_modules/');
    return {
        async start() {
            ensureExists(path_1.default.join(theApp.config.configPath, plugin_paths_1.PLUGIN_CONFIG_DATA_DIR));
            theApp.getPluginsList = async (enabled) => {
                return await getPluginsList(enabled);
            };
            theApp.use(backwardsCompat('/plugins/configure'), express_1.default.static(getPluginConfigPublic(theApp)));
            theApp.get(backwardsCompat('/plugins'), (req, res) => {
                getPluginResponseInfos()
                    .then((json) => res.json(json))
                    .catch((err) => {
                    console.error(err);
                    res.status(500);
                    res.json(err);
                });
            });
            await startPlugins(theApp);
        }
    };
    function getPluginResponseInfos() {
        const providerStatus = theApp.getProviderStatus();
        return Promise.all(lodash_1.default.sortBy(theApp.plugins, [
            (plugin) => {
                return plugin.name;
            }
        ]).map((plugin) => getPluginResponseInfo(plugin, providerStatus)));
    }
    function emitPluginsChanged() {
        getPluginResponseInfos()
            .then((plugins) => {
            theApp.emit('serverevent', {
                type: 'PLUGINS_CHANGED',
                from: 'signalk-server',
                data: plugins
            });
        })
            .catch((err) => {
            console.error('Failed to emit PLUGINS_CHANGED:', err);
        });
    }
    function getPluginsList(enabled) {
        return getPluginResponseInfos().then((pa) => {
            const res = pa.map((p) => {
                return {
                    id: p.id,
                    name: p.name,
                    version: p.version,
                    enabled: p.data.enabled ?? false
                };
            });
            if (typeof enabled === 'undefined') {
                return res;
            }
            else {
                return res.filter((p) => {
                    return p.enabled === enabled;
                });
            }
        });
    }
    function isBundledPlugin(plugin) {
        return plugin.packageLocation === appNodeModules;
    }
    function getPluginResponseInfo(plugin, providerStatus) {
        return new Promise((resolve, reject) => {
            let data = null;
            try {
                data = getPluginOptions(plugin.id);
            }
            catch (e) {
                console.error(e.code + ' ' + e.path);
            }
            if (data && lodash_1.default.isUndefined(data.enabled) && plugin.enabledByDefault) {
                data.enabled = true;
            }
            Promise.all([
                Promise.resolve(typeof plugin.schema === 'function'
                    ? (() => {
                        try {
                            return plugin.schema();
                        }
                        catch (e) {
                            console.error(e);
                            // return a fake schema to inform the user
                            // downside is that saving this may overwrite an existing configuration
                            return {
                                type: 'object',
                                required: ['error'],
                                properties: {
                                    error: {
                                        title: 'Error loading plugin configuration schema, check server log',
                                        type: 'string'
                                    }
                                }
                            };
                        }
                    })()
                    : plugin.schema),
                Promise.resolve(typeof plugin.uiSchema === 'function'
                    ? plugin.uiSchema()
                    : plugin.uiSchema)
            ])
                .then(([schema, uiSchema]) => {
                const status = providerStatus.find((p) => p.id === plugin.name);
                const statusMessage = status ? status.message : '';
                if (schema === undefined) {
                    console.error(`Error: plugin ${plugin.id} is missing configuration schema`);
                }
                resolve({
                    id: plugin.id,
                    name: plugin.name,
                    packageName: plugin.packageName,
                    keywords: plugin.keywords,
                    version: plugin.version,
                    description: plugin.description,
                    schema: schema || {},
                    statusMessage,
                    uiSchema,
                    state: plugin.state,
                    data,
                    type: plugin.type, // Include type to identify WASM plugins in Admin UI
                    bundled: isBundledPlugin(plugin)
                });
            })
                .catch((err) => {
                reject(err);
            });
        });
    }
    function ensureExists(dir) {
        if (!fs_1.default.existsSync(dir)) {
            fs_1.default.mkdirSync(dir);
        }
    }
    function pathForPluginId(id) {
        return (0, plugin_paths_1.pluginConfigPath)(theApp.config.configPath, id);
    }
    function dirForPluginId(id) {
        const dirName = (0, plugin_paths_1.pluginDataDir)(theApp.config.configPath, id);
        ensureExists(dirName);
        return dirName;
    }
    function savePluginOptions(pluginId, data, callback) {
        try {
            (0, atomicWrite_1.atomicWriteFileSync)(pathForPluginId(pluginId), JSON.stringify(data, null, 2));
            callback(null);
        }
        catch (err) {
            callback(err);
        }
    }
    function getPluginOptions(id) {
        let optionsAsString = '{}';
        try {
            optionsAsString = fs_1.default.readFileSync(pathForPluginId(id), 'utf8');
        }
        catch (_e) {
            debug('Could not find options for plugin ' +
                id +
                ', returning empty options: ');
        }
        try {
            const options = JSON.parse(optionsAsString);
            if (optionsAsString === '{}' && DEFAULT_ENABLED_PLUGINS.includes(id)) {
                debug('Override enable for plugin ' + id);
                options.enabled = true;
            }
            if (process.env.DISABLEPLUGINS) {
                debug('Plugins disabled by configuration');
                options.enabled = false;
            }
            debug(optionsAsString);
            return options;
        }
        catch (e) {
            console.error('Could not parse JSON options:' + e.message + ' ' + optionsAsString);
            return {};
        }
    }
    async function startPlugins(app) {
        app.plugins = [];
        app.pluginsMap = {};
        // Expose getPluginOptions for use by other modules (e.g., webapps.js)
        app.getPluginOptions = getPluginOptions;
        // Discover both Node.js and WASM plugins
        const jsModules = (0, modules_1.modulesWithKeyword)(app.config, 'signalk-node-server-plugin');
        const wasmModules = (0, modules_1.modulesWithKeyword)(app.config, 'signalk-wasm-plugin');
        // Combine and deduplicate by module name (a plugin might have both keywords)
        const seenModules = new Set();
        const modules = [...jsModules, ...wasmModules].filter((moduleData) => {
            if (seenModules.has(moduleData.module)) {
                return false;
            }
            seenModules.add(moduleData.module);
            return true;
        });
        await Promise.all(modules.map((moduleData) => {
            return registerPlugin(app, moduleData.module, moduleData.metadata, moduleData.location);
        }));
    }
    function handleMessageWrapper(app, id) {
        const pluginsLoggingEnabled = lodash_1.default.isUndefined(app.config.settings.enablePluginLogging) ||
            app.config.settings.enablePluginLogging;
        return (providerId, data) => {
            const plugin = app.pluginsMap[id];
            if (!lodash_1.default.isUndefined(plugin) &&
                pluginsLoggingEnabled &&
                plugin.enableLogging) {
                if (!plugin.logger) {
                    plugin.logger = (0, logging_1.getLogger)(app, providerId);
                }
                plugin.logger(data);
            }
            app.handleMessage(id, data);
        };
    }
    function getSelfPath(aPath) {
        return lodash_1.default.get(theApp.signalk.self, aPath);
    }
    function getPath(aPath) {
        if (aPath === '/sources') {
            return {
                ...theApp.signalk.retrieve().sources,
                ...theApp.deltaCache.getSources()
            };
        }
        else {
            return lodash_1.default.get(theApp.signalk.retrieve(), aPath);
        }
    }
    function putSelfPath(aPath, value, updateCb, source) {
        return _putPath(theApp, 'vessels.self', aPath, { value, source }, null, null, updateCb);
    }
    function putPath(aPath, value, updateCb, source) {
        const parts = aPath.length > 0 ? aPath.split('.') : [];
        if (parts.length <= 2) {
            updateCb(new Error(`Put path begin with a two part context:${aPath}`));
            return;
        }
        const context = `${parts[0]}.${parts[1]}`;
        const skpath = parts.slice(2).join('.');
        return _putPath(theApp, context, skpath, { value, source }, null, null, updateCb);
    }
    function getSerialPorts() {
        return (0, serialports_1.listAllSerialPorts)();
    }
    async function registerPlugin(app, pluginName, metadata, location) {
        debug('Registering plugin ' + pluginName);
        try {
            // Check if this is a WASM plugin (wasmManifest is now part of NpmPackageData)
            if (metadata.wasmManifest) {
                // This is a WASM plugin - check if WASM interface is enabled
                const wasmEnabled = app.config.settings.interfaces?.wasm !== false;
                if (!wasmEnabled) {
                    debug(`WASM plugin ${pluginName} discovered but WASM interface disabled - registering minimal entry`);
                    // Create minimal plugin entry so it appears in Plugin Config with "No WASM" badge
                    // Derive plugin ID from npm package name (@ → _, / → _)
                    const pluginId = (0, pluginid_1.derivePluginId)(pluginName);
                    // Use signalk.displayName (standard SignalK convention) or fall back to package name
                    const pluginDisplayName = metadata.signalk?.displayName || pluginName;
                    const minimalPlugin = {
                        id: pluginId,
                        name: pluginDisplayName,
                        type: 'wasm',
                        packageName: pluginName,
                        version: metadata.version,
                        description: metadata.description || '',
                        keywords: metadata.keywords || [],
                        packageLocation: location,
                        enabled: false,
                        state: 'disabled',
                        statusMessage: () => 'WASM interface disabled',
                        schema: () => ({}),
                        uiSchema: () => ({}),
                        start: () => { },
                        stop: () => Promise.resolve(),
                        enableLogging: false,
                        enableDebug: false
                    };
                    app.plugins.push(minimalPlugin);
                    app.pluginsMap[pluginId] = minimalPlugin;
                    debug(`Registered minimal WASM plugin entry: ${pluginId} (WASM disabled)`);
                    return;
                }
                // Route to WASM loader
                debug(`Detected WASM plugin: ${pluginName}`);
                const { registerWasmPlugin } = require('../wasm');
                await registerWasmPlugin(app, pluginName, metadata, location, theApp.config.configPath);
                return;
            }
            // Standard Node.js plugin
            await doRegisterPlugin(app, pluginName, metadata, location);
        }
        catch (e) {
            console.error(e);
        }
    }
    function stopPlugin(plugin) {
        debug('Stopping plugin ' + plugin.name);
        onStopHandlers[plugin.id].forEach((f) => {
            try {
                f();
            }
            catch (err) {
                console.error(err);
            }
        });
        onStopHandlers[plugin.id] = [];
        const result = Promise.resolve(plugin.stop());
        result.then(() => {
            theApp.setPluginStatus(plugin.id, 'Stopped');
            debug('Stopped plugin ' + plugin.name);
        });
        return result;
    }
    function setPluginStartedMessage(plugin) {
        const statusMessage = typeof plugin.statusMessage === 'function'
            ? plugin.statusMessage()
            : undefined;
        if (lodash_1.default.isUndefined(statusMessage) &&
            lodash_1.default.isUndefined(theApp.providerStatus[plugin.id]) &&
            lodash_1.default.isUndefined(plugin.statusMessage)) {
            theApp.setPluginStatus(plugin.id, 'Started');
        }
    }
    function doPluginStart(app, plugin, location, configuration, restart) {
        debug('Starting plugin %s from %s', plugin.name, location);
        try {
            app.setPluginStatus(plugin.id, null);
            if (plugin.enableDebug) {
                app.logging.addDebug(plugin.packageName);
            }
            else {
                app.logging.removeDebug(plugin.packageName);
            }
            let safeConfiguration = configuration;
            if (!safeConfiguration) {
                console.error(`${plugin.id}:no configuration data`);
                safeConfiguration = {};
            }
            onStopHandlers[plugin.id].push(() => {
                app.resourcesApi.unRegister(plugin.id);
                app.autopilotApi.unRegister(plugin.id);
                app.weatherApi.unRegister(plugin.id);
            });
            plugin.start(safeConfiguration, restart);
            debug('Started plugin ' + plugin.name);
            setPluginStartedMessage(plugin);
        }
        catch (e) {
            console.error('error starting plugin: ' + e);
            console.error(e.stack);
            app.setProviderError(plugin.id, `Failed to start: ${e.message}`);
        }
    }
    async function doRegisterPlugin(app, packageName, metadata, location) {
        let plugin;
        const appCopy = lodash_1.default.assign({}, app, {
            getSelfPath,
            getPath,
            putSelfPath,
            queryRequest: requestResponse_1.queryRequest,
            error: (msg) => {
                console.error(`${packageName}:${msg}`);
                if (msg instanceof Error) {
                    console.error(msg.stack);
                }
            },
            debug: (0, debug_1.createDebug)(packageName),
            registerDeltaInputHandler: (handler) => {
                onStopHandlers[plugin.id].push(app.registerDeltaInputHandler(handler));
            },
            setProviderStatus: (0, util_1.deprecate)((msg) => {
                app.setPluginStatus(plugin.id, msg);
            }, `[${packageName}] setProviderStatus() is deprecated, use setPluginStatus() instead`),
            setProviderError: (0, util_1.deprecate)((msg) => {
                app.setPluginError(plugin.id, msg);
            }, `[${packageName}] setProviderError() is deprecated, use setPluginError() instead`),
            setPluginStatus: (msg) => {
                app.setPluginStatus(plugin.id, msg);
            },
            setPluginError: (msg) => {
                app.setPluginError(plugin.id, msg);
            },
            emitPropertyValue(name, value) {
                const propValues = app.propertyValues; // just for typechecking
                propValues.emitPropertyValue({
                    timestamp: Date.now(),
                    setter: plugin.id,
                    name,
                    value
                });
            },
            onPropertyValues(name, cb) {
                return app.propertyValues.onPropertyValues(name, cb);
            },
            getSerialPorts,
            supportsMetaDeltas: true,
            getMetadata: signalk_schema_1.getMetadata,
            reportOutputMessages: (count) => {
                app.emit(deltastats_1.CONNECTION_WRITE_EVENT_NAME, {
                    providerId: plugin.id,
                    count
                });
            }
        });
        appCopy.putPath = putPath;
        appCopy.subscriptionmanager = {
            subscribe: (command, unsubscribes, errorCallback, callback, user) => {
                const safeCallback = (delta) => {
                    try {
                        callback(delta);
                    }
                    catch (err) {
                        const message = err instanceof Error ? err.message : String(err);
                        console.error(`${packageName} subscription callback error: ${message}`);
                        if (err instanceof Error && err.stack) {
                            console.error(err.stack);
                        }
                        app.setPluginError(plugin.id, `Runtime error: ${message}`);
                    }
                };
                app.subscriptionmanager.subscribe(command, unsubscribes, errorCallback, safeCallback, user);
            },
            unsubscribe: (msg, unsubscribes) => {
                app.subscriptionmanager.unsubscribe(msg, unsubscribes);
            }
        };
        const weatherApi = app.weatherApi;
        appCopy.registerWeatherProvider = (provider) => {
            weatherApi.register(plugin.id, provider);
        };
        const historyApiRegistry = app.historyApiHttpRegistry;
        delete appCopy.historyApiHttpRegistry; // expose only the plugin-specific proxy
        appCopy.registerHistoryApiProvider = (provider) => {
            historyApiRegistry.registerHistoryApiProvider(plugin.id, provider);
            onStopHandlers[plugin.id].push(() => {
                historyApiRegistry.unregisterHistoryApiProvider(plugin.id);
            });
        };
        const resourcesApi = app.resourcesApi;
        appCopy.registerResourceProvider = (provider) => {
            resourcesApi.register(plugin.id, provider);
        };
        const autopilotApi = app.autopilotApi;
        appCopy.registerAutopilotProvider = (provider, devices) => {
            autopilotApi.register(plugin.id, provider, devices);
        };
        appCopy.autopilotUpdate = (deviceId, apInfo) => {
            autopilotApi.apUpdate(plugin.id, deviceId, apInfo);
        };
        const courseApi = app.courseApi;
        appCopy.getCourse = () => {
            return courseApi.getCourse();
        };
        appCopy.clearDestination = () => {
            return courseApi.clearDestination();
        };
        appCopy.setDestination = (dest) => {
            return courseApi.destination(dest);
        };
        appCopy.activateRoute = (dest) => {
            return courseApi.activeRoute(dest);
        };
        try {
            const moduleDir = path_1.default.join(location, packageName);
            const pluginConstructor = await (0, modules_1.importOrRequire)(moduleDir);
            plugin = pluginConstructor(appCopy);
        }
        catch (e) {
            console.error(`${packageName} failed to start: ${e.message}`);
            console.error(e);
            app.setProviderError(packageName, `Failed to start: ${e.message}`);
            return;
        }
        onStopHandlers[plugin.id] = [];
        if (app.pluginsMap[plugin.id]) {
            console.log(`WARNING: found multiple copies of plugin with id ${plugin.id} at ${location} and ${app.pluginsMap[plugin.id].packageLocation}`);
            return;
        }
        appCopy.handleMessage = handleMessageWrapper(app, plugin.id);
        const boundEventMethods = app.wrappedEmitter.bindMethodsById(`plugin:${plugin.id}`);
        lodash_1.default.assign(appCopy, boundEventMethods);
        appCopy.savePluginOptions = (configuration, cb) => {
            savePluginOptions(plugin.id, { ...getPluginOptions(plugin.id), configuration }, cb);
        };
        appCopy.readPluginOptions = () => {
            return getPluginOptions(plugin.id);
        };
        appCopy.getDataDirPath = () => dirForPluginId(plugin.id);
        appCopy.registerPutHandler = (context, aPath, callback, source) => {
            appCopy.handleMessage(plugin.id, {
                updates: [
                    {
                        meta: [
                            {
                                path: aPath,
                                value: {
                                    supportsPut: true
                                }
                            }
                        ]
                    }
                ]
            });
            onStopHandlers[plugin.id].push(app.registerActionHandler(context, aPath, source || plugin.id, callback));
        };
        appCopy.registerActionHandler = appCopy.registerPutHandler;
        appCopy.registerHistoryProvider = (provider) => {
            app.registerHistoryProvider(provider);
            const apiList = app.apis;
            apiList.push('historyplayback');
            apiList.push('historysnapshot');
            onStopHandlers[plugin.id].push(() => {
                app.unregisterHistoryProvider(provider);
            });
        };
        const startupOptions = getPluginOptions(plugin.id);
        const restart = (newConfiguration) => {
            const pluginOptions = getPluginOptions(plugin.id);
            pluginOptions.configuration = newConfiguration;
            savePluginOptions(plugin.id, pluginOptions, (err) => {
                if (err) {
                    console.error(err);
                }
                else {
                    stopPlugin(plugin).then(() => {
                        doPluginStart(app, plugin, location, newConfiguration, restart);
                        emitPluginsChanged();
                    });
                }
            });
        };
        if (isEnabledByPackageEnableDefault(startupOptions, metadata)) {
            startupOptions.enabled = true;
            startupOptions.configuration = {};
            plugin.enabledByDefault = true;
            // Persist the default-enabled state to disk so the plugin can be disabled later
            savePluginOptions(plugin.id, startupOptions, (err) => {
                if (err) {
                    console.error(`Error saving default-enabled options for ${plugin.id}:`, err);
                }
            });
        }
        plugin.enableDebug = startupOptions.enableDebug;
        plugin.version = metadata.version;
        plugin.packageName = metadata.name;
        plugin.keywords = metadata.keywords;
        plugin.packageLocation = location;
        if (startupOptions && startupOptions.enabled) {
            doPluginStart(app, plugin, location, startupOptions.configuration, restart);
        }
        plugin.enableLogging = startupOptions.enableLogging;
        app.plugins.push(plugin);
        app.pluginsMap[plugin.id] = plugin;
        const router = express_1.default.Router();
        router.get('/', (req, res) => {
            const currentOptions = getPluginOptions(plugin.id);
            const enabledByDefault = isEnabledByPackageEnableDefault(currentOptions, metadata);
            res.json({
                enabled: enabledByDefault || currentOptions.enabled,
                enabledByDefault,
                id: plugin.id,
                name: plugin.name,
                version: plugin.version
            });
        });
        router.post('/config', (req, res) => {
            savePluginOptions(plugin.id, req.body, (err) => {
                if (err) {
                    console.error(err);
                    res.status(500);
                    res.json(err);
                    return;
                }
                res.json('Saved configuration for plugin ' + plugin.id);
                stopPlugin(plugin).then(() => {
                    const options = getPluginOptions(plugin.id);
                    plugin.enableLogging = options.enableLogging;
                    plugin.enableDebug = options.enableDebug;
                    if (options.enabled) {
                        doPluginStart(app, plugin, location, options.configuration, restart);
                    }
                    emitPluginsChanged();
                });
            });
        });
        router.get('/config', (req, res) => {
            res.json(getPluginOptions(plugin.id));
        });
        if (typeof plugin.registerWithRouter === 'function') {
            plugin.registerWithRouter(router);
            if (typeof plugin.getOpenApi === 'function') {
                app.setPluginOpenApi(plugin.id, plugin.getOpenApi());
            }
        }
        app.use(backwardsCompat('/plugins/' + plugin.id), router);
        if (typeof plugin.signalKApiRoutes === 'function') {
            app.use('/signalk/v1/api', plugin.signalKApiRoutes(express_1.default.Router()));
        }
    }
};
const isEnabledByPackageEnableDefault = (options, metadata) => lodash_1.default.isUndefined(options.enabled) &&
    metadata['signalk-plugin-enabled-by-default'];
//# sourceMappingURL=plugins.js.map