import {
  addTransactionToRenderedTransactions,
  removeTransactionFromRenderedTransactions,
  renderTxStatusButtons,
  updateTransactionDoneInRenderedTransactions,
} from "@src/components";
import {
  MSG_RECEIVED_EVENT_ABI,
  MSG_RECEIVED_EVENT_SIG,
  MSG_SENT_EVENT_ABI,
  MSG_SENT_EVENT_SIG,
  TX_RECEIPT_STATUS_LIFETIME,
} from "@src/constants";
import { ADDRESSES } from "@src/contracts";
import { Chain, Token, Transaction, Web3Environment } from "@src/models";
import { getChains, getTxStatus } from "@src/services";
import retry from "async-retry";
import { ethers } from "ethers";
import { getCustomTokenData } from "../blockchain";

export const renderTxStatus = async (
  txChainId: string,
  txHash: string,
  dstTokenAddress?: string,
  dstTokenAmount?: string,
): Promise<void> => {
  const txReceipt = await getTxReceipt(txChainId, txHash);
  const supportedChains: Chain[] = (await getChains()).filter(
    ({ web3Environment, swapSupported }) =>
      web3Environment === Web3Environment.MAINNET && swapSupported,
  );

  const messageSentEvent = txReceipt.logs?.find((log: { topics: string[] }) => {
    return log.topics[0] === ethers.utils.id(MSG_SENT_EVENT_SIG);
  });

  if (!messageSentEvent) {
    throw new Error(
      `No transaction event found for chain ${txChainId} tx ${txHash}`,
    );
  }

  const messageId = messageSentEvent?.topics[1] as string;
  const iface = new ethers.utils.Interface(MSG_SENT_EVENT_ABI);
  const decodedMessageSentEvent = iface.parseLog(messageSentEvent);

  if (!decodedMessageSentEvent) {
    throw new Error(
      `No transaction event arguments found for chain ${txChainId} tx ${txHash}`,
    );
  }

  const MessageSentEventArgs = decodedMessageSentEvent.args;

  const srcChain = supportedChains.find(
    (chain) => chain.chainId === txChainId.toString(),
  );
  if (!srcChain) {
    throw new Error(`Unknown src chain ${txChainId}`);
  }

  const srcTokenAddress = (
    MessageSentEventArgs?.token.toString() as string
  ).toLowerCase();

  let srcToken = srcChain.tokens.find(
    (token) => token.address.toLowerCase() === srcTokenAddress,
  );

  if (!srcToken) {
    srcToken = await getCustomTokenData(srcChain, srcTokenAddress);
  }

  const dstChain = supportedChains.find(
    (chain) =>
      chain.ccipChainId ===
      MessageSentEventArgs?.destinationChainSelector?.toString(),
  );

  const dstChainId = dstChain?.chainId;

  if (!dstChainId) {
    throw new Error(`Unknown destination chain!`);
  }

  let dstToken: Token | undefined = undefined;
  if (dstTokenAddress) {
    dstToken = dstChain.tokens.find(
      (token) => token.address.toLowerCase() === dstTokenAddress.toLowerCase(),
    );
  }

  const timestamp = Date.now() / 1000;
  const transaction: Transaction = {
    hash: txReceipt.transactionHash,
    timestamp,
    sourceChainId: srcChain.chainId,
    targetChainId: dstChain.chainId,
    amountWei: MessageSentEventArgs?.tokenAmount.toString(),
    tokenAddress: srcToken.address,
    tokenOutAddress: dstToken?.address,
    tokenOutAmount: dstTokenAmount,
    estimatedDeliveryTimestamp:
      MessageSentEventArgs?.valueForInstantCcipRecieve.toString() !== "0"
        ? timestamp * 1000 + 30 * 1000
        : timestamp * 1000 + 30 * 60 * 1000,
    status: "IN_PROGRESS",
  };

  addTransactionToRenderedTransactions({
    transaction,
    supportedChains,
    dstToken,
    srcToken,
  });
  renderTxStatusButtons();

  const xSwapRouterOnDestinationAddress = ADDRESSES[dstChainId]?.XSwapRouter;
  if (!xSwapRouterOnDestinationAddress) {
    throw new Error(`Unknown destination XSwapRouter!`);
  }

  const xSwapRouterOnDestination = new ethers.Contract(
    xSwapRouterOnDestinationAddress,
    MSG_RECEIVED_EVENT_ABI,
    ethers.getDefaultProvider(
      supportedChains.find((chain) => chain.chainId === dstChainId)
        ?.publicRpcUrls[0],
    ),
  );

  const msgReceivedEventFilter = {
    address: xSwapRouterOnDestinationAddress,
    topics: [ethers.utils.id(MSG_RECEIVED_EVENT_SIG), messageId],
  };

  const onSuccess = async () => {
    updateTransactionDoneInRenderedTransactions(txHash);
    renderTxStatusButtons();
    await new Promise((resolve) =>
      setTimeout(() => resolve(true), TX_RECEIPT_STATUS_LIFETIME),
    );
    removeTransactionFromRenderedTransactions(txHash);
    renderTxStatusButtons();
  };

  // check tx status on backend
  getTxStatus({ transferId: messageId }).then((response) => {
    if (response.status === "DONE") {
      onSuccess();
    } else {
      setTimeout(
        () =>
          getTxStatus({ transferId: messageId }).then((response) => {
            if (response.status === "DONE") {
              onSuccess();
            }
          }),
        30 * 60 * 1000,
      ); // additional check after 30 min

      xSwapRouterOnDestination.once(msgReceivedEventFilter, () => onSuccess());
    }
  });
};

const getTxReceipt = async (txChainId: string, txHash: string) => {
  try {
    const chain = (await getChains()).find(
      (chain) => chain.chainId === txChainId,
    );

    const provider = new ethers.providers.JsonRpcProvider(
      chain?.publicRpcUrls[0],
    );

    return await retry(async () => {
      const txReceipt = await provider.getTransactionReceipt(txHash);
      if (!txReceipt) {
        throw new Error(`Couldn't fetch tx receipt`);
      }
      return txReceipt;
    });
  } catch (e) {
    throw new Error(
      `Couldn't fetch tx ${txHash} receipt on chain ${txChainId}`,
    );
  }
};
