import { stringify } from "querystring";
import { InvalidTransactionError } from "@ledgerhq/errors";
import network from "@ledgerhq/live-network";
import { hours, makeLRUCache } from "@ledgerhq/live-network/cache";
import { log } from "@ledgerhq/logs";
import { Account, TokenAccount } from "@ledgerhq/types-live";
import { BigNumber } from "bignumber.js";
import compact from "lodash/compact";
import drop from "lodash/drop";
import sumBy from "lodash/sumBy";
import take from "lodash/take";
import { TronWeb, providers } from "tronweb";
import coinConfig from "../config";
import type {
  FreezeTransactionData,
  LegacyUnfreezeTransactionData,
  NetworkInfo,
  SendTransactionData,
  SendTransactionDataSuccess,
  SmartContractTransactionData,
  SuperRepresentative,
  SuperRepresentativeData,
  Transaction,
  TrongridTxInfo,
  TronResource,
  UnDelegateResourceTransactionData,
  UnFreezeTransactionData,
  WithdrawExpireUnfreezeTransactionData,
} from "../types";
import { TronTransactionExpired } from "../types/errors";
import {
  decode58Check,
  encode58Check,
  formatTrongridTrc20TxResponse,
  formatTrongridTxResponse,
} from "./format";
import {
  AccountTronAPI,
  Block,
  BlockWithTransactionsAPI,
  isTransactionTronAPI,
  MalformedTransactionTronAPI,
  TransactionInfoByBlockNumAPI,
  TransactionResponseTronAPI,
  TransactionTronAPI,
  Trc20API,
} from "./types";
import { abiEncodeTrc20Transfer, hexToAscii } from "./utils";

const getBaseApiUrl = () => coinConfig.getCoinConfig().explorer.url;

function isValidNativeTx(tx: TransactionTronAPI): boolean {
  // tx_id indicates a malformed/duplicated entry from TronGrid — these must be excluded.
  // Transactions with internal_transactions are valid and should be included.
  return !tx.tx_id;
}

function isSuccessfulTriggerSmartContract(tx: TrongridTxInfo): boolean {
  return tx.type === "TriggerSmartContract" && !tx.hasFailed;
}

export async function post<T, U extends object = any>(endPoint: string, body: T): Promise<U> {
  const { data } = await network<U, T>({
    method: "POST",
    url: `${getBaseApiUrl()}${endPoint}`,
    data: body,
  });

  // Ugly but trongrid send a 200 status event if there are errors
  if ("Error" in data) {
    const error = data.Error as any;
    const message = stringify(error);
    const nonEmptyMessage = message === "" ? error.toString() : message;
    log("tron-error", nonEmptyMessage, { endPoint, body });
    throw new Error(nonEmptyMessage);
  }

  return data;
}

async function fetch<T extends object = any>(endPoint: string): Promise<T> {
  return fetchWithBaseUrl<T>(`${getBaseApiUrl()}${endPoint}`);
}

async function fetchWithBaseUrl<T extends object = any>(url: string): Promise<T> {
  const { data } = await network<T>({ url });

  // Ugly but trongrid send a 200 status event if there are errors
  if ("Error" in data) {
    log("tron-error", stringify(data.Error as any), {
      url,
    });
    throw new Error(stringify(data.Error as any));
  }

  return data;
}

export const freezeTronTransaction = async (
  account: Account,
  transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
  const txData: FreezeTransactionData = {
    frozen_balance: transaction.amount.toNumber(),
    resource: transaction.resource,
    owner_address: decode58Check(account.freshAddress),
  };
  const url = `/wallet/freezebalancev2`;
  const result = await post(url, txData);

  return result;
};

export const unfreezeTronTransaction = async (
  account: Account,
  transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
  const txData: UnFreezeTransactionData = {
    owner_address: decode58Check(account.freshAddress),
    resource: transaction.resource,
    unfreeze_balance: transaction.amount.toNumber(),
  };
  const url = `/wallet/unfreezebalancev2`;
  const result = await post(url, txData);

  return result;
};

