"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateFirmwareTask = exports.getFlashMcuOrBootloaderDetails = void 0;
const errors_1 = require("@ledgerhq/errors");
const logs_1 = require("@ledgerhq/logs");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const getVersion_1 = require("../commands/getVersion");
const installFirmware_1 = require("../commands/firmwareUpdate/installFirmware");
const flashMcuOrBootloader_1 = require("../commands/firmwareUpdate/flashMcuOrBootloader");
const quitApp_1 = require("../commands/quitApp");
const api_1 = __importDefault(require("../../manager/api"));
const core_1 = require("./core");
const core_2 = require("../transports/core");
const getDeviceInfo_1 = require("./getDeviceInfo");
const device_management_kit_1 = require("@ledgerhq/device-management-kit");
// Wrapped get version command retries the command until it works, it will ignore both the disconnected
// device and unresponsive errors (usually meaning locked device), and return the info needed when
// the device is connected again and unlocked
const waitForGetVersion = (0, core_1.retryOnErrorsCommandWrapper)({
    command: ({ transport }) => (0, getVersion_1.getVersion)({ transport }).pipe((0, operators_1.filter)((e) => e.type === "data")),
    allowedErrors: [
        { maxRetries: "infinite", errorClass: errors_1.DisconnectedDeviceDuringOperation },
        { maxRetries: "infinite", errorClass: errors_1.DisconnectedDevice },
        { maxRetries: "infinite", errorClass: errors_1.CantOpenDevice },
    ],
    allowedDmkErrors: [
        new device_management_kit_1.DeviceDisconnectedWhileSendingError(),
        new device_management_kit_1.DeviceDisconnectedBeforeSendingApdu(),
        new device_management_kit_1.SendApduEmptyResponseError(),
    ],
});
function internalUpdateFirmwareTask({ deviceId, deviceName, updateContext, }) {
    const tracer = new logs_1.LocalTracer(core_1.LOG_TYPE, {
        function: "updateFirmwareTask",
    });
    return new rxjs_1.Observable(subscriber => {
        const sub = (0, core_2.withTransport)(deviceId, deviceName ? { matchDeviceByName: deviceName } : undefined)(({ transportRef }) => (0, rxjs_1.concat)((0, quitApp_1.quitApp)(transportRef.current).pipe((0, operators_1.switchMap)(() => {
            tracer.updateContext({ transportContext: transportRef.current.getTraceContext() });
            return waitForGetVersion(transportRef, {});
        }), (0, operators_1.switchMap)(value => {
            const { firmwareInfo } = value;
            return subscriber.closed
                ? rxjs_1.EMPTY
                : installOsuFirmware({
                    firmwareInfo,
                    updateContext,
                    transport: transportRef.current,
                    tracer,
                }).pipe((0, operators_1.map)(e => {
                    if (e.type === "unresponsive") {
                        return {
                            type: "error",
                            error: new errors_1.UnresponsiveDeviceError(),
                            // If unresponsive, the command is still waiting for a response
                            retrying: true,
                        };
                    }
                    return e;
                }));
        })), waitForGetVersion(transportRef, {}).pipe((0, operators_1.switchMap)(({ firmwareInfo }) => {
            return subscriber.closed
                ? rxjs_1.EMPTY
                : flashMcuOrBootloader(updateContext, firmwareInfo, transportRef, deviceId);
        })))).subscribe({
            next: event => {
                switch (event.type) {
                    case "allowSecureChannelRequested":
                        tracer.trace("allowSecureChannelRequested");
                        subscriber.next(event);
                        break;
                    case "firmwareInstallPermissionRequested":
                        tracer.trace("firmwareInstallPermissionRequested");
                        subscriber.next({ type: "installOsuDevicePermissionRequested" });
                        break;
                    case "firmwareInstallPermissionGranted":
                        tracer.trace("firmwareInstallPermissionGranted");
                        subscriber.next({ type: "installOsuDevicePermissionGranted" });
                        break;
                    case "progress":
                        subscriber.next({
                            type: "installingOsu",
                            progress: event.progress,
                        });
                        break;
                    default:
                        subscriber.next(event);
                }
            },
            complete: () => subscriber.complete(),
            error: error => {
                tracer.trace(`Error: ${error}`, { error });
                if (error instanceof errors_1.UserRefusedFirmwareUpdate) {
                    subscriber.next({ type: "installOsuDevicePermissionDenied" });
                }
                else if (error instanceof errors_1.UserRefusedAllowManager) {
                    subscriber.next({ type: "allowSecureChannelDenied" });
                }
                else {
                    subscriber.next({ type: "error", error, retrying: false });
                    subscriber.complete();
                }
            },
        });
        return {
            unsubscribe: () => sub.unsubscribe(),
        };
    });
}
/**
 * The final MCU version that is retrieved from the API has the information on which bootloader it
 * can be installed on. Therefore if the device is in bootloader mode, but its bootloader version
 * (deviceInfo.majMin) is NOT the version for on which the MCU should be installed
 * (majMin !== mcuVersion.from_bootloader_version), this means that we should first install a new
 * bootloader version before installing the MCU. We return this information (isMcuUpdate as false)
 * and the majMin formatted version of which bootloader should be installedb (bootloaderVersion).
 * Otherwise, if the current majMin version is indeed the bootloader version on which this MCU can
 * be installed, it means that we can directly install the MCU (isMcuUpdate as true)
 * @param deviceMajMin the current majMin version present in the device
 * @param mcuFromBootloaderVersion  the bootloader on which the MCU is able to be installed
 * @returns the formatted bootloader version to be installed and if it's actually needed
 */
