"use strict";
/* eslint-disable @typescript-eslint/no-explicit-any */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startWasmPlugin = startWasmPlugin;
exports.stopWasmPlugin = stopWasmPlugin;
exports.unloadWasmPlugin = unloadWasmPlugin;
exports.reloadWasmPlugin = reloadWasmPlugin;
exports.handleWasmPluginCrash = handleWasmPluginCrash;
exports.shutdownAllWasmPlugins = shutdownAllWasmPlugins;
/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * WASM Plugin Lifecycle Operations
 *
 * Manages plugin lifecycle operations including start, stop, reload, unload,
 * crash handling, and shutdown. Handles state transitions and cleanup.
 */
const debug_1 = __importDefault(require("debug"));
const plugin_registry_1 = require("./plugin-registry");
const wasm_runtime_1 = require("../wasm-runtime");
const wasm_subscriptions_1 = require("../wasm-subscriptions");
const plugin_routes_1 = require("./plugin-routes");
const resource_provider_1 = require("../bindings/resource-provider");
const weather_provider_1 = require("../bindings/weather-provider");
const radar_provider_1 = require("../bindings/radar-provider");
const socket_manager_1 = require("../bindings/socket-manager");
const debug = (0, debug_1.default)('signalk:wasm:loader');
// Track poll timers for plugins that request periodic polling
const pollTimers = new Map();
// Track delta subscription unsubscribe functions for plugins
const deltaUnsubscribers = new Map();
// Mutex for serializing network-capable plugin starts
// as-fetch uses global state that gets corrupted with parallel plugin starts
let networkPluginStartMutex = Promise.resolve();
/**
 * Start a WASM plugin
 */
