import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter/ledger_flutter.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_validator.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:collection/collection.dart';

part 'send_view_model.g.dart';

class SendViewModel = SendViewModelBase with _$SendViewModel;

abstract class SendViewModelBase extends WalletChangeListenerViewModel with Store {
  @override
  void onWalletChange(wallet) {
    currencies = wallet.balance.keys.toList();
    selectedCryptoCurrency = wallet.currency;
    hasMultipleTokens = isEVMCompatibleChain(wallet.type) ||
        wallet.type == WalletType.solana ||
        wallet.type == WalletType.tron;
  }

  SendViewModelBase(
    AppStore appStore,
    this.sendTemplateViewModel,
    this._fiatConversationStore,
    this.balanceViewModel,
    this.contactListViewModel,
    this.transactionDescriptionBox,
    this.ledgerViewModel,
  )   : state = InitialExecutionState(),
        currencies = appStore.wallet!.balance.keys.toList(),
        selectedCryptoCurrency = appStore.wallet!.currency,
        hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) ||
            appStore.wallet!.type == WalletType.solana ||
            appStore.wallet!.type == WalletType.tron,
        outputs = ObservableList<Output>(),
        _settingsStore = appStore.settingsStore,
        fiatFromSettings = appStore.settingsStore.fiatCurrency,
        super(appStore: appStore) {
    if (wallet.type == WalletType.bitcoin &&
        _settingsStore.priority[wallet.type] == bitcoinTransactionPriorityCustom) {
      setTransactionPriority(bitcoinTransactionPriorityMedium);
    }
    final priority = _settingsStore.priority[wallet.type];
    final priorities = priorityForWalletType(wallet.type);
    if (!priorityForWalletType(wallet.type).contains(priority) && priorities.isNotEmpty) {
      _settingsStore.priority[wallet.type] = priorities.first;
    }

    outputs
        .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
  }

  @observable
  ExecutionState state;

  ObservableList<Output> outputs;

  @action
  void addOutput() {
    outputs
        .add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
  }

  @action
  void removeOutput(Output output) {
    if (isBatchSending) {
      outputs.remove(output);
    }
  }

  @action
  void clearOutputs() {
    outputs.clear();
    addOutput();
  }

  @computed
  bool get isBatchSending => outputs.length > 1;

  bool get shouldDisplaySendALL => walletType != WalletType.solana || walletType != WalletType.tron;

  @computed
  String get pendingTransactionFiatAmount {
    if (pendingTransaction == null) {
      return '0.00';
    }

    try {
      final fiat = calculateFiatAmount(
          price: _fiatConversationStore.prices[selectedCryptoCurrency]!,
          cryptoAmount: pendingTransaction!.amountFormatted);
      return fiat;
    } catch (_) {
      return '0.00';
    }
  }

  @computed
  String get pendingTransactionFeeFiatAmount {
    try {
      if (pendingTransaction != null) {
        final currency = pendingTransactionFeeCurrency(walletType);
        final fiat = calculateFiatAmount(
            price: _fiatConversationStore.prices[currency]!,
            cryptoAmount: pendingTransaction!.feeFormatted);
        return fiat;
      } else {
        return '0.00';
      }
    } catch (_) {
      return '0.00';
    }
  }

  CryptoCurrency pendingTransactionFeeCurrency(WalletType type) {
    switch (type) {
      case WalletType.ethereum:
      case WalletType.polygon:
      case WalletType.tron:
      case WalletType.solana:
        return wallet.currency;
      default:
        return selectedCryptoCurrency;
    }
  }

  FiatCurrency get fiat => _settingsStore.fiatCurrency;

  TransactionPriority get transactionPriority {
    final priority = _settingsStore.priority[wallet.type];

    if (priority == null) {
      throw Exception('Unexpected type ${wallet.type}');
    }

    return priority;
  }

  int? getCustomPriorityIndex(List<TransactionPriority> priorities) {
    if (wallet.type == WalletType.bitcoin) {
      final customItem = priorities
          .firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom());

      return customItem != null ? priorities.indexOf(customItem) : null;
    }
    return null;
  }

  int? get maxCustomFeeRate {
    if (wallet.type == WalletType.bitcoin) {
      return bitcoin!.getMaxCustomFeeRate(wallet);
    }
    return null;
  }

  @computed
  int get customBitcoinFeeRate => _settingsStore.customBitcoinFeeRate;

  void set customBitcoinFeeRate(int value) => _settingsStore.customBitcoinFeeRate = value;

  CryptoCurrency get currency => wallet.currency;

  Validator<String> get amountValidator =>
      AmountValidator(currency: walletTypeToCryptoCurrency(wallet.type));

  Validator<String> get allAmountValidator => AllAmountValidator();

  Validator<String> get addressValidator => AddressValidator(type: selectedCryptoCurrency);

  Validator<String> get textValidator => TextValidator();

  final FiatCurrency fiatFromSettings;

  @observable
  PendingTransaction? pendingTransaction;

  @computed
  String get balance => wallet.balance[selectedCryptoCurrency]!.formattedAvailableBalance;

  @computed
  bool get isFiatDisabled => balanceViewModel.isFiatDisabled;

  @computed
  String get pendingTransactionFiatAmountFormatted =>
      isFiatDisabled ? '' : pendingTransactionFiatAmount + ' ' + fiat.title;

  @computed
  String get pendingTransactionFeeFiatAmountFormatted =>
      isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title;

  @computed
  bool get isReadyForSend =>
      wallet.syncStatus is SyncedSyncStatus ||
      // If silent payments scanning, can still send payments
      (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus);

  @computed
  List<Template> get templates => sendTemplateViewModel.templates
      .where((template) => _isEqualCurrency(template.cryptoCurrency))
      .toList();

  @computed
  bool get hasCoinControl =>
      wallet.type == WalletType.bitcoin ||
      wallet.type == WalletType.litecoin ||
      wallet.type == WalletType.monero ||
      wallet.type == WalletType.bitcoinCash;

  @computed
  bool get isElectrumWallet =>
      wallet.type == WalletType.bitcoin ||
      wallet.type == WalletType.litecoin ||
      wallet.type == WalletType.bitcoinCash;

  @computed
  bool get hasFees => wallet.type != WalletType.nano && wallet.type != WalletType.banano;

  @computed
  bool get hasFeesPriority =>
      wallet.type != WalletType.nano &&
      wallet.type != WalletType.banano &&
      wallet.type != WalletType.solana &&
      wallet.type != WalletType.tron;

  @observable
  CryptoCurrency selectedCryptoCurrency;

  List<CryptoCurrency> currencies;

  bool get hasYat => outputs
      .any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord);

  WalletType get walletType => wallet.type;

  String? get walletCurrencyName => wallet.currency.fullName?.toLowerCase() ?? wallet.currency.name;

  bool get hasCurrecyChanger => walletType == WalletType.haven;

  @computed
  FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency;

  final SettingsStore _settingsStore;
  final SendTemplateViewModel sendTemplateViewModel;
  final BalanceViewModel balanceViewModel;
  final ContactListViewModel contactListViewModel;
  final LedgerViewModel? ledgerViewModel;
  final FiatConversionStore _fiatConversationStore;
  final Box<TransactionDescription> transactionDescriptionBox;

  @observable
  bool hasMultipleTokens;

  @computed
  List<ContactRecord> get contactsToShow => contactListViewModel.contacts
      .where((element) => element.type == selectedCryptoCurrency)
      .toList();

  @computed
  List<WalletContact> get walletContactsToShow => contactListViewModel.walletContacts
      .where((element) => element.type == selectedCryptoCurrency)
      .toList();

  @action
  bool checkIfAddressIsAContact(String address) {
    final contactList = contactsToShow.where((element) => element.address == address).toList();

    return contactList.isNotEmpty;
  }

  @action
  bool checkIfWalletIsAnInternalWallet(String address) {
    final walletContactList =
        walletContactsToShow.where((element) => element.address == address).toList();

    return walletContactList.isNotEmpty;
  }

  @computed
  bool get shouldDisplayTOTP2FAForContact => _settingsStore.shouldRequireTOTP2FAForSendsToContact;

  @computed
  bool get shouldDisplayTOTP2FAForNonContact =>
      _settingsStore.shouldRequireTOTP2FAForSendsToNonContact;

  @computed
  bool get shouldDisplayTOTP2FAForSendsToInternalWallet =>
      _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;

  //* Still open to further optimize these checks
  //* It works but can be made better
  @action
  bool checkThroughChecksToDisplayTOTP(String address) {
    final isContact = checkIfAddressIsAContact(address);
    final isInternalWallet = checkIfWalletIsAnInternalWallet(address);

    if (isContact) {
      return shouldDisplayTOTP2FAForContact;
    } else if (isInternalWallet) {
      return shouldDisplayTOTP2FAForSendsToInternalWallet;
    } else {
      return shouldDisplayTOTP2FAForNonContact;
    }
  }

  bool shouldDisplayTotp() {
    List<bool> conditionsList = [];

    for (var output in outputs) {
      final show = checkThroughChecksToDisplayTOTP(output.extractedAddress);
      conditionsList.add(show);
    }

    return conditionsList.contains(true);
  }

  @action
  Future<PendingTransaction?> createTransaction({ExchangeProvider? provider}) async {
    try {
      state = IsExecutingState();

      if (wallet.isHardwareWallet) state = IsAwaitingDeviceResponseState();

      pendingTransaction = await wallet.createTransaction(_credentials());

      if (provider is ThorChainExchangeProvider) {
        final outputCount = pendingTransaction?.outputCount ?? 0;
        if (outputCount > 10) {
          throw Exception("THORChain does not support more than 10 outputs");
        }

        if (_hasTaprootInput(pendingTransaction)) {
          throw Exception("THORChain does not support Taproot addresses");
        }
      }
      state = ExecutedSuccessfullyState();
      return pendingTransaction;
    } catch (e) {
      if (e is LedgerException) {
        final errorCode = e.errorCode.toRadixString(16);
        final fallbackMsg =
            e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode";
        final errorMsg = ledgerViewModel!.interpretErrorCode(errorCode) ?? fallbackMsg;

        state = FailureState(errorMsg);
      } else {
        state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
      }
    }
    return null;
  }

  @action
  Future<void> replaceByFee(String txId, String newFee) async {
    state = IsExecutingState();

    final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, txId, newFee);

    if (!isSufficient) {
      state = AwaitingConfirmationState(
          title: S.current.confirm_fee_deduction,
          message: S.current.confirm_fee_deduction_content,
          onConfirm: () async {
            pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee);
            state = ExecutedSuccessfullyState();
          },
          onCancel: () {
            state = FailureState('Insufficient change for fee');
          });
    } else {
      pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee);
      state = ExecutedSuccessfullyState();
    }
  }

  @action
  Future<void> commitTransaction() async {
    if (pendingTransaction == null) {
      throw Exception("Pending transaction doesn't exist. It should not be happened.");
    }

    String address = outputs.fold('', (acc, value) {
      return value.isParsedAddress
          ? '$acc${value.address}\n${value.extractedAddress}\n\n'
          : '$acc${value.address}\n\n';
    });

    address = address.trim();

    String note = outputs.fold('', (acc, value) => '$acc${value.note}\n');

    note = note.trim();

    try {
      state = TransactionCommitting();
      await pendingTransaction!.commit();

      if (walletType == WalletType.nano) {
        nano!.updateTransactions(wallet);
      }

      if (pendingTransaction!.id.isNotEmpty) {
        _settingsStore.shouldSaveRecipientAddress
            ? await transactionDescriptionBox.add(TransactionDescription(
                id: pendingTransaction!.id, recipientAddress: address, transactionNote: note))
            : await transactionDescriptionBox
                .add(TransactionDescription(id: pendingTransaction!.id, transactionNote: note));
      }

      state = TransactionCommitted();
    } catch (e) {
      state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
    }
  }

  @action
  void setTransactionPriority(TransactionPriority priority) =>
      _settingsStore.priority[wallet.type] = priority;

  Object _credentials() {
    final priority = _settingsStore.priority[wallet.type];

    if (priority == null &&
        wallet.type != WalletType.nano &&
        wallet.type != WalletType.banano &&
        wallet.type != WalletType.solana &&
        wallet.type != WalletType.tron) {
      throw Exception('Priority is null for wallet type: ${wallet.type}');
    }

    switch (wallet.type) {
      case WalletType.bitcoin:
      case WalletType.litecoin:
      case WalletType.bitcoinCash:
        return bitcoin!.createBitcoinTransactionCredentials(outputs,
            priority: priority!, feeRate: customBitcoinFeeRate);

      case WalletType.monero:
        return monero!
            .createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!);

      case WalletType.haven:
        return haven!.createHavenTransactionCreationCredentials(
            outputs: outputs, priority: priority!, assetType: selectedCryptoCurrency.title);

      case WalletType.ethereum:
        return ethereum!.createEthereumTransactionCredentials(outputs,
            priority: priority!, currency: selectedCryptoCurrency);
      case WalletType.nano:
        return nano!.createNanoTransactionCredentials(outputs);
      case WalletType.polygon:
        return polygon!.createPolygonTransactionCredentials(outputs,
            priority: priority!, currency: selectedCryptoCurrency);
      case WalletType.solana:
        return solana!
            .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
      case WalletType.tron:
        return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency);
      default:
        throw Exception('Unexpected wallet type: ${wallet.type}');
    }
  }

  String displayFeeRate(dynamic priority, int? customValue) {
    final _priority = priority as TransactionPriority;

    if (walletType == WalletType.bitcoin) {
      final rate = bitcoin!.getFeeRate(wallet, _priority);
      return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, customRate: customValue);
    }

    if (isElectrumWallet) {
      final rate = bitcoin!.getFeeRate(wallet, _priority);
      return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate);
    }

    return priority.toString();
  }

  bool _isEqualCurrency(String currency) =>
      wallet.balance.keys.any((e) => currency.toLowerCase() == e.title.toLowerCase());

  TransactionPriority get bitcoinTransactionPriorityCustom =>
      bitcoin!.getBitcoinTransactionPriorityCustom();

  TransactionPriority get bitcoinTransactionPriorityMedium =>
      bitcoin!.getBitcoinTransactionPriorityMedium();

  @action
  void onClose() => _settingsStore.fiatCurrency = fiatFromSettings;

  @action
  void setFiatCurrency(FiatCurrency fiat) => _settingsStore.fiatCurrency = fiat;

  @action
  void setSelectedCryptoCurrency(String cryptoCurrency) {
    try {
      selectedCryptoCurrency = wallet.balance.keys
          .firstWhere((e) => cryptoCurrency.toLowerCase() == e.title.toLowerCase());
    } catch (e) {
      selectedCryptoCurrency = wallet.currency;
    }
  }

  ContactRecord? newContactAddress() {
    final Set<String> contactAddresses =
        Set.from(contactListViewModel.contacts.map((contact) => contact.address))
          ..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));

    for (var output in outputs) {
      String address;
      if (output.isParsedAddress) {
        address = output.parsedAddress.addresses.first;
      } else {
        address = output.address;
      }

      if (address.isNotEmpty &&
          !contactAddresses.contains(address) &&
          selectedCryptoCurrency.raw != -1) {
        return ContactRecord(
            contactListViewModel.contactSource,
            Contact(
              name: '',
              address: address,
              type: selectedCryptoCurrency,
            ));
      }
    }
    return null;
  }

  String translateErrorMessage(
    Object error,
    WalletType walletType,
    CryptoCurrency currency,
  ) {
    String errorMessage = error.toString();

    if (walletType == WalletType.ethereum ||
        walletType == WalletType.polygon ||
        walletType == WalletType.solana ||
        walletType == WalletType.haven) {
      if (errorMessage.contains('gas required exceeds allowance') ||
          errorMessage.contains('insufficient funds')) {
        return S.current.do_not_have_enough_gas_asset(currency.toString());
      }

      return errorMessage;
    }

    if (walletType == WalletType.tron) {
      if (errorMessage.contains('balance is not sufficient')) {
        return S.current.do_not_have_enough_gas_asset(currency.toString());
      }

      if (errorMessage.contains('Transaction expired')) {
        return 'An error occurred while processing the transaction. Please retry the transaction';
      }
    }

    if (walletType == WalletType.bitcoin ||
        walletType == WalletType.litecoin ||
        walletType == WalletType.bitcoinCash) {
      if (error is TransactionWrongBalanceException) {
        if (error.amount != null)
          return S.current
              .tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString());

        return S.current.tx_wrong_balance_exception(currency.toString());
      }
      if (error is TransactionNoInputsException) {
        return S.current.tx_not_enough_inputs_exception;
      }
      if (error is TransactionNoFeeException) {
        return S.current.tx_zero_fee_exception;
      }
      if (error is TransactionNoDustException) {
        return S.current.tx_no_dust_exception;
      }
      if (error is TransactionCommitFailed) {
        return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}";
      }
      if (error is TransactionCommitFailedDustChange) {
        return S.current.tx_rejected_dust_change;
      }
      if (error is TransactionCommitFailedDustOutput) {
        return S.current.tx_rejected_dust_output;
      }
      if (error is TransactionCommitFailedDustOutputSendAll) {
        return S.current.tx_rejected_dust_output_send_all;
      }
      if (error is TransactionCommitFailedVoutNegative) {
        return S.current.tx_rejected_vout_negative;
      }
      if (error is TransactionCommitFailedBIP68Final) {
        return S.current.tx_rejected_bip68_final;
      }
      if (error is TransactionNoDustOnChangeException) {
        return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max);
      }
      if (error is TransactionInputNotSupported) {
        return S.current.tx_invalid_input;
      }
    }

    return errorMessage;
  }

  bool _hasTaprootInput(PendingTransaction? pendingTransaction) {
    if (walletType == WalletType.bitcoin && pendingTransaction != null) {
      return bitcoin!.hasTaprootInput(pendingTransaction);
    }

    return false;
  }
}