import { secp256k1 } from "@noble/curves/secp256k1";
import { firstValueFrom, from, Observable } from "rxjs";
import { TransportStatusError, WrongDeviceForAccount } from "@ledgerhq/errors";

import { delay } from "../../../promise";
import {
  createExchange,
  ExchangeTypes,
  isExchangeTypeNg,
  PayloadSignatureComputedFormat,
} from "@ledgerhq/hw-app-exchange";
import { getAccountCurrency, getMainAccount } from "../../../account";
import { getAccountBridge } from "../../../bridge";
import { TransactionRefusedOnDevice } from "../../../errors";
import { withDevice } from "../../../hw/deviceAccess";
import { getCurrencyExchangeConfig } from "../..";
import { convertToAppExchangePartnerKey, getProviderConfig } from "../../providers";

import type {
  CompleteExchangeInputFund,
  CompleteExchangeInputSell,
  CompleteExchangeRequestEvent,
} from "../types";
import { CompleteExchangeError, CompleteExchangeStep, convertTransportError } from "../../error";

const withDevicePromise = (deviceId, fn) =>
  firstValueFrom(withDevice(deviceId)(transport => from(fn(transport))));

const completeExchange = (
  input: CompleteExchangeInputFund | CompleteExchangeInputSell,
): Observable<CompleteExchangeRequestEvent> => {
  let { transaction } = input; // TODO build a tx from the data

  const {
    deviceId,
    exchange,
    provider,
    binaryPayload,
    signature,
    exchangeType,
    rateType, // TODO Pass fixed/float for UI switch ?
  } = input;

  const { fromAccount, fromParentAccount } = exchange;

  return new Observable(o => {
    let unsubscribed = false;
    let ignoreTransportError = false;
    let currentStep: CompleteExchangeStep = "INIT";

    const confirmExchange = async () => {
      await withDevicePromise(deviceId, async transport => {
        const providerNameAndSignature = await getProviderConfig(exchangeType, provider);

        if (!providerNameAndSignature)
          throw new CompleteExchangeError(
            "INIT",
            "ProviderConfigError",
            "Could not get provider infos",
          );

        const exchange = createExchange(
          transport,
          exchangeType,
          rateType,
          providerNameAndSignature.version,
        );

        const mainAccount = getMainAccount(fromAccount, fromParentAccount);
        const accountBridge = getAccountBridge(mainAccount);
        const mainPayoutCurrency = getAccountCurrency(mainAccount);
        const payoutCurrency = getAccountCurrency(fromAccount);

        if (mainPayoutCurrency.type !== "CryptoCurrency")
          throw new CompleteExchangeError(
            "INIT",
            "InvalidCurrencyType",
            `This should be a cryptocurrency, got ${mainPayoutCurrency.type}`,
          );

        transaction = await accountBridge.prepareTransaction(mainAccount, transaction);
        if (unsubscribed) return;

        const { errors, estimatedFees } = await accountBridge.getTransactionStatus(
          mainAccount,
          transaction,
        );
        if (unsubscribed) return;

        const errorsKeys = Object.keys(errors);
        if (errorsKeys.length > 0) {
          const firstKey = errorsKeys[0];
          const validationError = errors[firstKey];
          throw new CompleteExchangeError(
            currentStep,
            firstKey,
            validationError.message ||
              validationError.name ||
              `Transaction validation failed: ${firstKey}`,
          );
        }

        currentStep = "SET_PARTNER_KEY";
        await exchange.setPartnerKey(convertToAppExchangePartnerKey(providerNameAndSignature));
        if (unsubscribed) return;

        currentStep = "CHECK_PARTNER";
        await exchange.checkPartner(providerNameAndSignature.signature!);
        if (unsubscribed) return;

        currentStep = "PROCESS_TRANSACTION";
        const { payload, format }: { payload: Buffer; format: PayloadSignatureComputedFormat } =
          isExchangeTypeNg(exchange.transactionType)
            ? { payload: Buffer.from("." + binaryPayload), format: "jws" }
            : { payload: Buffer.from(binaryPayload, "hex"), format: "raw" };
        await exchange.processTransaction(payload, estimatedFees, format);
        if (unsubscribed) return;

        const goodSign = convertSignature(signature, exchange.transactionType);

        currentStep = "CHECK_TRANSACTION_SIGNATURE";
        await exchange.checkTransactionSignature(goodSign);
        if (unsubscribed) return;

        const payoutAddressParameters = accountBridge.getSerializedAddressParameters(mainAccount);
        if (unsubscribed) return;
        if (!payoutAddressParameters) {
          throw new CompleteExchangeError(
            currentStep,
            "UnsupportedFamily",
            `Family not supported: ${mainPayoutCurrency.family}`,
          );
        }

        const { config: payoutAddressConfig, signature: payoutAddressConfigSignature } =
          await getCurrencyExchangeConfig(payoutCurrency);

        try {
          o.next({
            type: "complete-exchange-requested",
            estimatedFees: estimatedFees.toString(),
          });
          currentStep = "CHECK_PAYOUT_ADDRESS";
          await exchange.validatePayoutOrAsset(
            payoutAddressConfig,
            payoutAddressConfigSignature,
            payoutAddressParameters,
          );
        } catch (e) {
          if (e instanceof TransportStatusError && e.statusCode === 0x6a83) {
            throw new WrongDeviceForAccount();
          }

          throw convertTransportError(currentStep, e);
        }

        if (unsubscribed) return;
        ignoreTransportError = true;
        currentStep = "SIGN_COIN_TRANSACTION";
        await exchange.signCoinTransaction();
      }).catch(e => {
        if (ignoreTransportError) return;

        if (
          e instanceof TransportStatusError &&
          (e.statusCode === 0x6a84 || e.statusCode === 0x5501)
        ) {
          throw new TransactionRefusedOnDevice();
        }

        // Preserve known error types checked by instanceof downstream
        if (e instanceof CompleteExchangeError) throw e;
        if (e instanceof WrongDeviceForAccount || e instanceof TransactionRefusedOnDevice) throw e;
        // Wrap any remaining unknown errors with the current step context
        throw new CompleteExchangeError(
          currentStep,
          e?.name,
          e?.message || "Unknown exchange error",
        );
      });
      await delay(3000);
      o.next({
        type: "complete-exchange-result",
        completeExchangeResult: transaction,
      });
      if (unsubscribed) return;
    };

    confirmExchange().then(
      () => {
        o.complete();
        unsubscribed = true;
      },
      e => {
        o.next({
          type: "complete-exchange-error",
          error: e,
        });
        o.complete();
        unsubscribed = true;
      },
    );
    return () => {
      unsubscribed = true;
    };
  });
};

/**
 * For the Fund and Swap flow, the signature sent to the nano needs to
 * be in DER format, which is not the case for Sell flow. Hence the
 * ternary.
 * cf. https://github.com/LedgerHQ/app-exchange/blob/e67848f136dc7227521791b91f608f7cd32e7da7/src/check_tx_signature.c#L14-L32
 * @param {Buffer} bufferSignature
 * @param {ExchangeTypes} exchangeType
 * @return {Buffer} The correct format Buffer for AppExchange call.
 */
function convertSignature(signature: string, exchangeType: ExchangeTypes): Buffer {
  if (isExchangeTypeNg(exchangeType)) {
    const base64Signature = signature.replace(/-/g, "+").replace(/_/g, "/");
    return Buffer.from(base64Signature, "base64");
  }
  if (exchangeType === ExchangeTypes.Sell) return Buffer.from(signature, "hex");
  const sig = secp256k1.Signature.fromCompact(Buffer.from(signature, "hex"));
  return Buffer.from(sig.toDERRawBytes());
}

export default completeExchange;