export const withdrawExpireUnfreezeTronTransaction = async (
  account: Account,
  _transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
  const txData: WithdrawExpireUnfreezeTransactionData = {
    owner_address: decode58Check(account.freshAddress),
  };
  const url = `/wallet/withdrawexpireunfreeze`;
  const result = await post(url, txData);

  return result;
};

export const unDelegateResourceTransaction = async (
  account: Account,
  transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
  const txData: UnDelegateResourceTransactionData = {
    balance: transaction.amount.toNumber(),
    resource: transaction.resource,
    owner_address: decode58Check(account.freshAddress),
    receiver_address: decode58Check(transaction.recipient),
  };

  const url = `/wallet/undelegateresource`;
  const result = await post(url, txData);

  return result;
};

export const legacyUnfreezeTronTransaction = async (
  account: Account,
  transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
  const txData: LegacyUnfreezeTransactionData = {
    resource: transaction.resource,
    owner_address: decode58Check(account.freshAddress),
    receiver_address: transaction.recipient ? decode58Check(transaction.recipient) : undefined,
  };
  const url = `/wallet/unfreezebalance`;
  const result = await post(url, txData);
  return result;
};

export async function getDelegatedResource(
  account: Account,
  transaction: Transaction,
  resource: TronResource,
): Promise<BigNumber> {
  const url = `/wallet/getdelegatedresourcev2`;

  const {
    delegatedResource = [],
  }: {
    delegatedResource?: {
      frozen_balance_for_bandwidth: number;
      frozen_balance_for_energy: number;
    }[];
  } = await post(url, {
    fromAddress: decode58Check(account.freshAddress),
    toAddress: decode58Check(transaction.recipient),
  });

  const { frozen_balance_for_bandwidth, frozen_balance_for_energy } = delegatedResource.reduce(
    (accum, cur) => {
      if (cur.frozen_balance_for_bandwidth) {
        accum.frozen_balance_for_bandwidth += cur.frozen_balance_for_bandwidth;
      }
      if (cur.frozen_balance_for_energy) {
        accum.frozen_balance_for_energy += cur.frozen_balance_for_energy;
      }
      return accum;
    },
    { frozen_balance_for_bandwidth: 0, frozen_balance_for_energy: 0 },
  );

  const amount =
    resource === "BANDWIDTH" ? frozen_balance_for_bandwidth : frozen_balance_for_energy;

  return new BigNumber(amount);
}

export const DEFAULT_TRC20_FEES_LIMIT = 50000000;

export async function craftTrc20Transaction(
  tokenAddress: string,
  recipientAddress: string,
  senderAddress: string,
  amount: BigNumber,
  customFees?: number,
  expiration?: number,
): Promise<SendTransactionDataSuccess> {
  const txData: SmartContractTransactionData = {
    function_selector: "transfer(address,uint256)",
    fee_limit: customFees ? customFees : DEFAULT_TRC20_FEES_LIMIT,
    call_value: 0,
    contract_address: decode58Check(tokenAddress),
    parameter: abiEncodeTrc20Transfer(recipientAddress, new BigNumber(amount.toString())),
    owner_address: senderAddress,
  };
  const url = `/wallet/triggersmartcontract`;
  const { transaction: preparedTransaction } = await post(url, txData);
  return await extendExpiration(preparedTransaction, expiration);
}

export async function craftStandardTransaction(
  tokenAddress: string | undefined,
  recipientAddress: string,
  senderAddress: string,
  amount: BigNumber,
  isTransferAsset: boolean,
  memo?: string,
  expiration?: number,
): Promise<SendTransactionDataSuccess> {
  const url = isTransferAsset ? `/wallet/transferasset` : `/wallet/createtransaction`;
  const txData: SendTransactionData = {
    to_address: recipientAddress,
    owner_address: senderAddress,
    amount: Number(amount),
    asset_name: tokenAddress && Buffer.from(tokenAddress).toString("hex"),
    extra_data: memo && Buffer.from(memo).toString("hex"),
  };
  const preparedTransaction = await post(url, txData);
  return await extendExpiration(preparedTransaction, expiration);
}

