import semver from "semver";
import { Observable, concat, from, of, throwError, defer, merge } from "rxjs";
import { mergeMap, concatMap, map, catchError, delay } from "rxjs/operators";
import { TransportStatusError, FirmwareOrAppUpdateRequired, UserRefusedOnDevice, BtcUnmatchedApp, UpdateYourApp, DisconnectedDeviceDuringOperation, DisconnectedDevice, StatusCodes, LockedDeviceError, LatestFirmwareVersionRequired, } from "@ledgerhq/errors";
import { getCryptoCurrencyById } from "../currencies";
import appSupportsQuitApp from "../appSupportsQuitApp";
import { withDevice } from "./deviceAccess";
import inlineAppInstall from "../apps/inlineAppInstall";
import { isDashboardName } from "./isDashboardName";
import getAppAndVersion from "./getAppAndVersion";
import getDeviceInfo from "./getDeviceInfo";
import getAddress from "./getAddress";
import openApp from "./openApp";
import quitApp from "./quitApp";
import { mustUpgrade, getMinVersion } from "../apps";
import isUpdateAvailable from "./isUpdateAvailable";
import { getLatestFirmwareForDeviceUseCase } from "../device/use-cases/getLatestFirmwareForDeviceUseCase";
import { DeviceModelId, } from "@ledgerhq/device-management-kit";
import { ConnectAppDeviceAction } from "@ledgerhq/live-dmk-shared";
import { ConnectAppEventMapper } from "./connectAppEventMapper";
export const openAppFromDashboard = (transport, appName) => from(getDeviceInfo(transport)).pipe(mergeMap(deviceInfo => merge(
// Nb Allows LLD/LLM to update lastSeenDevice, this can run in parallel
// since there are no more device exchanges.
from(getLatestFirmwareForDeviceUseCase(deviceInfo)).pipe(concatMap(latestFirmware => of({
    type: "device-update-last-seen",
    deviceInfo,
    latestFirmware,
}))), concat(of({
    type: "ask-open-app",
    appName,
}), defer(() => from(openApp(transport, appName))).pipe(concatMap(() => of({
    type: "device-permission-granted",
})), catchError(e => {
    if (e && e instanceof TransportStatusError) {
        switch (e.statusCode) {
            case 0x6984: // No StatusCodes definition
            case 0x6807: // No StatusCodes definition
                return inlineAppInstall({
                    transport,
                    appNames: [appName],
                    onSuccessObs: () => from(openAppFromDashboard(transport, appName)),
                });
            case StatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED:
            case 0x5501: // No StatusCodes definition
                return throwError(() => new UserRefusedOnDevice());
        }
    }
    else if (e instanceof LockedDeviceError) {
        // openAppFromDashboard is exported, so LockedDeviceError should be handled here too
        return of({
            type: "lockedDevice",
        });
    }
    return throwError(() => e);
}))))));
const attemptToQuitApp = (transport, appAndVersion) => appAndVersion && appSupportsQuitApp(appAndVersion)
    ? from(quitApp(transport)).pipe(concatMap(() => of({
        type: "disconnected",
        expected: true,
    })), catchError(e => throwError(() => e)))
    : of({
        type: "ask-quit-app",
    });
const derivationLogic = (transport, { requiresDerivation: { currencyId, ...derivationRest }, appAndVersion, appName, }) => defer(() => from(getAddress(transport, {
    currency: getCryptoCurrencyById(currencyId),
    ...derivationRest,
}))).pipe(map(({ address }) => ({
    type: "opened",
    app: appAndVersion,
    derivation: {
        address,
    },
})), catchError(e => {
    if (!e)
        return throwError(() => e);
    if (e instanceof BtcUnmatchedApp) {
        return of({
            type: "ask-open-app",
            appName,
        });
    }
    if (e instanceof TransportStatusError) {
        const { statusCode } = e;
        if (statusCode === StatusCodes.SECURITY_STATUS_NOT_SATISFIED ||
            statusCode === StatusCodes.INCORRECT_LENGTH ||
            (0x6600 <= statusCode && statusCode <= 0x67ff)) {
            return of({
                type: "ask-open-app",
                appName,
            });
        }
        switch (statusCode) {
            case 0x6f04: // FW-90. app was locked... | No StatusCodes definition
            case StatusCodes.HALTED: // FW-90. app bricked, a reboot fixes it.
            case StatusCodes.INS_NOT_SUPPORTED:
                // this is likely because it's the wrong app (LNS 1.3.1)
                return attemptToQuitApp(transport, appAndVersion);
        }
    }
    else if (e instanceof LockedDeviceError) {
        // derivationLogic is also called inside the catchError of cmd below
        // so it needs to handle LockedDeviceError too
        return of({
            type: "lockedDevice",
        });
    }
    return throwError(() => e);
}));
/**
 * @param allowPartialDependencies If some dependencies need to be installed, and if set to true,
 *   skip any app install if the app is not found from the provider.
 */
