import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Events } from 'ionic-angular';
import * as _ from 'lodash';
import { Logger } from '../../providers/logger/logger';

// providers
import { AppProvider } from '../../providers/app/app';
import { LanguageProvider } from '../../providers/language/language';
import { BwcErrorProvider } from '../bwc-error/bwc-error';
import { BwcProvider } from '../bwc/bwc';
import { ConfigProvider } from '../config/config';
import { OnGoingProcessProvider } from '../on-going-process/on-going-process';
import { PersistenceProvider } from '../persistence/persistence';
import { PlatformProvider } from '../platform/platform';
import { PopupProvider } from '../popup/popup';

// models
import { Profile } from '../../models/profile/profile.model';

@Injectable()
export class ProfileProvider {
  public wallet: any = {};
  public profile: Profile;

  private UPDATE_PERIOD = 15;
  private throttledBwsEvent: any;
  private validationLock: boolean = false;
  private errors: any = this.bwcProvider.getErrors();

  constructor(
    private logger: Logger,
    private persistenceProvider: PersistenceProvider,
    private configProvider: ConfigProvider,
    private bwcProvider: BwcProvider,
    private bwcErrorProvider: BwcErrorProvider,
    private platformProvider: PlatformProvider,
    private appProvider: AppProvider,
    private languageProvider: LanguageProvider,
    private events: Events,
    private popupProvider: PopupProvider,
    private onGoingProcessProvider: OnGoingProcessProvider,
    private translate: TranslateService
  ) {
    this.throttledBwsEvent = _.throttle((n, wallet) => {
      this.newBwsEvent(n, wallet);
    }, 10000);
  }

  private updateWalletSettings(wallet: any): void {
    let config: any = this.configProvider.get();
    let defaults: any = this.configProvider.getDefaults();
    // this.config.whenAvailable( (config) => { TODO
    wallet.usingCustomBWS = config.bwsFor && config.bwsFor[wallet.id] && (config.bwsFor[wallet.id] != defaults.bws.url);
    wallet.name = (config.aliasFor && config.aliasFor[wallet.id]) || wallet.credentials.walletName;
    wallet.color = (config.colorFor && config.colorFor[wallet.id]) ? config.colorFor[wallet.id] : null;
    wallet.email = config.emailFor && config.emailFor[wallet.id];
    // });
  }

  public setWalletOrder(walletId: string, index: number): void {
    this.persistenceProvider.setWalletOrder(walletId, index);
    this.logger.debug('Wallet new order stored');
    this.wallet[walletId].order = index;
  }

  private getWalletOrder(wallet): Promise<any> {
    return new Promise((resolve, reject) => {
      this.persistenceProvider.getWalletOrder(wallet.credentials.walletId).then((order) => {
        return resolve(order);
      });
    })
  }

  public setBackupFlag(walletId: string): void {
    this.persistenceProvider.setBackupFlag(walletId);
    this.logger.debug('Backup flag stored');
    this.wallet[walletId].needsBackup = false;
  }

  private requiresBackup(wallet: any): boolean {
    if (wallet.isPrivKeyExternal()) return false;
    if (!wallet.credentials.mnemonic && !wallet.credentials.mnemonicEncrypted) return false;
    if (wallet.credentials.network == 'testnet') return false;

    return true;
  }

  private needsBackup(wallet: any): Promise<boolean> {
    return new Promise((resolve, reject) => {
      if (!this.requiresBackup(wallet)) {
        return resolve(false);
      }

      this.persistenceProvider.getBackupFlag(wallet.credentials.walletId).then((val: string) => {
        if (val) {
          return resolve(false);
        }
        return resolve(true);
      }).catch((err) => {
        this.logger.error(err);
      });
    })
  }

  private isBalanceHidden(wallet: any): Promise<boolean> {
    return new Promise((resolve, reject) => {
      this.persistenceProvider.getHideBalanceFlag(wallet.credentials.walletId).then((shouldHideBalance) => {
        let isHidden = shouldHideBalance && shouldHideBalance.toString() == 'true' ? true : false;
        return resolve(isHidden);
      }).catch((err) => {
        this.logger.error(err);
      });
    });
  }