const getTokenInfo = (subAccount: TokenAccount | null | undefined): string[] | undefined[] => {
  const tokenInfo =
    subAccount && subAccount.type === "TokenAccount"
      ? drop(subAccount.token.id.split("/"), 1)
      : [undefined, undefined];
  return tokenInfo;
};

// Send trx or trc10/trc20 tokens
export const createTronTransaction = async (
  account: Account,
  transaction: Transaction,
  subAccount: TokenAccount | null | undefined,
): Promise<SendTransactionDataSuccess> => {
  const [tokenType, tokenId] = getTokenInfo(subAccount);

  const decodeRecipient = decode58Check(transaction.recipient);
  const decodeSender = decode58Check(account.freshAddress);
  // trc20
  if (tokenType === "trc20" && tokenId) {
    const tokenContractAddress = (subAccount as TokenAccount).token.contractAddress;
    return craftTrc20Transaction(
      tokenContractAddress,
      decodeRecipient,
      decodeSender,
      transaction.amount,
    );
  } else {
    const isTransferAsset = subAccount ? true : false;
    return craftStandardTransaction(
      tokenId,
      decodeRecipient,
      decodeSender,
      transaction.amount,
      isTransferAsset,
    );
  }
};

/** Default expiration of 10 minutes (in seconds) after crafting time. */
export const DEFAULT_EXPIRATION = 600;

async function extendExpiration(
  preparedTransaction: any,
  expiration?: number,
): Promise<SendTransactionDataSuccess> {
  const extension = expiration ?? DEFAULT_EXPIRATION;
  const nodeExpiration: number = preparedTransaction.raw_data.expiration;
  const minFinalExpiration = Date.now() + 3000;

  // Tron nodes may not be properly synced, returning an expiration date in the past.
  // We throw an error that encourages users to drop their transaction and re-create a new one.
  // https://github.com/tronprotocol/tronweb/blob/9f8b559377d9215a4f5360e8526c6e7197bf5a5b/src/lib/TransactionBuilder/TransactionBuilder.ts#L2449-L2450
  if (nodeExpiration + extension * 1000 <= minFinalExpiration) {
    log("tron/extendExpiration", "Invalid extension provided", {
      preparedTransaction,
      extensionInS: extension,
      extensionInMs: extension * 1000,
      minFinalExpiration,
    });

    throw new InvalidTransactionError();
  }

  const HttpProvider = providers.HttpProvider;
  const fullNode = new HttpProvider(getBaseApiUrl());
  const solidityNode = new HttpProvider(getBaseApiUrl());
  const eventServer = new HttpProvider(getBaseApiUrl());
  const tronWeb = new TronWeb(fullNode, solidityNode, eventServer);

  return tronWeb.transactionBuilder.extendExpiration(preparedTransaction, extension);
}

type BroadcastSuccessResponseTronAPI = { result: true; txid: string };
type BroadcastErrorResponseTronAPI = {
  result?: boolean;
  txid: string;
  code: string;
  message: string;
};
type BroadcastResponseTronAPI = BroadcastSuccessResponseTronAPI | BroadcastErrorResponseTronAPI;
/**
 * @see https://github.com/tronprotocol/java-tron/blob/develop/framework/src/main/java/org/tron/core/services/http/BroadcastServlet.java
 * @param trxTransaction
 * @returns Transaction ID
 */
export const broadcastTron = async (
  trxTransaction: SendTransactionDataSuccess & { signature: string[] },
): Promise<string> => {
  const result: BroadcastResponseTronAPI = await post(
    "/wallet/broadcasttransaction",
    trxTransaction,
  );

  if (result.result !== true) {
    if (result.code === "TRANSACTION_EXPIRATION_ERROR") {
      throw new TronTransactionExpired();
    } else {
      throw new Error(`${result.code}: ${result.message}`);
    }
  }

  return result.txid;
};

