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

import { getEmitterAddressAlgorand } from "@certusone/wormhole-sdk";
import { calcLogicSigAccount } from "@certusone/wormhole-sdk/lib/cjs/algorand";

export const ALGORAND_HOST = {
  algodToken: "",
  algodServer: "https://mainnet-api.algonode.cloud", //"https://mainnet-idx.algonode.cloud", //"https://mainnet-api.algonode.cloud",
  algodPort: "",
};

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

require("dotenv").config();

import allowList = require("./allowList.json");
import algosdk from "algosdk";

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 findAlgoMetadata(provider, tokenAddress) {
  const index = parseInt(tokenAddress);
  if (index === 0) {
    const definition = {
      tokenAddress: tokenAddress,
      decimals: Number(6),
      name: "ALGO",
      symbol: "ALGO",
    };
    return definition;
  }

  let decimals = undefined;
  let name = undefined;
  let symbol = undefined;

  try {
    const resp = await provider.getAssetByID(index).do();
    decimals = resp.params.decimals;
    name = resp.params.name;
    symbol = resp.params["unit-name"];
  } catch {
    console.error(`could not get ${index} metadata`);
  }
  return {
    tokenAddress: tokenAddress,
    decimals: decimals,
    name: name,
    symbol: symbol,
  };
}

export async function getNativeAlgoAddress(
  algoClient: any,
  token_bridge: any,
  assetId: any
) {
  const { doesExist, lsa } = await calcLogicSigAccount(
    algoClient,
    BigInt(token_bridge),
    BigInt(assetId),
    Buffer.from("native", "binary").toString("hex")
  );
  return lsa.address();
}

async function getBridgeBalanceOnChain(chainInfo, tokenList: any[]) {
  const bridgeAddress = chainInfo.token_bridge_address;
  var tokenAccounts = [];
  const provider = new algosdk.Algodv2(
    ALGORAND_HOST.algodToken,
    ALGORAND_HOST.algodServer,
    ALGORAND_HOST.algodPort
  );
  tokenAccounts = [];
  let i = 0;
  while (i < tokenList.length) {
    let tokenAddress = tokenList[i].tokenAddress;
    const tokenInfo = await findAlgoMetadata(provider, tokenAddress);

    let balance = BigNumber.from("0");
    try {
      let nativeAlgoAddr = await getNativeAlgoAddress(
        provider,
        bridgeAddress,
        parseInt(tokenAddress)
      );

      let nativeAlgoInfo = await provider
        .accountInformation(nativeAlgoAddr)
        .do();
      if (tokenAddress == 0) {
        // native ALGO balance
        balance = nativeAlgoInfo?.amount;
      } else {
        // other native asset
        if (nativeAlgoInfo?.assets.length == 0) {
          balance = BigNumber.from("0");
        } else {
          balance = nativeAlgoInfo?.assets[0].amount;
        }
      }
    } catch {
      console.error(`could not get ${tokenAddress} balance`);
    }
    tokenAccounts.push({
      tokenAddress: tokenAddress,
      name: tokenInfo.name,
      decimals: tokenInfo.decimals,
      symbol: tokenInfo.symbol,
      balance: balance,
    });
    i++;
  }
  return tokenAccounts;
}

export async function getAlgoTokenAccounts(chainInfo, useAllowList) {
  const chainId = chainInfo.chain_id;
  console.log(chainId);

  var tokenAccounts = [];
  if (useAllowList) {
    const allowList = getAllowList(chainId);
    var tokenList = [];
    Object.keys(allowList).forEach((address) => {
      tokenList.push({
        tokenAddress: address,
      });
    });
    tokenAccounts = await getBridgeBalanceOnChain(chainInfo, tokenList);
    // console.log("tokenAccounts", tokenAccounts);
  } /*else {
        tokenAccounts = await getBridgeBalanceScanner(chainInfo);
    }*/
  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);
      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];
          }
        }
      }
    } 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 getAlgoCustody(chainInfo, useAllowList = true) {
  const tokenAccounts = await getAlgoTokenAccounts(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 grabAlgoCustodyData(chain, useAllowList) {
  const chainInfo = CHAIN_INFO_MAP[chain];
  var balances = [];
  try {
    balances = await getAlgoCustody(chainInfo, useAllowList);
  } catch (e) {
    console.log(`could not grab ${chainInfo.name} data`);
  }
  // await updateTable(chainInfo, balances);
  const chainInfo_ = {
    ...chainInfo,
    emitter_address: getEmitterAddressAlgorand(
      BigInt(chainInfo.token_bridge_address)
    ),
    balances: balances,
  };
  return chainInfo_;
}

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

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