const cmd = (transport, { request }) => {
    const { appName, requiresDerivation, dependencies, requireLatestFirmware, outdatedApp, allowPartialDependencies = false, } = request;
    return new Observable(o => {
        const timeoutSub = of({
            type: "unresponsiveDevice",
        })
            .pipe(delay(1000))
            .subscribe(e => o.next(e));
        const innerSub = ({ appName, dependencies, requireLatestFirmware, }) => defer(() => from(getAppAndVersion(transport))).pipe(concatMap((appAndVersion) => {
            timeoutSub.unsubscribe();
            if (isDashboardName(appAndVersion.name)) {
                // check if we meet minimum fw
                if (requireLatestFirmware || outdatedApp) {
                    return from(getDeviceInfo(transport)).pipe(mergeMap((deviceInfo) => from(getLatestFirmwareForDeviceUseCase(deviceInfo)).pipe(mergeMap((latest) => {
                        const isLatest = !latest || semver.eq(deviceInfo.version, latest.final.version);
                        if ((!requireLatestFirmware || (requireLatestFirmware && isLatest)) &&
                            outdatedApp) {
                            return from(isUpdateAvailable(deviceInfo, outdatedApp)).pipe(mergeMap(isAvailable => isAvailable
                                ? throwError(() => new UpdateYourApp(undefined, {
                                    managerAppName: outdatedApp.name,
                                }))
                                : throwError(() => new LatestFirmwareVersionRequired("LatestFirmwareVersionRequired", {
                                    latest: latest?.final.version,
                                    current: deviceInfo.version,
                                }))));
                        }
                        if (isLatest) {
                            o.next({ type: "latest-firmware-resolved" });
                            return innerSub({
                                appName,
                                dependencies,
                                allowPartialDependencies,
                                // requireLatestFirmware // Resolved!.
                            });
                        }
                        else {
                            return throwError(() => new LatestFirmwareVersionRequired("LatestFirmwareVersionRequired", {
                                latest: latest.final.version,
                                current: deviceInfo.version,
                            }));
                        }
                    }))));
                }
                // check if we meet dependencies
                if (dependencies?.length) {
                    const completesInDashboard = isDashboardName(appName);
                    return inlineAppInstall({
                        transport,
                        appNames: [...(completesInDashboard ? [] : [appName]), ...dependencies],
                        onSuccessObs: () => {
                            o.next({
                                type: "dependencies-resolved",
                            });
                            return innerSub({
                                appName,
                                allowPartialDependencies,
                                // dependencies // Resolved!
                            });
                        },
                        allowPartialDependencies,
                    });
                }
                // maybe we want to be in the dashboard
                if (appName === appAndVersion.name) {
                    const e = {
                        type: "opened",
                        app: appAndVersion,
                    };
                    return of(e);
                }
                // we're in dashboard
                return openAppFromDashboard(transport, appName);
            }
            const appNeedsUpgrade = mustUpgrade(appAndVersion.name, appAndVersion.version);
            if (appNeedsUpgrade) {
                // quit app, check provider's app update for device's minimum requirements.
                o.next({
                    type: "has-outdated-app",
                    outdatedApp: appAndVersion,
                });
            }
            // need dashboard to check firmware, install dependencies, or verify app update
            if (dependencies?.length ||
                requireLatestFirmware ||
                appAndVersion.name !== appName ||
                appNeedsUpgrade) {
                return attemptToQuitApp(transport, appAndVersion);
            }
            if (requiresDerivation) {
                return derivationLogic(transport, {
                    requiresDerivation,
                    appAndVersion: appAndVersion,
                    appName,
                });
            }
            else {
                const e = {
                    type: "opened",
                    app: appAndVersion,
                };
                return of(e);
            }
        }), catchError((e) => {
            if ((typeof e === "object" &&
                e !== null &&
                "_tag" in e &&
                e._tag === "DeviceDisconnectedWhileSendingError") ||
                e instanceof DisconnectedDeviceDuringOperation ||
                e instanceof DisconnectedDevice) {
                return of({
                    type: "disconnected",
                });
            }
            if (e && e instanceof TransportStatusError) {
                switch (e.statusCode) {
                    case StatusCodes.CLA_NOT_SUPPORTED: // in 1.3.1 dashboard
                    case StatusCodes.INS_NOT_SUPPORTED: // in 1.3.1 and bitcoin app
                        // fallback on "old way" because device does not support getAppAndVersion
                        if (!requiresDerivation) {
                            // if there is no derivation, there is nothing we can do to check an app (e.g. requiring non coin app)
                            return throwError(() => new FirmwareOrAppUpdateRequired());
                        }
                        return derivationLogic(transport, {
                            requiresDerivation,
                            appName,
                        });
                }
            }
            else if (e instanceof LockedDeviceError) {
                return of({
                    type: "lockedDevice",
                });
            }
            return throwError(() => e);
        }));
        const sub = innerSub({
            appName,
            dependencies,
            requireLatestFirmware,
            allowPartialDependencies,
        }).subscribe(o);
        return () => {
            timeoutSub.unsubscribe();
            sub.unsubscribe();
        };
    });
};
const isDmkTransport = (transport) => {
    return ("dmk" in transport &&
        transport.dmk !== undefined &&
        "sessionId" in transport &&
        transport.sessionId !== undefined);
};
const appNameToDependency = (appName) => {
    const constraints = Object.values(DeviceModelId).reduce((result, model) => {
        const minVersion = getMinVersion(appName, model);
        if (minVersion) {
            result.push({
                minVersion: minVersion,
                applicableModels: [model],
            });
        }
        return result;
    }, []);
    return {
        name: appName,
        constraints,
    };
};
export default function connectAppFactory({ isLdmkConnectAppEnabled, } = { isLdmkConnectAppEnabled: false }) {
    if (!isLdmkConnectAppEnabled) {
        return ({ deviceId, deviceName, request }) => withDevice(deviceId, deviceName ? { matchDeviceByName: deviceName } : undefined)(transport => cmd(transport, { deviceId, deviceName, request }));
    }
    return ({ deviceId, deviceName, request }) => {
        const { appName, requiresDerivation, dependencies, requireLatestFirmware, allowPartialDependencies = false, } = request;
        return withDevice(deviceId, deviceName ? { matchDeviceByName: deviceName } : undefined)(transport => {
            if (!isDmkTransport(transport)) {
                return cmd(transport, { deviceId, deviceName, request });
            }
            const { dmk, sessionId } = transport;
            const deviceAction = new ConnectAppDeviceAction({
                input: {
                    application: appNameToDependency(appName),
                    dependencies: dependencies ? dependencies.map(name => appNameToDependency(name)) : [],
                    requireLatestFirmware,
                    allowMissingApplication: allowPartialDependencies,
                    unlockTimeout: 0,
                    requiredDerivation: requiresDerivation
                        ? async () => {
                            const { currencyId, ...derivationRest } = requiresDerivation;
                            const derivation = await getAddress(transport, {
                                currency: getCryptoCurrencyById(currencyId),
                                ...derivationRest,
                            });
                            return derivation.address;
                        }
                        : undefined,
                },
            });
            const observable = dmk.executeDeviceAction({
                sessionId,
                deviceAction,
            });
            return new ConnectAppEventMapper(dmk, sessionId, appName, observable).map();
        });
    };
}
//# sourceMappingURL=connectApp.js.map