type TronGridBroadcastResponse = {
  result: boolean;
  code: string;
  txid: string;
  message: string;
  transaction: {
    raw_data: Record<string, unknown>;
    signature: string[];
  };
};
export const broadcastHexTron = async (rawTransaction: string): Promise<string> => {
  const result = await post<{ transaction: string }, TronGridBroadcastResponse>(
    `/wallet/broadcasthex`,
    { transaction: rawTransaction },
  );

  if (!result.result) {
    throw Error(`Broadcast failed due to ${result.code}`);
  }

  return result.txid;
};

/**
 * {@link https://github.com/tronprotocol/java-tron/blob/develop/framework/src/main/java/org/tron/core/services/http/GetAccountServlet.java | Tron Framework}
 */
export async function fetchTronAccount(addr: string): Promise<AccountTronAPI[]> {
  try {
    const data = await fetch(`/v1/accounts/${addr}`);
    return data.data;
  } catch {
    return [];
  }
}

export async function getLastBlock(): Promise<Block> {
  const data = await fetch(`/wallet/getnowblock`);
  return toBlock(data);
}

export async function getBlock(blockNumber: number): Promise<Block> {
  const data: BlockWithTransactionsAPI = await post(`/wallet/getblock`, {
    id_or_num: String(blockNumber),
    detail: false,
  });
  return toBlock(data);
}

export async function getBlockWithTransactions(
  blockNumber: number,
): Promise<BlockWithTransactionsAPI> {
  return post(`/wallet/getblock`, { id_or_num: String(blockNumber), detail: true });
}

function toBlock(data: BlockWithTransactionsAPI): Block {
  const timestamp = data.block_header.raw_data.timestamp;
  const ret: Block = {
    height: data.block_header.raw_data.number,
    hash: data.blockID,
  };
  if (timestamp) {
    ret.time = new Date(timestamp);
  }
  return ret;
}

export async function getTransactionInfoByBlockNum(
  blockNum: number,
): Promise<TransactionInfoByBlockNumAPI[]> {
  return post<{ num: number }, TransactionInfoByBlockNumAPI[]>(
    `/wallet/gettransactioninfobyblocknum`,
    { num: blockNum },
  );
}

async function getAllTransactions<T>(
  initialUrl: string,
  shouldFetchMoreTxs: (txs: T[]) => boolean,
  getTxs: (url: string) => Promise<{
    results: Array<T>;
    nextUrl?: string;
  }>,
) {
  let all: Array<T> = [];
  let url: string | undefined = initialUrl;
  while (url && shouldFetchMoreTxs(all)) {
    const { nextUrl, results } = await getTxs(url);
    url = nextUrl;
    all = all.concat(results);
  }

  return all;
}

const getTransactions = async (
  url: string,
): Promise<{
  results: Array<TransactionTronAPI | MalformedTransactionTronAPI>;
  nextUrl?: string;
}> => {
  const transactions =
    await fetchWithBaseUrl<
      TransactionResponseTronAPI<TransactionTronAPI | MalformedTransactionTronAPI>
    >(url);
  const nextUrl = transactions.meta.links?.next?.replace(
    /https:\/\/api(\.[a-z]*)?.trongrid.io/,
    getBaseApiUrl(),
  );
  const results = transactions.data ?? [];
  return {
    results,
    nextUrl,
  };
};

const getTrc20 = async (
  url: string,
): Promise<{
  results: Array<Trc20API>;
  nextUrl?: string;
}> => {
  const transactions = await fetchWithBaseUrl<TransactionResponseTronAPI<Trc20API>>(url);

  return {
    results: transactions.data,
    nextUrl: transactions.meta.links?.next?.replace(
      /https:\/\/api(\.[a-z]*)?.trongrid.io/,
      getBaseApiUrl(),
    ),
  };
};

export type FetchTxsStopPredicate = (
  txs: Array<TransactionTronAPI | Trc20API | MalformedTransactionTronAPI>,
) => boolean;

export type FetchParams = {
  /** The maximum number of transactions to fetch per call. */
  limitPerCall: number;
  /** Hint about the number of transactions to be fetched in total (hint to optimize `limitPerCall`) */
  hintGlobalLimit?: number;
  minTimestamp: number;
  order: "asc" | "desc";
};