async function startWasmPlugin(app, pluginId) {
    const plugin = plugin_registry_1.wasmPlugins.get(pluginId);
    if (!plugin) {
        throw new Error(`WASM plugin ${pluginId} not found`);
    }
    if (plugin.status === 'running') {
        debug(`Plugin ${pluginId} already running`);
        return;
    }
    // Serialize starts for network-capable plugins to avoid as-fetch global state corruption
    if (plugin.metadata?.capabilities?.network) {
        debug(`Plugin ${pluginId} has network capability, waiting for mutex...`);
        const previousMutex = networkPluginStartMutex;
        let releaseMutex;
        networkPluginStartMutex = new Promise((resolve) => {
            releaseMutex = resolve;
        });
        await previousMutex;
        debug(`Plugin ${pluginId} acquired start mutex`);
        try {
            await startWasmPluginInternal(app, plugin, pluginId);
        }
        finally {
            debug(`Plugin ${pluginId} releasing start mutex`);
            releaseMutex();
        }
    }
    else {
        await startWasmPluginInternal(app, plugin, pluginId);
    }
}
async function startWasmPluginInternal(app, plugin, pluginId) {
    debug(`Starting WASM plugin: ${pluginId}`);
    (0, plugin_registry_1.setPluginStatus)(plugin, 'starting');
    plugin.errorMessage = undefined;
    try {
        if (!plugin.instance) {
            throw new Error('Plugin instance not loaded');
        }
        // Call plugin start() with configuration
        // Pass the entire configuration object including enableDebug at root level
        const startConfig = {
            ...plugin.configuration,
            enableDebug: plugin.enableDebug
        };
        const configJson = JSON.stringify(startConfig);
        debug(`Starting plugin with config: ${configJson}`);
        const result = await plugin.instance.exports.start(configJson);
        if (result !== 0) {
            throw new Error(`Plugin start() returned error code: ${result}`);
        }
        // Update provider instance references after plugin_start() completes
        // Providers are registered during start(), so we need to update
        // references using BOTH the packageName (used in env bindings) and real pluginId
        if (plugin.packageName) {
            (0, resource_provider_1.updateResourceProviderInstance)(plugin.packageName, plugin.instance);
            (0, weather_provider_1.updateWeatherProviderInstance)(plugin.packageName, plugin.instance);
            (0, radar_provider_1.updateRadarProviderInstance)(plugin.packageName, plugin.instance);
        }
        (0, resource_provider_1.updateResourceProviderInstance)(pluginId, plugin.instance);
        (0, weather_provider_1.updateWeatherProviderInstance)(pluginId, plugin.instance);
        (0, radar_provider_1.updateRadarProviderInstance)(pluginId, plugin.instance);
        (0, plugin_registry_1.setPluginStatus)(plugin, 'running');
        plugin.statusMessage = 'Running';
        plugin.crashCount = 0; // Reset crash count on successful start
        plugin.restartBackoff = 1000;
        // Set up periodic polling for plugins that export poll()
        // This is a generic mechanism for plugins that need to poll hardware,
        // sockets, or external systems (e.g., radar, NMEA receivers, sensors)
        if (plugin.instance?.exports?.poll) {
            const pollInterval = 1000; // Poll every 1 second
            debug(`Setting up poll timer for ${pluginId} (${pollInterval}ms)`);
            const pollTimer = setInterval(() => {
                try {
                    if (plugin.status === 'running' && plugin.instance?.exports?.poll) {
                        const result = plugin.instance.exports.poll();
                        if (result !== 0) {
                            debug(`[${pluginId}] poll() returned: ${result}`);
                        }
                    }
                }
                catch (pollError) {
                    debug(`[${pluginId}] poll() error: ${pollError}`);
                }
            }, pollInterval);
            pollTimers.set(pluginId, pollTimer);
        }
        // Set up delta subscription if plugin exports delta_handler
        if (plugin.instance?.exports?.delta_handler) {
            debug(`Setting up delta subscription for ${pluginId}`);
            // Subscribe to deltas from the server
            if (app.signalk && typeof app.signalk.on === 'function') {
                const deltaHandler = (delta) => {
                    try {
                        if (plugin.status === 'running' &&
                            plugin.instance?.exports?.delta_handler) {
                            const deltaJson = JSON.stringify(delta);
                            plugin.instance.exports.delta_handler(deltaJson);
                        }
                    }
                    catch (deltaError) {
                        debug(`[${pluginId}] delta_handler error: ${deltaError}`);
                    }
                };
                // Subscribe to delta events
                app.signalk.on('delta', deltaHandler);
                // Store unsubscribe function
                deltaUnsubscribers.set(pluginId, () => {
                    if (app.signalk && typeof app.signalk.removeListener === 'function') {
                        app.signalk.removeListener('delta', deltaHandler);
                    }
                });
                debug(`Delta subscription active for ${pluginId}`);
            }
            else {
                debug(`Warning: app.signalk not available for delta subscription`);
            }
        }
        debug(`Successfully started WASM plugin: ${pluginId}`);
    }
    catch (error) {
        const errorMsg = error instanceof Error ? error.message : String(error);
        (0, plugin_registry_1.setPluginStatus)(plugin, 'error');
        plugin.errorMessage = errorMsg;
        debug(`Failed to start WASM plugin ${pluginId}: ${errorMsg}`);
        throw error;
    }
}
/**
 * Stop a WASM plugin
 */
async function stopWasmPlugin(pluginId) {
    const plugin = plugin_registry_1.wasmPlugins.get(pluginId);
    if (!plugin) {
        throw new Error(`WASM plugin ${pluginId} not found`);
    }
    debug(`Stopping WASM plugin: ${pluginId}`);
    try {
        // Cancel any pending restart timers
        const timer = plugin_registry_1.restartTimers.get(pluginId);
        if (timer) {
            clearTimeout(timer);
            plugin_registry_1.restartTimers.delete(pluginId);
        }
        // Cancel any poll timers
        const pollTimer = pollTimers.get(pluginId);
        if (pollTimer) {
            clearInterval(pollTimer);
            pollTimers.delete(pluginId);
            debug(`Stopped poll timer for ${pluginId}`);
        }
        // Unsubscribe from delta events
        const deltaUnsubscriber = deltaUnsubscribers.get(pluginId);
        if (deltaUnsubscriber) {
            deltaUnsubscriber();
            deltaUnsubscribers.delete(pluginId);
            debug(`Stopped delta subscription for ${pluginId}`);
        }
        if (plugin.instance) {
            // Call plugin stop()
            const result = plugin.instance.exports.stop();
            if (result !== 0) {
                debug(`Plugin stop() returned error code: ${result}`);
            }
        }
        // Clean up any sockets opened by this plugin
        socket_manager_1.socketManager.closeAllForPlugin(pluginId);
        (0, plugin_registry_1.setPluginStatus)(plugin, 'stopped');
        plugin.statusMessage = 'Stopped';
        debug(`Successfully stopped WASM plugin: ${pluginId}`);
    }
    catch (error) {
        const errorMsg = error instanceof Error ? error.message : String(error);
        debug(`Error stopping WASM plugin ${pluginId}: ${errorMsg}`);
        (0, plugin_registry_1.setPluginStatus)(plugin, 'error');
        plugin.errorMessage = errorMsg;
        throw error;
    }
}
/**
 * Unload a WASM plugin completely (remove from memory and unregister routes)
 */
