"use strict";

const fs   = require('fs');
const path = require('path');

const PluginPaths     = require('./pluginPaths');
const PluginRegistry  = require('./pluginRegistry');
const PluginPackage   = require('./pluginPackage');
const PluginAllowlist = require('./pluginAllowlist');
const manifestUtil    = require('./pluginManifest');
const PluginProcess   = require('./pluginProcess');
const PluginStorage   = require('./pluginStorage');
const PluginDownload  = require('./pluginDownload');
const msg             = require('../../client/modules/mctools/plugin/messages.js');
const console         = require('gifer-console');

const DEFAULT_IDLE_MS = 60000;

const ICON_MIME = {
    ".png" : "image/png",
    ".svg" : "image/svg+xml",
    ".jpg" : "image/jpeg",
    ".jpeg": "image/jpeg"
};

// Ранг рівня diagnostics — той самий словник, що в логері (gifer-console ERROR(2)…TRACE(6)).
const DIAG_RANK = { error: 2, warn: 3, info: 4, debug: 5, trace: 6 };

function diagRank(level){ return DIAG_RANK[level] || 0; }

function defaultProcessFactory(spec){
    const { utilityProcess } = require('electron');

    return new PluginProcess({
        id            : spec.id,
        processFactory: function(bootstrap, args){
            // stdio:'pipe' → child.stdout/stderr читні (Electron 39) для захоплення bg-виводу (спека 3.2).
            return utilityProcess.fork(bootstrap, args, { serviceName: 'mc-plugin-' + spec.id, stdio: [ 'ignore', 'pipe', 'pipe' ] });
        },
        bootstrap: path.join(__dirname, 'pluginBootstrap.js'),
        args     : [ spec.id, spec.entry ],
        onMessage: spec.onMessage,
        onExit   : spec.onExit
    });
}

