import { from, of, throwError, TimeoutError, timer } from "rxjs";
import { map, catchError, first, timeout, repeat, switchMap } from "rxjs/operators";
import { getVersion } from "../device/use-cases/getVersionUseCase";
import { withDevice } from "./deviceAccess";
import { TransportStatusError, DeviceOnboardingStatePollingError, DeviceExtractOnboardingStateError, DisconnectedDevice, CantOpenDevice, TransportRaceCondition, LockedDeviceError, UnexpectedBootloader, TransportExchangeTimeoutError, DisconnectedDeviceDuringOperation, } from "@ledgerhq/errors";
import { extractOnboardingState } from "./extractOnboardingState";
import { DeviceDisconnectedWhileSendingError } from "@ledgerhq/device-management-kit";
import { quitApp } from "../deviceSDK/commands/quitApp";
/**
 * Polls the device onboarding state at a given frequency
 *
 * @param deviceId A device id
 * @param pollingPeriodMs The period in ms after which the device onboarding state is fetched again
 * @param transportAbortTimeoutMs Depending on the transport implementation, an "abort timeout" will be set (and throw an error) when:
 *  - opening a transport instance
 *  - on called commands (where `getVersion`)
 *  Default to (pollingPeriodMs - 100) ms
 * @param safeGuardTimeoutMs For Transport implementations not implementing an "abort timeout", a timeout will be triggered (and throw an error) at this function call level
 * @returns An Observable that polls the device onboarding state and pushes an object containing:
 * - onboardingState: the device state during the onboarding
 * - allowedError: any error that is allowed and does not stop the polling
 * - lockedDevice: a boolean set to true if the device is currently locked, false otherwise
 */
export const getOnboardingStatePolling = ({ deviceId, deviceName, pollingPeriodMs, transportAbortTimeoutMs = pollingPeriodMs - 100, safeGuardTimeoutMs = pollingPeriodMs * 10, // Nb Empirical value
allowedErrorChecks = [], }) => {
    let hasQuitAppAlreadyRun = false;
    const getOnboardingStateOnce = () => {
        return withDevice(deviceId, {
            openTimeoutMs: transportAbortTimeoutMs,
            matchDeviceByName: deviceName ?? undefined,
        })(t => {
            const getVersionObs = from(getVersion(t, { abortTimeoutMs: transportAbortTimeoutMs }));
            // only run quitApp the first time
            if (hasQuitAppAlreadyRun) {
                return getVersionObs;
            }
            hasQuitAppAlreadyRun = true;
            return quitApp(t).pipe(switchMap(() => getVersionObs));
        }).pipe(timeout(safeGuardTimeoutMs), // Throws a TimeoutError
        first(), catchError((error) => {
            if (isAllowedOnboardingStatePollingError(error) ||
                allowedErrorChecks?.some(fn => fn(error))) {
                // Pushes the error to the next step to be processed (no retry from the beginning)
                return of(error);
            }
            return throwError(() => error);
        }), map((event) => {
            if ("flags" in event) {
                const firmwareInfo = event;
                let onboardingState = null;
                if (firmwareInfo.isBootloader) {
                    // Throws so it will be considered a fatal error
                    throw new UnexpectedBootloader("Device in bootloader during the polling");
                }
                try {
                    onboardingState = extractOnboardingState(firmwareInfo.flags, firmwareInfo.charonState);
                }
                catch (error) {
                    if (error instanceof DeviceExtractOnboardingStateError) {
                        return {
                            onboardingState: null,
                            allowedError: error,
                            lockedDevice: false,
                        };
                    }
                    else {
                        let errorMessage = "";
                        if (error instanceof Error) {
                            errorMessage = `${error.name}: ${error.message}`;
                        }
                        else {
                            errorMessage = `${error}`;
                        }
                        return {
                            onboardingState: null,
                            allowedError: new DeviceOnboardingStatePollingError(`SyncOnboarding: Unknown error while extracting the onboarding state ${errorMessage}`),
                            lockedDevice: false,
                        };
                    }
                }
                return {
                    onboardingState,
                    allowedError: null,
                    lockedDevice: false,
                };
            }
            else {
                // If an error is caught previously, and this error is "allowed",
                // the value from the observable is not a FirmwareInfo but an Error
                const allowedError = event;
                return {
                    onboardingState: null,
                    allowedError: allowedError,
                    lockedDevice: allowedError instanceof LockedDeviceError,
                };
            }
        }));
    };
    return getOnboardingStateOnce().pipe(repeat({
        delay: _count => timer(pollingPeriodMs),
    }));
};
export const isAllowedOnboardingStatePollingError = (error) => {
    if (error &&
        // Timeout error is thrown by rxjs's timeout
        (error instanceof TimeoutError ||
            error instanceof TransportExchangeTimeoutError ||
            error instanceof DisconnectedDevice ||
            error instanceof DisconnectedDeviceDuringOperation ||
            error instanceof DeviceDisconnectedWhileSendingError ||
            error instanceof CantOpenDevice ||
            error instanceof TransportRaceCondition ||
            error instanceof TransportStatusError ||
            // A locked device is handled as an allowed error
            error instanceof LockedDeviceError)) {
        return true;
    }
    return false;
};
//# sourceMappingURL=getOnboardingStatePolling.js.map