export const defaultFetchParams: FetchParams = {
  limitPerCall: 100,
  minTimestamp: 0,
  order: "desc",
} as const;

export type TxPageResult = {
  txs: TrongridTxInfo[];
  hasNextPage: boolean;
};

export type FetchTxsPageParams = {
  limit: number;
  minTimestamp: number;
  maxTimestamp?: number;
  order: "asc" | "desc";
};

export type FetchTxsPageResult = {
  nativeTxs: TxPageResult;
  trc20Txs: TxPageResult;
};

async function fetchSinglePage<T>(
  url: string,
  getTxs: (url: string) => Promise<{ results: Array<T>; nextUrl?: string }>,
): Promise<{ results: Array<T>; hasNextPage: boolean }> {
  const { results, nextUrl } = await getTxs(url);
  return { results, hasNextPage: !!nextUrl };
}

export async function fetchTronAccountTxsPage(
  addr: string,
  params: FetchTxsPageParams,
): Promise<FetchTxsPageResult> {
  const maxTimestampParam =
    params.maxTimestamp !== undefined ? `&max_timestamp=${params.maxTimestamp}` : "";
  const queryParams = `limit=${params.limit}&min_timestamp=${params.minTimestamp}${maxTimestampParam}&order_by=block_timestamp,${params.order}`;

  const [nativeResult, trc20Result] = await Promise.all([
    fetchSinglePage<TransactionTronAPI | MalformedTransactionTronAPI>(
      `${getBaseApiUrl()}/v1/accounts/${addr}/transactions?${queryParams}`,
      getTransactions,
    ),
    fetchSinglePage<Trc20API>(
      `${getBaseApiUrl()}/v1/accounts/${addr}/transactions/trc20?${queryParams}&get_detail=true`,
      getTrc20,
    ),
  ]);

  const nativeTxsFormatted = await Promise.all(
    nativeResult.results
      .filter(isTransactionTronAPI)
      .filter(isValidNativeTx)
      .map(tx => formatTrongridTxResponse(tx, accountNamesCache)),
  );

  const trc20TxsFormatted = compact(trc20Result.results.map(formatTrongridTrc20TxResponse));
  const trc20TxIds = new Set(trc20TxsFormatted.map(t => t.txID));
  const nativeDeduped = compact(nativeTxsFormatted)
    .filter(tx => !trc20TxIds.has(tx.txID))
    .filter(tx => !isSuccessfulTriggerSmartContract(tx));

  return {
    nativeTxs: { txs: nativeDeduped, hasNextPage: nativeResult.hasNextPage },
    trc20Txs: { txs: trc20TxsFormatted, hasNextPage: trc20Result.hasNextPage },
  };
}