  private bindWalletClient(wallet: any, opts?: any): boolean {
    opts = opts ? opts : {};
    let walletId = wallet.credentials.walletId;

    if ((this.wallet[walletId] && this.wallet[walletId].started) && !opts.force) return false;

    // INIT WALLET VIEWMODEL
    wallet.id = walletId;
    wallet.started = true;
    wallet.network = wallet.credentials.network;
    wallet.copayerId = wallet.credentials.copayerId;
    wallet.m = wallet.credentials.m;
    wallet.n = wallet.credentials.n;
    wallet.coin = wallet.credentials.coin;
    wallet.status = {};

    this.updateWalletSettings(wallet);
    this.wallet[walletId] = wallet;

    this.needsBackup(wallet).then((val: any) => {
      wallet.needsBackup = val;
    });

    this.isBalanceHidden(wallet).then((val: any) => {
      wallet.balanceHidden = val;
    });

    this.getWalletOrder(wallet).then((val: number) => {
      wallet.order = val;
    });

    wallet.removeAllListeners();

    wallet.on('report', (n: any) => {
      this.logger.info('BWC Report:' + n);
    });

    wallet.on('notification', (n: any) => {
      this.logger.debug('BWC Notification:', JSON.stringify(n));

      if (n.type == "NewBlock" && n.data.network == "testnet") {
        this.throttledBwsEvent(n, wallet);
      } else this.newBwsEvent(n, wallet);
    });

    wallet.on('walletCompleted', () => {
      this.logger.debug('Wallet completed');
      this.updateCredentials(JSON.parse(wallet.export()))
      this.events.publish('status:updated');
    });

    wallet.initialize({
      notificationIncludeOwn: true,
    }, (err: any) => {
      if (err) {
        this.logger.error('Could not init notifications err:', err);
        return;
      }
      wallet.setNotificationsInterval(this.UPDATE_PERIOD);
      wallet.openWallet((err: any) => {
        if (wallet.status !== true)
          this.logger.debug('Wallet + ' + walletId + ' status:' + JSON.stringify(wallet.status));
      });
    });
    this.events.subscribe('wallet:updated', (walletId: string) => {
      if (walletId && walletId == wallet.id) {
        this.logger.debug('Updating settings for wallet:' + wallet.id);
        this.updateWalletSettings(wallet);
      }
    });

    return true;
  }

  private newBwsEvent(n: any, wallet: any): void {
    if (wallet.cachedStatus)
      wallet.cachedStatus.isValid = false;

    if (wallet.completeHistory)
      wallet.completeHistory.isValid = false;

    if (wallet.cachedActivity)
      wallet.cachedActivity.isValid = false;

    if (wallet.cachedTxps)
      wallet.cachedTxps.isValid = false;

    this.events.publish('bwsEvent', wallet.id, n.type, n);
  }

  public updateCredentials(credentials: any): void {
    this.profile.updateWallet(credentials);
    this.persistenceProvider.storeProfile(this.profile);
  }

  public getLastKnownBalance(wid: string): Promise<string> {
    return new Promise((resolve, reject) => {
      this.persistenceProvider.getBalanceCache(wid).then((data: string) => {
        return resolve(data);
      }).catch((err: any) => {
        return reject(err);
      });
    });
  }

  private addLastKnownBalance(wallet: any): Promise<any> {
    return new Promise((resolve, reject) => {
      let now = Math.floor(Date.now() / 1000);
      let showRange = 600; // 10min;

      this.getLastKnownBalance(wallet.id).then((data: any) => {
        if (data) {
          let parseData: any = data;
          wallet.cachedBalance = parseData.balance;
          wallet.cachedBalanceUpdatedOn = (parseData.updatedOn < now - showRange) ? parseData.updatedOn : null;
        }
        return resolve();
      }).catch((err: any) => {
        this.logger.warn(err);
      });
    });
  }

  public setLastKnownBalance(wid: string, balance: number): void {
    this.persistenceProvider.setBalanceCache(wid, { balance, updatedOn: Math.floor(Date.now() / 1000) });
  }

  private runValidation(wallet: any, delay?: number, retryDelay?: number) {

    delay = delay ? delay : 500;
    retryDelay = retryDelay ? retryDelay : 50;

    if (this.validationLock) {
      return setTimeout(() => {
        this.logger.debug('ValidatingWallet Locked: Retrying in: ' + retryDelay);
        return this.runValidation(wallet, delay, retryDelay);
      }, retryDelay);
    }
    this.validationLock = true;

    // IOS devices are already checked
    let skipDeviceValidation = this.platformProvider.isIOS || this.profile.isDeviceChecked(this.platformProvider.ua);
    let walletId = wallet.credentials.walletId;

    this.logger.debug('ValidatingWallet: ' + walletId + ' skip Device:' + skipDeviceValidation);
    setTimeout(() => {
      wallet.validateKeyDerivation({
        skipDeviceValidation,
      }, (err: any, isOK: any) => {
        this.validationLock = false;

        this.logger.debug('ValidatingWallet End:  ' + walletId + ' isOK:' + isOK);
        if (isOK) {
          this.profile.setChecked(this.platformProvider.ua, walletId);
        } else {
          this.logger.warn('Key Derivation failed for wallet:' + walletId);
          this.persistenceProvider.clearLastAddress(walletId).then(() => {
          });
        };

        this.storeProfileIfDirty();
      });
    }, delay);
  }

