import { getCryptoCurrencyById } from "@ledgerhq/ledger-wallet-framework/currencies";
import { decodeAccountId } from "@ledgerhq/ledger-wallet-framework/account/index";
import {
  encodeOperationId,
  encodeSubOperationId,
} from "@ledgerhq/ledger-wallet-framework/operation";
import { log } from "@ledgerhq/logs";
import { Account, Operation, OperationType } from "@ledgerhq/types-live";
import {
  makeUnsignedSTXTokenTransfer,
  UnsignedTokenTransferOptions,
  createMessageSignature,
  deserializeCV,
  cvToJSON,
  uintCV,
  standardPrincipalCV,
  stringAsciiCV,
  someCV,
  noneCV,
  makeUnsignedContractCall,
  StacksMessageType,
  PostConditionType,
  createStandardPrincipal,
  FungibleConditionCode,
  createAssetInfo,
} from "@stacks/transactions";
import { BigNumber } from "bignumber.js";
import {
  fetchFullMempoolTxs,
  fetchNonce,
  DecodedSendManyFunctionArgsCV,
  MempoolTransaction,
  StacksNetwork,
  TransactionResponse,
} from "../../network/index";
import { StacksOperation } from "../../types";
import { bufferMemoToString, hexMemoToString } from "./memoUtils";

type ContractCallArg = {
  hex: string;
  repr: string;
  name: string;
  type: string;
};
type Sip010ContractCallArgs = [ContractCallArg, ContractCallArg, ContractCallArg, ContractCallArg];

export const getTxToBroadcast = async (
  operation: StacksOperation,
  signature: string,
  rawData: Record<string, any>,
): Promise<Buffer> => {
  const {
    value,
    recipients,
    senders,
    fee,
    extra: { memo },
  } = operation;

  const { anchorMode, network, xpub, contractAddress, contractName, assetName } = rawData;

  if (contractAddress && contractName && assetName) {
    // Create the function arguments for the SIP-010 transfer function
    const functionArgs = [
      uintCV(value.toFixed()), // Amount
      standardPrincipalCV(senders[0]), // Sender
      standardPrincipalCV(recipients[0]), // Recipient
      memo ? someCV(stringAsciiCV(memo)) : noneCV(), // Memo (optional)
    ];

    const tx = await makeUnsignedContractCall({
      contractAddress,
      contractName,
      functionName: "transfer",
      functionArgs,
      anchorMode,
      network: StacksNetwork[network],
      publicKey: xpub,
      fee: fee.toFixed(),
      nonce: operation.transactionSequenceNumber?.toString() ?? "0",
      postConditions: [
        {
          type: StacksMessageType.PostCondition,
          conditionType: PostConditionType.Fungible,
          principal: createStandardPrincipal(senders[0]),
          conditionCode: FungibleConditionCode.Equal,
          amount: BigInt(value.toFixed()),
          assetInfo: createAssetInfo(contractAddress, contractName, assetName),
        },
      ],
    });

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore need to ignore the TS error here
    tx.auth.spendingCondition.signature = createMessageSignature(signature);

    return Buffer.from(tx.serialize());
  } else {
    const options: UnsignedTokenTransferOptions = {
      amount: BigNumber(value).minus(fee).toFixed(),
      recipient: recipients[0],
      anchorMode,
      memo,
      network: StacksNetwork[network],
      publicKey: xpub,
      fee: BigNumber(fee).toFixed(),
      nonce: operation.transactionSequenceNumber?.toString() ?? "0",
    };

    const tx = await makeUnsignedSTXTokenTransfer(options);

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore need to ignore the TS error here
    tx.auth.spendingCondition.signature = createMessageSignature(signature);

    return Buffer.from(tx.serialize());
  }
};

export const getUnit = () => getCryptoCurrencyById("stacks").units[0];

export const getAddress = (
  account: Account,
): {
  address: string;
  derivationPath: string;
} => ({ address: account.freshAddress, derivationPath: account.freshAddressPath });

