import 'dart:convert';
import 'dart:io';
import 'package:cw_decred/api/libdcrwallet.dart';
import 'package:cw_decred/wallet_creation_credentials.dart';
import 'package:cw_decred/wallet.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/unspent_coins_info.dart';

class DecredWalletService extends WalletService<
    DecredNewWalletCredentials,
    DecredRestoreWalletFromSeedCredentials,
    DecredRestoreWalletFromPubkeyCredentials,
    DecredRestoreWalletFromHardwareCredentials> {
  DecredWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);

  final Box<WalletInfo> walletInfoSource;
  final Box<UnspentCoinsInfo> unspentCoinsInfoSource;
  final seedRestorePath = "m/44'/42'";
  static final seedRestorePathTestnet = "m/44'/1'";
  static final pubkeyRestorePath = "m/44'/42'/0'";
  static final pubkeyRestorePathTestnet = "m/44'/1'/0'";
  final mainnet = "mainnet";
  final testnet = "testnet";
  static Libwallet? libwallet;

  Future<void> init() async {
    if (libwallet != null) {
      return;
    }
    libwallet = await Libwallet.spawn();
    // Init logging with no directory to force printing to stdout and only
    // print ERROR level logs.
    libwallet!.initLibdcrwallet("", "err");
  }

  void closeLibwallet() {
    if (libwallet == null) {
      return;
    }
    libwallet!.close();
    libwallet = null;
  }

  @override
  WalletType getType() => WalletType.decred;

  @override
  Future<bool> isWalletExit(String name) async =>
      File(await pathForWallet(name: name, type: getType())).existsSync();

  @override
  Future<DecredWallet> create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async {
    await this.init();
    final config = {
      "name": credentials.walletInfo!.name,
      "datadir": credentials.walletInfo!.dirPath,
      "pass": credentials.password!,
      "net": isTestnet == true ? testnet : mainnet,
      "unsyncedaddrs": true,
    };
    await libwallet!.createWallet(jsonEncode(config));
    final di = DerivationInfo(
        derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath);
    credentials.walletInfo!.derivationInfo = di;
    final wallet = DecredWallet(credentials.walletInfo!, credentials.password!,
        this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
    await wallet.init();
    return wallet;
  }

  @override
  Future<DecredWallet> openWallet(String name, String password) async {
    final walletInfo = walletInfoSource.values
        .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!;
    final network = walletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet ||
            walletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet
        ? testnet
        : mainnet;

    await this.init();
    final walletDirExists = Directory(walletInfo.dirPath).existsSync();
    if (!walletDirExists) {
      walletInfo.dirPath = await pathForWalletDir(name: name, type: getType());
    }

    final config = {
      "name": walletInfo.name,
      "datadir": walletInfo.dirPath,
      "net": network,
      "unsyncedaddrs": true,
    };
    await libwallet!.loadWallet(jsonEncode(config));
    final wallet =
        DecredWallet(walletInfo, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
    await wallet.init();
    return wallet;
  }

  @override
  Future<void> remove(String wallet) async {
    File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
    final walletInfo = walletInfoSource.values
        .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
    await walletInfoSource.delete(walletInfo.key);
  }

  @override
  Future<void> rename(String currentName, String password, String newName) async {
    final currentWalletInfo = walletInfoSource.values
        .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!;
    final network = currentWalletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet ||
            currentWalletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet
        ? testnet
        : mainnet;
    final currentWallet = DecredWallet(
        currentWalletInfo, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet);

    await currentWallet.renameWalletFiles(newName);

    final newDirPath = await pathForWalletDir(name: newName, type: getType());
    final newWalletInfo = currentWalletInfo;
    newWalletInfo.id = WalletBase.idFor(newName, getType());
    newWalletInfo.name = newName;
    newWalletInfo.dirPath = newDirPath;
    newWalletInfo.network = network;

    await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
  }

  @override
  Future<DecredWallet> restoreFromSeed(DecredRestoreWalletFromSeedCredentials credentials,
      {bool? isTestnet}) async {
    await this.init();
    final config = {
      "name": credentials.walletInfo!.name,
      "datadir": credentials.walletInfo!.dirPath,
      "pass": credentials.password!,
      "mnemonic": credentials.mnemonic,
      "net": isTestnet == true ? testnet : mainnet,
      "unsyncedaddrs": true,
    };
    await libwallet!.createWallet(jsonEncode(config));
    final di = DerivationInfo(
        derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath);
    credentials.walletInfo!.derivationInfo = di;
    final wallet = DecredWallet(credentials.walletInfo!, credentials.password!,
        this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
    await wallet.init();
    return wallet;
  }

  // restoreFromKeys only supports restoring a watch only wallet from an account
  // pubkey.
  @override
  Future<DecredWallet> restoreFromKeys(DecredRestoreWalletFromPubkeyCredentials credentials,
      {bool? isTestnet}) async {
    await this.init();
    final config = {
      "name": credentials.walletInfo!.name,
      "datadir": credentials.walletInfo!.dirPath,
      "pubkey": credentials.pubkey,
      "net": isTestnet == true ? testnet : mainnet,
      "unsyncedaddrs": true,
    };
    await libwallet!.createWatchOnlyWallet(jsonEncode(config));
    final di = DerivationInfo(
        derivationPath: isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath);
    credentials.walletInfo!.derivationInfo = di;
    final wallet = DecredWallet(credentials.walletInfo!, credentials.password!,
        this.unspentCoinsInfoSource, libwallet!, closeLibwallet);
    await wallet.init();
    return wallet;
  }

  @override
  Future<DecredWallet> restoreFromHardwareWallet(
          DecredRestoreWalletFromHardwareCredentials credentials) async =>
      throw UnimplementedError();
}