import { formatCurrencyUnit } from "@ledgerhq/coin-module-framework/currencies/index";
import {
  AmountRequired,
  FeeNotLoaded,
  FeeTooHigh,
  InvalidAddress,
  NotEnoughBalance,
  NotEnoughBalanceBecauseDestinationNotCreated,
  NotEnoughBalanceInParentAccount,
  NotEnoughSpendableBalance,
  RecipientRequired,
} from "@ledgerhq/ledger-wallet-framework/errors";
import { AccountBridge } from "@ledgerhq/types-live";
import BigNumber from "bignumber.js";
import { isRecipientValid } from "../common-logic/utils";
import coinConfig from "../config";
import { CANTON_UNSET_RECIPIENT } from "../constants";
import { isTopologyChangeRequiredCached } from "../network/gateway";
import {
  Transaction,
  TransactionStatus,
  CantonAccount,
  TooManyUtxosCritical,
  TooManyUtxosWarning,
} from "../types";
import { TopologyChangeError } from "../types/errors";

export const TO_MANY_UTXOS_CRITICAL_COUNT = 24;
export const TO_MANY_UTXOS_WARNING_COUNT = 10;

export const getTransactionStatus: AccountBridge<
  Transaction,
  CantonAccount,
  TransactionStatus
>["getTransactionStatus"] = async (account, transaction) => {
  const errors: Record<string, Error> = {};
  const warnings: Record<string, Error> = {};

  // reserveAmount is the minimum amount of currency that an account must hold in order to stay activated
  const reserveAmount = new BigNumber(
    coinConfig.getCoinConfig(account.currency.id).minReserve || 0,
  );
  const estimatedFees = new BigNumber(transaction.fee || 0);
  const amount = new BigNumber(transaction.amount);

  const isTokenTransaction = !!transaction.subAccountId;
  // For token transactions, fees are paid from parent account, so totalSpent is just amount
  const totalSpent = isTokenTransaction ? amount : amount.plus(estimatedFees);

  // Get the account balance to check against (subAccount for tokens, main account for native)
  let accountBalance = account.spendableBalance;
  if (isTokenTransaction && account.subAccounts) {
    const subAccount = account.subAccounts.find(
      sub => sub.type === "TokenAccount" && sub.id === transaction.subAccountId,
    );
    accountBalance = subAccount?.spendableBalance ?? new BigNumber(0);
  }

  if (amount.gt(0) && estimatedFees.times(10).gt(amount) && !isTokenTransaction) {
    // if the fee is more than 10 times the amount, we warn the user that fee is high compared to what he is sending
    warnings.feeTooHigh = new FeeTooHigh();
  }

  if (transaction.fee === null || transaction.fee === undefined) {
    // if the fee is not loaded, we can't do much
    errors.fee = new FeeNotLoaded();
  }

  if (!transaction.recipient) {
    errors.recipient = new RecipientRequired("");
  } else if (!isRecipientValid(transaction.recipient)) {
    // We want to prevent user from sending to an invalid address
    errors.recipient = new InvalidAddress("", {
      currencyName: account.currency.name,
    });
  }

  if (amount.eq(0)) {
    // if the amount is 0, we prevent the user from sending the tx (even if it's technically feasible)
    errors.amount = new AmountRequired();
  }

  // For token transactions, check that parent account has enough native balance to pay fees
  if (isTokenTransaction && estimatedFees.gt(account.balance)) {
    errors.amount = new NotEnoughBalanceInParentAccount();
  }

  // Check if total spent exceeds available balance
  if (!errors.amount && totalSpent.gt(accountBalance)) {
    errors.amount = new NotEnoughBalance();
  }

  // For native coin transactions, check reserve amount constraints
  if (!isTokenTransaction && !errors.amount) {
    if (totalSpent.gt(account.balance.minus(reserveAmount))) {
      errors.amount = new NotEnoughSpendableBalance("", {
        minimumAmount: formatCurrencyUnit(account.currency.units[0], reserveAmount, {
          disableRounding: true,
          useGrouping: false,
          showCode: true,
        }),
      });
    } else if (transaction.recipient && amount.lt(reserveAmount)) {
      // if we send an amount lower than reserve amount AND target account is new, we need to warn the user that the target account will not be activated
      errors.amount = new NotEnoughBalanceBecauseDestinationNotCreated("", {
        minimalAmount: formatCurrencyUnit(account.currency.units[0], reserveAmount, {
          disableRounding: true,
          useGrouping: false,
          showCode: true,
        }),
      });
    }
  }

  const utxoWarning = validateUtxoCount(account, transaction);
  if (utxoWarning) {
    warnings.tooManyUtxos = utxoWarning;
  }

  const topologyError = await validateTopology(account);
  if (topologyError) {
    errors.topologyChange = topologyError;
  }

  return {
    errors,
    warnings,
    estimatedFees,
    amount,
    totalSpent,
  };
};

function validateUtxoCount(account: CantonAccount, transaction: Transaction): Error | null {
  if (!account.cantonResources?.instrumentUtxoCounts) {
    return null;
  }

  if (!transaction.recipient) {
    return null;
  }

  if (!isRecipientValid(transaction.recipient) || account.xpub === transaction.recipient) {
    return null;
  }

  if (transaction.recipient.includes(CANTON_UNSET_RECIPIENT)) {
    return null;
  }

  const { instrumentUtxoCounts } = account.cantonResources;
  const instrumentKey = transaction.instrumentAdmin
    ? `${transaction.tokenId}-${transaction.instrumentAdmin}`
    : transaction.tokenId;
  const instrumentUtxoCount = instrumentUtxoCounts[instrumentKey] || 0;

  if (instrumentUtxoCount > TO_MANY_UTXOS_CRITICAL_COUNT) {
    return new TooManyUtxosCritical();
  }

  if (instrumentUtxoCount > TO_MANY_UTXOS_WARNING_COUNT) {
    return new TooManyUtxosWarning("families.canton.tooManyUtxos.warning");
  }

  return null;
}

export async function validateTopology(account: CantonAccount): Promise<Error | null> {
  const publicKey = account.cantonResources?.publicKey;
  if (!publicKey) {
    return null;
  }

  try {
    const isTopologyChangeRequired = await isTopologyChangeRequiredCached(
      account.currency,
      publicKey,
    );

    if (!isTopologyChangeRequired) {
      return null;
    }

    return new TopologyChangeError("Topology change detected. Re-onboarding required.");
  } catch {
    // If topology check fails, don't block the transaction
    return null;
  }
}