  public storeProfileIfDirty(): void {
    if (this.profile.dirty) {
      this.persistenceProvider.storeProfile(this.profile).then((err: any) => {
        this.logger.debug('Saved modified Profile');
        return;
      });
    } else {
      return;
    };
  }

  public importWallet(str: string, opts: any): Promise<any> {
    return new Promise((resolve, reject) => {
      let walletClient = this.bwcProvider.getClient(null, opts);

      this.logger.debug('Importing Wallet:', opts);

      try {
        let c = JSON.parse(str);

        if (c.xPrivKey && c.xPrivKeyEncrypted) {
          this.logger.warn('Found both encrypted and decrypted key. Deleting the encrypted version');
          delete c.xPrivKeyEncrypted;
          delete c.mnemonicEncrypted;
        }

        str = JSON.stringify(c);

        walletClient.import(str, {
          compressed: opts.compressed,
          password: opts.password
        });
      } catch (err) {
        return reject(this.translate.instant('Could not import. Check input file and spending password'));
      }

      let strParsed: any = JSON.parse(str);

      if (!strParsed.n) {
        return reject("Backup format not recognized. If you are using a Copay Beta backup and version is older than 0.10, please see: https://github.com/bitpay/copay/issues/4730#issuecomment-244522614");
      }

      let addressBook = strParsed.addressBook ? strParsed.addressBook : {};

      this.addAndBindWalletClient(walletClient, {
        bwsurl: opts.bwsurl
      }).then((walletId: string) => {
        this.setMetaData(walletClient, addressBook).then(() => {
          return resolve(walletClient);
        }).catch((err: any) => {
          this.logger.warn(err);
          return reject(err);
        });
      }).catch((err: any) => {
        return reject(err);
      });
    });
  }

  // An alert dialog
  private askPassword(name: string, title: string): Promise<any> {
    return new Promise((resolve, reject) => {
      let opts = {
        type: 'password'
      }
      this.popupProvider.ionicPrompt(title, name, opts).then((res: any) => {
        return resolve(res);
      });
    });
  }

  private showWarningNoEncrypt(): Promise<any> {
    return new Promise((resolve, reject) => {
      let title = this.translate.instant('Are you sure?');
      let msg = this.translate.instant('Without encryption, a thief or another application on this device may be able to access your funds.');
      let okText = this.translate.instant('I\'m sure');
      let cancelText = this.translate.instant('Go Back');
      this.popupProvider.ionicConfirm(title, msg, okText, cancelText).then((res: any) => {
        return resolve(res);
      });
    });
  }

  private askToEncryptWallet(wallet: any): Promise<any> {
    return new Promise((resolve, reject) => {
      var title = this.translate.instant('Would you like to protect this wallet with a password?');
      var message = this.translate.instant('Encryption can protect your funds if this device is stolen or compromised by malicious software.');
      var okText = this.translate.instant('Yes');
      var cancelText = this.translate.instant('No');
      this.popupProvider.ionicConfirm(title, message, okText, cancelText).then((res: any) => {
        if (!res) {
          return this.showWarningNoEncrypt().then((res) => {
            if (res) return resolve()
            return this.encrypt(wallet).then(() => {
              return resolve();
            });
          });
        }
        return this.encrypt(wallet).then(() => {
          return resolve();
        });
      });
    });
  }

  private encrypt(wallet: any): Promise<any> {
    return new Promise((resolve, reject) => {
      let title = this.translate.instant('Enter a password to encrypt your wallet');
      let warnMsg = this.translate.instant('This password is only for this device, and it cannot be recovered. To avoid losing funds, write your password down.');
      this.askPassword(warnMsg, title).then((password: string) => {
        if (!password) {
          this.showWarningNoEncrypt().then((res: any) => {
            if (res) return resolve();
            this.encrypt(wallet).then(() => {
              return resolve();
            });
          });
        }
        else {
          title = this.translate.instant('Enter your password again to confirm');
          this.askPassword(warnMsg, title).then((password2: string) => {
            if (!password2 || password != password2) {
              this.encrypt(wallet).then(() => {
                return resolve();
              });
            } else {
              wallet.encryptPrivateKey(password);
              return resolve();
            }
          });
        }
      });
    });
  }

