import { AccountId, TransactionId } from "@hashgraph/sdk";
import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state";
import { getEnv } from "@ledgerhq/live-env";
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
import type { Operation, OperationType } from "@ledgerhq/types-live";
import BigNumber from "bignumber.js";
import type { HederaConfig } from "../config";
import { SUPPORTED_ERC20_TOKENS } from "../constants";
import { nanosToSeconds, toEntityId, toTimestamp } from "../logic/utils";
import type {
  HederaMirrorTokenTransfer,
  HederaMirrorCoinTransfer,
  HederaThirdwebTransaction,
  HederaThirdwebDecodedTransferParams,
  OperationERC20,
  HederaERC20TokenBalance,
  ERC20TokenTransfer,
  EnrichedERC20Transfer,
} from "../types";
import { apiClient } from "./api";
import { hgraphClient } from "./hgraph";

export async function createTransactionId(
  accountId: string,
  config: HederaConfig,
): Promise<TransactionId> {
  if (!config.useNetworkTimestamp) {
    return TransactionId.generate(accountId);
  }

  try {
    const lastBlock = await apiClient.getLatestBlock();
    const validStart = toTimestamp(lastBlock.timestamp.to ?? lastBlock.timestamp.from);

    return TransactionId.withValidStart(AccountId.fromString(accountId), validStart);
  } catch {
    return TransactionId.generate(accountId);
  }
}

function isValidRecipient(accountId: AccountId, recipients: string[]): boolean {
  if (accountId.shard.eq(0) && accountId.realm.eq(0)) {
    // account is a node, only add to list if we have none
    if (accountId.num.lt(100)) {
      return recipients.length === 0;
    }

    // account is a system account that is not a node, do NOT add
    if (accountId.num.lt(1000)) {
      return false;
    }
  }

  return true;
}

export function parseTransfers(
  mirrorTransfers: (HederaMirrorCoinTransfer | HederaMirrorTokenTransfer)[],
  address: string,
  stakingReward = new BigNumber(0),
): Pick<Operation, "type" | "value" | "senders" | "recipients"> {
  let value = new BigNumber(0);
  let type: OperationType = "NONE";

  const senders: string[] = [];
  const recipients: string[] = [];
  const rewardPayerAddress = getEnv("HEDERA_STAKING_REWARD_ACCOUNT_ID");

  for (const transfer of mirrorTransfers) {
    const amount = new BigNumber(transfer.amount);
    const accountId = AccountId.fromString(transfer.account);

    // staking reward is included in transfer, so it can be positive even if user sent less HBARs than the reward is
    const amountWithoutReward = transfer.account === address ? amount.minus(stakingReward) : amount;

    if (transfer.account === address) {
      value = amountWithoutReward.abs();
      type = amountWithoutReward.isNegative() ? "OUT" : "IN";
    }

    if (amountWithoutReward.isNegative()) {
      // exclude reward payer from senders list, because rewards are shown as separate operations
      const shouldIgnoreAddress = transfer.account === rewardPayerAddress && stakingReward.gt(0);

      if (shouldIgnoreAddress) {
        continue;
      }

      senders.push(transfer.account);
    } else if (isValidRecipient(accountId, recipients)) {
      recipients.push(transfer.account);
    }
  }

  // NOTE: earlier addresses are the "fee" addresses
  senders.reverse();
  recipients.reverse();

  return {
    type,
    value,
    senders,
    recipients,
  };
}

// TODO: remove once migration to new API is complete
export async function getERC20BalancesForAccount(
  evmAccountId: string,
  supportedTokenIds = SUPPORTED_ERC20_TOKENS.map(token => token.id),
): Promise<HederaERC20TokenBalance[]> {
  const availableTokens: TokenCurrency[] = [];

  for (const erc20TokenId of supportedTokenIds) {
    const calToken = await getCryptoAssetsStore().findTokenById(erc20TokenId);

    if (calToken) {
      availableTokens.push(calToken);
    }
  }

  const promises = availableTokens.map(async erc20token => {
    const balance = await apiClient.getERC20Balance(evmAccountId, erc20token.contractAddress);

    return {
      balance,
      token: erc20token,
    };
  });

  const balances = await Promise.all(promises);

  return balances;
}

