"use strict";
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkForUpdates = exports.autoCheckUpdateResult = void 0;
exports.getBestUpdate = getBestUpdate;
exports.getVersions = getVersions;
exports.getUpdates = getUpdates;
exports.localUpdateAvailable = localUpdateAvailable;
exports.previousAvailable = previousAvailable;
exports.updateSupported = updateSupported;
exports.update = update;
const github_1 = require("./github");
const const_1 = require("./const");
const path_1 = require("path");
const child_process_1 = require("child_process");
const misc_1 = require("./misc");
const fs_1 = require("fs");
const plugins_1 = require("./plugins");
const promises_1 = require("fs/promises");
const open_1 = __importDefault(require("open"));
const config_1 = require("./config");
const util_os_1 = require("./util-os");
const first_1 = require("./first");
const persistence_1 = require("./persistence");
const lodash_1 = __importDefault(require("lodash"));
const argv_1 = require("./argv");
const promises_2 = require("stream/promises");
const updateToBeta = (0, config_1.defineConfig)('update_to_beta', false);
const autoCheckUpdate = (0, config_1.defineConfig)('auto_check_update', true);
const lastCheckUpdate = persistence_1.storedMap.singleSync('lastCheckUpdate', 0);
const AUTO_CHECK_EVERY = misc_1.DAY;
exports.autoCheckUpdateResult = persistence_1.storedMap.singleSync('autoCheckUpdateResult', undefined);
exports.autoCheckUpdateResult.ready().then(() => {
    exports.autoCheckUpdateResult.set(v => {
        if (!v)
            return; // refresh isNewer, as currentVersion may have changed
        v.isNewer = config_1.currentVersion.olderThan(v.tag_name);
        return v;
    });
});
config_1.configReady.then(lastCheckUpdate.ready).then(() => (0, misc_1.repeat)(misc_1.HOUR, () => {
    if (autoCheckUpdate.get() && Date.now() > lastCheckUpdate.get() + AUTO_CHECK_EVERY)
        return (0, exports.checkForUpdates)();
}));
exports.checkForUpdates = (0, misc_1.debounceAsync)(async () => {
    try {
        const u = await getBestUpdate();
        if (u)
            console.log("New version available", u.name);
        exports.autoCheckUpdateResult.set(u);
        lastCheckUpdate.set(Date.now());
    }
    catch { }
}, { reuseRunning: true });
async function getBestUpdate() {
    return (await getUpdates(true))[0];
}
const ReleaseKeys = ['prerelease', 'tag_name', 'name', 'body', 'assets', 'isNewer', 'versionScalar'];
const ReleaseAssetKeys = ['name', 'browser_download_url'];
const curV = config_1.currentVersion.scalar;
function prepareRelease(r) {
    const v = (0, config_1.versionToScalar)(r.name);
    return Object.assign(lodash_1.default.pick(r, ReleaseKeys), {
        versionScalar: v,
        isNewer: v > curV, // make easy to know what's newer
        assets: r.assets.map((a) => lodash_1.default.pick(a, ReleaseAssetKeys))
    });
}
async function getVersions(filter, max = 30) {
    const ret = [];
    for await (const x of (0, github_1.apiGithubPaginated)(`repos/${const_1.HFS_REPO}/releases`)) {
        if (x.name.endsWith('-ignore'))
            continue;
        const rel = prepareRelease(x);
        if (rel.versionScalar === curV)
            continue;
        if (!filter || filter(rel))
            ret.push(rel);
        if (ret.length >= max)
            break;
    }
    return lodash_1.default.sortBy(ret, x => -x.versionScalar);
}
async function getUpdates(strict = false) {
    console.log("Checking for updates");
    void (0, github_1.getProjectInfo)(); // also check for alerts and print them asap in the console
    const stable = prepareRelease(await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases/latest'));
    const includeBetas = updateToBeta.get() || const_1.RUNNING_BETA;
    // we don't consider betas before stable
    const betas = !includeBetas ? [] : await getVersions(x => x.prerelease && x.versionScalar > stable.versionScalar && (!strict || x.isNewer));
    if (stable.isNewer || const_1.RUNNING_BETA && !strict)
        betas.push(stable);
    return betas;
}
const LOCAL_UPDATE = 'hfs-update.zip'; // update from file takes precedence over net
const INSTALLED_FN = 'hfs-installed.zip';
const PREVIOUS_FN = 'hfs-previous.zip';
function localUpdateAvailable() {
    return (0, misc_1.exists)(LOCAL_UPDATE);
}
function previousAvailable() {
    return (0, misc_1.exists)(PREVIOUS_FN);
}
function updateSupported() {
    return !process.env.DISABLE_UPDATE && (argv_1.argv.forceupdate || const_1.IS_BINARY);
}
async function update(tagOrUrl = '') {
    if (!updateSupported())
        throw process.env.DISABLE_UPDATE ? "Automatic updates are disabled"
            : "Only binary versions support automatic updates";
    let url = tagOrUrl.includes('://') && tagOrUrl;
    if (tagOrUrl === const_1.PREVIOUS_TAG)
        await (0, promises_1.rename)(PREVIOUS_FN, LOCAL_UPDATE);
    else if (tagOrUrl ? !url : !await localUpdateAvailable()) {
        if (/^\d/.test(tagOrUrl)) // work even if the tag is passed without the initial 'v' (useful for console commands)
            tagOrUrl = 'v' + tagOrUrl;
        const update = !tagOrUrl ? await getBestUpdate()
            : await (0, github_1.getRepoInfo)(const_1.HFS_REPO + '/releases/tags/' + tagOrUrl).catch(e => {
                if (e.message === '404')
                    console.error("Version not found");
                else
                    throw e;
            });
        if (!update)
            throw "No update has been found";
        const plat = '-' + (0, misc_1.xlate)(process.platform, { win32: 'windows', darwin: 'mac' });
        const assetSearch = `${plat}-${process.arch}`;
        const asset = update.assets.find((x) => x.name.includes(assetSearch) && x.name.endsWith('.zip'));
        if (!asset)
            throw `Asset not found: ${assetSearch}`;
        url = asset.browser_download_url;
    }
    if (url) {
        console.log("Downloading", url);
        const temp = LOCAL_UPDATE + '-temp';
        await (0, promises_1.rm)(temp, { force: true });
        try {
            const stream = await (0, misc_1.httpStream)(url);
            const total = Number(stream.headers['content-length']) || 0;
            let downloadedSize = 0;
            const progress = total && setInterval(() => console.log("Download progress", (0, misc_1.formatPerc)(downloadedSize / total)), 5_000);
            stream.on('data', chunk => downloadedSize += chunk.length);
            await (0, promises_2.pipeline)(stream, (0, fs_1.createWriteStream)(temp))
                .finally(() => clearInterval(progress));
        }
        catch (e) {
            await (0, promises_1.rm)(temp).catch(() => { }); // no leftovers
            throw "Download failed for " + url + (0, misc_1.prefix)(' – ', e?.message);
        }
        await (0, promises_1.rename)(temp, LOCAL_UPDATE);
        console.debug("Download finished");
    }
    const bin = process.execPath;
    const binPath = (0, path_1.dirname)(bin);
    const binFile = 'hfs' + (const_1.IS_WINDOWS ? '.exe' : ''); // the bin we are currently running could have been renamed
    let newBinFile = binFile;
    do {
        newBinFile = 'new-' + newBinFile;
    } while ((0, fs_1.existsSync)((0, path_1.join)(binPath, newBinFile)));
    plugins_1.pluginsWatcher.pause();
    try {
        await (0, misc_1.unzip)((0, fs_1.createReadStream)(LOCAL_UPDATE), path => (0, path_1.join)(binPath, path === binFile ? newBinFile : path));
        const newBin = (0, path_1.join)(binPath, newBinFile);
        if (!(0, fs_1.existsSync)(newBin)) {
            if (url) // the file was downloaded, and the UI would show the "update from local file" button until we remove it
                await (0, promises_1.rm)(LOCAL_UPDATE).catch(e => console.warn(String(e)));
            throw "Missing executable in the archive";
        }
        if (!const_1.IS_WINDOWS) {
            const { mode } = await (0, misc_1.statWithTimeout)(bin);
            await (0, promises_1.chmod)(newBin, mode).catch(console.error);
        }
        await (0, promises_1.rename)(INSTALLED_FN, PREVIOUS_FN).catch(e => e?.code !== 'ENOENT' && console.warn(String(e)));
        await (0, promises_1.rename)(LOCAL_UPDATE, INSTALLED_FN).catch(e => console.warn(String(e)));
        // the bridge process exists to preserve terminal access; skip it when there's no terminal
        const preserveTerminal = process.stdin.isTTY && !await util_os_1.runningAsWindowsService; // NSSM fakes a TTY, so we need explicit detection on Windows
        (0, first_1.onProcessExit)(() => {
            const oldBinFile = 'old-' + binFile;
            const oldBin = (0, path_1.join)(binPath, oldBinFile);
            try {
                (0, fs_1.unlinkSync)(oldBin);
            }
            catch { }
            (0, fs_1.renameSync)(bin, oldBin);
            if (!preserveTerminal) {
                try {
                    (0, misc_1.retrySync)(() => (0, fs_1.renameSync)(newBin, (0, path_1.join)(binPath, binFile)));
                }
                catch (e) {
                    try {
                        (0, fs_1.renameSync)(oldBin, bin);
                    } // restore the service target because hfs.exe was already moved aside
                    catch (rollbackError) {
                        console.error("Couldn't restore original binary after failed update", rollbackError);
                    }
                    throw e;
                }
                console.log("Updated binary in place, exiting for process supervisor to restart");
                return;
            }
            // preserving the terminal requires a longer trip
            console.log("Launching new version in background", newBinFile);
            (0, child_process_1.spawnSync)((0, util_os_1.cmdEscape)(newBin), ['--updating', binFile, '--cwd .'], { shell: true, stdio: [0, 1, 2] }); // sync necessary to work on Mac by double-click
        });
        console.log("Quitting");
        setTimeout(() => (0, first_1.quit)(100), // non-zero, otherwise some service managers (like Shawl) won't restart the process.
        200); // give time to return (and caller to complete, eg: rest api to reply)
    }
    catch (e) {
        plugins_1.pluginsWatcher.unpause();
        throw e?.message || String(e);
    }
}
if (argv_1.argv.updating) { // we were launched with a temporary name, restore original name to avoid breaking references
    const bin = process.execPath;
    const dest = (0, path_1.join)((0, path_1.dirname)(bin), argv_1.argv.updating);
    (0, fs_1.renameSync)(bin, dest);
    // have to relaunch with the new name, or otherwise the next update will fail with EBUSY on hfs.exe
    console.log(`Renamed binary file to "${argv_1.argv.updating}" and now restarting`);
    // if you change anything, be sure to test launching both double-clicking and in a terminal
    if (const_1.IS_WINDOWS) // windows-only; this method on mac+linux works only once, and without the console
        (0, first_1.onProcessExit)(() => (0, child_process_1.spawn)((0, util_os_1.cmdEscape)(dest), ['--updated', '--cwd .'], { detached: true, shell: true, stdio: [0, 1, 2] })); // launch+sync here would cause the old process to stay open, locking ports
    else if (const_1.IS_MAC) {
        // open() is the only consistent way that I could find working on macos preserving console input/output over relaunching,
        // and it doesn't let us pass cli arguments, so we pass them through a temp file consumed at the next startup.
        // For the record, on mac you can: write "./hfs arg1 arg2" to /tmp/tmp.sh with 0o700, and then spawn "open -a Terminal /tmp/tmp.sh"
        try {
            (0, fs_1.writeFileSync)(const_1.ARGS_FILE, JSON.stringify(['--updated', '--cwd', process.cwd()]));
        }
        catch { }
        console.log('Open-ing');
        void (0, open_1.default)(dest);
    }
    else // linux and *nix on terminal: in interactive terminals, block this bridge process on the restarted hfs so the terminal session stays attached
        (0, child_process_1.spawnSync)(dest, ['--updated', '--cwd', process.cwd()], { stdio: [0, 1, 2] });
    process.exit();
}