  // Adds and bind a new client to the profile
  private addAndBindWalletClient(wallet: any, opts: any): Promise<any> {
    return new Promise((resolve, reject) => {
      if (!wallet || !wallet.credentials) {
        return reject(this.translate.instant('Could not access wallet'));
      }

      // Encrypt wallet
      this.onGoingProcessProvider.pause();
      this.askToEncryptWallet(wallet).then(() => {
        this.onGoingProcessProvider.resume();

        let walletId: string = wallet.credentials.walletId

        if (!this.profile.addWallet(JSON.parse(wallet.export()))) {
          let message = "Wallet already in " + this.appProvider.info.nameCase; // TODO: translate
          return reject(message);
        }

        let skipKeyValidation: boolean = this.shouldSkipValidation(walletId);
        if (!skipKeyValidation)
          this.runValidation(wallet);

        this.bindWalletClient(wallet);

        let saveBwsUrl = (): Promise<any> => {
          return new Promise((resolve, reject) => {
            let defaults: any = this.configProvider.getDefaults();
            let bwsFor: any = {};
            bwsFor[walletId] = opts.bwsurl || defaults.bws.url;

            // Dont save the default
            if (bwsFor[walletId] == defaults.bws.url) {
              return resolve();
            }

            this.configProvider.set({ bwsFor });
            return resolve();
          });
        };

        saveBwsUrl().then(() => {
          this.persistenceProvider.storeProfile(this.profile).then(() => {
            return resolve(wallet);
          }).catch((err: any) => {
            return reject(err);
          });
        });
      });
    });
  }

  private shouldSkipValidation(walletId: string): boolean {
    return this.profile.isChecked(this.platformProvider.ua, walletId) || this.platformProvider.isIOS;
  }

  private setMetaData(wallet: any, addressBook: any): Promise<any> {
    return new Promise((resolve, reject) => {
      this.persistenceProvider.getAddressbook(wallet.credentials.network).then((localAddressBook: any) => {
        let localAddressBook1 = {};
        try {
          localAddressBook1 = JSON.parse(localAddressBook);
        } catch (ex) {
          this.logger.warn(ex);
        }
        let mergeAddressBook = _.merge(addressBook, localAddressBook1);
        this.persistenceProvider.setAddressbook(wallet.credentials.network, JSON.stringify(mergeAddressBook)).then(() => {
          return resolve();
        }).catch((err: any) => {
          return reject(err);
        });
      }).catch((err: any) => {
        return reject(err);
      });
    });
  }

  public importExtendedPrivateKey(xPrivKey: string, opts: any): Promise<any> {
    return new Promise((resolve, reject) => {

      let walletClient = this.bwcProvider.getClient(null, opts);
      this.logger.debug('Importing Wallet xPrivKey');

      walletClient.importFromExtendedPrivateKey(xPrivKey, opts, (err: any) => {
        if (err) {
          if (err instanceof this.errors.NOT_AUTHORIZED) return reject(err);
          this.bwcErrorProvider.cb(err, this.translate.instant('Could not import')).then((msg: string) => {
            return reject(msg);
          });
        } else {
          this.addAndBindWalletClient(walletClient, {
            bwsurl: opts.bwsurl
          }).then((wallet: any) => {
            return resolve(wallet);
          }).catch((err: any) => {
            return reject(err);
          });
        };
      });
    });
  }

  private normalizeMnemonic(words: string): string {
    if (!words || !words.indexOf) return words;
    let isJA = words.indexOf('\u3000') > -1;
    let wordList = words.split(/[\u3000\s]+/);

    return wordList.join(isJA ? '\u3000' : ' ');
  };

  public importMnemonic(words: string, opts: any): Promise<any> {
    return new Promise((resolve, reject) => {

      let walletClient = this.bwcProvider.getClient(null, opts);

      this.logger.debug('Importing Wallet Mnemonic');

      words = this.normalizeMnemonic(words);
      walletClient.importFromMnemonic(words, {
        network: opts.networkName,
        passphrase: opts.passphrase,
        entropySourcePath: opts.entropySourcePath,
        derivationStrategy: opts.derivationStrategy || 'BIP44',
        account: opts.account || 0,
        coin: opts.coin
      }, (err: any) => {
        if (err) {
          if (err instanceof this.errors.NOT_AUTHORIZED) {
            return reject(err);
          }

          this.bwcErrorProvider.cb(err, this.translate.instant('Could not import')).then((msg: string) => {
            return reject(msg);
          });

        }

        this.addAndBindWalletClient(walletClient, {
          bwsurl: opts.bwsurl
        }).then((wallet: any) => {
          return resolve(wallet);
        }).catch((err: any) => {
          return reject(err);
        });
      });
    });
  }

