text: // @flow
  import { getAbandonSeedAddress } from "@ledgerhq/cryptoassets";
  import { scanAccounts } from "../../../libcore/scanAccounts";
  import { sync } from "../../../libcore/syncAccount";
  import type {
    AccountBridge,
    CurrencyBridge,
    CryptoCurrency,
  } from "../../../types";
  import type { Transaction } from "../types";
  import invariant from "invariant";
  import { BigNumber } from "bignumber.js";
  import broadcast from "../libcore-broadcast";
  import signOperation from "../libcore-signOperation";
  import { getMainAccount } from "../../../account";
  import { validateRecipient } from "../../../bridge/shared";
  
  import {
    NotEnoughBalance,
    InvalidAddressBecauseDestinationIsAlsoSource,
    InvalidAddress,
    AmountRequired,
    RecommendUndelegation,
    FeeNotLoaded,
  } from "@ledgerhq/errors";
  import {
    CosmosRedelegationInProgress,
    ClaimRewardsFeesWarning,
    CosmosDelegateAllFundsWarning,
    CosmosTooManyValidators,
    NotEnoughDelegationBalance,
  } from "../../../errors";
  import {
    setCosmosPreloadData,
    asSafeCosmosPreloadData,
  } from "../preloadedData";
  import { getValidators, hydrateValidators } from "../validators";
  import {
    calculateFees,
    getMaxEstimatedBalance,
    COSMOS_MAX_REDELEGATIONS,
    COSMOS_MAX_UNBONDINGS,
    COSMOS_MAX_DELEGATIONS,
  } from "../logic";
  import { makeAccountBridgeReceive } from "../../../bridge/jsHelpers";
  
  const receive = makeAccountBridgeReceive();
  
  const createTransaction = () => ({
    family: "cosmos",
    mode: "send",
    amount: BigNumber(0),
    fees: null,
    gas: null,
    recipient: "",
    useAllAmount: false,
    networkInfo: null,
    memo: null,
    cosmosSourceValidator: null,
    validators: [],
  });
  
  const updateTransaction = (t, patch) => {
    if ("mode" in patch && patch.mode !== t.mode) {
      return { ...t, ...patch, gas: null, fees: null };
    }
    if (
      "validators" in patch &&
      patch.validators.length !== t.validators.length
    ) {
      return { ...t, ...patch, gas: null, fees: null };
    }
    return { ...t, ...patch };
  };
  
  const isDelegable = (a, address, amount) => {
    const { cosmosResources } = a;
    invariant(cosmosResources, "cosmosResources should exist");
    if (
      cosmosResources.delegations.some(
        (delegation) =>
          delegation.validatorAddress === address && delegation.amount.lt(amount)
      )
    ) {
      return new NotEnoughDelegationBalance();
    }
  
    return null;
  };
  
  const redelegationStatusError = (a, t) => {
    if (a.cosmosResources) {
      const redelegations = a.cosmosResources.redelegations;
  
      invariant(
        redelegations.length < COSMOS_MAX_REDELEGATIONS,
        "redelegation should not have more than 6 entries"
      );
  
      if (
        redelegations.some((redelegation) => {
          let dstValidator = redelegation.validatorDstAddress;
          return (
            dstValidator === t.cosmosSourceValidator &&
            redelegation.completionDate > new Date()
          );
        })
      )
        return new CosmosRedelegationInProgress();
  
      if (t.cosmosSourceValidator === t.validators[0].address)
        return new InvalidAddressBecauseDestinationIsAlsoSource();
    }
  
    return isDelegable(a, t.cosmosSourceValidator, t.validators[0].amount);
  };
  
  const getSendTransactionStatus = async (a, t) => {
    const errors = {};
    const warnings = {};
  
    if (a.freshAddress === t.recipient) {
      errors.recipient = new InvalidAddressBecauseDestinationIsAlsoSource();
    } else {
      const { recipientError, recipientWarning } = await validateRecipient(
        a.currency,
        t.recipient
      );
  
      if (recipientError) {
        errors.recipient = recipientError;
      }
  
      if (recipientWarning) {
        warnings.recipient = recipientWarning;
      }
    }
  
    let amount = t.amount;
  
    if (amount.lte(0) && !t.useAllAmount) {
      errors.amount = new AmountRequired();
    }
  
    let estimatedFees = t.fees || BigNumber(0);
  
    if (!t.fees || !t.fees.gt(0)) {
      errors.fees = new FeeNotLoaded();
    }
  
    amount = t.useAllAmount ? getMaxEstimatedBalance(a, estimatedFees) : amount;
  
    let totalSpent = amount.plus(estimatedFees);
  
    if (
      (amount.lte(0) && t.useAllAmount) || // if use all Amount sets an amount at 0
      (!errors.recipient && !errors.amount && totalSpent.gt(a.spendableBalance)) // if spendable balance lower than total
    ) {
      errors.amount = new NotEnoughBalance();
    }
  
    if (
      a.cosmosResources &&
      a.cosmosResources.delegations.length > 0 &&
      t.useAllAmount
    ) {
      warnings.amount = new RecommendUndelegation();
    }
  
    return Promise.resolve({
      errors,
      warnings,
      estimatedFees,
      amount,
      totalSpent,
    });
  };
  
  const getDelegateTransactionStatus = async (a, t) => {
    const errors = {};
    const warnings = {};
  
    if (
      t.validators.some(
        (v) => !v.address || !v.address.includes("cosmosvaloper")
      ) ||
      t.validators.length === 0
    )
      errors.recipient = new InvalidAddress(null, {
        currencyName: a.currency.name,
      });
  
    if (t.validators.length > COSMOS_MAX_DELEGATIONS) {
      errors.validators = new CosmosTooManyValidators();
    }
  
    let amount = t.validators.reduce(
      (old, current) => old.plus(current.amount),
      BigNumber(0)
    );
  
    if (amount.eq(0)) {
      errors.amount = new AmountRequired();
    }
  
    let estimatedFees = t.fees || BigNumber(0);
  
    if (!t.fees) {
      errors.fees = new FeeNotLoaded();
    }
  
    let totalSpent = amount.plus(estimatedFees);
  
    if (totalSpent.eq(a.spendableBalance)) {
      warnings.delegate = new CosmosDelegateAllFundsWarning();
    }
  
    if (
      !errors.recipient &&
      !errors.amount &&
      (amount.lt(0) || totalSpent.gt(a.spendableBalance))
    ) {
      errors.amount = new NotEnoughBalance();
      amount = BigNumber(0);
      totalSpent = BigNumber(0);
    }
  
    return Promise.resolve({
      errors,
      warnings,
      estimatedFees,
      amount,
      totalSpent,
    });
  };
  
  const getTransactionStatus = async (a, t) => {
    if (t.mode === "send") {
      // We isolate the send transaction that it's a little bit different from the rest
      return await getSendTransactionStatus(a, t);
    } else if (t.mode === "delegate") {
      return await getDelegateTransactionStatus(a, t);
    }
  
    const errors = {};
    const warnings = {};
  
    // here we only treat about all other mode than delegate and send
    if (
      t.validators.some(
        (v) => !v.address || !v.address.includes("cosmosvaloper")
      ) ||
      t.validators.length === 0
    )
      errors.recipient = new InvalidAddress(null, {
        currencyName: a.currency.name,
      });
    if (t.mode === "redelegate") {
      const redelegationError = redelegationStatusError(a, t);
      if (redelegationError) {
        // Note : note sure if I have to put this error on this field
        errors.redelegation = redelegationError;
      }
    } else if (t.mode === "undelegate") {
      invariant(
        a.cosmosResources &&
          a.cosmosResources.unbondings.length < COSMOS_MAX_UNBONDINGS,
        "unbondings should not have more than 6 entries"
      );
      if (t.validators.length === 0)
        errors.recipient = new InvalidAddress(null, {
          currencyName: a.currency.name,
        });
      const [first] = t.validators;
      const unbondingError = first && isDelegable(a, first.address, first.amount);
      if (unbondingError) {
        errors.unbonding = unbondingError;
      }
    }
  
    let validatorAmount = t.validators.reduce(
      (old, current) => old.plus(current.amount),
      BigNumber(0)
    );
  
    if (t.mode !== "claimReward" && validatorAmount.lte(0)) {
      errors.amount = new AmountRequired();
    }
  
    let estimatedFees = t.fees || BigNumber(0);
  
    if (!t.fees) {
      errors.fees = new FeeNotLoaded();
    }
  
    let totalSpent = estimatedFees;
  
    if (["claimReward", "claimRewardCompound"].includes(t.mode)) {
      const { cosmosResources } = a;
      invariant(cosmosResources, "cosmosResources should exist");
      const claimReward = t.validators.length
        ? cosmosResources.delegations.find(
            (delegation) =>
              delegation.validatorAddress === t.validators[0].address
          )
        : null;
      if (claimReward && estimatedFees.gt(claimReward.pendingRewards)) {
        warnings.claimReward = new ClaimRewardsFeesWarning();
      }
    }
  
    if (
      !errors.recipient &&
      !errors.amount &&
      (validatorAmount.lt(0) || totalSpent.gt(a.spendableBalance))
    ) {
      errors.amount = new NotEnoughBalance();
      totalSpent = BigNumber(0);
    }
  
    return Promise.resolve({
      errors,
      warnings,
      estimatedFees,
      amount: BigNumber(0),
      totalSpent,
    });
  };
  
  const isTransactionValidForEstimatedFees = async (a, t) => {
    let errors = null;
    if (t.mode === "send" && (t.amount.gt(0) || t.useAllAmount)) {
      errors = (await validateRecipient(a.currency, t.recipient)).recipientError;
    } else {
      errors =
        t.validators.some(
          (v) => !v.address || !v.address.includes("cosmosvaloper")
        ) ||
        (t.mode !== "claimReward" &&
          t.validators
            .reduce((old, current) => old.plus(current.amount), BigNumber(0))
            .eq(0));
    }
  
    return errors;
  };
  
  const sameFees = (a, b) => (!a || !b ? a === b : a.eq(b));
  
  const prepareTransaction = async (a, t) => {
    let memo = t.memo;
    let fees = t.fees;
    let gas = t.gas;
  
    if (t.recipient || t.mode !== "send") {
      const errors = await isTransactionValidForEstimatedFees(a, t);
      if (!errors) {
        let amount;
        if (t.useAllAmount) {
          amount = getMaxEstimatedBalance(a, BigNumber(0));
        }
  
        if ((amount && amount.gt(0)) || !amount) {
          const res = await calculateFees({
            a,
            t: { ...t, amount: amount || t.amount },
          });
  
          fees = res.estimatedFees;
          gas = res.estimatedGas;
        }
      }
    }
  
    if (t.mode !== "send" && !memo) {
      memo = "Ledger Live";
    }
  
    if (t.memo !== memo || !sameFees(t.fees, fees)) {
      return { ...t, memo, fees, gas };
    }
  
    return t;
  };
  
  const currencyBridge: CurrencyBridge = {
    preload: async (currency: CryptoCurrency) => {
      const validators = await getValidators(currency);
      setCosmosPreloadData({ validators });
      return Promise.resolve({ validators });
    },
    hydrate: (data: mixed) => {
      if (!data || typeof data !== "object") return;
      const { validators } = data;
      if (
        !validators ||
        typeof validators !== "object" ||
        !Array.isArray(validators)
      )
        return;
      hydrateValidators(validators);
      setCosmosPreloadData(asSafeCosmosPreloadData(data));
    },
    scanAccounts,
  };
  
  const estimateMaxSpendable = async ({
    account,
    parentAccount,
    transaction,
  }) => {
    const mainAccount = getMainAccount(account, parentAccount);
    const t = await prepareTransaction(mainAccount, {
      ...createTransaction(),
      ...transaction,
      recipient:
        transaction?.recipient || getAbandonSeedAddress(mainAccount.currency.id),
      useAllAmount: true,
    });
    const s = await getTransactionStatus(mainAccount, t);
    return s.amount;
  };
  
  const accountBridge: AccountBridge<Transaction> = {
    createTransaction,
    updateTransaction,
    prepareTransaction,
    getTransactionStatus,
    estimateMaxSpendable,
    sync,
    receive,
    signOperation,
    broadcast,
  };
  
  export default { currencyBridge, accountBridge };
  