import { DisconnectedDevice, UnresponsiveDeviceError } from "@ledgerhq/errors";
import { log } from "@ledgerhq/logs";
import type { DeviceId, DeviceInfo, FirmwareInfo } from "@ledgerhq/types-live";

import { getVersion } from "../commands/getVersion";

import isDevFirmware from "../../hw/isDevFirmware";
import { PROVIDERS } from "../../manager/provider";
import { Observable } from "rxjs";
import { map, switchMap } from "rxjs/operators";
import { SharedTaskEvent, retryOnErrorsCommandWrapper, sharedLogicTaskWrapper } from "./core";
import { quitApp } from "../commands/quitApp";
import { withTransport } from "../transports/core";
import { SendApduEmptyResponseError } from "@ledgerhq/device-management-kit";

const ManagerAllowedFlag = 0x08;
const PinValidatedFlag = 0x80;

export type GetDeviceInfoTaskArgs = { deviceId: DeviceId; deviceName: string | null };

// No taskError for getDeviceInfoTask. Kept for consistency with other tasks.
export type GetDeviceInfoTaskError = "None";

export type GetDeviceInfoTaskErrorEvent = {
  type: "taskError";
  error: GetDeviceInfoTaskError;
};

export type GetDeviceInfoTaskEvent =
  | { type: "data"; deviceInfo: DeviceInfo }
  | GetDeviceInfoTaskErrorEvent
  | SharedTaskEvent;

// Exported for tests
export function internalGetDeviceInfoTask({
  deviceId,
  deviceName,
}: GetDeviceInfoTaskArgs): Observable<GetDeviceInfoTaskEvent> {
  return new Observable(subscriber => {
    return (
      withTransport(
        deviceId,
        deviceName ? { matchDeviceByName: deviceName } : undefined,
      )(({ transportRef }) =>
        quitApp(transportRef.current).pipe(
          switchMap(() => {
            return retryOnErrorsCommandWrapper({
              command: getVersion,
              allowedErrors: [{ maxRetries: 3, errorClass: DisconnectedDevice }],
              allowedDmkErrors: [new SendApduEmptyResponseError()],
            })(transportRef, {});
          }),
          map(value => {
            if (value.type === "unresponsive") {
              return {
                type: "error" as const,
                error: new UnresponsiveDeviceError(),
                retrying: true,
              };
            }

            const { firmwareInfo } = value;

            const deviceInfo = parseDeviceInfo(firmwareInfo);

            return { type: "data" as const, deviceInfo };
          }),
        ),
      )
        // Any error will be handled by the sharedLogicTaskWrapper, which will map it a relevant event
        .subscribe(subscriber)
    );
  });
}

export const parseDeviceInfo = (firmwareInfo: FirmwareInfo): DeviceInfo => {
  const {
    isBootloader,
    rawVersion,
    targetId,
    seVersion,
    seTargetId,
    mcuBlVersion,
    mcuVersion,
    mcuTargetId,
    flags,
    bootloaderVersion,
    hardwareVersion,
    languageId,
    charonState,
  } = firmwareInfo;

  const isOSU = rawVersion.includes("-osu");
  const version = rawVersion.replace("-osu", "");
  const m = rawVersion.match(/([0-9]+.[0-9]+(.[0-9]+){0,1})?(-(.*))?/);
  const [, majMin, , , postDash] = m || [];
  const providerName = PROVIDERS[postDash] ? postDash : null;
  const flag = flags.length > 0 ? flags[0] : 0;
  const managerAllowed = !!(flag & ManagerAllowedFlag);
  const pinValidated = !!(flag & PinValidatedFlag);

  let isRecoveryMode = false;
  let onboarded = true;
  if (flags.length === 4) {
    // Nb Since LNS+ unseeded devices are visible + extra flags
    isRecoveryMode = !!(flags[0] & 0x01);
    onboarded = !!(flags[0] & 0x04);
  }

  log(
    "hw",
    "deviceInfo: se@" +
      version +
      " mcu@" +
      mcuVersion +
      (isOSU ? " (osu)" : isBootloader ? " (bootloader)" : ""),
  );

  const hasDevFirmware = isDevFirmware(seVersion);
  const deviceInfo: DeviceInfo = {
    version,
    mcuVersion,
    seVersion,
    mcuBlVersion,
    majMin,
    providerName: providerName || null,
    targetId,
    hasDevFirmware,
    seTargetId,
    mcuTargetId,
    isOSU,
    isBootloader,
    isRecoveryMode,
    managerAllowed,
    pinValidated,
    onboarded,
    bootloaderVersion,
    hardwareVersion,
    languageId,
    seFlags: flags,
    charonState: charonState,
  };

  return deviceInfo;
};

/**
 * Task to get the `DeviceInfo` of a device
 *
 * @param `deviceId` A device id, or an empty string if device is usb plugged
 * @returns An observable that emits `GetDeviceInfoTaskEvent` events
 */
export const getDeviceInfoTask = sharedLogicTaskWrapper(internalGetDeviceInfoTask);
