"use strict";

const fs       = require('fs');
const path     = require('path');
const crypto   = require('crypto');
const AdmZip   = require('adm-zip');
const manifest = require('./pluginManifest');
const console  = require('gifer-console');

function PluginPackage(opts){
    var paths       = opts.paths;
    var registry    = opts.registry;
    var deactivate  = opts.deactivate;
    var hostVersion = opts.hostVersion;

    // Гейт версії клієнта. Розрізняємо два різні стани, які раніше зливались в один:
    //   hostVersion === undefined — гейт не сконфігуровано (так конструюють у тестах) → пропускаємо;
    //   hostVersion задано, але порожнє/невалідне — jPackage не прочитався (main.js:186-192)
    //     → мовчазний пропуск перетворив би єдиний запобіжник на фікцію, тому відхиляємо.
    function assertHostVersion(minHost){
        if (!minHost || hostVersion === undefined){
            return;
        }

        if (!manifest.isVersion(hostVersion)){
            throw new Error('plugin requires client >= ' + minHost + ' but client version is unknown');
        }

        if (manifest.compareVersions(minHost, hostVersion) > 0){
            throw new Error('plugin requires client >= ' + minHost + ' (current ' + hostVersion + ')');
        }
    }

    // adm-zip глобально підміняє fs.existsSync (кидає на відсутньому файлі)
    function safeExists(target){
        try { return fs.existsSync(target); } catch (e) { return false; }
    }

    function sha256(filePath){
        return 'sha256:' + crypto.createHash('sha256')
            .update(fs.readFileSync(filePath)).digest('hex');
    }

    function assertArchivePaths(zip){
        zip.getEntries().forEach(function(entry){
            var name = entry.entryName.replace(/\\/g, '/');

            if (path.posix.isAbsolute(name) || name.split('/').indexOf('..') !== -1){
                console.error('[plugins] zip-slip rejected: ' + entry.entryName, console.logLevel.ERROR);
                throw new Error('archive entry escapes target: ' + entry.entryName);
            }
        });
    }

    async function install(zipPath){
        var checksum = sha256(zipPath);

        console.info('[plugins] install start: ' + zipPath + ' (' + checksum + ')', console.logLevel.INFO);

        var zip      = new AdmZip(zipPath);
        var token    = Date.now() + '-' + crypto.randomBytes(6).toString('hex');
        var temp     = path.join(paths.installRoot, token);
        var final;
        var backup;
        var oldMeta;
        var parsed;
        var swapped = false;

        try {
            assertArchivePaths(zip);
            fs.mkdirSync(temp, { recursive: true });
            zip.extractAllTo(temp, true);

            parsed = manifest.validateManifest(JSON.parse(
                fs.readFileSync(path.join(temp, 'manifest.json'), 'utf8')
            ));
            manifest.validateEntries(temp, parsed);

            // Гейт версії — ДО незворотного swap теки: помилка тут іде наявним
            // rollback-шляхом (swapped ще false, тека плагіна не зачеплена).
            assertHostVersion(parsed.minHost);

            final   = paths.pluginCode(parsed.id);
            backup  = final + '.backup-' + token;
            oldMeta = registry.get(parsed.id);

            await deactivate(parsed.id);
            if (safeExists(final)) fs.renameSync(final, backup);
            fs.renameSync(temp, final);
            swapped = true;

            registry.upsert(parsed.id, {
                version    : parsed.version,
                checksum   : checksum,
                installedAt: Date.now(),
                enabled    : oldMeta ? oldMeta.enabled : false,
                keepAlive  : oldMeta ? oldMeta.keepAlive : false
            });

            if (safeExists(backup)){
                try { fs.rmSync(backup, { recursive: true, force: true }); }
                catch (cleanupError) { /* повторити cleanup окремою maintenance-операцією */ }
            }
            console.info('[plugin:' + parsed.id + '] installed v' + parsed.version + ' ' + checksum, console.logLevel.INFO);
            return { id: parsed.id, checksum: checksum, manifest: parsed };
        } catch (e) {
            console.warn('[plugins] install failed' + (parsed ? ' (' + parsed.id + ')' : '') + ': ' + e.message + (swapped ? ' — rolling back' : ''), console.logLevel.WARN);
            if (swapped && safeExists(final)) fs.rmSync(final, { recursive: true, force: true });
            if (backup && safeExists(backup)) fs.renameSync(backup, final);
            if (safeExists(temp)) fs.rmSync(temp, { recursive: true, force: true });
            throw e;
        }
    }

    async function remove(id){
        if (!registry.get(id)) throw new Error('plugin is not installed: ' + id);

        await deactivate(id);
        [ paths.pluginCode(id), paths.pluginData(id) ].forEach(function(dir){
            if (safeExists(dir)) fs.rmSync(dir, { recursive: true, force: true });
        });
        registry.remove(id);
        console.info('[plugin:' + id + '] removed', console.logLevel.INFO);
    }

    this.install = install;
    this.remove  = remove;
}

module.exports = PluginPackage;
