import { formatUnits } from "ethers/lib/utils";
import { ethers } from "ethers";
import { getTokenPricesCGID, getTokenPricesGET } from "./getCoinGeckoPrices";

import {
  CHAIN_ID_OASIS,
  isEVMChain,
  CHAIN_ID_KARURA,
  CHAIN_ID_ACALA,
  CHAIN_ID_CELO,
  getEmitterAddressEth,
} from "@certusone/wormhole-sdk";

import { abi as Erc20Abi } from "./abi/erc20.json";

import axios from "axios";
import {
  DISALLOWLISTED_ADDRESSES,
  CHAIN_INFO_MAP,
  newProvider,
  sleepFor,
} from "./utils";

// current allowlist used for stats/govenor
import allowList = require("./allowList.json");

require("dotenv").config();

function getAllowList(chainId) {
  if (Object.keys(allowList).includes(chainId.toString())) {
    return allowList[chainId];
  } else {
    return [];
  }
}

function calcTokenQty(tokenInfo) {
  return Number(formatUnits(tokenInfo.balance, tokenInfo.decimals));
}

async function getBridgeBalanceScanner(chainInfo) {
  const chainId = chainInfo.chain_id;
  const bridgeAddress = chainInfo.token_bridge_address;
  const url = chainInfo.urlStem;

  // Get native token balances locked in contract
  const balance_apireqstring = `${url}/api?module=account&action=balance&address=${bridgeAddress}`;
  const balance_resp = await axios.get(balance_apireqstring);
  var tokenAccounts = [];
  if (balance_resp.status == 200) {
    const data = balance_resp["data"];
    if (data.hasOwnProperty("result")) {
      const balance = data["result"];
      var nativeInfo = {};
      var symbol = "";
      if (chainId == 7) {
        symbol = "ROSE";
      } else if (chainId == 11) {
        symbol = "KAR";
      } else if (chainId == 12) {
        symbol: "ACA";
      } else if (chainId == 14) {
        symbol: "CELO";
      }
      nativeInfo = {
        decimals: 18,
        name: symbol,
        tokenAddress: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee2",
        balance: balance,
        symbol: symbol,
      };
      tokenAccounts.push(nativeInfo);
    } else {
      console.log("object has no native balance data");
    }
  } else {
    console.log(balance_resp.status);
  }

  // Get locked token list from scan site
  const apiReqString = `${url}/api?module=account&action=tokenlist&address=${bridgeAddress}`;
  console.log(apiReqString);
  const response = await axios.get(apiReqString);
  if (response.status == 200) {
    const data = response["data"];
    if (data.hasOwnProperty("result")) {
      const tokenData = data["result"];
      tokenAccounts = [
        ...tokenAccounts,
        ...tokenData.map((x) => ({
          decimals: x.decimals,
          name: x.name,
          tokenAddress: x.contractAddress,
          balance: x.balance,
          symbol: x.symbol,
        })),
      ];
      return tokenAccounts;
    } else {
      console.log("object has no data");
      return [];
    }
  } else {
    console.log(response.status);
    return [];
  }
}

async function getBridgeBalanceCovalent(chainInfo) {
  /* Get token list (also has bridge balance and prices, but could be stale, so will query onchain)*/

  const covalentChainId: string = chainInfo.covalentChain;
  const bridgeAddress: string = chainInfo.token_bridge_address;
  const covalentApiKey = process.env.REACT_APP_COVALENT_API_KEY;
  const apiReqString = `https://api.covalenthq.com/v1/${covalentChainId}/address/${bridgeAddress}/balances_v2/?quote-currency=USD&format=JSON&nft=true&no-nft-fetch=false&key=${covalentApiKey}`;
  console.log(apiReqString);
  const response = await axios.get(apiReqString, {
    headers: { "User-Agent": "Mozilla/5.0" },
  });
  if (response.status == 200) {
    const data = response["data"];
    if (data.hasOwnProperty("data")) {
      const tokenData = data["data"];
      // tokenData["items"].forEach((x) => console.log(x));
      const tokenAccounts = tokenData["items"]
        .filter((item) => item.type !== "nft")
        .map((x) => ({
          decimals: x.contract_decimals,
          name: x.contract_name,
          tokenAddress: x.contract_address,
          balance: x.balance,
          price: x.quote_rate,
        }));
      return tokenAccounts;
    } else {
      console.log("object has no data");
      return [];
    }
  } else {
    console.log(response.status);
    return [];
  }
}

