import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state";
import {
  decodeAccountId,
  encodeTokenAccountId,
} from "@ledgerhq/ledger-wallet-framework/account/index";
import { encodeOperationId } from "@ledgerhq/ledger-wallet-framework/operation";
import { Operation } from "@ledgerhq/types-live";
import { Address, BitReader, BitString, Cell, Slice } from "@ton/core";
import BigNumber from "bignumber.js";
import { TonOperation } from "../../types";
import { addressesAreEqual, isAddressValid } from "../../utils";
import { fetchJettonTransactions, fetchTransactions } from "./api";
import {
  TonAddressBook,
  TonJettonTransfer,
  TonTransaction,
  TonTransactionsList,
} from "./api.types";

export async function getTransactions(
  addr: string,
  startLt?: string,
): Promise<TonTransactionsList> {
  const txs = await fetchTransactions(addr, { startLt });
  if (txs.transactions.length === 0) return txs;
  let tmpTxs: TonTransactionsList;
  let isUncompletedResult = true;

  while (isUncompletedResult) {
    const { lt, hash } = txs.transactions[txs.transactions.length - 1];
    tmpTxs = await fetchTransactions(addr, { startLt, endLt: lt });
    // we found the last transaction
    if (tmpTxs.transactions.length === 1) {
      isUncompletedResult = false;
      break;
    }
    // it should always match
    if (hash !== tmpTxs.transactions[0].hash) throw Error("[ton] transaction hash does not match");
    tmpTxs.transactions.shift(); // first element is repeated
    txs.transactions.push(...tmpTxs.transactions);
    txs.address_book = { ...txs.address_book, ...tmpTxs.address_book };
  }
  return txs;
}

export async function getJettonTransfers(
  addr: string,
  startLt?: string,
): Promise<TonJettonTransfer[]> {
  const txs = await fetchJettonTransactions(addr, { startLt });
  if (txs.length === 0) return txs;
  let tmpTxs: TonJettonTransfer[];
  let isUncompletedResult = true;

  while (isUncompletedResult) {
    const { transaction_hash, transaction_lt } = txs[txs.length - 1];
    tmpTxs = await fetchJettonTransactions(addr, { startLt, endLt: transaction_lt });
    // we found the last transaction
    if (tmpTxs.length === 1) {
      isUncompletedResult = false;
      break;
    }
    // it should always match
    if (transaction_hash !== tmpTxs[0].transaction_hash)
      throw Error("[ton] transaction hash does not match");
    tmpTxs.shift(); // first element is repeated
    txs.push(...tmpTxs);
  }
  return txs;
}

function getFriendlyAddress(addressBook: TonAddressBook, rawAddr?: string | null): string[] {
  if (!rawAddr) return [];
  if (addressBook[rawAddr]) return [addressBook[rawAddr].user_friendly];
  if (!isAddressValid(rawAddr)) throw new Error("[ton] address is not valid");
  return [Address.parse(rawAddr).toString({ urlSafe: true, bounceable: true })];
}

export function mapTxToOps(
  accountId: string,
  addr: string,
  addressBook: TonAddressBook,
): (tx: TonTransaction) => TonOperation[] {
  return (tx: TonTransaction): TonOperation[] => {
    const ops: TonOperation[] = [];

    if (tx.out_msgs.length > 1) throw Error(`[ton] txn with > 1 output not expected ${tx}`);

    const accountAddr = Address.parse(tx.account).toString({ urlSafe: true, bounceable: false });

    if (accountAddr !== addr) throw Error(`[ton] unexpected address ${accountAddr} ${addr}`);

    const isReceiving =
      tx.in_msg &&
      tx.in_msg.source &&
      tx.in_msg.source !== "" &&
      tx.in_msg.value &&
      tx.in_msg.value !== "0" &&
      tx.account === tx.in_msg.destination;

    const isSending =
      tx.out_msgs.length !== 0 &&
      tx.out_msgs[0].source &&
      tx.out_msgs[0].source !== "" &&
      tx.out_msgs[0].value &&
      tx.out_msgs[0].value !== "0" &&
      tx.account === tx.out_msgs[0].source;

    const date = new Date(tx.now * 1000); // now is defined in seconds
    const hash = tx.in_msg?.hash ?? tx.hash; // this is the hash we know in signature time
    const hasFailed =
      tx.description.compute_ph.success === false && tx.description.compute_ph.exit_code !== 0;

    if (isReceiving) {
      let subOperations: Operation[] | undefined;
      if (tx.total_fees !== "0") {
        // these are small amount of fees payed when receiving
        // we don't want to show them in the charts
        subOperations = [
          {
            id: encodeOperationId(accountId, hash, "NONE"),
            hash,
            type: "NONE",
            value: BigNumber(tx.total_fees),
            fee: BigNumber(0),
            blockHeight: tx.mc_block_seqno ?? 1,
            blockHash: null,
            hasFailed,
            accountId,
            senders: [accountAddr],
            recipients: [],
            date,
            extra: {
              lt: tx.lt,
              explorerHash: tx.hash,
              comment: {
                isEncrypted: false,
                text: "",
              },
            },
          },
        ];
      }
      ops.push({
        id: encodeOperationId(accountId, hash, "IN"),
        hash,
        type: "IN",
        value: BigNumber(tx.in_msg?.value ?? 0),
        fee: BigNumber(tx.total_fees),
        blockHeight: tx.mc_block_seqno ?? 1,
        blockHash: null,
        hasFailed,
        accountId,
        senders: getFriendlyAddress(addressBook, tx.in_msg?.source),
        recipients: [accountAddr],
        date,
        extra: {
          lt: tx.lt,
          explorerHash: tx.hash,
          comment: {
            isEncrypted: tx.in_msg?.message_content?.decoded?.type === "binary_comment",
            text:
              tx.in_msg?.message_content?.decoded?.type === "text_comment"
                ? tx.in_msg.message_content.decoded.comment
                : "",
          },
        },
        subOperations,
      });
    }

    if (isSending) {
      ops.push({
        id: encodeOperationId(accountId, hash, "OUT"),
        hash: tx.out_msgs[0].hash,
        type: "OUT",
        value: BigNumber(tx.out_msgs[0].value ?? 0),
        fee: BigNumber(tx.total_fees),
        blockHeight: tx.mc_block_seqno ?? 1,
        blockHash: null,
        hasFailed,
        accountId,
        senders: [accountAddr],
        recipients: getFriendlyAddress(addressBook, tx.out_msgs[0].destination),
        date,
        extra: {
          lt: tx.lt,
          explorerHash: tx.hash,
          comment: {
            isEncrypted: tx.out_msgs[0].message_content?.decoded?.type === "binary_comment",
            text:
              tx.out_msgs[0].message_content?.decoded?.type === "text_comment"
                ? tx.out_msgs[0].message_content.decoded.comment
                : "",
          },
        },
      });
    }

    return ops;
  };
}