export const mapPendingTxToOps =
  (accountID: string, address: string) =>
  (tx: MempoolTransaction): StacksOperation[] => {
    const { sender_address, receipt_time, fee_rate, tx_id, token_transfer, tx_status, nonce } = tx;
    if (tx.tx_type !== "token_transfer" || tx_status !== "pending") {
      return [];
    }

    const memo = hexMemoToString(token_transfer.memo);
    const feeToUse = new BigNumber(fee_rate || "0");

    const date = new Date(receipt_time * 1000);

    const operationCommons = {
      hash: tx_id,
      fee: feeToUse,
      accountId: accountID,
      senders: [sender_address],
      recipients: [token_transfer.recipient_address],
      transactionSequenceNumber: new BigNumber(nonce),
      value: new BigNumber(token_transfer.amount).plus(feeToUse),
      date,
      extra: {
        memo,
      },
      blockHeight: null,
      blockHash: null,
    };

    const isSending = address === sender_address;
    const isReceiving = token_transfer.recipient_address === address;

    const ops: StacksOperation[] = [];
    if (isSending) {
      const type: OperationType = "OUT";
      ops.push({
        ...operationCommons,
        id: encodeOperationId(accountID, tx_id, type),
        type,
      });
    } else if (isReceiving) {
      const type: OperationType = "IN";
      ops.push({
        ...operationCommons,
        id: encodeOperationId(accountID, tx_id, type),
        type,
      });
    }

    return ops;
  };

export const mapTxToOps =
  (accountID: string, address: string) =>
  (tx: TransactionResponse): StacksOperation[] => {
    try {
      const {
        tx_id,
        fee_rate,
        nonce,
        block_height,
        burn_block_time,
        sender_address,
        block_hash: blockHash,
      } = tx.tx;
      const { stx_received: receivedValue, stx_sent: sentValue } = tx;

      let recipients: string[] = [];
      if (tx.tx.tx_type === "token_transfer" && tx.tx.token_transfer) {
        recipients = [tx.tx.token_transfer.recipient_address];
      }

      const memoHex = tx.tx.token_transfer?.memo;
      const memo: string = hexMemoToString(memoHex ?? "");

      const ops: StacksOperation[] = [];

      const date = new Date(burn_block_time * 1000);
      const feeToUse = new BigNumber(fee_rate || "0");

      const isSending = sentValue !== "0" && receivedValue === "0";
      const isReceiving = receivedValue !== "0";

      const operationCommons = {
        hash: tx_id,
        blockHeight: block_height,
        blockHash,
        fee: feeToUse,
        accountId: accountID,
        senders: [sender_address],
        transactionSequenceNumber: new BigNumber(nonce),
        date,
        extra: {
          memo,
        },
      };

      if (isSending) {
        const type: OperationType = "OUT";
        let internalOperations: StacksOperation[] | undefined = undefined;

        if (tx.tx.tx_type === "contract_call" && tx.tx.contract_call) {
          internalOperations = [];
          const deserialized = deserializeCV(tx.tx.contract_call.function_args[0].hex);
          const decodedArgs: DecodedSendManyFunctionArgsCV = cvToJSON(deserialized);
          for (const [idx, t] of decodedArgs.value.entries()) {
            internalOperations.push({
              ...operationCommons,
              id: encodeSubOperationId(accountID, tx_id, type, idx),
              contract: "send-many",
              type,
              value: new BigNumber(t.value.ustx.value),
              senders: [sender_address],
              recipients: [t.value.to.value],
              extra: {
                memo: hexMemoToString(t.value.memo?.value ?? ""),
              },
            });
          }
        }

        ops.push({
          ...operationCommons,
          id: encodeOperationId(accountID, tx_id, type),
          value: new BigNumber(sentValue),
          recipients,
          type,
          internalOperations,
        });
      }

      if (isReceiving) {
        const type: OperationType = "IN";
        ops.push({
          ...operationCommons,
          id: encodeOperationId(accountID, tx_id, type),
          value: new BigNumber(receivedValue),
          recipients: recipients.length ? recipients : [address],
          type,
        });
      }

      return ops;
    } catch (err) {
      log("warn", "mapTxToOps failed for stacks", err);
      return [];
    }
  };