async function unloadWasmPlugin(app, pluginId) {
    const plugin = plugin_registry_1.wasmPlugins.get(pluginId);
    if (!plugin) {
        throw new Error(`WASM plugin ${pluginId} not found`);
    }
    debug(`Unloading WASM plugin: ${pluginId}`);
    try {
        // Stop the plugin first if running
        if (plugin.status === 'running') {
            await stopWasmPlugin(pluginId);
        }
        // Remove HTTP routes from Express
        if (plugin.router) {
            debug(`Removing HTTP routes for ${pluginId}`);
            // Express doesn't have a built-in way to remove routes, so we need to
            // remove the middleware from the app stack
            const paths = (0, plugin_routes_1.backwardsCompat)(`/plugins/${pluginId}`);
            // Remove all route handlers for this plugin
            paths.forEach((path) => {
                if (app._router && app._router.stack) {
                    app._router.stack = app._router.stack.filter((layer) => {
                        // Remove layers that match this plugin's path
                        if (layer.route) {
                            const routePath = layer.route.path;
                            // Handle both string and array cases for route.path
                            if (typeof routePath === 'string') {
                                return !routePath.startsWith(path);
                            }
                            else if (Array.isArray(routePath)) {
                                return !routePath.some((p) => p.startsWith(path));
                            }
                            return true;
                        }
                        if (layer.name === 'router' && layer.regexp) {
                            return !layer.regexp.test(path);
                        }
                        return true;
                    });
                }
            });
            plugin.router = undefined;
            debug(`Removed HTTP routes for ${pluginId}`);
        }
        // Destroy WASM instance and free memory
        if (plugin.instance) {
            debug(`Destroying WASM instance for ${pluginId}`);
            // Clear any references to help garbage collection
            plugin.instance = undefined;
            debug(`Destroyed WASM instance for ${pluginId}`);
        }
        (0, plugin_registry_1.setPluginStatus)(plugin, 'stopped');
        plugin.statusMessage = 'Unloaded';
        debug(`Successfully unloaded WASM plugin: ${pluginId}`);
    }
    catch (error) {
        const errorMsg = error instanceof Error ? error.message : String(error);
        debug(`Error unloading WASM plugin ${pluginId}: ${errorMsg}`);
        (0, plugin_registry_1.setPluginStatus)(plugin, 'error');
        plugin.errorMessage = errorMsg;
        throw error;
    }
}
/**
 * Reload a WASM plugin (hot-reload without server restart)
 */
async function reloadWasmPlugin(app, pluginId) {
    const plugin = plugin_registry_1.wasmPlugins.get(pluginId);
    if (!plugin) {
        throw new Error(`WASM plugin ${pluginId} not found`);
    }
    debug(`Reloading WASM plugin: ${pluginId}`);
    try {
        const wasRunning = plugin.status === 'running';
        // Stop the plugin
        if (wasRunning) {
            await stopWasmPlugin(pluginId);
        }
        // Save current configuration
        const savedConfig = plugin.configuration;
        // Reload WASM module
        const runtime = (0, wasm_runtime_1.getWasmRuntime)();
        await runtime.reloadPlugin(pluginId);
        // Get new instance
        const newInstance = runtime.getInstance(pluginId);
        if (!newInstance) {
            throw new Error('Failed to get reloaded instance');
        }
        plugin.instance = newInstance;
        // Update schema from new instance
        const schemaJson = newInstance.exports.schema();
        plugin.schema = schemaJson ? JSON.parse(schemaJson) : {};
        // Restart if it was running
        if (wasRunning) {
            await startWasmPlugin(app, pluginId);
        }
        plugin.statusMessage = 'Reloaded successfully';
        debug(`Successfully reloaded WASM plugin: ${pluginId}`);
    }
    catch (error) {
        const errorMsg = error instanceof Error ? error.message : String(error);
        (0, plugin_registry_1.setPluginStatus)(plugin, 'error');
        plugin.errorMessage = `Reload failed: ${errorMsg}`;
        debug(`Failed to reload WASM plugin ${pluginId}: ${errorMsg}`);
        throw error;
    }
}
/**
 * Handle WASM plugin crash with automatic restart
 */