export async function fetchTronAccountTxs(
  addr: string,
  shouldFetchMoreTxs: FetchTxsStopPredicate,
  params: FetchParams,
): Promise<TrongridTxInfo[]> {
  const adjustedLimitPerCall = params.hintGlobalLimit
    ? Math.min(params.limitPerCall, params.hintGlobalLimit)
    : params.limitPerCall;
  const queryParams = `limit=${adjustedLimitPerCall}&min_timestamp=${params.minTimestamp}&order_by=block_timestamp,${params.order}`;
  const nativeTxs = await Promise.all(
    (
      await getAllTransactions<TransactionTronAPI | MalformedTransactionTronAPI>(
        `${getBaseApiUrl()}/v1/accounts/${addr}/transactions?${queryParams}`,
        shouldFetchMoreTxs,
        getTransactions,
      )
    )
      .filter(isTransactionTronAPI)
      .filter(isValidNativeTx)
      .map(tx => formatTrongridTxResponse(tx, accountNamesCache)),
  );

  // we need to fetch and filter trc20 transactions from another endpoint
  // doc https://developers.tron.network/reference/get-trc20-transaction-info-by-account-address

  const callTrc20Endpoint = async () =>
    await getAllTransactions<Trc20API>(
      `${getBaseApiUrl()}/v1/accounts/${addr}/transactions/trc20?${queryParams}&get_detail=true`,
      shouldFetchMoreTxs,
      getTrc20,
    );

  type Acc = {
    txs: Trc20API[];
    invalids: number[];
  };

  function isValid(tx: Trc20API): boolean {
    const ret = tx?.detail?.ret;
    return Array.isArray(ret) && ret.length > 0;
  }

  function getInvalidTxIndexes(txs: Trc20API[]): number[] {
    const invalids: number[] = [];
    for (let i = 0; i < txs.length; i++) {
      if (!isValid(txs[i])) {
        invalids.push(i);
      }
    }
    txs.filter(tx => !isValid(tx)).map((_tx, index) => index);
    return invalids;
  }

  function assert(predicate: boolean, message: string) {
    if (!predicate) {
      throw new Error(message);
    }
  }

  // Merge the two results
  function mergeAccs(acc1: Acc, acc2: Acc): Acc {
    assert(acc1.txs.length === acc2.txs.length, "accs should have the same length");
    const accRet: Acc = { txs: acc1.txs, invalids: [] };
    acc1.invalids.forEach(invalidIndex => {
      acc2.invalids.includes(invalidIndex)
        ? accRet.invalids.push(invalidIndex)
        : (accRet.txs[invalidIndex] = acc2.txs[invalidIndex]);
    });
    return accRet;
  }

  // see LIVE-18992 for an explanation to why we need this
  async function getTrc20TxsWithRetry(acc: Acc | null, times: number): Promise<Trc20API[]> {
    assert(
      times > 0,
      "getTrc20TxsWithRetry: couldn't fetch trc20 transactions after several attempts",
    );
    const ret = await callTrc20Endpoint();
    const thisAcc: Acc = {
      txs: ret,
      invalids: getInvalidTxIndexes(ret),
    };
    const newAcc = acc ? mergeAccs(acc, thisAcc) : thisAcc;
    if (newAcc.invalids.length === 0) {
      return newAcc.txs;
    } else {
      log(
        "coin-tron",
        `getTrc20TxsWithRetry: got ${newAcc.invalids.length} invalid trc20 transactions, retrying...`,
      );
      return await getTrc20TxsWithRetry(newAcc, times - 1);
    }
  }

  const trc20Txs = compact(
    (await getTrc20TxsWithRetry(null, 3)).map(formatTrongridTrc20TxResponse),
  );
  const trc20TxIds = new Set(trc20Txs.map(t => t.txID));
  const nativeDeduped = compact(nativeTxs)
    .filter(tx => !trc20TxIds.has(tx.txID))
    .filter(tx => !isSuccessfulTriggerSmartContract(tx));

  const txInfos: TrongridTxInfo[] = nativeDeduped
    .concat(trc20Txs)
    .sort((a, b) => b.date.getTime() - a.date.getTime());
  return txInfos;
}

export const getContractUserEnergyRatioConsumption = async (address: string): Promise<number> => {
  const result = await fetchTronContract(address);
  if (result) {
    const { consume_user_resource_percent } = result;
    return consume_user_resource_percent;
  }
  return 0;
};

export const fetchTronContract = async (addr: string): Promise<Record<string, any> | undefined> => {
  try {
    const data = await post(`/wallet/getcontract`, {
      value: decode58Check(addr),
    });
    return Object.keys(data).length !== 0 ? data : undefined;
  } catch {
    return undefined;
  }
};

export const getTronAccountNetwork = async (address: string): Promise<NetworkInfo> => {
  const result = await fetch(
    `/wallet/getaccountresource?address=${encodeURIComponent(decode58Check(address))}`,
  );
  const {
    freeNetUsed = 0,
    freeNetLimit = 0,
    NetUsed = 0,
    NetLimit = 0,
    EnergyUsed = 0,
    EnergyLimit = 0,
  } = result;
  return {
    family: "tron",
    freeNetUsed: new BigNumber(freeNetUsed),
    freeNetLimit: new BigNumber(freeNetLimit),
    netUsed: new BigNumber(NetUsed),
    netLimit: new BigNumber(NetLimit),
    energyUsed: new BigNumber(EnergyUsed),
    energyLimit: new BigNumber(EnergyLimit),
  };
};