export const sip010TxnToOperation = (
  tx: TransactionResponse,
  address: string,
  accountId: string,
): StacksOperation[] => {
  try {
    const { tx_id, fee_rate, nonce, block_height, burn_block_time, block_hash: blockHash } = tx.tx;
    if (!tx.tx.contract_call) {
      log("error", "contract_call is not defined", tx);
      return [];
    }
    const contractCallData = tx.tx.contract_call;
    const contractCallArgs = contractCallData.function_args;

    if (contractCallArgs.length !== 4) {
      log("error", "contractCallArgs len is not sip010 token transfer", tx);
      return [];
    }

    const [valueArg, senderArg, receiverArg, memoArg] = contractCallArgs as Sip010ContractCallArgs;

    const sender = cvToJSON(deserializeCV(senderArg.hex)).value;
    const receiver = cvToJSON(deserializeCV(receiverArg.hex)).value;
    const valueStr = cvToJSON(deserializeCV(valueArg.hex)).value;
    const memoJson = cvToJSON(deserializeCV(memoArg.hex)).value;

    const memo = bufferMemoToString(memoJson);

    const value = new BigNumber(valueStr);

    const recipients: string[] = [receiver];
    const senders: string[] = [sender];

    const ops: StacksOperation[] = [];

    const date = new Date(burn_block_time * 1000);
    const fee = new BigNumber(fee_rate || "0");

    const hasFailed = tx.tx.tx_status !== "success";

    let type: OperationType | "" = "";
    if (address === sender) {
      type = "OUT";
    } else if (address === receiver) {
      type = "IN";
    }

    if (type === "") {
      log("error", "op type is not known", tx);
      return [];
    }

    ops.push({
      hash: tx_id,
      blockHeight: block_height,
      blockHash,
      fee,
      accountId,
      senders,
      recipients,
      transactionSequenceNumber: BigNumber(nonce),
      date,
      value,
      hasFailed,
      extra: {
        memo,
      },
      type,
      id: encodeOperationId(accountId, tx_id, type),
    });

    return ops;
  } catch (e) {
    log("error", "stacks error converting sip010 transaction to operation", e);
    return [];
  }
};

export function reconciliatePublicKey(
  publicKey: string | undefined,
  initialAccount: Account | undefined,
): string {
  if (publicKey) return publicKey;
  if (initialAccount) {
    const { xpubOrAddress } = decodeAccountId(initialAccount.id);
    return xpubOrAddress;
  }
  throw new Error("publicKey wasn't properly restored");
}

export const sip010OpToParentOp = (tokenOps: Operation[], parentAccountId: string): Operation[] => {
  return tokenOps.map(op => ({
    ...op,
    accountId: parentAccountId,
    value: BigNumber(0),
    subOperations: [op],
    id: encodeOperationId(parentAccountId, op.hash, op.type),
  }));
};

export const findNextNonce = async (
  senderAddress: string,
  pendingOps: Operation[],
): Promise<BigNumber> => {
  let nextNonce = BigNumber(0);

  for (const op of pendingOps) {
    const nonce = op.transactionSequenceNumber ? op.transactionSequenceNumber : new BigNumber(0);

    if (nonce.gt(nextNonce)) {
      nextNonce = nonce;
    }
  }

  const allMempoolTxns = await fetchFullMempoolTxs(senderAddress);
  for (const tx of allMempoolTxns) {
    const nonce = BigNumber(tx.nonce);
    if (nonce.gt(nextNonce)) {
      nextNonce = nonce;
    }
  }

  if (!nextNonce.eq(0)) {
    nextNonce = nextNonce.plus(1);
  }

  const nonceResp = await fetchNonce(senderAddress);
  const possibleNextNonce = new BigNumber(nonceResp.possible_next_nonce);
  if (possibleNextNonce.gt(nextNonce)) {
    nextNonce = possibleNextNonce;
  }

  return nextNonce;
};