async function handleWasmPluginCrash(app, pluginId, error) {
    const plugin = plugin_registry_1.wasmPlugins.get(pluginId);
    if (!plugin) {
        return;
    }
    plugin.crashCount++;
    plugin.lastCrash = new Date();
    (0, plugin_registry_1.setPluginStatus)(plugin, 'crashed');
    plugin.errorMessage = `Crashed: ${error.message}`;
    debug(`WASM plugin ${pluginId} crashed (count: ${plugin.crashCount}): ${error.message}`);
    // Give up after 3 crashes in quick succession
    if (plugin.crashCount >= 3) {
        (0, plugin_registry_1.setPluginStatus)(plugin, 'error');
        plugin.errorMessage =
            'Plugin repeatedly crashing, automatic restart disabled';
        debug(`Plugin ${pluginId} disabled after 3 crashes`);
        return;
    }
    // Schedule restart with exponential backoff
    plugin.restartBackoff = Math.min(plugin.restartBackoff * 2, 30000); // Max 30 seconds
    debug(`Scheduling restart for ${pluginId} in ${plugin.restartBackoff}ms`);
    const timer = setTimeout(async () => {
        try {
            debug(`Attempting automatic restart of ${pluginId}`);
            await reloadWasmPlugin(app, pluginId);
            plugin.statusMessage = 'Recovered from crash';
        }
        catch (restartError) {
            debug(`Failed to restart ${pluginId}:`, restartError);
            (0, plugin_registry_1.setPluginStatus)(plugin, 'error');
            plugin.errorMessage = 'Failed to recover from crash';
        }
    }, plugin.restartBackoff);
    plugin_registry_1.restartTimers.set(pluginId, timer);
}
/**
 * Shutdown all WASM plugins
 */
async function shutdownAllWasmPlugins() {
    debug('Shutting down all WASM plugins');
    debug(`Number of plugins in registry: ${plugin_registry_1.wasmPlugins.size}`);
    // Clear all restart timers
    for (const timer of plugin_registry_1.restartTimers.values()) {
        clearTimeout(timer);
    }
    plugin_registry_1.restartTimers.clear();
    // Stop all plugins
    const plugins = Array.from(plugin_registry_1.wasmPlugins.values());
    debug(`Plugins to shutdown: ${plugins.map((p) => `${p.id}(${p.status})`).join(', ')}`);
    for (const plugin of plugins) {
        try {
            if (plugin.status === 'running') {
                debug(`Stopping plugin ${plugin.id}...`);
                await stopWasmPlugin(plugin.id);
                debug(`Plugin ${plugin.id} stopped, status now: ${plugin.status}`);
            }
            else {
                debug(`Plugin ${plugin.id} not running (status=${plugin.status}), skipping stop`);
            }
        }
        catch (error) {
            debug(`Error stopping plugin ${plugin.id}:`, error);
        }
    }
    // Shutdown runtime
    const runtime = (0, wasm_runtime_1.getWasmRuntime)();
    await runtime.shutdown();
    // Reset singletons
    (0, wasm_runtime_1.resetWasmRuntime)();
    (0, wasm_subscriptions_1.resetSubscriptionManager)();
    plugin_registry_1.wasmPlugins.clear();
    debug('All WASM plugins shut down');
}
//# sourceMappingURL=plugin-lifecycle.js.map