import { BALANCES_CHUNK_SIZE } from "@src/constants";
import { ADDRESSES, BatchQueryAbi, ERC20Abi } from "@src/contracts";
import { Chain, TokenBalances } from "@src/models";
import { chunkArray } from "@src/utils";
import { BigNumber, ethers } from "ethers";

export const IERC20 = new ethers.utils.Interface(ERC20Abi);

export const getBalanceOf = async (
  wallet: string,
  token: string,
  rpcUrl: string,
): Promise<string> => {
  const provider = ethers.getDefaultProvider(rpcUrl);

  if (token === ethers.constants.AddressZero) {
    return ethers.utils.formatEther(await provider.getBalance(wallet));
  }

  const contract = new ethers.Contract(token, ERC20Abi, provider);
  return ethers.utils.formatUnits(
    await contract.balanceOf(wallet),
    await contract.decimals(),
  );
};
export const getAllowanceOf = async (
  token: string,
  wallet: string,
  spender: string,
  rpcUrl: string,
): Promise<string> => {
  const provider = ethers.getDefaultProvider(rpcUrl);

  if (token === ethers.constants.AddressZero) {
    return ethers.constants.MaxUint256.toString();
  }

  const contract = new ethers.Contract(token, ERC20Abi, provider);
  return ethers.utils.formatUnits(
    await contract.allowance(wallet, spender),
    await contract.decimals(),
  );
};

export const getBalances = async (
  chain: Chain,
  wallet: string,
): Promise<TokenBalances> => {
  return {
    ...(await getNativeBalance(chain, wallet)),
    ...(await getErc20Balances(chain, wallet)),
  };
};

const getNativeBalance = async (
  chain: Chain,
  wallet: string,
): Promise<TokenBalances> => {
  const native = chain.tokens.find(
    ({ address }) =>
      address.toLowerCase() === ethers.constants.AddressZero.toLowerCase(),
  );

  if (native) {
    const provider = ethers.getDefaultProvider(chain.publicRpcUrls[0]);
    const balance = await provider.getBalance(wallet);
    return {
      [native.address]: balance,
    };
  }
  return {};
};

const getErc20Balances = async (
  chain: Chain,
  wallet: string,
): Promise<TokenBalances> => {
  const erc20Tokens = chain.tokens.filter(
    ({ address }) => address !== ethers.constants.AddressZero,
  );
  const tokenChunks = chunkArray(erc20Tokens, BALANCES_CHUNK_SIZE);
  const promises = tokenChunks.map(async (tokenChunk) => {
    const tokenAddresses = tokenChunk.map((token) => token.address);
    const calldatas = tokenChunk.map(() =>
      IERC20.encodeFunctionData("balanceOf", [wallet]),
    );

    const contractAddress = ADDRESSES[chain.chainId]?.BatchQuery;
    const rpcUrl = chain.publicRpcUrls[0];
    if (contractAddress && rpcUrl) {
      const contract = new ethers.Contract(
        contractAddress,
        BatchQueryAbi,
        ethers.getDefaultProvider(rpcUrl),
      );
      return await contract["batchQuery"](tokenAddresses, calldatas);
    }
    return Promise.resolve();
  });

  const tokenBalances: BigNumber[] = (await Promise.all(promises))
    .flat()
    .map(
      (encodedBalance) =>
        ethers.utils.defaultAbiCoder.decode(["uint256"], encodedBalance)[0],
    );

  const balances: TokenBalances = {};

  erc20Tokens.forEach((token, index) => {
    balances[token.address] = tokenBalances[index];
  });
  return balances;
};

/**
 * Calculate param's value offset within the encoded function's bytecode.
 * @param abi
 * @param funName
 * @param funParams
 * @param placeholderValue
 */
export const findPlaceholderIndex = (
  abi: string,
  funName: string,
  // eslint-disable-next-line
  funParams: any[],
  placeholderValue: BigNumber,
) => {
  if (
    funParams.flat(Infinity).filter((param) => param === placeholderValue)
      .length !== 1
  ) {
    throw new Error("Random placeholder value must be provided and unique.");
  }

  const iface = new ethers.utils.Interface(abi);
  const functionFragment = iface.getFunction(funName);

  if (!functionFragment) {
    throw new Error(`Can't find function "${funName}" in provided ABI.`);
  }

  const encodedFunData = iface.encodeFunctionData(functionFragment, funParams);

  // Remove the 4-byte selector
  const paramData = encodedFunData.slice(10);

  // Split into 32-byte (64 hex characters) chunks
  const chunks: string[] = [];
  for (let i = 0; i < paramData.length; i += 64) {
    chunks.push(paramData.slice(i, i + 64));
  }

  // Find the index of the chunk containing the random value
  const searchValue = placeholderValue.toHexString().slice(2).padStart(64, "0");
  const result = chunks.findIndex((chunk) => chunk === searchValue);

  if (result === -1) {
    throw new Error("Randomized parameter not found in the encoded data.");
  }

  return result;
};

export const getEventsFromReceipt = async (
  receipt: ethers.providers.TransactionReceipt,
  abi: ethers.ContractInterface,
  rpcUrl: string,
  eventName: string,
) => {
  const contract = new ethers.Contract(
    receipt.to,
    abi,
    ethers.getDefaultProvider(rpcUrl),
  );

  const eventSignature = contract.interface.getEvent(eventName).format();

  return receipt.logs
    .filter((log) => log.topics[0] === ethers.utils.id(eventSignature))
    .map((log) => contract.interface.parseLog(log));
};

export const trimTxHash = (hash: string, beginAndEndCharacters: number = 5) => {
  if (!hash) {
    return "";
  }
  return `${hash?.substring(0, beginAndEndCharacters)}...${hash.substring(
    hash.length - beginAndEndCharacters,
    hash.length,
  )}`;
};