//for evm chains
async function getTokenContract(
  address: string,
  provider:
    | ethers.providers.JsonRpcProvider
    | ethers.providers.JsonRpcBatchProvider
) {
  const contract = new ethers.Contract(address, Erc20Abi, provider);
  return contract;
}

async function getBridgeBalanceOnChain(chainInfo, tokenList: any[]) {
  const bridgeAddress = chainInfo.token_bridge_address;
  let provider = newProvider(
    chainInfo.endpoint_url,
    true
  ) as ethers.providers.JsonRpcBatchProvider;

  var tokenAccounts = [];
  let i = 0;
  let chunksize = 100;
  while (i < tokenList.length) {
    const tokenContracts = await Promise.all(
      tokenList
        .slice(i, i + chunksize)
        .map((token) => getTokenContract(token.tokenAddress, provider))
    );
    const tokenInfos = await Promise.all(
      tokenContracts.map((tokenContract) =>
        Promise.all([
          tokenContract.address.toLowerCase(),
          tokenContract.name(),
          tokenContract.decimals(),
          tokenContract.symbol(),
          tokenContract.balanceOf(bridgeAddress),
        ])
      )
    );

    tokenInfos.forEach((tokenInfo) => {
      tokenAccounts.push({
        tokenAddress: tokenInfo[0],
        name: tokenInfo[1],
        decimals: tokenInfo[2],
        symbol: tokenInfo[3],
        balance: tokenInfo[4],
      });
    });

    i += chunksize;
  }
  return tokenAccounts;
}

export async function getEvmTokenAccounts(chainInfo, useAllowList) {
  const chainId = chainInfo.chain_id;
  console.log(chainId);
  if (!isEVMChain(chainId)) {
    console.log(`error. ${chainId} is not evm chain`);
    return [];
  }

  var tokenAccounts = [];
  if (useAllowList) {
    const allowList = getAllowList(chainId);
    var tokenList = [];
    Object.keys(allowList).forEach((address) => {
      tokenList.push({
        tokenAddress: address,
      });
    });
    // console.log(tokenList);
    tokenAccounts = await getBridgeBalanceOnChain(
      chainInfo,
      tokenList.filter(
        (token) =>
          token.tokenAddress.toLowerCase() !=
          "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" // filter out native token
      )
    );
    // console.log("tokenAccounts", tokenAccounts);
  } else {
    if (
      chainId == CHAIN_ID_OASIS ||
      chainId == CHAIN_ID_KARURA ||
      chainId == CHAIN_ID_ACALA ||
      chainId == CHAIN_ID_CELO
    ) {
      tokenAccounts = await getBridgeBalanceScanner(chainInfo);
    } else {
      // use convalent to get token addresses
      const tokenList = await getBridgeBalanceCovalent(chainInfo);
      // console.log(tokenList.filter((token) => console.log(token)));
      tokenAccounts = await getBridgeBalanceOnChain(
        chainInfo,
        tokenList.filter(
          (token) =>
            token.tokenAddress.toLowerCase() !=
            "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" // native token
        )
      );
      // console.log(tokenAccounts);
    }
  }
  return tokenAccounts;
}