  public importExtendedPublicKey(opts: any): Promise<any> {
    return new Promise((resolve, reject) => {

      let walletClient = this.bwcProvider.getClient(null, opts);
      this.logger.debug('Importing Wallet XPubKey');

      walletClient.importFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource, {
        account: opts.account || 0,
        derivationStrategy: opts.derivationStrategy || 'BIP44',
        coin: opts.coin
      }, (err: any) => {
        if (err) {

          // in HW wallets, req key is always the same. They can't addAccess.
          if (err instanceof this.errors.NOT_AUTHORIZED)
            err.name = 'WALLET_DOES_NOT_EXIST';

          this.bwcErrorProvider.cb(err, this.translate.instant('Could not import')).then((msg: string) => {
            return reject(msg);
          });

        }

        this.addAndBindWalletClient(walletClient, {
          bwsurl: opts.bwsurl
        }).then((wallet: any) => {
          return resolve(wallet);
        }).catch((err: any) => {
          return reject(err);
        });
      });
    });
  }

  public createProfile(): void {
    this.logger.info('Creating profile');
    this.profile = new Profile();
    this.profile = this.profile.create();
    this.persistenceProvider.storeNewProfile(this.profile);
  }

  public bindProfile(profile: any): Promise<any> {
    return new Promise((resolve, reject) => {
      let bindWallets = (): Promise<any> => {
        return new Promise((resolve, reject) => {

          let l = profile.credentials.length;
          let i = 0;
          let totalBound = 0;

          if (!l) {
            return resolve();
          }

          _.each(profile.credentials, (credentials) => {
            this.bindWallet(credentials).then((bound: number) => {
              i++;
              totalBound += bound;
              if (i == l) {
                this.logger.info('Bound ' + totalBound + ' out of ' + l + ' wallets');
                return resolve();
              }
            }).catch((err) => {
              return reject(err);
            });
          });
        });
      };

      bindWallets().then(() => {
        this.isOnboardingCompleted().then(() => {
          this.isDisclaimerAccepted().then(() => {
            return resolve();
          }).catch(() => {
            return reject(new Error('NONAGREEDDISCLAIMER: Non agreed disclaimer'));
          });
        }).catch(() => {
          this.isDisclaimerAccepted().then(() => {
            this.setOnboardingCompleted().then(() => {
              return resolve();
            }).catch((err: any) => {
              this.logger.error(err);
            });
          }).catch(() => {
            return reject(new Error('ONBOARDINGNONCOMPLETED: Onboarding non completed'));
          });
        });
      }).catch((err: any) => {
        return reject(err);
      });
    });
  }

  public isDisclaimerAccepted(): Promise<any> {
    return new Promise((resolve, reject) => {

      let disclaimerAccepted = this.profile && this.profile.disclaimerAccepted;
      if (disclaimerAccepted) return resolve();

      // OLD flag
      this.persistenceProvider.getCopayDisclaimerFlag().then((val) => {
        if (val) {
          this.profile.disclaimerAccepted = true;
          return resolve();
        } else {
          return reject();
        }
      });
    });
  }

  public isOnboardingCompleted(): Promise<any> {
    return new Promise((resolve, reject) => {

      let onboardingCompleted = this.profile && this.profile.onboardingCompleted;
      if (onboardingCompleted) return resolve();

      this.persistenceProvider.getCopayOnboardingFlag().then((val) => {
        if (val) {
          this.profile.onboardingCompleted = true;
          return resolve();
        } else {
          return reject();
        }
      });
    });
  }


  private bindWallet(credentials: any): Promise<any> {
    return new Promise((resolve, reject) => {

      if (!credentials.walletId || !credentials.m) {
        return reject('bindWallet should receive credentials JSON');
      }

      // Create the client
      let getBWSURL = (walletId: string) => {
        let config: any = this.configProvider.get();
        let defaults: any = this.configProvider.getDefaults();
        return ((config.bwsFor && config.bwsFor[walletId]) || defaults.bws.url);
      };

      let walletClient = this.bwcProvider.getClient(JSON.stringify(credentials), {
        bwsurl: getBWSURL(credentials.walletId),
      });

      let skipKeyValidation = this.shouldSkipValidation(credentials.walletId);
      if (!skipKeyValidation) this.runValidation(walletClient, 500);

      this.logger.info('Binding wallet:' + credentials.walletId + ' Validating?:' + !skipKeyValidation);
      return resolve(this.bindWalletClient(walletClient));
    });
  }

  public loadAndBindProfile(): Promise<any> {
    return new Promise((resolve, reject) => {
      this.persistenceProvider.getProfile().then((profile: any) => {

        if (!profile) {
          return resolve();
        }
        this.profile = new Profile();
        this.profile = this.profile.fromObj(profile);
        // Deprecated: storageService.tryToMigrate
        this.logger.debug('Profile read');
        this.bindProfile(this.profile).then(() => {
          return resolve(this.profile);
        }).catch((err: any) => {
          return reject(err);
        });
      }).catch((err: any) => {
        return reject(err);
      });
    });
  }

  private seedWallet(opts: any): Promise<any> {
    return new Promise((resolve, reject) => {

      opts = opts ? opts : {};
      let walletClient = this.bwcProvider.getClient(null, opts);
      let network = opts.networkName || 'livenet';

      if (opts.mnemonic) {
        try {
          opts.mnemonic = this.normalizeMnemonic(opts.mnemonic);
          walletClient.seedFromMnemonic(opts.mnemonic, {
            network,
            passphrase: opts.passphrase,
            account: opts.account || 0,
            derivationStrategy: opts.derivationStrategy || 'BIP44',
            coin: opts.coin
          });

        } catch (ex) {
          this.logger.info(ex);
          return reject(this.translate.instant('Could not create: Invalid wallet recovery phrase'));
        }
      } else if (opts.extendedPrivateKey) {
        try {
          walletClient.seedFromExtendedPrivateKey(opts.extendedPrivateKey, {
            network,
            account: opts.account || 0,
            derivationStrategy: opts.derivationStrategy || 'BIP44',
            coin: opts.coin,
          });
        } catch (ex) {
          this.logger.warn(ex);
          return reject(this.translate.instant('Could not create using the specified extended private key'));
        }
      } else if (opts.extendedPublicKey) {
        try {
          walletClient.seedFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource, {
            account: opts.account || 0,
            derivationStrategy: opts.derivationStrategy || 'BIP44',
            coin: opts.coin
          });
          walletClient.credentials.hwInfo = opts.hwInfo;
        } catch (ex) {
          this.logger.warn("Creating wallet from Extended Public Key Arg:", ex, opts);
          return reject(this.translate.instant('Could not create using the specified extended public key'));
        }
      } else {
        let lang = this.languageProvider.getCurrent();
        try {
          walletClient.seedFromRandomWithMnemonic({
            network,
            passphrase: opts.passphrase,
            language: lang,
            account: 0,
            coin: opts.coin
          });
        } catch (e) {
          this.logger.info('Error creating recovery phrase: ' + e.message);
          if (e.message.indexOf('language') > 0) {
            this.logger.info('Using default language for recovery phrase');
            walletClient.seedFromRandomWithMnemonic({
              network,
              passphrase: opts.passphrase,
              account: 0,
              coin: opts.coin
            });
          } else {
            return reject(e);
          }
        }
      }
      return resolve(walletClient);
    });
  }

  // Creates a wallet on BWC/BWS
  private doCreateWallet(opts: any): Promise<any> {
    return new Promise((resolve, reject) => {

      let showOpts = _.clone(opts);
      if (showOpts.extendedPrivateKey) showOpts.extendedPrivateKey = '[hidden]';
      if (showOpts.mnemonic) showOpts.mnemonic = '[hidden]';

      this.logger.debug('Creating Wallet:', JSON.stringify(showOpts));
      setTimeout(() => {
        this.seedWallet(opts).then((walletClient: any) => {

          let name = opts.name || this.translate.instant('Personal Wallet');
          let myName = opts.myName || this.translate.instant('me');

          walletClient.createWallet(name, myName, opts.m, opts.n, {
            network: opts.networkName,
            singleAddress: opts.singleAddress,
            walletPrivKey: opts.walletPrivKey,
            coin: opts.coin
          }, (err: any, secret: any) => {
            if (err) {
              this.bwcErrorProvider.cb(err, this.translate.instant('Error creating wallet')).then((msg: string) => {
                return reject(msg);
              });
            } else {
              return resolve(walletClient);
            }
          });
        }).catch((err: any) => {
          return reject(err);
        });
      }, 50);
    });
  }

  // create and store a wallet
  public createWallet(opts: any): Promise<any> {
    return new Promise((resolve, reject) => {
      this.doCreateWallet(opts).then((walletClient: any) => {
        this.addAndBindWalletClient(walletClient, {
          bwsurl: opts.bwsurl
        }).then((wallet: any) => {
          return resolve(wallet);
        });
      }).catch((err: any) => {
        return reject(err);
      });
    });
  }

  // joins and stores a wallet
  public joinWallet(opts: any): Promise<any> {
    return new Promise((resolve, reject) => {
      this.logger.debug('Joining Wallet:', opts);

      try {
        var walletData = this.bwcProvider.parseSecret(opts.secret);

        // check if exist
        if (_.find(this.profile.credentials, {
          'walletId': walletData.walletId
        })) {
          return reject(this.translate.instant('Cannot join the same wallet more that once'));
        }
      } catch (ex) {
        this.logger.debug(ex);
        return reject(this.translate.instant('Bad wallet invitation'));
      }
      opts.networkName = walletData.network;
      this.logger.debug('Joining Wallet:', opts);

      this.seedWallet(opts).then((walletClient: any) => {
        walletClient.joinWallet(opts.secret, opts.myName || 'me', {
          coin: opts.coin
        }, (err: any) => {
          if (err) {
            this.bwcErrorProvider.cb(err, this.translate.instant('Could not join wallet')).then((msg: string) => {
              return reject(msg);
            });
          } else {
            this.addAndBindWalletClient(walletClient, {
              bwsurl: opts.bwsurl
            }).then((wallet: any) => {
              return resolve(wallet);
            });
          };
        });
      }).catch((err: any) => {
        return reject(err);
      });
    });
  }

  public getWallet(walletId: string): any {
    return this.wallet[walletId];
  };

  public deleteWalletClient(wallet: any): Promise<any> {
    return new Promise((resolve, reject) => {
      let walletId = wallet.credentials.walletId;

      this.logger.debug('Deleting Wallet:', wallet.credentials.walletName);
      wallet.removeAllListeners();

      this.profile.deleteWallet(walletId);

      delete this.wallet[walletId];

      this.persistenceProvider.removeAllWalletData(walletId).catch((err: any) => {
        this.logger.warn(err);
      });

      this.persistenceProvider.storeProfile(this.profile).then(() => {
        return resolve();
      }).catch((err: any) => {
        return reject(err);
      });
    });
  };

  public createDefaultWallet(): Promise<any> {
    return new Promise((resolve, reject) => {
      let opts: any = {};
      opts.m = 1;
      opts.n = 1;
      opts.networkName = 'livenet';
      opts.coin = 'btc';
      this.createWallet(opts).then((wallet: any) => {
        return resolve(wallet);
      }).catch((err) => {
        return reject(err);
      });
    });
  };

  public setDisclaimerAccepted(): Promise<any> {
    return new Promise((resolve, reject) => {
      this.profile.disclaimerAccepted = true;
      this.persistenceProvider.storeProfile(this.profile).then(() => {
        return resolve();
      }).catch((err) => {
        return reject(err);
      });
    });
  }

  public setOnboardingCompleted(): Promise<any> {
    return new Promise((resolve, reject) => {
      this.profile.onboardingCompleted = true;
      this.persistenceProvider.storeProfile(this.profile).then(() => {
        return resolve();
      }).catch((err) => {
        return reject(err);
      });
    });
  }

  public getWallets(opts?: any) {

    if (opts && !_.isObject(opts)) throw new Error("bad argument");

    opts = opts || {};

    let ret = _.values(this.wallet);

    if (opts.coin) {
      ret = _.filter(ret, (x: any) => {
        return (x.credentials.coin == opts.coin);
      });
    }

    if (opts.network) {
      ret = _.filter(ret, (x: any) => {
        return (x.credentials.network == opts.network);
      });
    }

    if (opts.n) {
      ret = _.filter(ret, (w: any) => {
        return (w.credentials.n == opts.n);
      });
    }

    if (opts.m) {
      ret = _.filter(ret, (w: any) => {
        return (w.credentials.m == opts.m);
      });
    }

    if (opts.hasFunds) {
      ret = _.filter(ret, (w: any) => {
        if (!w.status) return;
        return (w.status.availableBalanceSat > 0);
      });
    }

    if (opts.minAmount) {
      ret = _.filter(ret, (w: any) => {
        if (!w.status) return;
        return (w.status.availableBalanceSat > opts.minAmount);
      });
    }

    if (opts.onlyComplete) {
      ret = _.filter(ret, (w: any) => {
        return w.isComplete();
      });
    } else { }

    // Add cached balance async
    _.each(ret, (x: any) => {
      this.addLastKnownBalance(x);
    });

    return _.sortBy(ret, 'order');
  }

  public toggleHideBalanceFlag(walletId: string): void {
    this.wallet[walletId].balanceHidden = !this.wallet[walletId].balanceHidden;
    this.persistenceProvider.setHideBalanceFlag(walletId, this.wallet[walletId].balanceHidden);
  }

  public getNotifications(opts: any): Promise<any> {
    return new Promise((resolve, reject) => {
      opts = opts ? opts : {};

      let TIME_STAMP = 60 * 60 * 6;
      let MAX = 30;

      let typeFilter = {
        'NewOutgoingTx': 1,
        'NewIncomingTx': 1
      };

      let w = this.getWallets();
      if (_.isEmpty(w)) return reject('Could not find any wallet');

      let l = w.length;
      let j = 0;
      let notifications = [];


      let isActivityCached = (wallet: any): boolean => {
        return wallet.cachedActivity && wallet.cachedActivity.isValid;
      }


      let updateNotifications = (wallet: any): Promise<any> => {
        return new Promise((resolve, reject) => {

          if (isActivityCached(wallet) && !opts.force) {
            return resolve();
          }

          wallet.getNotifications({
            timeSpan: TIME_STAMP,
            includeOwn: true,
          }, (err: any, n: any) => {
            if (err) {
              return reject(err);
            }
            wallet.cachedActivity = {
              n: n.slice(-MAX),
              isValid: true,
            };

            return resolve();
          });
        });
      }

      let process = (notifications: any): any[] => {
        if (!notifications) return [];

        let shown = _.sortBy(notifications, 'createdOn').reverse();

        shown = shown.splice(0, opts.limit || MAX);

        _.each(shown, (x: any) => {
          x.txpId = x.data ? x.data.txProposalId : null;
          x.txid = x.data ? x.data.txid : null;
          x.types = [x.type];

          x.action = () => {
            // TODO?
            // $state.go('tabs.wallet', {
            //   walletId: x.walletId,
            //   txpId: x.txpId,
            //   txid: x.txid,
            // });
          };
        });

        // let finale = shown; GROUPING DISABLED!

        let finale = [];
        let prev: any;


        // Item grouping... DISABLED.

        // REMOVE (if we want 1-to-1 notification) ????
        _.each(shown, (x: any) => {
          if (prev && prev.walletId === x.walletId && prev.txpId && prev.txpId === x.txpId && prev.creatorId && prev.creatorId === x.creatorId) {
            prev.types.push(x.type);
            prev.data = _.assign(prev.data, x.data);
            prev.txid = prev.txid || x.txid;
            prev.creatorName = prev.creatorName || x.creatorName;
          } else {
            finale.push(x);
            prev = x;
          }
        });

        let u = this.bwcProvider.getUtils();
        _.each(finale, (x: any) => {
          if (x.data && x.data.message && x.wallet && x.wallet.credentials.sharedEncryptingKey) {
            // TODO TODO TODO => BWC
            x.message = u.decryptMessage(x.data.message, x.wallet.credentials.sharedEncryptingKey);
          }
        });

        return finale;
      }

      let pr = (wallet, cb) => {
        updateNotifications(wallet).then(() => {
          let n = _.filter(wallet.cachedActivity.n, (x: any) => {
            return typeFilter[x.type];
          });

          let idToName = {};
          if (wallet.cachedStatus) {
            _.each(wallet.cachedStatus.wallet.copayers, (c: any) => {
              idToName[c.id] = c.name;
            });
          }

          _.each(n, (x: any) => {
            x.wallet = wallet;
            if (x.creatorId && wallet.cachedStatus) {
              x.creatorName = idToName[x.creatorId];
            };
          });

          notifications.push(n);
          return cb();
        }).catch((err: any) => {
          return cb(err);
        });
      };

      _.each(w, (wallet: any) => {
        pr(wallet, (err) => {
          if (err)
            this.logger.warn(this.bwcErrorProvider.msg(err, 'Error updating notifications for ' + wallet.name));
          if (++j == l) {
            notifications = _.sortBy(notifications, 'createdOn').reverse();
            notifications = _.compact(_.flatten(notifications)).slice(0, MAX);
            let total = notifications.length;
            let processArray = process(notifications);
            return resolve({ notifications: processArray, total });
          }
        });
      });
    });
  }

  public getTxps(opts: any): Promise<any> {
    return new Promise((resolve, reject) => {
      let MAX = 100;
      opts = opts ? opts : {};

      let w = this.getWallets();
      if (_.isEmpty(w)) {
        return reject('No wallets available');
      }

      let txps = [];

      _.each(w, (x: any) => {
        if (x.pendingTxps)
          txps = txps.concat(x.pendingTxps);
      });
      let n = txps.length;
      txps = _.sortBy(txps, 'createdOn').reverse();
      txps = _.compact(_.flatten(txps)).slice(0, opts.limit || MAX);
      return resolve({ txps, n });
    });
  };

}
