"use strict";
const Promise = require("bluebird");
const electron_1 = require("electron");
const Utils = require("../util/electronutils");
const events_1 = require("events");
const os_1 = require("os");
const osxupdater_1 = require("./osxupdater");
const path = require("path");
const productloader_1 = require("../util/productloader");
const semver = require("semver");
const Telemetry = require("../util/telemetry");
const windowsupdater_1 = require("./windowsupdater");
(function (UpdateEventType) {
    UpdateEventType[UpdateEventType["Critical"] = 1] = "Critical";
    UpdateEventType[UpdateEventType["Notification"] = 2] = "Notification";
    UpdateEventType[UpdateEventType["Prompt"] = 3] = "Prompt";
})(exports.UpdateEventType || (exports.UpdateEventType = {}));
var UpdateEventType = exports.UpdateEventType;
/**
 * This class is the main entry point for updates in the app. It is responsible for checking if updates are available,
 * downloading updates, applying updates, and understanding the release manifest of the target. In addition to the
 * public method it exposes, this class also emits the following events:
 *   critical-update:       Emitted when the current version is blacklisted in the release manifest
 *   update-available:      Emitted when an update is available
 *   update-not-available:  Emitted when no update is available
 *   update-check-error:    Emitted when there was an error while checking for an update
 *   update-download-error: Emitted when there was an error while downloading an update
 *
 * These events are emitted with an UpdateEventInfo object, which gives the handlers relevant info.
 */