async function getTokenValues(chainInfo, tokenInfos: any[], useAllowList) {
  console.log("allowlist?", useAllowList);
  try {
    const custody = tokenInfos.map((tokenInfo) => ({
      ...tokenInfo,
      qty: calcTokenQty(tokenInfo),
    }));
    const custodyFiltered = custody.filter((c) => c.qty > 0);
    var tokenPrices = {};
    var prices = [];

    if (useAllowList) {
      // use coingecko ids from allowlist
      const allowList = getAllowList(chainInfo.chain_id);
      const cgids: string[] = Object.values(allowList);

      // input array of cgids, returns json with cgid:price
      prices = await getTokenPricesCGID(cgids);
      if (prices === undefined) {
        console.log(`could not find ids for ${chainInfo.chain_id}`);
        return [];
      }
      for (const [key, value] of Object.entries(prices)) {
        if (!value.hasOwnProperty("usd")) {
          prices[key] = { usd: 0 };
        }
      }
      // have to map cgid: price to tokenAddress: price
      for (const [key, value] of Object.entries(allowList)) {
        for (const [key1, value1] of Object.entries(prices)) {
          if (key1 === value) {
            tokenPrices[key] = prices[key1];
          }
        }
      }
      // console.log(tokenPrices);
    } else {
      // use tokenAddresses from tokenInfos/custody

      let j = 0;
      let chunk_size = 100;
      while (j < custodyFiltered.length) {
        prices = await getTokenPricesGET(
          chainInfo.chain_id,
          chainInfo.platform,
          custodyFiltered.slice(j, j + chunk_size).map((x) => x.tokenAddress)
        );
        for (const [key, value] of Object.entries(prices)) {
          if (!value.hasOwnProperty("usd")) {
            prices[key] = { usd: 0 };
          }
        }

        tokenPrices = { ...tokenPrices, ...prices };
        j += chunk_size;
      }
    }

    // filter list by those with coin gecko prices
    const filteredBalances = custodyFiltered.filter((x) =>
      Object.keys(tokenPrices).includes(x.tokenAddress)
    );
    // calculate usd balances. add price and usd balance to tokenInfos
    const balancesUSD = filteredBalances.map((tokenInfo) => ({
      ...tokenInfo,
      tokenPrice: tokenPrices[tokenInfo.tokenAddress]["usd"],
      tokenBalanceUSD:
        tokenInfo.qty * tokenPrices[tokenInfo.tokenAddress]["usd"],
    }));

    // filter out disallowlist addresses
    const balancesUSDFiltered = balancesUSD.filter(
      (x) => !DISALLOWLISTED_ADDRESSES.includes(x.tokenAddress)
    );
    const sorted = balancesUSDFiltered.sort((a, b) =>
      a.tokenBalanceUSD < b.tokenBalanceUSD ? 1 : -1
    );

    return sorted;
  } catch (e) {
    console.log(e);
  }
  return [];
}

export async function getEvmCustody(chainInfo, useAllowList = true) {
  const tokenAccounts = await getEvmTokenAccounts(chainInfo, useAllowList);
  console.log(
    `Num of ${chainInfo.platform} token accounts=${tokenAccounts.length}`
  );
  const custody = await getTokenValues(chainInfo, tokenAccounts, useAllowList);
  console.log(
    `Num of filtered ${chainInfo.platform} token accounts=${custody.length}`
  );
  return custody;
}

export async function grabEvmCustodyData(chain, useAllowList) {
  const chainInfo = CHAIN_INFO_MAP[chain];
  var balances = [];
  try {
    balances = await getEvmCustody(chainInfo, useAllowList);
  } catch (e) {
    console.log(`could not grab ${chainInfo.name} data`);
  }
  // await updateTable(chainInfo, balances);
  const chainInfo_ = {
    ...chainInfo,
    emitter_address: getEmitterAddressEth(chainInfo.token_bridge_address),
    balances: balances,
  };
  return chainInfo_;
}

// const chain = process.env.chain;
// const useAllowListstr = process.env.chain || "false";

// (async () => {
//   const chainInfo = CHAIN_INFO_MAP[chain];
//   const useAllowList = true ? useAllowListstr === "true" : false;
//   const balances = await getEvmCustody(chainInfo, useAllowList);
//   console.log(balances);
//   await updateTable(chainInfo, balances);
// })();
