import { FeeNotLoaded } from "@ledgerhq/errors";
import { encodeOperationId } from "@ledgerhq/ledger-wallet-framework/operation";
import { SignerContext } from "@ledgerhq/ledger-wallet-framework/signer";
import type {
  Account,
  AccountBridge,
  DeviceId,
  Operation,
  SignOperationEvent,
} from "@ledgerhq/types-live";
import { BigNumber } from "bignumber.js";
import IconService, { IcxTransaction } from "icon-sdk-js";
import { Observable } from "rxjs";

import { buildTransaction } from "./buildTransaction";
import { calculateAmount, getNonce } from "./logic";
import { IconSignature, IconSigner } from "./signer";
import type { IconAccount, Transaction } from "./types";
const { IconUtil, IconConverter } = IconService;

const buildOptimisticOperation = (
  account: Account,
  transaction: Transaction,
  fee: BigNumber,
): Operation => {
  const type = "OUT";
  const value = new BigNumber(transaction.amount).plus(fee);
  const operation: Operation = {
    id: encodeOperationId(account.id, "", type),
    hash: "",
    type,
    value,
    fee,
    blockHash: null,
    blockHeight: null,
    senders: [account.freshAddress],
    recipients: [transaction.recipient].filter(Boolean),
    accountId: account.id,
    transactionSequenceNumber: new BigNumber(getNonce(account as IconAccount)),
    date: new Date(),
    extra: {},
  };

  return operation;
};

/**
 * Adds signature to unsigned transaction. Will likely be a call to Icon SDK
 */
const addSignature = (rawTransaction: IcxTransaction, signature: string) => {
  return {
    rawTransaction: {
      ...rawTransaction,
      signature: signature,
    },
    signature,
  };
};

/**
 * Sign Transaction with Ledger hardware
 */
export const buildSignOperation =
  (signerContext: SignerContext<IconSigner>): AccountBridge<Transaction>["signOperation"] =>
  ({
    account,
    transaction,
    deviceId,
  }: {
    account: Account;
    transaction: Transaction;
    deviceId: DeviceId;
  }): Observable<SignOperationEvent> =>
    new Observable(o => {
      let cancelled = false;
      async function main() {
        if (!transaction.fees) {
          throw new FeeNotLoaded();
        }

        const transactionToSign = {
          ...transaction,
          amount: calculateAmount({
            account: account as IconAccount,
            transaction: transaction,
          }),
        };
        const { unsigned } = await buildTransaction(
          account as IconAccount,
          transactionToSign,
          transactionToSign.stepLimit,
        );

        o.next({ type: "device-signature-requested" });

        const res = (await signerContext(deviceId, signer =>
          signer.signTransaction(
            account.freshAddressPath,
            IconUtil.generateHashKey(IconConverter.toRawTransaction(unsigned)),
          ),
        )) as IconSignature;

        const signed = addSignature(unsigned, res.signedRawTxBase64);

        if (cancelled) return;
        o.next({ type: "device-signature-granted" });

        if (!signed.signature) {
          throw new Error("No signature");
        }

        const operation = buildOptimisticOperation(
          account,
          transactionToSign,
          transactionToSign.fees ?? new BigNumber(0),
        );

        o.next({
          type: "signed",
          signedOperation: {
            operation,
            signature: signed.signature,
            rawData: signed.rawTransaction,
          },
        });
      }

      main().then(
        () => o.complete(),
        e => o.error(e),
      );

      return () => {
        cancelled = true;
      };
    });

export default buildSignOperation;
