import { readContracts } from "@wagmi/core"
import { Address, erc20Abi, zeroAddress } from "viem"
import multicallAbi from "../multicallAbi.json"

import {
  MULTICALL_CONTRACT_ADDRESS,
  SCAM_TOKEN_ADDRESSES,
  SCAM_TOKEN_WORDS,
  SUPPORTED_CHAINS_IDS,
} from "../constants"
import { SupportedChainIds, TAsset } from "../types"
import { formatUnits, sortAssets } from "./actions"
import { baseWagmiConfig as wagmiConfig } from "../wagmi"

type FetchContractBalance = {
  accountAddress: Address
  assets: TAsset[]
}

type TokenCall = {
  abi: unknown
  functionName: string
  address?: Address
  args?: readonly unknown[]
}

type CallType = {
  result: bigint
  status: "success" | "failure"
}

type TokenMultiCall = TokenCall[]

const MULTI_CALL_FUNCTIONS = ["balanceOf"]

const fetchAssetsBalanceMultiCall = async ({
  accountAddress,
  assets,
}: FetchContractBalance): Promise<TAsset[]> => {
  try {
    const filteredByDeployedChains = assets.filter((asset) => {
      return SUPPORTED_CHAINS_IDS.includes(asset.chainId)
    })

    const getBalanceOfCall = (
      tokenAddress: Address,
      chainId: SupportedChainIds,
    ): TokenMultiCall => {
      const erc20Config = {
        abi: erc20Abi,
        address: tokenAddress,
        chainId,
      } as const

      return [
        {
          ...erc20Config,
          functionName: MULTI_CALL_FUNCTIONS[0],
          args: [accountAddress],
        },
      ]
    }

    const getETHBalanceOfCall = (
      chainId: SupportedChainIds,
    ): TokenMultiCall => {
      const callConfig = {
        abi: multicallAbi,
        address: MULTICALL_CONTRACT_ADDRESS,
        chainId,
      } as const

      return [
        {
          ...callConfig,
          functionName: "getEthBalance",
          args: [accountAddress],
        },
      ]
    }

    const tokenCalls: TokenMultiCall = []
    filteredByDeployedChains
      .filter((asset) => asset.address.includes("0x"))
      .forEach((token) => {
        const tokenCall =
          token.address.toLowerCase() === zeroAddress.toLowerCase()
            ? getETHBalanceOfCall(token.chainId)
            : getBalanceOfCall(token.address, token.chainId)

        tokenCall.forEach((call) => {
          tokenCalls.push(call)
        })
      })

    const multiCallResult = await readContracts(wagmiConfig, {
      allowFailure: true,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      contracts: tokenCalls as any,
      multicallAddress: MULTICALL_CONTRACT_ADDRESS,
      batchSize: 0, // 0 is unlimited
    })

    const assetsWithBalance = []
    for (
      let i = 0;
      i < multiCallResult.length;
      i += MULTI_CALL_FUNCTIONS.length
    ) {
      const assetWithBalance = formatMultiCallResult(
        multiCallResult[i] as CallType,
        filteredByDeployedChains[i],
      )
      assetsWithBalance.push(assetWithBalance)
    }

    return assetsWithBalance
  } catch (err) {
    console.error("[ERROR] on balances multiCall", err)
    return []
  }
}

const formatMultiCallResult = (
  call: CallType,
  selectedAsset: TAsset,
): TAsset => {
  try {
    const assetBalanceOf = call.result ? call.result : BigInt(0)

    const asset: TAsset = {
      ...selectedAsset,
      balanceOf: {
        decimals: selectedAsset.decimals,
        formatted: formatUnits(assetBalanceOf, selectedAsset.decimals),
        symbol: selectedAsset.name,
        value: assetBalanceOf,
      },
    }
    return asset
  } catch (err) {
    console.error("[ERROR] on formatMultiCallResult", err)
    return {
      ...selectedAsset,
      prices: selectedAsset.prices || { default: 0 },
      value: "0", // formatUnits(totalAssetValue, selectedAsset.decimals),
      balanceOf: {
        decimals: selectedAsset.decimals,
        formatted: "0.0", //formatUnits(assetBalanceOf, selectedAsset.decimals),
        symbol: selectedAsset.name,
        value: BigInt(0), // assetBalanceOf
      },
    }
  }
}

/**
 * Fetches and filters asset balances for a given account address.
 *
 * @param {Address} accountAddress The address to fetch balances for.
 * @param {TAsset[]} assets An array of asset objects to fetch balances for.
 * @returns {Promise<TAsset[]>} A promise that resolves to an array of filtered asset objects.
 * @throws {Error} If there is an error fetching the asset balances.
 */
export const fetchAndFilterAssetBalances = async (
  accountAddress: Address,
  assets: TAsset[],
): Promise<TAsset[]> => {
  try {
    const balances = await fetchAssetsBalanceMultiCall({
      accountAddress: accountAddress as Address,
      assets: assets,
    })

    const filteredUserAssets = balances.filter((asset) => {
      const isNonZeroBalance =
        !asset.balanceOf || asset.balanceOf.value !== BigInt(0)

      const containsScamWords = SCAM_TOKEN_WORDS.some((scamWord) =>
        asset.name.toLowerCase().includes(scamWord),
      )

      const containsScamAddresses = SCAM_TOKEN_ADDRESSES.includes(
        asset.address.toLowerCase(),
      )

      return isNonZeroBalance && !containsScamWords && !containsScamAddresses
    })

    // Filter out duplicate assets based on address
    const uniqueAssets = filteredUserAssets.reduce<TAsset[]>((acc, asset) => {
      if (
        !acc.find(
          (a) => a.address.toLowerCase() === asset.address.toLowerCase(),
        )
      ) {
        acc.push(asset)
      }
      return acc
    }, [])

    return sortAssets(uniqueAssets)
  } catch (err) {
    console.error("Error fetching and filtering asset balances:", err)
    throw err // Re-throw the error to be handled by the caller
  }
}