function PluginHost(opts){
    const paths = new PluginPaths(opts.profileRoot);

    paths.ensureDirs();
    const registry  = new PluginRegistry(paths.registry);
    const allowlist = opts.allowlist || new PluginAllowlist({ profileRoot: opts.profileRoot });
    const toRender  = opts.toRender || function(){};
    const getDialog = opts.dialog || function(){ return require('electron').dialog; };

    const storage   = new PluginStorage(paths);
    const downloads = new PluginDownload({
        paths      : paths,
        storage    : storage,
        transport  : opts.downloadTransport,
        emitService: function(id, type, payload){ emitService(id, type, payload); }
    });

    var health      = {};
    var currentLang = 'en';

    const processFactory = opts.processFactory || defaultProcessFactory;
    const IDLE_MS        = opts.idleMs || DEFAULT_IDLE_MS;

    var processes  = {};
    var idleTimers = {};
    var busy       = {};
    var lastBgOutput = {};   // ring останнього завершеного BG — переживає onExit заради діагностики краху
    var contextSubs  = {};   // BG, що просили потік context.changed; живе рівно стільки, скільки процес

    var diagConfig = {};   // manifest.diagnostics на плагін (для контракт-перевірки й агрегації)
    var diagWarned = {};   // «no diagnostics contract» — раз на плагін
    var bgPending  = {};   // host→bg request/ack (diagnostics.collect) за reqId
    var bgSeq      = 0;

    const packages = new PluginPackage({
        paths      : paths,
        registry   : registry,
        deactivate : opts.deactivate || deactivate,
        hostVersion: opts.hostVersion
    });

    // ---- execution gate --------------------------------------------------

    function readValidatedManifest(id){
        var codeDir = paths.pluginCode(id);
        var parsed  = manifestUtil.validateManifest(JSON.parse(
            fs.readFileSync(path.join(codeDir, 'manifest.json'), 'utf8')
        ));

        manifestUtil.validateEntries(codeDir, parsed);
        return parsed;
    }

    function canRun(id){
        var meta = registry.get(id);

        if (!meta || !meta.enabled){
            return { ok: false, state: "disabled" };
        }

        // DEBUG: тимчасово вимкнено allowlist/checksum-перевірку для отладки плагіна (mcwhisper).
        // ПОВЕРНУТИ перед релізом — розкоментувати блок нижче.
        // if (!allowlist.verify(id, meta.checksum)){
        //     return { ok: false, state: "blocked", reason: "allowlist/checksum" };
        // }

        try {
            var manifest = readValidatedManifest(id);

            return { ok: true, manifest: manifest, meta: meta };
        } catch (e) {
            return { ok: false, state: "error", reason: e.message };
        }
    }

    function requireRunnable(id){
        var gate = canRun(id);

        if (!gate.ok){
            setHealth(id, gate);
            return null;
        }

        return gate;
    }

    // details приходить із BG як довільний JSON плагіна. Рядок теж має .length,
    // тож без перевірки типу det.missing.join() кинув би TypeError прямо в
    // child.on('message') — некерований виняток у main-процесі.
    function hasMissing(det){
        return !!(det && Array.isArray(det.missing) && det.missing.length);
    }

    function setHealth(id, patch){
        var prev = health[id] ? health[id].state : null;
        var next = Object.assign({
            state: 'inactive', reason: '', lastError: '', bgAlive: false, details: null
        }, health[id], patch);

        delete next.ok;
        delete next.manifest;
        delete next.meta;
        health[id] = next;

        var det = next.details;

        if (next.state !== prev){
            var line = '[plugin:' + id + '] health ' + (prev || '∅') + '→' + next.state
                     + (next.reason ? ' (' + next.reason + ')' : '')
                     + (next.lastError ? ' ' + next.lastError : '')
                     + (hasMissing(det) ? ' missing=[' + det.missing.join(', ') + ']' : '')
                     + (det && det.hint ? ' hint=' + det.hint : '');

            if (next.state === 'error' || next.state === 'blocked'){
                console.warn(line, console.logLevel.WARN);
            } else {
                console.info(line, console.logLevel.INFO);
            }
        } else {
            console.log('[plugin:' + id + '] health ' + next.state + ' (repeat)', console.logLevel.DEBUG);
        }

        // Контракт-порушення: оголосив level ≥ info, а деталі при error порожні (спека 3.1).
        if (next.state === 'error' && diagConfig[id] && diagRank(diagConfig[id].level) >= diagRank('info')){
            var hasDetail = det && (hasMissing(det) || det.probes || det.hint);

            if (!hasDetail){
                console.warn('[plugin:' + id + '] diagnostics contract violated: level=' + diagConfig[id].level + ' but health.details empty', console.logLevel.WARN);
            }
        }

        toRender({ pluginId: id, src: 'host', dst: 'ui', type: msg.std.health, payload: health[id] });
        return health[id];
    }

    // ---- BG runtime ------------------------------------------------------

    function ensureProc(id, knownGate){
        var gate = knownGate || requireRunnable(id);

        if (!gate) throw new Error('plugin cannot run: ' + (health[id] ? health[id].state : 'unknown'));
        if (processes[id]) return processes[id];

        var entry = manifestUtil.resolveEntry(paths.pluginCode(id), gate.manifest.entry.background);

        console.log('[plugin:' + id + '] bg spawn ' + entry, console.logLevel.DEBUG);

        processes[id] = processFactory({
            id       : id,
            entry    : entry,
            onMessage: function(env){ onBgMessage(id, env); touchIdle(id); },
            onExit   : function(code){
                clearIdle(id);

                // Знімаємо ring ДО видалення процесу: діагностику дивляться саме після
                // краху, а разом із processes[id] зникали б рядки, що його пояснюють.
                if (processes[id] && processes[id].getOutput){
                    lastBgOutput[id] = processes[id].getOutput();
                }

                delete processes[id];
                delete busy[id];
                delete contextSubs[id];   // підписка не переживає процес, який її створив
                setHealth(id, {
                    state    : code === 0 ? 'inactive' : 'error',
                    bgAlive  : false,
                    lastError: code === 0 ? '' : 'exit ' + code
                });
            }
        });
        setHealth(id, { state: 'running', bgAlive: true });
        if (!gate.meta.keepAlive) scheduleIdle(id);
        return processes[id];
    }

    function sendToBg(id, envelope){
        var gate = requireRunnable(id);

        if (!gate){
            return Promise.reject(new Error((health[id] && (health[id].reason || health[id].state)) || 'blocked'));
        }

        try {
            ensureProc(id, gate).send(envelope);
        } catch (e) {
            return Promise.reject(e);
        }
        touchIdle(id);
        return Promise.resolve();
    }

    function touchIdle(id){
        var meta = registry.get(id);

        if (!busy[id] && meta && !meta.keepAlive && processes[id]) scheduleIdle(id);
    }

    function scheduleIdle(id){
        clearIdle(id);
        idleTimers[id] = setTimeout(function(){
            console.log('[plugin:' + id + '] idle timeout — stopping bg', console.logLevel.DEBUG);
            stopProc(id, 2000);
        }, IDLE_MS);
    }

    function clearIdle(id){
        if (idleTimers[id]){
            clearTimeout(idleTimers[id]);
            delete idleTimers[id];
        }
    }

    function stopProc(id, graceMs){
        var proc = processes[id];

        clearIdle(id);
        if (!proc) return Promise.resolve();
        return proc.stop(graceMs);
    }

    function shutdownAll(graceMs){
        return Promise.all(Object.keys(processes).map(function(id){
            return stopProc(id, graceMs);
        }));
    }

    function requestBg(id, type, payload){
        return new Promise(function(resolve, reject){
            var reqId = 'host-' + id + '-' + (++bgSeq);
            var timer = setTimeout(function(){
                delete bgPending[reqId];
                reject(new Error('bg request timeout: ' + type));
            }, 15000);

            bgPending[reqId] = { resolve: resolve, reject: reject, timer: timer, owner: id };
            sendToBg(id, { pluginId: id, src: 'host', dst: 'bg', type: type, reqId: reqId, payload: payload || {} })
                .catch(function(error){
                    clearTimeout(timer);
                    delete bgPending[reqId];
                    reject(error);
                });
        });
    }

    function onBgMessage(ownerId, envelope){
        if (!envelope || envelope.pluginId !== ownerId){
            console.warn('[plugin:' + ownerId + '] bg envelope owner mismatch — killing', console.logLevel.WARN);
            return stopProc(ownerId, 0);
        }

        if (envelope.type === msg.std.ack && envelope.reqId && bgPending[envelope.reqId]){
            var waiter = bgPending[envelope.reqId];

            // reqId передбачуваний ('host-<id>-<n>'), тож без перевірки власника плагін B
            // резолвив би запит плагіна A своїм payload'ом. Перевірка pluginId вище цього
            // не ловить: B шле конверт від власного імені, підміняючи лише reqId.
            if (waiter.owner !== ownerId){
                console.warn('[plugin:' + ownerId + '] ack for foreign reqId ' + envelope.reqId + ' — ignored', console.logLevel.WARN);
                return;
            }

            clearTimeout(waiter.timer);
            delete bgPending[envelope.reqId];
            return envelope.error ? waiter.reject(new Error(envelope.error)) : waiter.resolve(envelope.payload);
        }

        if (envelope.type === msg.std.lifecycle){
            busy[ownerId] = !!(envelope.payload && envelope.payload.busy === true);
            return busy[ownerId] ? clearIdle(ownerId) : touchIdle(ownerId);
        }

        if (envelope.type === msg.std.health) setHealth(ownerId, envelope.payload || {});
        routeBgEnvelope(ownerId, envelope);
    }

    var rendererHostTypes = [
        'composer.insert', 'context.get', 'context.ensureFile', 'notify', 'alert',
        'client.reload', 'panel.open', 'panel.close', 'contribution.setState'
    ];

    function routeBgEnvelope(ownerId, envelope){
        if (envelope.pluginId !== ownerId) throw new Error('pluginId spoofing');

        if (envelope.dst === 'host' && mainHostHandlers[envelope.type]){
            return Promise.resolve()
                .then(function(){ return mainHostHandlers[envelope.type](ownerId, envelope.payload || {}); })
                .then(function(payload){ ackBg(ownerId, envelope, payload); })
                .catch(function(error){ ackBg(ownerId, envelope, null, error); });
        }

        if (envelope.dst === 'host' && rendererHostTypes.indexOf(envelope.type) !== -1){
            return toRender(envelope);
        }

        if (envelope.dst === 'ui') return toRender(envelope);
        if (envelope.dst === 'host' && envelope.reqId){
            return ackBg(ownerId, envelope, null, new Error('unsupported BG envelope: ' + envelope.type));
        }
    }

    function ackBg(id, request, payload, error){
        if (!processes[id]) return;
        processes[id].send({
            pluginId: id, src: 'host', dst: 'bg', type: msg.std.ack, reqId: request.reqId,
            payload: payload, error: error ? error.message : undefined
        });
    }

    // ---- lifecycle -------------------------------------------------------

    function readManifestSafe(id){
        try {
            return readValidatedManifest(id);
        } catch (e) {
            return null;
        }
    }

    function readIconDataUri(id, iconRel){
        try {
            var codeDir  = paths.pluginCode(id);
            var iconPath = manifestUtil.resolveEntry(codeDir, iconRel);  // safe-resolve + isFile; кидає, якщо поза codeDir/нема файлу
            var mime     = ICON_MIME[path.extname(iconPath).toLowerCase()];

            if (!mime){
                return null;
            }

            return "data:" + mime + ";base64," + fs.readFileSync(iconPath).toString("base64");
        } catch (e) {
            return null;   // нема icon / поза межами / adm-zip existsSync-патч кидає → fallback на глиф у renderer
        }
    }

    function managerRow(id){
        var meta     = registry.get(id);
        var manifest = meta ? readManifestSafe(id) : null;
        var gate     = meta ? canRun(id) : { ok: false, state: 'unknown' };

        return {
            id       : id,
            name     : manifest ? manifest.name : id,
            version  : meta ? meta.version : '',
            kind     : manifest ? manifest.kind : '',
            enabled  : meta ? meta.enabled : false,
            keepAlive: meta ? meta.keepAlive : false,
            hasSettings: !!(manifest && manifest.entry && manifest.entry.settings),
            requiresReload: !!(manifest && manifest.requiresReload),
            iconDataUri: manifest && manifest.icon ? readIconDataUri(id, manifest.icon) : null,
            health   : health[id] || { state: gate.ok ? 'inactive' : gate.state, reason: gate.reason || '' }
        };
    }

    function managerList(){
        return registry.list().map(function(row){ return managerRow(row.id); });
    }

    function resolveManifestI18n(id, manifest, lang){
        var codeDir = paths.pluginCode(id);
        var strings = {};

        [ 'en', lang ].forEach(function(locale){
            try {
                Object.assign(strings, JSON.parse(
                    fs.readFileSync(path.join(codeDir, 'i18n', locale + '.json'), 'utf8')
                ));
            } catch (e) { /* i18n необов'язковий */ }
        });

        function resolveValue(value){
            if (typeof value === 'string' && value.indexOf('@i18n:') === 0){
                var key = value.slice(6);

                return strings[key] !== undefined ? strings[key] : key;
            }
            if (Array.isArray(value)) return value.map(resolveValue);
            if (value && typeof value === 'object'){
                var out = {};

                Object.keys(value).forEach(function(name){ out[name] = resolveValue(value[name]); });
                return out;
            }
            return value;
        }

        return resolveValue(manifest);
    }

    function activate(id, lang){
        var gate = requireRunnable(id);

        if (!gate) return Promise.resolve(managerRow(id));

        diagConfig[id] = gate.manifest.diagnostics || null;

        if (gate.manifest.kind === 'ui+bg' && !diagConfig[id] && !diagWarned[id]){
            diagWarned[id] = true;
            console.warn('[plugin:' + id + '] no diagnostics contract (kind=ui+bg)', console.logLevel.WARN);
        }

        var selectedLang = [ 'en', 'ru', 'uk' ].indexOf(lang) === -1 ? currentLang : lang;

        currentLang = selectedLang;

        toRender({
            pluginId: id, src: 'host', dst: 'ui', type: 'activate',
            payload: {
                id      : id,
                codePath: paths.pluginCode(id),
                manifest: resolveManifestI18n(id, gate.manifest, selectedLang)
            }
        });
        setHealth(id, { state: 'active' });
        if (gate.manifest.kind === 'ui+bg' && gate.meta.keepAlive) ensureProc(id, gate);
        return Promise.resolve(managerRow(id));
    }

    function deactivate(id){
        toRender({
            pluginId: id, src: 'host', dst: 'ui', type: 'deactivate',
            payload: { id: id }
        });
        return stopProc(id, 2000).then(function(){
            if (registry.get(id)) setHealth(id, { state: 'inactive', bgAlive: false });
        });
    }

    function setEnabled(id, enabled, lang){
        if (!registry.get(id)) return Promise.reject(new Error('plugin is not installed: ' + id));

        console.info('[plugin:' + id + '] enabled=' + (!!enabled), console.logLevel.INFO);

        if (!enabled){
            return deactivate(id).then(function(){
                registry.setEnabled(id, false);
                return managerRow(id);
            });
        }

        registry.setEnabled(id, true);
        return activate(id, lang).then(function(){ return managerRow(id); });
    }

    function setKeepAlive(id, keepAlive){
        if (!registry.get(id)) return Promise.reject(new Error('plugin is not installed: ' + id));

        console.info('[plugin:' + id + '] keepAlive=' + (!!keepAlive), console.logLevel.INFO);

        registry.setKeepAlive(id, !!keepAlive);

        if (!keepAlive){
            touchIdle(id);
            return Promise.resolve(managerRow(id));
        }

        var gate = requireRunnable(id);

        if (gate && gate.manifest.kind === 'ui+bg') ensureProc(id, gate);
        return Promise.resolve(managerRow(id));
    }

    function activateEnabled(lang){
        if (lang) currentLang = lang;

        var enabledRows = registry.list().filter(function(row){ return row.enabled; });

        console.info('[plugins] activating ' + enabledRows.length + ' enabled plugin(s)', console.logLevel.INFO);

        return Promise.all(enabledRows.map(function(row){
            return activate(row.id, lang);
        })).then(function(){ return managerList(); });
    }

    // ---- diagnostics -----------------------------------------------------

    // logPath форми "@data/<rel>" → safe-resolve під pluginData(id); поза межами / ".." → null.
    function resolvePluginLog(id, logPath){
        if (typeof logPath !== 'string' || logPath.indexOf('@data/') !== 0) return null;

        var rel = logPath.slice('@data/'.length);

        if (rel.split(/[\\/]/).indexOf('..') !== -1) return null;

        var root     = path.resolve(paths.pluginData(id));
        var resolved = path.resolve(root, rel);

        return (resolved === root || resolved.indexOf(root + path.sep) === 0) ? resolved : null;
    }

    function tailFile(filePath, maxBytes){
        try {
            var stat  = fs.statSync(filePath);
            var start = Math.max(0, stat.size - maxBytes);
            var len   = stat.size - start;
            var fd    = fs.openSync(filePath, 'r');
            var buf   = Buffer.alloc(len);

            fs.readSync(fd, buf, 0, len, start);
            fs.closeSync(fd);
            return buf.toString('utf8');
        } catch (e) {
            return null;
        }
    }

    // Зшиває джерела в один звіт: structured health + хвіст bg-виводу + хвіст плагін-логу +
    // on-demand bg-звіт (лише ui+bg && level ≥ debug). Знімок додатково у node.log (спека 3.3).
    function collectDiagnostics(id){
        var meta     = registry.get(id);
        var manifest = meta ? readManifestSafe(id) : null;
        var diag     = (manifest && manifest.diagnostics) || {};
        var report   = {
            id       : id,
            health   : health[id] || null,
            bgOutput : null,
            pluginLog: null,
            collect  : null
        };

        var logFile = diag.logPath ? resolvePluginLog(id, diag.logPath) : null;

        if (logFile) report.pluginLog = tailFile(logFile, 64 * 1024);

        var wantBg = manifest && manifest.kind === 'ui+bg' && diagRank(diag.level) >= diagRank('debug');
        var chain  = wantBg
            ? requestBg(id, msg.std.diagnostics, {})
                  .then(function(payload){ report.collect = payload; })
                  .catch(function(error){ report.collect = { error: error.message }; })
            : Promise.resolve();

        return chain.then(function(){
            // після можливого spawn/збору — ring-buffer міг наповнитись під час collect;
            // процесу вже нема (крах/idle) → віддаємо збережений хвіст останнього життя
            report.bgOutput = (processes[id] && processes[id].getOutput)
                ? processes[id].getOutput()
                : (lastBgOutput[id] || null);
            console.info('[plugin:' + id + '] diagnostics collected', console.logLevel.INFO);
            console.log('[plugin:' + id + '] diagnostics report ' + JSON.stringify(report), console.logLevel.DEBUG);
            return report;
        });
    }

    // ---- renderer dispatcher ---------------------------------------------

    var renderHandlers = {
        'manager.list': function(){
            return managerList();
        },
        'manager.pickAndInstall': function(){
            return getDialog().showOpenDialog({
                properties: [ 'openFile' ],
                filters   : [ { name: 'Plugin package', extensions: [ 'zip' ] } ]
            }).then(function(result){
                if (result.canceled || !result.filePaths[0]) return null;
                return packages.install(result.filePaths[0]);
            }).then(function(installed){
                if (!installed) return null;
                var meta = registry.get(installed.id);

                // Обидві гілки віддають manager row: рендерер читає з відповіді
                // requiresReload, і форма payload не має залежати від enabled.
                return meta.enabled ? activate(installed.id, currentLang) : managerRow(installed.id);
            });
        },
        'manager.remove': function(payload){
            return packages.remove(payload.id);
        },
        'manager.enable': function(payload){
            return setEnabled(payload.id, payload.enabled, payload.lang);
        },
        'manager.keepAlive': function(payload){
            return setKeepAlive(payload.id, payload.keepAlive);
        },
        'manager.check': function(payload){
            console.info('[plugin:' + payload.id + '] manual health check', console.logLevel.INFO);

            var gate = canRun(payload.id);

            if (!gate.ok) return Promise.resolve(setHealth(payload.id, gate));
            return sendToBg(payload.id, { type: msg.std.healthCheck, payload: {} })
                .catch(function(error){
                    return setHealth(payload.id, { state: 'error', lastError: error.message });
                });
        },
        'manager.activateEnabled': function(payload){
            return activateEnabled(payload.lang);
        },
        'manager.collect': function(payload){
            return collectDiagnostics(payload.id);
        },
        'manager.reportHealth': function(payload){
            return setHealth(payload.id, payload);
        },
        // Рендерер шле ОДИН конверт на зміну; кому саме доставити — знає host.
        // Пряме send замість sendToBg навмисно: той пройшов би ensureProc і підняв
        // сплячий BG. touchIdle теж не чіпаємо — подія від хоста не є активністю
        // плагіна, інакше перемикання діалогів тримало б чужі процеси живими вічно.
        'context.changed': function(payload){
            Object.keys(contextSubs).forEach(function(id){
                if (!processes[id]) return;

                processes[id].send({
                    pluginId: id, src: 'host', dst: 'bg',
                    type: msg.std.contextChanged, payload: payload
                });
            });
        }
    };

    function emitService(id, type, payload){
        var envelope = { pluginId: id, src: 'host', type: type, payload: payload };

        if (processes[id]){
            processes[id].send(Object.assign({}, envelope, { dst: 'bg' }));
        }
        toRender(Object.assign({}, envelope, { dst: 'ui' }));
    }

    var mainHostHandlers = {
        // Опт-ін на потік context.changed. Обробники саме тут, а не в rendererHostTypes:
        // реєстр має вмирати разом із процесом, а про його смерть знає лише main (onExit).
        // Обидва ідемпотентні — плагінові не потрібен власний стан «чи я вже підписаний».
        'context.subscribe'  : function(id){ contextSubs[id] = true; return true; },
        'context.unsubscribe': function(id){ delete contextSubs[id]; return true; },
        'storage.get' : function(id, p){ return storage.get(id, p.key); },
        'storage.set' : function(id, p){ return storage.set(id, p.key, p.value); },
        'fs.tempPath' : function(id){ return storage.tempPath(id); },
        'download.url'       : function(id, p){ return downloads.url(id, p); },
        'download.serverFile': function(id, p){ return downloads.serverFile(id, p); },
        'download.cancel'    : function(id, p){ return downloads.cancel(id, p.id); }
    };

    function sendAckToRenderer(request, payload){
        toRender({
            pluginId: request.pluginId, src: 'host', dst: 'ui', type: msg.std.ack,
            reqId: request.reqId, payload: payload
        });
    }

    function sendErrorToRenderer(request, error){
        toRender({
            pluginId: request.pluginId, src: 'host', dst: 'ui', type: msg.std.ack,
            reqId: request.reqId, payload: null, error: error.message
        });
    }

    function onRenderMessage(envelope){
        if (!envelope || typeof envelope.type !== 'string') return;

        if (envelope.type === msg.std.ack && envelope.dst === 'bg'){
            return sendToBg(envelope.pluginId, envelope).catch(function(){});
        }

        if (envelope.dst === 'bg'){
            return sendToBg(envelope.pluginId, envelope).catch(function(error){
                setHealth(envelope.pluginId, { state: 'error', lastError: error.message });
            });
        }

        if (envelope.dst === 'host' && renderHandlers[envelope.type]){
            return Promise.resolve()
                .then(function(){ return renderHandlers[envelope.type](envelope.payload || {}); })
                .then(function(payload){ sendAckToRenderer(envelope, payload); })
                .catch(function(error){ sendErrorToRenderer(envelope, error); });
        }

        if (envelope.dst === 'host' && mainHostHandlers[envelope.type]){
            if (!registry.get(envelope.pluginId)){
                return sendErrorToRenderer(envelope, new Error('unknown plugin'));
            }
            return Promise.resolve()
                .then(function(){ return mainHostHandlers[envelope.type](envelope.pluginId, envelope.payload || {}); })
                .then(function(payload){ sendAckToRenderer(envelope, payload); })
                .catch(function(error){ sendErrorToRenderer(envelope, error); });
        }

        sendErrorToRenderer(envelope, new Error('unsupported plugin message'));
    }

    this.paths     = paths;
    this.registry  = registry;
    this.packages  = packages;
    this.allowlist = allowlist;
    this.storage   = storage;

    this.canRun          = canRun;
    this.onRenderMessage = onRenderMessage;
    this.activate        = activate;
    this.deactivate      = deactivate;
    this.sendToBg        = sendToBg;
    this.shutdownAll     = shutdownAll;
}

module.exports = PluginHost;
