import 'dart:async';

import 'package:bip39/bip39.dart' as bip39;
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/balance.dart' as SWBalance;
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'
    as SWAddress;
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'
    as SWTransaction;
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/test_stellar_node_connection.dart';
import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart';
import 'package:tuple/tuple.dart';

const int MINIMUM_CONFIRMATIONS = 1;

class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
  late StellarSDK stellarSdk;
  late Network stellarNetwork;

  StellarWallet({
    required String walletId,
    required String walletName,
    required Coin coin,
    required TransactionNotificationTracker tracker,
    required SecureStorageInterface secureStore,
    MainDB? mockableOverride,
  }) {
    txTracker = tracker;
    _walletId = walletId;
    _walletName = walletName;
    _coin = coin;
    _secureStore = secureStore;
    initCache(walletId, coin);
    initWalletDB(mockableOverride: mockableOverride);

    if (coin.isTestNet) {
      stellarNetwork = Network.TESTNET;
    } else {
      stellarNetwork = Network.PUBLIC;
    }

    _updateNode();
  }

  void _updateNode() {
    _xlmNode = NodeService(secureStorageInterface: _secureStore)
            .getPrimaryNodeFor(coin: coin) ??
        DefaultNodes.getNodeFor(coin);
    stellarSdk = StellarSDK("${_xlmNode!.host}:${_xlmNode!.port}");
  }

  late final TransactionNotificationTracker txTracker;
  late SecureStorageInterface _secureStore;

  @override
  bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
  bool? _isFavorite;

  @override
  set isFavorite(bool isFavorite) {
    _isFavorite = isFavorite;
    updateCachedIsFavorite(isFavorite);
  }

  @override
  bool get shouldAutoSync => _shouldAutoSync;
  bool _shouldAutoSync = true;

  Timer? timer;

  final _prefs = Prefs.instance;

  @override
  set shouldAutoSync(bool shouldAutoSync) {
    if (_shouldAutoSync != shouldAutoSync) {
      _shouldAutoSync = shouldAutoSync;
      if (!shouldAutoSync) {
        timer?.cancel();
        timer = null;
        stopNetworkAlivePinging();
      } else {
        startNetworkAlivePinging();
        refresh();
      }
    }
  }

  Timer? _networkAliveTimer;

  void startNetworkAlivePinging() {
    // call once on start right away
    _periodicPingCheck();

    // then periodically check
    _networkAliveTimer = Timer.periodic(
      Constants.networkAliveTimerDuration,
      (_) async {
        _periodicPingCheck();
      },
    );
  }

  void stopNetworkAlivePinging() {
    _networkAliveTimer?.cancel();
    _networkAliveTimer = null;
  }

  void _periodicPingCheck() async {
    bool hasNetwork = await testNetworkConnection();

    if (_isConnected != hasNetwork) {
      NodeConnectionStatus status = hasNetwork
          ? NodeConnectionStatus.connected
          : NodeConnectionStatus.disconnected;

      GlobalEventBus.instance.fire(
        NodeConnectionStatusChangedEvent(
          status,
          walletId,
          coin,
        ),
      );

      _isConnected = hasNetwork;
      if (hasNetwork) {
        unawaited(refresh());
      }
    }
  }

  @override
  String get walletName => _walletName;
  late String _walletName;

  @override
  set walletName(String name) => _walletName = name;

  @override
  SWBalance.Balance get balance => _balance ??= getCachedBalance();
  SWBalance.Balance? _balance;

  @override
  Coin get coin => _coin;
  late Coin _coin;

  Future<bool> _accountExists(String accountId) async {
    bool exists = false;

    try {
      AccountResponse receiverAccount =
          await stellarSdk.accounts.account(accountId);
      if (receiverAccount.accountId != "") {
        exists = true;
      }
    } catch (e, s) {
      Logging.instance.log(
          "Error getting account  ${e.toString()} - ${s.toString()}",
          level: LogLevel.Error);
    }
    return exists;
  }

  @override
  Future<Map<String, dynamic>> prepareSend(
      {required String address,
      required Amount amount,
      Map<String, dynamic>? args}) async {
    try {
      final feeRate = args?["feeRate"];
      var fee = 1000;
      if (feeRate is FeeRateType) {
        final theFees = await fees;
        switch (feeRate) {
          case FeeRateType.fast:
            fee = theFees.fast;
          case FeeRateType.slow:
            fee = theFees.slow;
          case FeeRateType.average:
          default:
            fee = theFees.medium;
        }
      }
      Map<String, dynamic> txData = {
        "fee": fee,
        "address": address,
        "recipientAmt": amount,
        "memo": args?["memo"] as String?,
      };

      Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
      return txData;
    } catch (e, s) {
      Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error);
      rethrow;
    }
  }

  @override
  Future<String> confirmSend({required Map<String, dynamic> txData}) async {
    final secretSeed = await _secureStore.read(key: '${_walletId}_secretSeed');
    KeyPair senderKeyPair = KeyPair.fromSecretSeed(secretSeed!);
    AccountResponse sender =
        await stellarSdk.accounts.account(senderKeyPair.accountId);
    final amountToSend = txData['recipientAmt'] as Amount;
    final memo = txData["memo"] as String?;

    //First check if account exists, can be skipped, but if the account does not exist,
    // the transaction fee will be charged when the transaction fails.
    bool validAccount = await _accountExists(txData['address'] as String);
    TransactionBuilder transactionBuilder;

    if (!validAccount) {
      //Fund the account, user must ensure account is correct
      CreateAccountOperationBuilder createAccBuilder =
          CreateAccountOperationBuilder(
              txData['address'] as String, amountToSend.decimal.toString());
      transactionBuilder =
          TransactionBuilder(sender).addOperation(createAccBuilder.build());
    } else {
      transactionBuilder = TransactionBuilder(sender).addOperation(
          PaymentOperationBuilder(txData['address'] as String, Asset.NATIVE,
                  amountToSend.decimal.toString())
              .build());
    }

    if (memo != null) {
      transactionBuilder.addMemo(MemoText(memo));
    }

    final transaction = transactionBuilder.build();

    transaction.sign(senderKeyPair, stellarNetwork);
    try {
      SubmitTransactionResponse response = await stellarSdk
          .submitTransaction(transaction)
          .onError((error, stackTrace) => throw (error.toString()));
      if (!response.success) {
        throw ("${response.extras?.resultCodes?.transactionResultCode}"
            " ::: ${response.extras?.resultCodes?.operationsResultCodes}");
      }
      return response.hash!;
    } catch (e, s) {
      Logging.instance.log("Error sending TX $e - $s", level: LogLevel.Error);
      rethrow;
    }
  }

  Future<SWAddress.Address?> get _currentReceivingAddress => db
      .getAddresses(walletId)
      .filter()
      .typeEqualTo(SWAddress.AddressType.unknown)
      .and()
      .subTypeEqualTo(SWAddress.AddressSubType.unknown)
      .sortByDerivationIndexDesc()
      .findFirst();

  @override
  Future<String> get currentReceivingAddress async =>
      (await _currentReceivingAddress)?.value ?? await getAddressSW();

  Future<int> getBaseFee() async {
    var fees = await stellarSdk.feeStats.execute();
    return int.parse(fees.lastLedgerBaseFee);
  }

  @override
  Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
    var baseFee = await getBaseFee();
    return Amount(
        rawValue: BigInt.from(baseFee), fractionDigits: coin.decimals);
  }

  @override
  Future<void> exit() async {
    _hasCalledExit = true;
    timer?.cancel();
    timer = null;
    stopNetworkAlivePinging();
  }

  NodeModel? _xlmNode;

  NodeModel getCurrentNode() {
    if (_xlmNode != null) {
      return _xlmNode!;
    } else if (NodeService(secureStorageInterface: _secureStore)
            .getPrimaryNodeFor(coin: coin) !=
        null) {
      return NodeService(secureStorageInterface: _secureStore)
          .getPrimaryNodeFor(coin: coin)!;
    } else {
      return DefaultNodes.getNodeFor(coin);
    }
  }

  @override
  Future<FeeObject> get fees async {
    int fee = await getBaseFee();
    return FeeObject(
        numberOfBlocksFast: 10,
        numberOfBlocksAverage: 10,
        numberOfBlocksSlow: 10,
        fast: fee,
        medium: fee,
        slow: fee);
  }

  @override
  Future<void> fullRescan(
    int maxUnusedAddressGap,
    int maxNumberOfIndexesToCheck,
  ) async {
    try {
      Logging.instance.log("Starting full rescan!", level: LogLevel.Info);
      longMutex = true;
      GlobalEventBus.instance.fire(
        WalletSyncStatusChangedEvent(
          WalletSyncStatus.syncing,
          walletId,
          coin,
        ),
      );

      final _mnemonic = await mnemonicString;
      final _mnemonicPassphrase = await mnemonicPassphrase;

      await db.deleteWalletBlockchainData(walletId);

      await _recoverWalletFromBIP32SeedPhrase(
        mnemonic: _mnemonic!,
        mnemonicPassphrase: _mnemonicPassphrase!,
        isRescan: true,
      );

      await refresh();
      Logging.instance.log("Full rescan complete!", level: LogLevel.Info);
      GlobalEventBus.instance.fire(
        WalletSyncStatusChangedEvent(
          WalletSyncStatus.synced,
          walletId,
          coin,
        ),
      );
    } catch (e, s) {
      GlobalEventBus.instance.fire(
        WalletSyncStatusChangedEvent(
          WalletSyncStatus.unableToSync,
          walletId,
          coin,
        ),
      );

      Logging.instance.log(
        "Exception rethrown from fullRescan(): $e\n$s",
        level: LogLevel.Error,
      );
      rethrow;
    } finally {
      longMutex = false;
    }
  }

  @override
  Future<bool> generateNewAddress() {
    // not used for stellar(?)
    throw UnimplementedError();
  }

  @override
  bool get hasCalledExit => _hasCalledExit;
  bool _hasCalledExit = false;

  @override
  Future<void> initializeExisting() async {
    await _prefs.init();
  }

  @override
  Future<void> initializeNew(
    ({String mnemonicPassphrase, int wordCount})? data,
  ) async {
    if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) {
      throw Exception(
          "Attempted to overwrite mnemonic on generate new wallet!");
    }

    await _prefs.init();

    final int strength;
    if (data == null || data.wordCount == 12) {
      strength = 128;
    } else if (data.wordCount == 24) {
      strength = 256;
    } else {
      throw Exception("Invalid word count");
    }
    final String mnemonic = bip39.generateMnemonic(strength: strength);
    final String passphrase = data?.mnemonicPassphrase ?? "";
    await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic);
    await _secureStore.write(
      key: '${_walletId}_mnemonicPassphrase',
      value: passphrase,
    );

    Wallet wallet = await Wallet.from(
      mnemonic,
      passphrase: passphrase,
    );
    KeyPair keyPair = await wallet.getKeyPair(index: 0);
    String address = keyPair.accountId;
    String secretSeed =
        keyPair.secretSeed; //This will be required for sending a tx

    await _secureStore.write(key: '${_walletId}_secretSeed', value: secretSeed);

    final swAddress = SWAddress.Address(
        walletId: walletId,
        value: address,
        publicKey: keyPair.publicKey,
        derivationIndex: 0,
        derivationPath: null,
        type: SWAddress.AddressType.unknown, // TODO: set type
        subType: SWAddress.AddressSubType.unknown);

    await db.putAddress(swAddress);

    await Future.wait(
        [updateCachedId(walletId), updateCachedIsFavorite(false)]);
  }

  Future<String> getAddressSW() async {
    var mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic');

    Wallet wallet = await Wallet.from(mnemonic!);
    KeyPair keyPair = await wallet.getKeyPair(index: 0);

    return Future.value(keyPair.accountId);
  }

  @override
  bool get isConnected => _isConnected;
  bool _isConnected = false;

  @override
  bool get isRefreshing => refreshMutex;
  bool refreshMutex = false;

  @override
  // TODO: implement maxFee
  Future<int> get maxFee => throw UnimplementedError();

  @override
  Future<List<String>> get mnemonic =>
      mnemonicString.then((value) => value!.split(" "));

  @override
  Future<String?> get mnemonicPassphrase =>
      _secureStore.read(key: '${_walletId}_mnemonicPassphrase');

  @override
  Future<String?> get mnemonicString =>
      _secureStore.read(key: '${_walletId}_mnemonic');

  Future<void> _recoverWalletFromBIP32SeedPhrase({
    required String mnemonic,
    required String mnemonicPassphrase,
    bool isRescan = false,
  }) async {
    final Wallet wallet = await Wallet.from(
      mnemonic,
      passphrase: mnemonicPassphrase,
    );
    final KeyPair keyPair = await wallet.getKeyPair(index: 0);
    final String address = keyPair.accountId;
    String secretSeed =
        keyPair.secretSeed; //This will be required for sending a tx

    await _secureStore.write(
      key: '${_walletId}_secretSeed',
      value: secretSeed,
    );

    final swAddress = SWAddress.Address(
      walletId: walletId,
      value: address,
      publicKey: keyPair.publicKey,
      derivationIndex: 0,
      derivationPath: null,
      type: SWAddress.AddressType.unknown,
      subType: SWAddress.AddressSubType.unknown,
    );

    if (isRescan) {
      await db.updateOrPutAddresses([swAddress]);
    } else {
      await db.putAddress(swAddress);
    }
  }

  bool longMutex = false;

  @override
  Future<void> recoverFromMnemonic({
    required String mnemonic,
    String? mnemonicPassphrase,
    required int maxUnusedAddressGap,
    required int maxNumberOfIndexesToCheck,
    required int height,
  }) async {
    longMutex = true;
    try {
      if ((await mnemonicString) != null ||
          (await this.mnemonicPassphrase) != null) {
        throw Exception("Attempted to overwrite mnemonic on restore!");
      }

      await _secureStore.write(
        key: '${_walletId}_mnemonic',
        value: mnemonic.trim(),
      );
      await _secureStore.write(
        key: '${_walletId}_mnemonicPassphrase',
        value: mnemonicPassphrase ?? "",
      );

      await _recoverWalletFromBIP32SeedPhrase(
        mnemonic: mnemonic,
        mnemonicPassphrase: mnemonicPassphrase ?? "",
        isRescan: false,
      );

      await Future.wait([
        updateCachedId(walletId),
        updateCachedIsFavorite(false),
      ]);
    } catch (e, s) {
      Logging.instance.log(
          "Exception rethrown from recoverFromMnemonic(): $e\n$s",
          level: LogLevel.Error);

      rethrow;
    } finally {
      longMutex = false;
    }
  }

  Future<void> updateChainHeight() async {
    final height = await stellarSdk.ledgers
        .order(RequestBuilderOrder.DESC)
        .limit(1)
        .execute()
        .then((value) => value.records!.first.sequence)
        .onError((error, stackTrace) => throw ("Error getting chain height"));
    await updateCachedChainHeight(height);
  }

  Future<void> updateTransactions() async {
    try {
      List<Tuple2<SWTransaction.Transaction, SWAddress.Address?>>
          transactionList = [];
      Page<OperationResponse> payments;
      try {
        payments = await stellarSdk.payments
            .forAccount(await getAddressSW())
            .order(RequestBuilderOrder.DESC)
            .execute()
            .onError((error, stackTrace) => throw error!);
      } catch (e) {
        if (e is ErrorResponse &&
            e.body.contains("The resource at the url requested was not found.  "
                "This usually occurs for one of two reasons:  "
                "The url requested is not valid, or no data in our database "
                "could be found with the parameters provided.")) {
          // probably just doesn't have any history yet or whatever stellar needs
          return;
        } else {
          Logging.instance.log(
            "Stellar $walletName $walletId failed to fetch transactions",
            level: LogLevel.Warning,
          );
          rethrow;
        }
      }
      for (OperationResponse response in payments.records!) {
        // PaymentOperationResponse por;
        if (response is PaymentOperationResponse) {
          PaymentOperationResponse por = response;

          SWTransaction.TransactionType type;
          if (por.sourceAccount == await getAddressSW()) {
            type = SWTransaction.TransactionType.outgoing;
          } else {
            type = SWTransaction.TransactionType.incoming;
          }
          final amount = Amount(
            rawValue: BigInt.parse(float
                .parse(por.amount!)
                .toStringAsFixed(coin.decimals)
                .replaceAll(".", "")),
            fractionDigits: coin.decimals,
          );
          int fee = 0;
          int height = 0;
          //Query the transaction linked to the payment,
          // por.transaction returns a null sometimes
          TransactionResponse tx =
              await stellarSdk.transactions.transaction(por.transactionHash!);

          if (tx.hash.isNotEmpty) {
            fee = tx.feeCharged!;
            height = tx.ledger;
          }
          var theTransaction = SWTransaction.Transaction(
            walletId: walletId,
            txid: por.transactionHash!,
            timestamp:
                DateTime.parse(por.createdAt!).millisecondsSinceEpoch ~/ 1000,
            type: type,
            subType: SWTransaction.TransactionSubType.none,
            amount: 0,
            amountString: amount.toJsonString(),
            fee: fee,
            height: height,
            isCancelled: false,
            isLelantus: false,
            slateId: "",
            otherData: "",
            inputs: [],
            outputs: [],
            nonce: 0,
            numberOfMessages: null,
          );
          SWAddress.Address? receivingAddress = await _currentReceivingAddress;
          SWAddress.Address address =
              type == SWTransaction.TransactionType.incoming
                  ? receivingAddress!
                  : SWAddress.Address(
                      walletId: walletId,
                      value: por.sourceAccount!,
                      publicKey:
                          KeyPair.fromAccountId(por.sourceAccount!).publicKey,
                      derivationIndex: 0,
                      derivationPath: null,
                      type: SWAddress.AddressType.unknown, // TODO: set type
                      subType: SWAddress.AddressSubType.unknown);
          Tuple2<SWTransaction.Transaction, SWAddress.Address> tuple =
              Tuple2(theTransaction, address);
          transactionList.add(tuple);
        } else if (response is CreateAccountOperationResponse) {
          CreateAccountOperationResponse caor = response;
          SWTransaction.TransactionType type;
          if (caor.sourceAccount == await getAddressSW()) {
            type = SWTransaction.TransactionType.outgoing;
          } else {
            type = SWTransaction.TransactionType.incoming;
          }
          final amount = Amount(
            rawValue: BigInt.parse(float
                .parse(caor.startingBalance!)
                .toStringAsFixed(coin.decimals)
                .replaceAll(".", "")),
            fractionDigits: coin.decimals,
          );
          int fee = 0;
          int height = 0;
          TransactionResponse tx =
              await stellarSdk.transactions.transaction(caor.transactionHash!);
          if (tx.hash.isNotEmpty) {
            fee = tx.feeCharged!;
            height = tx.ledger;
          }
          var theTransaction = SWTransaction.Transaction(
            walletId: walletId,
            txid: caor.transactionHash!,
            timestamp:
                DateTime.parse(caor.createdAt!).millisecondsSinceEpoch ~/ 1000,
            type: type,
            subType: SWTransaction.TransactionSubType.none,
            amount: 0,
            amountString: amount.toJsonString(),
            fee: fee,
            height: height,
            isCancelled: false,
            isLelantus: false,
            slateId: "",
            otherData: "",
            inputs: [],
            outputs: [],
            nonce: 0,
            numberOfMessages: null,
          );
          SWAddress.Address? receivingAddress = await _currentReceivingAddress;
          SWAddress.Address address =
              type == SWTransaction.TransactionType.incoming
                  ? receivingAddress!
                  : SWAddress.Address(
                      walletId: walletId,
                      value: caor.sourceAccount!,
                      publicKey:
                          KeyPair.fromAccountId(caor.sourceAccount!).publicKey,
                      derivationIndex: 0,
                      derivationPath: null,
                      type: SWAddress.AddressType.unknown, // TODO: set type
                      subType: SWAddress.AddressSubType.unknown);
          Tuple2<SWTransaction.Transaction, SWAddress.Address> tuple =
              Tuple2(theTransaction, address);
          transactionList.add(tuple);
        }
      }
      await db.addNewTransactionData(transactionList, walletId);
    } catch (e, s) {
      Logging.instance.log(
          "Exception rethrown from updateTransactions(): $e\n$s",
          level: LogLevel.Error);
      rethrow;
    }
  }

  Future<void> updateBalance() async {
    try {
      AccountResponse accountResponse;

      try {
        accountResponse = await stellarSdk.accounts
            .account(await getAddressSW())
            .onError((error, stackTrace) => throw error!);
      } catch (e) {
        if (e is ErrorResponse &&
            e.body.contains("The resource at the url requested was not found.  "
                "This usually occurs for one of two reasons:  "
                "The url requested is not valid, or no data in our database "
                "could be found with the parameters provided.")) {
          // probably just doesn't have any history yet or whatever stellar needs
          return;
        } else {
          Logging.instance.log(
            "Stellar $walletName $walletId failed to fetch transactions",
            level: LogLevel.Warning,
          );
          rethrow;
        }
      }

      for (Balance balance in accountResponse.balances) {
        switch (balance.assetType) {
          case Asset.TYPE_NATIVE:
            _balance = SWBalance.Balance(
              total: Amount(
                rawValue: BigInt.from(float.parse(balance.balance) * 10000000),
                fractionDigits: coin.decimals,
              ),
              spendable: Amount(
                rawValue: BigInt.from(float.parse(balance.balance) * 10000000),
                fractionDigits: coin.decimals,
              ),
              blockedTotal: Amount(
                rawValue: BigInt.from(0),
                fractionDigits: coin.decimals,
              ),
              pendingSpendable: Amount(
                rawValue: BigInt.from(0),
                fractionDigits: coin.decimals,
              ),
            );
            Logging.instance.log(_balance, level: LogLevel.Info);
            await updateCachedBalance(_balance!);
        }
      }
    } catch (e, s) {
      Logging.instance.log(
        "ERROR GETTING BALANCE $e\n$s",
        level: LogLevel.Info,
      );
      rethrow;
    }
  }

  @override
  Future<void> refresh() async {
    if (refreshMutex) {
      Logging.instance.log(
        "$walletId $walletName refreshMutex denied",
        level: LogLevel.Info,
      );
      return;
    } else {
      refreshMutex = true;
    }

    try {
      await _prefs.init();

      GlobalEventBus.instance.fire(
        WalletSyncStatusChangedEvent(
          WalletSyncStatus.syncing,
          walletId,
          coin,
        ),
      );

      await updateChainHeight();
      await updateTransactions();
      await updateBalance();

      GlobalEventBus.instance.fire(
        WalletSyncStatusChangedEvent(
          WalletSyncStatus.synced,
          walletId,
          coin,
        ),
      );

      if (shouldAutoSync) {
        timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
          Logging.instance.log(
              "Periodic refresh check for $walletId $walletName in object instance: $hashCode",
              level: LogLevel.Info);

          await refresh();
          GlobalEventBus.instance.fire(
            UpdatedInBackgroundEvent(
              "New data found in $walletId $walletName in background!",
              walletId,
            ),
          );
        });
      }
    } catch (e, s) {
      Logging.instance.log(
        "Failed to refresh stellar wallet $walletId: '$walletName': $e\n$s",
        level: LogLevel.Warning,
      );
      GlobalEventBus.instance.fire(
        WalletSyncStatusChangedEvent(
          WalletSyncStatus.unableToSync,
          walletId,
          coin,
        ),
      );
    }

    refreshMutex = false;
  }

  @override
  int get storedChainHeight => getCachedChainHeight();

  @override
  Future<bool> testNetworkConnection() async {
    return await testStellarNodeConnection(_xlmNode!.host, _xlmNode!.port);
  }

  @override
  Future<List<SWTransaction.Transaction>> get transactions =>
      db.getTransactions(walletId).findAll();

  @override
  Future<void> updateNode(bool shouldRefresh) async {
    _updateNode();
    if (shouldRefresh) {
      unawaited(refresh());
    }
  }

  @override
  Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
    final transaction = SWTransaction.Transaction(
      walletId: walletId,
      txid: txData["txid"] as String,
      timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
      type: SWTransaction.TransactionType.outgoing,
      subType: SWTransaction.TransactionSubType.none,
      // precision may be lost here hence the following amountString
      amount: (txData["recipientAmt"] as Amount).raw.toInt(),
      amountString: (txData["recipientAmt"] as Amount).toJsonString(),
      fee: txData["fee"] as int,
      height: null,
      isCancelled: false,
      isLelantus: false,
      otherData: null,
      slateId: null,
      nonce: null,
      inputs: [],
      outputs: [],
      numberOfMessages: null,
    );

    final address = txData["address"] is String
        ? await db.getAddress(walletId, txData["address"] as String)
        : null;

    await db.addNewTransactionData(
      [
        Tuple2(transaction, address),
      ],
      walletId,
    );
  }

  @override
  // not used
  Future<List<UTXO>> get utxos => throw UnimplementedError();

  @override
  bool validateAddress(String address) {
    return RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address);
  }

  @override
  String get walletId => _walletId;
  late String _walletId;
}