export async function getERC20BalancesForAccountV2(
  address: string,
): Promise<HederaERC20TokenBalance[]> {
  const balances: HederaERC20TokenBalance[] = [];

  const rawBalances = await hgraphClient.getERC20Balances({ address });

  for (const rawBalance of rawBalances) {
    const rawBalanceTokenId = toEntityId({ num: rawBalance.token_id });

    const supportedToken = SUPPORTED_ERC20_TOKENS.find(token => {
      return token.tokenId === rawBalanceTokenId;
    });

    if (!supportedToken) {
      continue;
    }

    const calToken = await getCryptoAssetsStore().findTokenById(supportedToken.id);

    if (!calToken) {
      continue;
    }

    balances.push({
      token: calToken,
      balance: new BigNumber(rawBalance.balance),
    });
  }

  return balances;
}

// TODO: remove once migration to new API is complete
export const getERC20Operations = async (
  latestERC20Transactions: HederaThirdwebTransaction[],
): Promise<OperationERC20[]> => {
  const latestERC20Operations: OperationERC20[] = [];

  for (const thirdwebTransaction of latestERC20Transactions) {
    const tokenId = thirdwebTransaction.address;
    const token = await getCryptoAssetsStore().findTokenByAddressInCurrency(tokenId, "hedera");

    if (!token) continue;

    const hash = thirdwebTransaction.transactionHash;
    const contractCallResult = await apiClient.getContractCallResult(hash);
    const mirrorTransaction = await apiClient.findTransactionByContractCall(
      contractCallResult.timestamp,
      contractCallResult.contract_id,
    );

    if (!mirrorTransaction) continue;

    latestERC20Operations.push({
      thirdwebTransaction,
      mirrorTransaction,
      contractCallResult,
      token,
    });
  }

  return latestERC20Operations;
};

// TODO: remove once migration to new API is complete
export function parseThirdwebTransactionParams(
  transaction: HederaThirdwebTransaction,
): HederaThirdwebDecodedTransferParams | null {
  const { from, to, value } = transaction.decoded.params;

  if (typeof from !== "string" || typeof to !== "string" || typeof value !== "string") {
    return null;
  }

  return { from, to, value };
}

/**
 * Enriches raw ERC20 transfers from Hgraph with additional data needed for operations:
 * - fetches contract call result containing gas metrics and block hash
 * - finds the corresponding Mirror Node transaction by consensus timestamp
 *
 * @param erc20Transfers - Raw ERC20 transfers from Hgraph API
 * @returns Array of enriched transfers with complete operation data, filtered to supported tokens only
 */
export const enrichERC20Transfers = async (erc20Transfers: ERC20TokenTransfer[]) => {
  const enrichedTransfers: EnrichedERC20Transfer[] = [];

  // with hgraph we can get two different transfers with the same transaction hash
  const groupedByTxHash = new Map<string, [ERC20TokenTransfer, ...ERC20TokenTransfer[]]>();
  for (const transfer of erc20Transfers) {
    const group = groupedByTxHash.get(transfer.transaction_hash);

    if (!group) {
      groupedByTxHash.set(transfer.transaction_hash, [transfer]);
      continue;
    }

    group.push(transfer);
  }

  for (const [txHash, transfers] of groupedByTxHash.entries()) {
    const payerAddress = toEntityId({ num: transfers[0].payer_account_id });
    const inaccurateConsensusTimestampNs = new BigNumber(transfers[0].consensus_timestamp);
    const inaccurateConsensusTimestamp = nanosToSeconds(inaccurateConsensusTimestampNs).toFixed(9);

    const [contractCallResult, mirrorTransaction] = await Promise.all([
      apiClient.getContractCallResult(txHash),
      apiClient.findTransactionByContractCallV2({
        payerAddress,
        timestamp: inaccurateConsensusTimestamp,
      }),
    ]);

    if (!mirrorTransaction) {
      continue;
    }

    enrichedTransfers.push({
      transfers,
      contractCallResult,
      mirrorTransaction,
    });
  }

  return enrichedTransfers;
};
