const Backbone = require('backbone');
const RuntimeInfo = require('./runtime-info');
const Links = require('../const/links');
const Launcher = require('../comp/launcher');
const AppSettingsModel = require('../models/app-settings-model');
const UpdateModel = require('../models/update-model');
const Transport = require('../comp/transport');
const Logger = require('../util/logger');
const SemVer = require('../util/semver');
const publicKey = require('public-key.pem').default;

const logger = new Logger('updater');

const Updater = {
    UpdateInterval: 1000 * 60 * 60 * 24,
    MinUpdateTimeout: 500,
    MinUpdateSize: 10000,
    UpdateCheckFiles: ['app.asar'],
    nextCheckTimeout: null,
    updateCheckDate: new Date(0),
    enabled: Launcher && Launcher.updaterEnabled(),

    getAutoUpdateType() {
        if (!this.enabled) {
            return false;
        }
        let autoUpdate = AppSettingsModel.instance.get('autoUpdate');
        if (autoUpdate && autoUpdate === true) {
            autoUpdate = 'install';
        }
        return autoUpdate;
    },

    updateInProgress() {
        return (
            UpdateModel.instance.get('status') === 'checking' ||
            ['downloading', 'extracting'].indexOf(UpdateModel.instance.get('updateStatus')) >= 0
        );
    },

    init() {
        this.scheduleNextCheck();
        if (!Launcher && window.applicationCache) {
            window.applicationCache.addEventListener(
                'updateready',
                this.checkAppCacheUpdateReady.bind(this)
            );
            this.checkAppCacheUpdateReady();
        }
    },

    scheduleNextCheck() {
        if (this.nextCheckTimeout) {
            clearTimeout(this.nextCheckTimeout);
            this.nextCheckTimeout = null;
        }
        if (!this.getAutoUpdateType()) {
            return;
        }
        let timeDiff = this.MinUpdateTimeout;
        const lastCheckDate = UpdateModel.instance.get('lastCheckDate');
        if (lastCheckDate) {
            timeDiff = Math.min(
                Math.max(this.UpdateInterval + (lastCheckDate - new Date()), this.MinUpdateTimeout),
                this.UpdateInterval
            );
        }
        this.nextCheckTimeout = setTimeout(this.check.bind(this), timeDiff);
        logger.info('Next update check will happen in ' + Math.round(timeDiff / 1000) + 's');
    },

    check(startedByUser) {
        if (!this.enabled || this.updateInProgress()) {
            return;
        }
        UpdateModel.instance.set('status', 'checking');
        if (!startedByUser) {
            // additional protection from broken program logic, to ensure that auto-checks are not performed more than once an hour
            const diffMs = new Date() - this.updateCheckDate;
            if (isNaN(diffMs) || diffMs < 1000 * 60 * 60) {
                logger.error(
                    'Prevented update check; last check was performed at ' + this.updateCheckDate
                );
                this.scheduleNextCheck();
                return;
            }
            this.updateCheckDate = new Date();
        }
        logger.info('Checking for update...');
        Transport.httpGet({
            url: Links.Manifest,
            utf8: true,
            success: data => {
                const dt = new Date();
                const match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/);
                logger.info('Update check: ' + (match ? match[0] : 'unknown'));
                if (!match) {
                    const errMsg = 'No version info found';
                    UpdateModel.instance.set({
                        status: 'error',
                        lastCheckDate: dt,
                        lastCheckError: errMsg
                    });
                    UpdateModel.instance.save();
                    this.scheduleNextCheck();
                    return;
                }
                const updateMinVersionMatch = data.match(/#\s*updmin:v([\d+\.\w]+)/);
                const prevLastVersion = UpdateModel.instance.get('lastVersion');
                UpdateModel.instance.set({
                    status: 'ok',
                    lastCheckDate: dt,
                    lastSuccessCheckDate: dt,
                    lastVersionReleaseDate: new Date(match[1]),
                    lastVersion: match[2],
                    lastCheckError: null,
                    lastCheckUpdMin: updateMinVersionMatch ? updateMinVersionMatch[1] : null
                });
                UpdateModel.instance.save();
                this.scheduleNextCheck();
                if (!this.canAutoUpdate()) {
                    return;
                }
                if (
                    prevLastVersion === UpdateModel.instance.get('lastVersion') &&
                    UpdateModel.instance.get('updateStatus') === 'ready'
                ) {
                    logger.info('Waiting for the user to apply downloaded update');
                    return;
                }
                if (!startedByUser && this.getAutoUpdateType() === 'install') {
                    this.update(startedByUser);
                } else if (
                    SemVer.compareVersions(
                        UpdateModel.instance.get('lastVersion'),
                        RuntimeInfo.version
                    ) > 0
                ) {
                    UpdateModel.instance.set('updateStatus', 'found');
                }
            },
            error: e => {
                logger.error('Update check error', e);
                UpdateModel.instance.set({
                    status: 'error',
                    lastCheckDate: new Date(),
                    lastCheckError: 'Error checking last version'
                });
                UpdateModel.instance.save();
                this.scheduleNextCheck();
            }
        });
    },

    canAutoUpdate() {
        const minLauncherVersion = UpdateModel.instance.get('lastCheckUpdMin');
        if (minLauncherVersion) {
            const cmp = SemVer.compareVersions(Launcher.version, minLauncherVersion);
            if (cmp < 0) {
                UpdateModel.instance.set({ updateStatus: 'ready', updateManual: true });
                return false;
            }
        }
        return true;
    },

    update(startedByUser, successCallback) {
        const ver = UpdateModel.instance.get('lastVersion');
        if (!this.enabled) {
            logger.info('Updater is disabled');
            return;
        }
        if (SemVer.compareVersions(RuntimeInfo.version, ver) >= 0) {
            logger.info('You are using the latest version');
            return;
        }
        UpdateModel.instance.set({ updateStatus: 'downloading', updateError: null });
        logger.info('Downloading update', ver);
        Transport.httpGet({
            url: Links.UpdateDesktop.replace('{ver}', ver),
            file: 'KeeWeb-' + ver + '.zip',
            cache: !startedByUser,
            success: filePath => {
                UpdateModel.instance.set('updateStatus', 'extracting');
                logger.info('Extracting update file', this.UpdateCheckFiles, filePath);
                this.extractAppUpdate(filePath, err => {
                    if (err) {
                        logger.error('Error extracting update', err);
                        UpdateModel.instance.set({
                            updateStatus: 'error',
                            updateError: 'Error extracting update'
                        });
                    } else {
                        UpdateModel.instance.set({ updateStatus: 'ready', updateError: null });
                        if (!startedByUser) {
                            Backbone.trigger('update-app');
                        }
                        if (typeof successCallback === 'function') {
                            successCallback();
                        }
                    }
                });
            },
            error(e) {
                logger.error('Error downloading update', e);
                UpdateModel.instance.set({
                    updateStatus: 'error',
                    updateError: 'Error downloading update'
                });
            }
        });
    },

    extractAppUpdate(updateFile, cb) {
        const expectedFiles = this.UpdateCheckFiles;
        const appPath = Launcher.getUserDataPath();
        const StreamZip = Launcher.req('node-stream-zip');
        StreamZip.setFs(Launcher.req('original-fs'));
        const zip = new StreamZip({ file: updateFile, storeEntries: true });
        zip.on('error', cb);
        zip.on('ready', () => {
            const containsAll = expectedFiles.every(expFile => {
                const entry = zip.entry(expFile);
                return entry && entry.isFile;
            });
            if (!containsAll) {
                return cb('Bad archive');
            }
            const validationError = this.validateArchiveSignature(updateFile, zip);
            if (validationError) {
                return cb('Invalid archive: ' + validationError);
            }
            zip.extract(null, appPath, err => {
                zip.close();
                if (err) {
                    return cb(err);
                }
                Launcher.deleteFile(updateFile);
                cb();
            });
        });
    },

    validateArchiveSignature(archivePath, zip) {
        if (!zip.comment) {
            return 'No comment in ZIP';
        }
        if (zip.comment.length !== 512) {
            return 'Bad comment length in ZIP: ' + zip.comment.length;
        }
        try {
            const zipFileData = Launcher.req('fs').readFileSync(archivePath);
            const verify = Launcher.req('crypto').createVerify('RSA-SHA256');
            verify.write(zipFileData.slice(0, zip.centralDirectory.headerOffset + 22));
            verify.end();
            const signature = window.Buffer.from(zip.comment, 'hex');
            if (!verify.verify(publicKey, signature)) {
                return 'Invalid signature';
            }
        } catch (err) {
            return err.toString();
        }
        return null;
    },

    checkAppCacheUpdateReady() {
        if (window.applicationCache.status === window.applicationCache.UPDATEREADY) {
            try {
                window.applicationCache.swapCache();
            } catch (e) {}
            UpdateModel.instance.set('updateStatus', 'ready');
        }
    }
};

module.exports = Updater;
