import type { SocketEvent } from "@ledgerhq/types-live";
import { from, Observable } from "rxjs";
import { mergeMap, retryWhen } from "rxjs/operators";
import { LockedDeviceError } from "@ledgerhq/errors";
import getDeviceInfo from "./getDeviceInfo";
import { retryWhileErrors, withDevice } from "./deviceAccess";
import genuineCheck from "./genuineCheck";

export type GetGenuineCheckFromDeviceIdArgs = {
  deviceId: string;
  deviceName: string | null;
  lockedDeviceTimeoutMs?: number;
};

export type GetGenuineCheckFromDeviceIdResult = {
  socketEvent: SocketEvent | null;
  lockedDevice: boolean;
};

export type GetGenuineCheckFromDeviceIdOutput = Observable<GetGenuineCheckFromDeviceIdResult>;

/**
 * Get a genuine check for a device only from its id
 * @param deviceId A device id, or an empty string if device is usb plugged
 * @param lockedDeviceTimeoutMs Time of no response from device after which the device is considered locked, in ms. Default 1000ms.
 * @returns An Observable pushing objects containing:
 * - socketEvent: a SocketEvent giving the current status of the genuine check,
 *     null if the genuine check process did not reach any state yet
 * - lockedDevice: a boolean set to true if the device is currently locked, false otherwise
 */
export const getGenuineCheckFromDeviceId = ({
  deviceId,
  deviceName,
  lockedDeviceTimeoutMs = 1000,
}: GetGenuineCheckFromDeviceIdArgs): GetGenuineCheckFromDeviceIdOutput => {
  return new Observable(subscriber => {
    // In order to know if a device is locked or not.
    // As we're not timing out inside the genuineCheckObservable flow (with rxjs timeout for ex)
    // once the device is unlock, getDeviceInfo should return the device info and
    // the flow will continue. No need to handle a retry strategy
    const lockedDeviceTimeout = setTimeout(() => {
      subscriber.next({ socketEvent: null, lockedDevice: true });
    }, lockedDeviceTimeoutMs);

    // Returns a Subscription that can be unsubscribed/cleaned
    return (
      withDevice(
        deviceId,
        deviceName ? { matchDeviceByName: deviceName } : undefined,
      )(t =>
        from(getDeviceInfo(t)).pipe(
          mergeMap(deviceInfo => {
            clearTimeout(lockedDeviceTimeout);
            subscriber.next({ socketEvent: null, lockedDevice: false });
            return genuineCheck(t, deviceInfo);
          }),
        ),
      )
        // Needs to retry with withDevice
        .pipe(
          retryWhen(
            retryWhileErrors((e: Error) => {
              // Cancels the locked-device unresponsive/timeout strategy if received any response/error
              clearTimeout(lockedDeviceTimeout);

              if (e instanceof LockedDeviceError) {
                subscriber.next({ socketEvent: null, lockedDevice: true });
                return true;
              }

              return false;
            }),
          ),
        )
        .subscribe({
          next: (socketEvent: SocketEvent) => subscriber.next({ socketEvent, lockedDevice: false }),
          error: e => subscriber.error(e),
          complete: () => subscriber.complete(),
        })
    );
  });
};