export function mapJettonTxToOps(
  accountId: string,
  addr: string,
  addressBook: TonAddressBook,
  jettonTxMessageHashesMap?: Map<string, string>,
): (tx: TonJettonTransfer) => Promise<TonOperation[]> {
  return async (tx: TonJettonTransfer): Promise<TonOperation[]> => {
    const accountAddr = Address.parse(addr).toString({ urlSafe: true, bounceable: false });
    if (accountAddr !== addr) throw Error(`[ton] unexpected address ${accountAddr} ${addr}`);

    const jettonMasterAddr = Address.parse(tx.jetton_master).toString({
      urlSafe: true,
      bounceable: true,
    });
    const tokenCurrency = await getCryptoAssetsStore().findTokenByAddressInCurrency(
      jettonMasterAddr.toLowerCase(),
      decodeAccountId(accountId).currencyId,
    );
    if (!tokenCurrency) return [];
    const tokenAccountId = encodeTokenAccountId(accountId, tokenCurrency);

    const ops: TonOperation[] = [];
    const isReceiving = addressesAreEqual(
      accountAddr,
      Address.parse(tx.destination).toString({ urlSafe: true, bounceable: false }),
    );
    const isSending = addressesAreEqual(
      accountAddr,
      Address.parse(tx.source).toString({ urlSafe: true, bounceable: false }),
    );
    if (!isSending && !isReceiving) throw Error("[ton] unexpected addresses");

    const date = new Date(tx.transaction_now * 1000); // now is defined in seconds
    const hash = tx.transaction_hash;

    if (isReceiving) {
      ops.push({
        id: encodeOperationId(tokenAccountId, hash, "IN"),
        hash,
        type: "IN",
        value: BigNumber(tx.amount),
        fee: BigNumber(0),
        blockHeight: 1, // we don't have block info
        blockHash: null,
        hasFailed: false,
        accountId: tokenAccountId,
        senders: getFriendlyAddress(addressBook, tx.source),
        recipients: [accountAddr],
        date,
        extra: {
          lt: tx.transaction_lt,
          explorerHash: hash,
          comment: {
            isEncrypted: false,
            text: tx.forward_payload ? decodeForwardPayload(tx.forward_payload) : "",
          },
        },
      });
    }

    if (isSending) {
      const hash_message = jettonTxMessageHashesMap
        ? (jettonTxMessageHashesMap.get(hash) ?? hash)
        : hash;

      ops.push({
        id: encodeOperationId(tokenAccountId, hash_message, "OUT"),
        hash,
        type: "OUT",
        value: BigNumber(tx.amount),
        fee: BigNumber(0),
        blockHeight: 1, // we don't have block info
        blockHash: null,
        hasFailed: false,
        accountId: tokenAccountId,
        senders: [accountAddr],
        recipients: getFriendlyAddress(addressBook, tx.destination),
        date,
        extra: {
          lt: tx.transaction_lt,
          explorerHash: hash,
          comment: {
            isEncrypted: false,
            text: tx.forward_payload ? decodeForwardPayload(tx.forward_payload) : "",
          },
        },
      });
    }
    return ops;
  };
}

// Export these functions for testing
export function dataToSlice(data: string): Slice | undefined {
  let buffer: Buffer;
  if (typeof data === "string") {
    buffer = Buffer.from(data, "base64");

    try {
      return Cell.fromBoc(buffer)[0].beginParse();
    } catch {
      return new Slice(new BitReader(new BitString(buffer, 0, buffer.length * 8)), []);
    }
  }

  return undefined;
}

export function loadSnakeBytes(slice: Slice): Buffer {
  let buffer = Buffer.alloc(0);

  while (slice.remainingBits >= 8) {
    buffer = Buffer.concat([buffer, slice.loadBuffer(slice.remainingBits / 8)]);
    if (slice.remainingRefs) {
      slice = slice.loadRef().beginParse();
    } else {
      break;
    }
  }

  return buffer;
}

export function decodeForwardPayload(payload: string | null): string {
  if (!payload) return "";

  try {
    const slice = dataToSlice(payload);

    if (!slice) return "";

    const opcode = slice.loadUint(32);

    // Format with opcode 0 followed by text
    if (opcode !== 0) {
      return "";
    }
    const buffer = loadSnakeBytes(slice);
    const comment = buffer.toString("utf-8");

    return comment;
  } catch {
    // Silent failure, returning empty string
    return "";
  }
}
