import {
  AccountUpdateTransaction,
  ContractExecuteTransaction,
  ContractFunctionParameters,
  ContractId,
  Hbar,
  TokenAssociateTransaction,
  TransactionId,
  TransferTransaction,
} from "@hashgraph/sdk";
import type { FeeEstimation, TransactionIntent } from "@ledgerhq/coin-module-framework/api/index";
import BigNumber from "bignumber.js";
import invariant from "invariant";
import type { HederaConfig } from "../config";
import {
  DEFAULT_GAS_LIMIT,
  HEDERA_TRANSACTION_MODES,
  TRANSACTION_VALID_DURATION_SECONDS,
} from "../constants";
import { rpcClient } from "../network/rpc";
import { createTransactionId } from "../network/utils";
import type { HederaMemo, HederaTxData } from "../types";
import { hasSpecificIntentData, serializeTransaction, toEVMAddress } from "./utils";

interface BuilderOperator {
  accountId: string;
}

interface BuilderCommonTransactionFields {
  transactionId: TransactionId;
  maxFee: BigNumber | undefined;
  memo: string;
}

interface BuilderCoinTransferTransaction extends BuilderCommonTransactionFields {
  type: HEDERA_TRANSACTION_MODES.Send;
  amount: BigNumber;
  recipient: string;
}

interface BuilderHTSTokenTransferTransaction extends BuilderCommonTransactionFields {
  type: HEDERA_TRANSACTION_MODES.Send;
  tokenAddress: string;
  amount: BigNumber;
  recipient: string;
}

interface BuilderERC20TokenTransferTransaction extends BuilderCommonTransactionFields {
  type: HEDERA_TRANSACTION_MODES.Send;
  tokenAddress: string;
  amount: BigNumber;
  recipient: string;
  gasLimit: BigNumber;
}

interface BuilderTokenAssociateTransaction extends BuilderCommonTransactionFields {
  type: HEDERA_TRANSACTION_MODES.TokenAssociate;
  tokenId: string;
}

interface BuilderUpdateAccountTransaction extends BuilderCommonTransactionFields {
  type:
    | HEDERA_TRANSACTION_MODES.Delegate
    | HEDERA_TRANSACTION_MODES.Undelegate
    | HEDERA_TRANSACTION_MODES.Redelegate;
  stakingNodeId: number | null | undefined;
}

async function buildUnsignedCoinTransaction({
  account,
  transaction,
}: {
  account: BuilderOperator;
  transaction: BuilderCoinTransferTransaction;
}): Promise<TransferTransaction> {
  const accountId = account.accountId;
  const hbarAmount = Hbar.fromTinybars(transaction.amount);

  const tx = new TransferTransaction()
    .setTransactionValidDuration(TRANSACTION_VALID_DURATION_SECONDS)
    .setTransactionId(transaction.transactionId)
    .setTransactionMemo(transaction.memo)
    .addHbarTransfer(accountId, hbarAmount.negated())
    .addHbarTransfer(transaction.recipient, hbarAmount);

  if (transaction.maxFee) {
    tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber()));
  }

  return tx.freezeWith(await rpcClient.getInstance());
}

async function buildUnsignedHTSTokenTransaction({
  account,
  transaction,
}: {
  account: BuilderOperator;
  transaction: BuilderHTSTokenTransferTransaction;
}): Promise<TransferTransaction> {
  const accountId = account.accountId;
  const tokenId = transaction.tokenAddress;

  const tx = new TransferTransaction()
    .setTransactionValidDuration(TRANSACTION_VALID_DURATION_SECONDS)
    .setTransactionId(transaction.transactionId)
    .setTransactionMemo(transaction.memo)
    .addTokenTransfer(tokenId, accountId, transaction.amount.negated().toNumber())
    .addTokenTransfer(tokenId, transaction.recipient, transaction.amount.toNumber());

  if (transaction.maxFee) {
    tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber()));
  }

  return tx.freezeWith(await rpcClient.getInstance());
}

async function buildUnsignedERC20TokenTransaction({
  transaction,
}: {
  transaction: BuilderERC20TokenTransferTransaction;
}): Promise<ContractExecuteTransaction> {
  const contractId = ContractId.fromEvmAddress(0, 0, transaction.tokenAddress);
  const recipientEvmAddress = await toEVMAddress(transaction.recipient);
  invariant(recipientEvmAddress, `hedera: EVM address is missing ${transaction.recipient}`);
  const gas = transaction.gasLimit.toNumber();

  // create function parameters for ERC20 transfer function
  // transfer(address to, uint256 amount) returns (bool)
  const functionParameters = new ContractFunctionParameters()
    .addAddress(recipientEvmAddress)
    .addUint256(transaction.amount.toNumber());

  const tx = new ContractExecuteTransaction()
    .setTransactionValidDuration(TRANSACTION_VALID_DURATION_SECONDS)
    .setTransactionId(transaction.transactionId)
    .setTransactionMemo(transaction.memo ?? "")
    .setContractId(contractId)
    .setGas(gas)
    .setFunction("transfer", functionParameters);

  if (transaction.maxFee) {
    tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber()));
  }

  return tx.freezeWith(await rpcClient.getInstance());
}