function getFlashMcuOrBootloaderDetails(deviceMajMin, mcuFromBootloaderVersion) {
    // converts the version into the majMin format
    const bootloaderVersion = (mcuFromBootloaderVersion ?? "").split(".").slice(0, 3).join(".");
    const isMcuUpdate = deviceMajMin === bootloaderVersion;
    return { bootloaderVersion, isMcuUpdate };
}
exports.getFlashMcuOrBootloaderDetails = getFlashMcuOrBootloaderDetails;
const MAX_FLASH_REPETITIONS = 5;
// recursive loop function that will continue flashing either MCU or the Bootloader, until
// the device is no longer on bootloader mode
const flashMcuOrBootloader = (updateContext, firmwareInfo, transportRef, deviceId, repetitions = 0) => {
    return new rxjs_1.Observable(subscriber => {
        if (!updateContext.shouldFlashMCU) {
            // if we don't need to flash the MCU then OSU install was all that was needed
            // that means that only the Secure Element firmware has been updated, the update is complete
            subscriber.next({
                type: "firmwareUpdateCompleted",
                updatedDeviceInfo: (0, getDeviceInfo_1.parseDeviceInfo)(firmwareInfo),
            });
            subscriber.complete();
            return;
        }
        if (!firmwareInfo.isBootloader) {
            subscriber.next({
                type: "taskError",
                error: "DeviceOnBootloaderExpected",
            });
            subscriber.complete();
        }
        api_1.default.retrieveMcuVersion(updateContext.final).then(mcuVersion => {
            if (mcuVersion && !subscriber.closed) {
                const majMinRegexMatch = firmwareInfo.rawVersion.match(/([0-9]+.[0-9]+(.[0-9]+){0,1})?(-(.*))?/);
                const [, majMin] = majMinRegexMatch ?? [];
                const { bootloaderVersion, isMcuUpdate } = getFlashMcuOrBootloaderDetails(majMin, mcuVersion.from_bootloader_version);
                (0, flashMcuOrBootloader_1.flashMcuOrBootloaderCommand)(transportRef.current, {
                    targetId: firmwareInfo.targetId,
                    version: isMcuUpdate ? mcuVersion.name : bootloaderVersion,
                    // whether this is an mcu update or a bootloader one is decided by the isMcuUpdate variable
                    // we only need to use the correct version here to flash the right thing
                }).subscribe({
                    next: event => subscriber.next({
                        type: isMcuUpdate ? "flashingMcu" : "flashingBootloader",
                        progress: event.progress,
                    }),
                    complete: () => {
                        waitForGetVersion(transportRef, {})
                            .pipe((0, operators_1.switchMap)(({ firmwareInfo }) => {
                            if (firmwareInfo.isBootloader) {
                                // if we're still in the bootloader, it means that we still have things to flash
                                // if we've already flashed too many times, we're probably stuck in an infinite loop
                                // this should never happen, but in case it happens, it's better to warn the user
                                // and track the issue rather than keep in an infinite loop
                                if (repetitions > MAX_FLASH_REPETITIONS) {
                                    return (0, rxjs_1.of)({
                                        type: "taskError",
                                        error: "TooManyMcuOrBootloaderFlashes",
                                    });
                                }
                                else {
                                    return flashMcuOrBootloader(updateContext, firmwareInfo, transportRef, deviceId, repetitions + 1);
                                }
                            }
                            else {
                                // if we're not in the bootloader anymore, it means that the update has been completed
                                return (0, rxjs_1.of)({
                                    type: "firmwareUpdateCompleted",
                                    updatedDeviceInfo: (0, getDeviceInfo_1.parseDeviceInfo)(firmwareInfo),
                                });
                            }
                        }))
                            .subscribe(subscriber);
                    },
                    error: (error) => {
                        subscriber.next({ type: "error", error: error, retrying: false });
                        subscriber.complete();
                    },
                });
            }
            else {
                subscriber.next({
                    type: "taskError",
                    error: "McuVersionNotFound",
                });
                subscriber.complete();
            }
        });
    });
};
const installOsuFirmware = ({ transport, updateContext, firmwareInfo, tracer, }) => {
    const { targetId } = firmwareInfo;
    const { osu } = updateContext;
    tracer.trace("Initiating osu firmware installation", { targetId, osu });
    // install OSU firmware
    return (0, installFirmware_1.installFirmwareCommand)(transport, {
        targetId,
        firmware: osu,
    });
};
const FIRMWARE_UPDATE_TIMEOUT = 60000;
// 60 seconds since firmware update has lots of places where a wait is normal
exports.updateFirmwareTask = (0, core_1.sharedLogicTaskWrapper)(internalUpdateFirmwareTask, FIRMWARE_UPDATE_TIMEOUT);
//# sourceMappingURL=updateFirmware.js.map