class UpdateService extends events_1.EventEmitter {
    constructor() {
        super();
        this.UPDATE_CACHE_NAME = `${productloader_1.default.targetId}-update`;
        this.RELEASE_MANIFEST_NAME = "release.json";
        this.CACHE_LIFE_MS = 20 * 60 * 1000; // 20 minutes before re-fetching the release manifest
        this.updaterImpl = null;
        this._cacheInfo = null;
        switch (process.platform) {
            case "win32":
                this.updaterImpl = new windowsupdater_1.WindowsUpdater(this.cacheInfo);
                break;
            case "darwin":
                this.updaterImpl = new osxupdater_1.OsxUpdater();
                break;
        }
        Telemetry.tickEvent("electron.update.enabled");
    }
    /**
     * Information about the cache in the temp folder where we store the release manifest (and also the installers on
     * Windows).
     */
    get cacheInfo() {
        if (!this._cacheInfo) {
            const updateCache = path.join(os_1.tmpdir(), this.UPDATE_CACHE_NAME);
            Utils.mkdirp(updateCache);
            this._cacheInfo = {
                cachePath: updateCache,
                releaseManifestFileName: this.RELEASE_MANIFEST_NAME,
                releaseManifestPath: path.join(updateCache, this.RELEASE_MANIFEST_NAME)
            };
        }
        return this._cacheInfo;
    }
    /**
     * Queries the release manifest, and based on the app's current version, emits either critical-update or
     * update-available, with UpdateEventInfo.isInitialCheck set to true. If the current app is up-to-date, then no
     * events are emitted from this check.
     */
    initialCheck() {
        this.getReleaseManifest()
            .then((releaseManifest) => {
            const targetVersion = this.getTargetRelease(releaseManifest);
            const versionInfo = this.getCurrentVersionInfo(releaseManifest);
            const criticalPrompt = versionInfo.banned.some((banRange) => {
                return semver.satisfies(productloader_1.default.version, banRange);
            });
            const prompt = semver.lte(productloader_1.default.version, versionInfo.prompt);
            const notification = semver.lte(productloader_1.default.version, versionInfo.notification);
            if (targetVersion) {
                if (criticalPrompt) {
                    Telemetry.tickEvent("electron.update.available", {
                        initial: "true",
                        type: "critical"
                    });
                    this.emit("critical-update", this.makeUpdateInfo(targetVersion, UpdateEventType.Critical));
                }
                else if (prompt) {
                    Telemetry.tickEvent("electron.update.available", {
                        initial: "true",
                        type: "prompt"
                    });
                    this.emit("update-available", this.makeUpdateInfo(targetVersion, UpdateEventType.Prompt, /*isInitialCheck*/ true));
                }
                else if (notification) {
                    Telemetry.tickEvent("electron.update.available", {
                        initial: "true",
                        type: "notification"
                    });
                    this.emit("update-available", this.makeUpdateInfo(targetVersion, UpdateEventType.Notification, /*isInitialCheck*/ true));
                }
            }
            else {
                Telemetry.tickEvent("electron.update.notavailable", {
                    initial: "true"
                });
            }
        })
            .catch((e) => {
            // In case of error, be permissive (swallow the error and let the app continue normally)
            console.log("Error during initial version check: " + e);
            Telemetry.tickEvent("electron.update.initialcheckfailed");
        });
    }
    /**
     * Queries the release manifest to check if an update is available. Emits either update-available or
     * update-not-available if the check was successful, or update-check-error if there was an error during the check.
     */
    checkForUpdate() {
        this.getReleaseManifest()
            .then((releaseManifest) => {
            const targetVersion = this.getTargetRelease(releaseManifest);
            if (targetVersion) {
                Telemetry.tickEvent("electron.update.available");
                this.emit("update-available", this.makeUpdateInfo(targetVersion, UpdateEventType.Prompt));
            }
            else {
                Telemetry.tickEvent("electron.update.notavailable");
                this.emit("update-not-available");
            }
        })
            .catch((e) => {
            Telemetry.tickEvent("electron.update.checkerror");
            this.emit("update-check-error");
            console.log("Error during update check: " + e);
        });
    }
    /**
     * Performs an update. If the target version is a URL, the URL is opened in the default browser. If the target
     * version is a tag, the associated installer is downloaded and run.
     */
    update(targetVersion, isCritical = false) {
        if (/^https:\/\//.test(targetVersion)) {
            Telemetry.tickEvent("electron.update.websiteupdate", {
                critical: isCritical.toString()
            });
            electron_1.shell.openExternal(targetVersion);
            if (isCritical) {
                electron_1.app.exit();
            }
            return;
        }
        Telemetry.tickEvent("electron.update.installerupdate", {
            critical: isCritical.toString()
        });
        const deferred = Utils.defer();
        this.updaterImpl.addListener("update-downloaded", () => {
            Telemetry.tickEvent("electron.update.downloaded", {
                critical: isCritical.toString()
            });
            this.updaterImpl.quitAndInstall();
        });
        this.updaterImpl.addListener("update-not-available", () => {
            Telemetry.tickEvent("electron.update.nolongeravailable", {
                critical: isCritical.toString()
            });
            deferred.reject(new Error("Update is no longer available"));
        });
        this.updaterImpl.addListener("error", (e) => {
            Telemetry.tickEvent("electron.update.updateerror", {
                critical: isCritical.toString()
            });
            deferred.reject(e);
        });
        try {
            let downloadUrl = productloader_1.default.updateDownloadUrl;
            const platformString = productloader_1.default.updateTag ? `${process.platform}-${productloader_1.default.updateTag}` : process.platform;
            downloadUrl = downloadUrl.replace(/{{version}}/, targetVersion);
            downloadUrl = downloadUrl.replace(/{{platform}}/, platformString);
            this.updaterImpl.setFeedURL(downloadUrl);
        }
        catch (e) {
            // On OSX, an error here means the current app isn't signed. Update is not possible.
            deferred.reject(e);
        }
        // The following call downloads the release and emits "update-downloaded" when done
        this.updaterImpl.checkForUpdates();
        deferred.promise.catch((e) => {
            this.emit("update-download-error", {
                critical: isCritical.toString()
            });
            console.log("Error during update: " + e);
        });
    }
    getCurrentVersionInfo(releaseManifest) {
        const major = semver.major(productloader_1.default.version);
        return releaseManifest.versions[major];
    }
    /**
     * Decides which version to update to. First looks in the list of URLs to see if the current version is listed, and
     * returns the URL to visit if found. If current version is not listed in the URL updates, then returns the latest
     * version, if it is higher than the current version. Returns null if no update is available.
     */
    getTargetRelease(releaseManifest) {
        const versionInfo = this.getCurrentVersionInfo(releaseManifest);
        let urlUpdate;
        versionInfo.urls && Object.keys(versionInfo.urls).find((semverRange) => {
            if (semver.satisfies(productloader_1.default.version, semverRange)) {
                urlUpdate = versionInfo.urls[semverRange];
                return true;
            }
            return false;
        });
        if (urlUpdate) {
            return urlUpdate;
        }
        if (versionInfo && semver.lt(productloader_1.default.version, versionInfo.latest)) {
            return versionInfo.latest;
        }
        return null;
    }
    makeUpdateInfo(targetVersion, type, isInitialCheck = false) {
        return {
            appName: productloader_1.default.nameLong,
            isInitialCheck,
            targetVersion,
            type
        };
    }
    /**
     * Gets the target's release manifest. We try to use a cached copy first, but if there is none, or if the cache is
     * outdated, we download a new one by using the URL specified in the product info. The release manifest is cached
     * after download.
     */
    getReleaseManifest() {
        return Promise.resolve()
            .then(() => {
            return Utils.readJsonFileAsync(this.cacheInfo.releaseManifestPath);
        })
            .then((releaseManifest) => {
            if (releaseManifest && releaseManifest.timestamp) {
                const cachedTime = Date.parse(releaseManifest.timestamp);
                const currentTime = Date.parse((new Date()).toISOString());
                if (!isNaN(cachedTime) && currentTime - cachedTime <= this.CACHE_LIFE_MS) {
                    return Promise.resolve(releaseManifest);
                }
            }
            return this.downloadReleaseManifest()
                .catch((e) => {
                Telemetry.tickEvent("electron.update.downloadmanifesterror");
                // Error downloading a new manifest; if we had one in the cache, fallback to that even if it
                // was outdated
                if (releaseManifest) {
                    return releaseManifest;
                }
                // If not, propagate the error
                throw new Error("Error downloading the release manifest: " + e);
            });
        });
    }
    downloadReleaseManifest() {
        if (!productloader_1.default.releaseManifestUrl) {
            return Promise.reject(null);
        }
        return Utils.requestAsStream({ url: productloader_1.default.releaseManifestUrl })
            .then(Utils.asJson)
            .then((releaseManifest) => {
            return this.cacheReleaseManifest(releaseManifest)
                .catch((e) => {
                Telemetry.tickEvent("electron.update.cachemanifesterror");
                // No-op; failing to cache the manifest is not a critical error
                console.log("Error caching the release manifest: " + e);
            })
                .then(() => releaseManifest);
        });
    }
    cacheReleaseManifest(releaseManifest) {
        releaseManifest.timestamp = (new Date()).toISOString();
        return Utils.fsWriteFilePromise(this.cacheInfo.releaseManifestPath, JSON.stringify(releaseManifest));
    }
}
exports.UpdateService = UpdateService;