async function buildTokenAssociateTransaction({
  account,
  transaction,
}: {
  account: BuilderOperator;
  transaction: BuilderTokenAssociateTransaction;
}): Promise<TokenAssociateTransaction> {
  const accountId = account.accountId;

  const tx = new TokenAssociateTransaction()
    .setTransactionValidDuration(TRANSACTION_VALID_DURATION_SECONDS)
    .setTransactionId(transaction.transactionId)
    .setTransactionMemo(transaction.memo)
    .setAccountId(accountId)
    .setTokenIds([transaction.tokenId]);

  if (transaction.maxFee) {
    tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber()));
  }

  return tx.freezeWith(await rpcClient.getInstance());
}

async function buildUnsignedUpdateAccountTransaction({
  account,
  transaction,
}: {
  account: BuilderOperator;
  transaction: BuilderUpdateAccountTransaction;
}): Promise<AccountUpdateTransaction> {
  const accountId = account.accountId;

  const tx = new AccountUpdateTransaction()
    .setTransactionValidDuration(TRANSACTION_VALID_DURATION_SECONDS)
    .setTransactionId(transaction.transactionId)
    .setTransactionMemo(transaction.memo ?? "")
    .setAccountId(accountId);

  if (transaction.maxFee) {
    tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber()));
  }

  if (typeof transaction.stakingNodeId === "number") {
    tx.setStakedNodeId(transaction.stakingNodeId);
  }

  if (transaction.stakingNodeId === null) {
    tx.clearStakedNodeId();
  }

  return tx.freezeWith(await rpcClient.getInstance());
}

function isStakingMode(type: string): type is BuilderUpdateAccountTransaction["type"] {
  return (
    type === HEDERA_TRANSACTION_MODES.Redelegate ||
    type === HEDERA_TRANSACTION_MODES.Undelegate ||
    type === HEDERA_TRANSACTION_MODES.Delegate
  );
}

export async function craftTransaction({
  txIntent,
  customFees,
  config,
}: {
  txIntent: TransactionIntent<HederaMemo, HederaTxData>;
  customFees?: FeeEstimation;
  config: HederaConfig;
}) {
  const account = { accountId: txIntent.sender };
  const maxFee = customFees ? new BigNumber(customFees.value.toString()) : undefined;

  const transactionId = await createTransactionId(account.accountId, config);

  let tx;

  if (txIntent.type === HEDERA_TRANSACTION_MODES.TokenAssociate) {
    invariant(txIntent.asset.type !== "native", "hedera: invalid asset type");
    invariant("assetReference" in txIntent.asset, "hedera: assetReference is missing");

    tx = await buildTokenAssociateTransaction({
      account,
      transaction: {
        type: txIntent.type,
        transactionId,
        tokenId: txIntent.asset.assetReference,
        memo: txIntent.memo.value,
        maxFee,
      },
    });
  } else if (txIntent.type === HEDERA_TRANSACTION_MODES.Send && txIntent.asset.type === "hts") {
    invariant("assetReference" in txIntent.asset, "hedera: no assetReference in token transfer");

    const amount = new BigNumber(txIntent.amount.toString());

    tx = await buildUnsignedHTSTokenTransaction({
      account,
      transaction: {
        type: txIntent.type,
        transactionId,
        tokenAddress: txIntent.asset.assetReference,
        amount,
        recipient: txIntent.recipient,
        memo: txIntent.memo.value,
        maxFee,
      },
    });
  } else if (txIntent.type === HEDERA_TRANSACTION_MODES.Send && txIntent.asset.type === "erc20") {
    invariant("assetReference" in txIntent.asset, "hedera: no assetReference in token transfer");

    const amount = new BigNumber(txIntent.amount.toString());
    const gasLimit = hasSpecificIntentData(txIntent, "erc20")
      ? new BigNumber(txIntent.data.gasLimit.toString())
      : DEFAULT_GAS_LIMIT;

    tx = await buildUnsignedERC20TokenTransaction({
      transaction: {
        type: txIntent.type,
        transactionId,
        tokenAddress: txIntent.asset.assetReference,
        amount,
        recipient: txIntent.recipient,
        memo: txIntent.memo.value,
        maxFee,
        gasLimit,
      },
    });
  } else if (isStakingMode(txIntent.type)) {
    const stakingNodeId = hasSpecificIntentData(txIntent, "staking")
      ? txIntent.data.stakingNodeId
      : undefined;

    tx = await buildUnsignedUpdateAccountTransaction({
      account,
      transaction: {
        type: txIntent.type,
        transactionId,
        memo: txIntent.memo.value,
        maxFee,
        stakingNodeId,
      },
    });
  }
  // HEDERA_TRANSACTION_MODES.ClaimRewards is just a coin transfer that triggers staking rewards claim
  else {
    const amount = new BigNumber(txIntent.amount.toString());

    tx = await buildUnsignedCoinTransaction({
      account,
      transaction: {
        type: HEDERA_TRANSACTION_MODES.Send,
        transactionId,
        amount,
        recipient: txIntent.recipient,
        memo: txIntent.memo.value,
        maxFee,
      },
    });
  }

  const serializedTx = serializeTransaction(tx);

  return { tx, serializedTx };
}