export const validateAddress = async (address: string): Promise<boolean> => {
  try {
    const result = await post(`/wallet/validateaddress`, {
      address: decode58Check(address),
    });
    return result.result || false;
  } catch (e: any) {
    // FIXME we should not silent errors!
    log("tron-error", "validateAddress fails with " + e.message, {
      address,
    });
    return false;
  }
};

// cache for account names (name is unchanged over time)
export const accountNamesCache = makeLRUCache(
  async (addr: string): Promise<string | null | undefined> => getAccountName(addr),
  (addr: string) => addr,
  hours(3, 300),
);

export const getAccountName = async (addr: string): Promise<string | null | undefined> => {
  const tronAcc = await fetchTronAccount(addr);
  const acc = tronAcc[0];
  const accountName: string | null | undefined =
    acc && acc.account_name ? hexToAscii(acc.account_name) : undefined;
  accountNamesCache.hydrate(addr, accountName); // put it in cache

  return accountName;
};

const superRepresentativesCache = makeLRUCache(
  async (): Promise<SuperRepresentative[]> => {
    const superRepresentatives = await fetchSuperRepresentatives();
    log(
      "tron/superRepresentatives",
      "loaded " + superRepresentatives.length + " super representatives",
    );
    return superRepresentatives;
  },
  () => "",
  hours(1, 300),
);

export const getTronSuperRepresentatives = async (): Promise<SuperRepresentative[]> => {
  return await superRepresentativesCache();
};

export const hydrateSuperRepresentatives = (list: SuperRepresentative[]) => {
  log("tron/superRepresentatives", "hydrate " + list.length + " super representatives");
  superRepresentativesCache.hydrate("", list);
};

const fetchSuperRepresentatives = async (): Promise<SuperRepresentative[]> => {
  const result = await fetch<{ witnesses: SuperRepresentative[] }>(`/wallet/listwitnesses`);
  const sorted = result.witnesses.sort((a, b) => b.voteCount - a.voteCount);
  const superRepresentatives = sorted.map(w => ({
    ...w,
    address: encode58Check(w.address),
    voteCount: w.voteCount || 0,
    isJobs: w.isJobs || false,
  }));
  hydrateSuperRepresentatives(superRepresentatives); // put it in cache

  return superRepresentatives;
};

export const getNextVotingDate = async (): Promise<Date> => {
  const { num } = await fetch(`/wallet/getnextmaintenancetime`);
  return new Date(num);
};

export const getTronSuperRepresentativeData = async (
  max: number | null | undefined,
): Promise<SuperRepresentativeData> => {
  const list = await getTronSuperRepresentatives();
  const nextVotingDate = await getNextVotingDate();
  return {
    list: max ? take(list, max) : list,
    totalVotes: sumBy(list, "voteCount"),
    nextVotingDate,
  };
};

export const voteTronSuperRepresentatives = async (
  account: Account,
  transaction: Transaction,
): Promise<SendTransactionDataSuccess> => {
  const payload = {
    owner_address: decode58Check(account.freshAddress),
    votes: transaction.votes.map(v => ({
      vote_address: decode58Check(v.address),
      vote_count: v.voteCount,
    })),
  };
  return await post(`/wallet/votewitnessaccount`, payload);
};

export const getUnwithdrawnReward = async (addr: string): Promise<BigNumber> => {
  try {
    const { reward = 0 } = await fetch(
      `/wallet/getReward?address=${encodeURIComponent(decode58Check(addr))}`,
    );
    return new BigNumber(reward);
  } catch {
    return Promise.resolve(new BigNumber(0));
  }
};

export const claimRewardTronTransaction = async (
  account: Account,
): Promise<SendTransactionDataSuccess> => {
  const url = `/wallet/withdrawbalance`;
  const data = {
    owner_address: decode58Check(account.freshAddress),
  };
  const result = await post(url, data);
  return result;
};
