'use strict';
angular.module('copayApp.services')
  .factory('profileService', function profileServiceFactory($rootScope, $timeout, $filter, $log, sjcl, lodash, storageService, bwcService, configService, gettextCatalog, bwcError, uxLanguage, platformInfo, txFormatService, $state) {


    var isChromeApp = platformInfo.isChromeApp;
    var isCordova = platformInfo.isCordova;
    var isWP = platformInfo.isWP;
    var isIOS = platformInfo.isIOS;

    var root = {};
    var errors = bwcService.getErrors();
    var usePushNotifications = isCordova && !isWP;

    var UPDATE_PERIOD = 15;

    root.profile = null;

    Object.defineProperty(root, "focusedClient", {
      get: function() {
        throw "focusedClient is not used any more"
      },
      set: function() {
        throw "focusedClient is not used any more"
      }
    });


    root.wallet = {}; // decorated version of client

    root.updateWalletSettings = function(wallet) {
      var defaults = configService.getDefaults();
      configService.whenAvailable(function(config) {
        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]);
        wallet.email = config.emailFor && config.emailFor[wallet.id];
      });
    }

    root.setBackupFlag = function(walletId) {
      storageService.setBackupFlag(walletId, function(err) {
        if (err) $log.error(err);
        $log.debug('Backup flag stored');
        root.wallet[walletId].needsBackup = false;
      });
    };

    function _requiresBackup(wallet) {
      if (wallet.isPrivKeyExternal()) return false;
      if (!wallet.credentials.mnemonic) return false;
      if (wallet.credentials.network == 'testnet') return false;

      return true;
    };

    function _needsBackup(wallet, cb) {
      if (!_requiresBackup(wallet))
        return cb(false);

      storageService.getBackupFlag(wallet.credentials.walletId, function(err, val) {
        if (err) $log.error(err);
        if (val) return cb(false);
        return cb(true);
      });
    };

    function _balanceIsHidden(wallet, cb) {
      storageService.getHideBalanceFlag(wallet.credentials.walletId, function(err, shouldHideBalance) {
        if (err) $log.error(err);
        var hideBalance = (shouldHideBalance == 'true') ? true : false;
        return cb(hideBalance);
      });
    };
    // Adds a wallet client to profileService
    root.bindWalletClient = function(wallet, opts) {
      var opts = opts || {};
      var walletId = wallet.credentials.walletId;

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

      // INIT WALLET VIEWMODEL
      wallet.id = walletId;
      wallet.started = true;
      wallet.doNotVerifyPayPro = isChromeApp;
      wallet.network = wallet.credentials.network;
      wallet.copayerId = wallet.credentials.copayerId;
      wallet.m = wallet.credentials.m;
      wallet.n = wallet.credentials.n;

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

      _needsBackup(wallet, function(val) {
        wallet.needsBackup = val;
      });

      _balanceIsHidden(wallet, function(val) {
        wallet.balanceHidden = val;
      });

      wallet.removeAllListeners();

      wallet.on('report', function(n) {
        $log.info('BWC Report:' + n);
      });

      wallet.on('notification', function(n) {

        $log.debug('BWC Notification:', n);

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

      wallet.on('walletCompleted', function() {
        $log.debug('Wallet completed');

        root.updateCredentials(JSON.parse(wallet.export()), function() {
          $rootScope.$emit('Local/WalletCompleted', walletId);
        });
      });

      wallet.initialize({
        notificationIncludeOwn: true,
      }, function(err) {
        if (err) {
          $log.error('Could not init notifications err:', err);
          return;
        }
        wallet.setNotificationsInterval(UPDATE_PERIOD);
        wallet.openWallet(function(err) {
          if (wallet.status !== true)
            $log.log('Wallet + ' + walletId + ' status:' + wallet.status)
        });
      });

      $rootScope.$on('Local/SettingsUpdated', function(e, walletId) {
        if (!walletId || walletId == wallet.id) {
          $log.debug('Updating settings for wallet:' + wallet.id);
          root.updateWalletSettings(wallet);
        }
      });

      return true;
    };

    var throttledBwsEvent = lodash.throttle(function(n, wallet) {
      newBwsEvent(n, wallet);
    }, 10000);

    var newBwsEvent = function(n, wallet) {
      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;

      $rootScope.$emit('bwsEvent', wallet.id, n.type, n);
    };

    var validationLock = false;

    root.runValidation = function(client, delay, retryDelay) {

      delay = delay || 500;
      retryDelay = retryDelay || 50;

      if (validationLock) {
        return $timeout(function() {
          $log.debug('ValidatingWallet Locked: Retrying in: ' + retryDelay);
          return root.runValidation(client, delay, retryDelay);
        }, retryDelay);
      }
      validationLock = true;

      // IOS devices are already checked
      var skipDeviceValidation = isIOS || root.profile.isDeviceChecked(platformInfo.ua);
      var walletId = client.credentials.walletId;

      $log.debug('ValidatingWallet: ' + walletId + ' skip Device:' + skipDeviceValidation);
      $timeout(function() {
        client.validateKeyDerivation({
          skipDeviceValidation: skipDeviceValidation,
        }, function(err, isOK) {
          validationLock = false;

          $log.debug('ValidatingWallet End:  ' + walletId + ' isOK:' + isOK);
          if (isOK) {
            root.profile.setChecked(platformInfo.ua, walletId);
          } else {
            $log.warn('Key Derivation failed for wallet:' + walletId);
            storageService.clearLastAddress(walletId, function() {});
          }

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

    var shouldSkipValidation = function(walletId) {
      return root.profile.isChecked(platformInfo.ua, walletId) || isIOS || isWP;
    }
    // Used when reading wallets from the profile
    root.bindWallet = function(credentials, cb) {
      if (!credentials.walletId || !credentials.m)
        return cb('bindWallet should receive credentials JSON');

      // Create the client
      var getBWSURL = function(walletId) {
        var config = configService.getSync();
        var defaults = configService.getDefaults();
        return ((config.bwsFor && config.bwsFor[walletId]) || defaults.bws.url);
      };


      var client = bwcService.getClient(JSON.stringify(credentials), {
        bwsurl: getBWSURL(credentials.walletId),
      });

      var skipKeyValidation = shouldSkipValidation(credentials.walletId);
      if (!skipKeyValidation)
        root.runValidation(client, 500);

      $log.info('Binding wallet:' + credentials.walletId + ' Validating?:' + !skipKeyValidation);
      return cb(null, root.bindWalletClient(client));
    };

    root.bindProfile = function(profile, cb) {
      root.profile = profile;

      configService.get(function(err) {
        $log.debug('Preferences read');
        if (err) return cb(err);

        function bindWallets(cb) {
          var l = root.profile.credentials.length;
          var i = 0,
            totalBound = 0;

          if (!l) return cb();

          lodash.each(root.profile.credentials, function(credentials) {
            root.bindWallet(credentials, function(err, bound) {
              i++;
              totalBound += bound;
              if (i == l) {
                $log.info('Bound ' + totalBound + ' out of ' + l + ' wallets');
                return cb();
              }
            });
          });
        }

        bindWallets(function() {
          root.isBound = true;

          lodash.each(root._queue, function(x) {
            $timeout(function() {
              return x();
            }, 1);
          });
          root._queue = [];



          root.isDisclaimerAccepted(function(val) {
            if (!val) {
              return cb(new Error('NONAGREEDDISCLAIMER: Non agreed disclaimer'));
            }
            return cb();
          });
        });
      });
    };

    root._queue = [];
    root.whenAvailable = function(cb) {
      if (!root.isBound) {
        root._queue.push(cb);
        return;
      }
      return cb();
    };

    root.loadAndBindProfile = function(cb) {
      storageService.getProfile(function(err, profile) {
        if (err) {
          $rootScope.$emit('Local/DeviceError', err);
          return cb(err);
        }
        if (!profile) {
          // Migration??
          storageService.tryToMigrate(function(err, migratedProfile) {
            if (err) return cb(err);
            if (!migratedProfile)
              return cb(new Error('NOPROFILE: No profile'));

            profile = migratedProfile;
            return root.bindProfile(profile, cb);
          })
        } else {
          $log.debug('Profile read');
          return root.bindProfile(profile, cb);
        }
      });
    };

    var seedWallet = function(opts, cb) {
      opts = opts || {};
      var walletClient = bwcService.getClient(null, opts);
      var network = opts.networkName || 'livenet';

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

        } catch (ex) {
          $log.info(ex);
          return cb(gettextCatalog.getString('Could not create: Invalid wallet recovery phrase'));
        }
      } else if (opts.extendedPrivateKey) {
        try {
          walletClient.seedFromExtendedPrivateKey(opts.extendedPrivateKey);
        } catch (ex) {
          $log.warn(ex);
          return cb(gettextCatalog.getString('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',
          });
          walletClient.credentials.hwInfo = opts.hwInfo;
        } catch (ex) {
          $log.warn("Creating wallet from Extended Public Key Arg:", ex, opts);
          return cb(gettextCatalog.getString('Could not create using the specified extended public key'));
        }
      } else {
        var lang = uxLanguage.getCurrentLanguage();
        try {
          walletClient.seedFromRandomWithMnemonic({
            network: network,
            passphrase: opts.passphrase,
            language: lang,
            account: 0,
          });
        } catch (e) {
          $log.info('Error creating recovery phrase: ' + e.message);
          if (e.message.indexOf('language') > 0) {
            $log.info('Using default language for recovery phrase');
            walletClient.seedFromRandomWithMnemonic({
              network: network,
              passphrase: opts.passphrase,
              account: 0,
            });
          } else {
            return cb(e);
          }
        }
      }
      return cb(null, walletClient);
    };

    // Creates a wallet on BWC/BWS
    var doCreateWallet = function(opts, cb) {
      $log.debug('Creating Wallet:', opts);
      $timeout(function() {
        seedWallet(opts, function(err, walletClient) {
          if (err) return cb(err);

          var name = opts.name || gettextCatalog.getString('Personal Wallet');
          var myName = opts.myName || gettextCatalog.getString('me');

          walletClient.createWallet(name, myName, opts.m, opts.n, {
            network: opts.networkName,
            singleAddress: opts.singleAddress,
            walletPrivKey: opts.walletPrivKey,
          }, function(err, secret) {
            if (err) return bwcError.cb(err, gettextCatalog.getString('Error creating wallet'), cb);
            return cb(null, walletClient, secret);
          });
        });
      }, 50);
    };

    // create and store a wallet
    root.createWallet = function(opts, cb) {
      doCreateWallet(opts, function(err, walletClient, secret) {
        if (err) return cb(err);

        addAndBindWalletClient(walletClient, {
          bwsurl: opts.bwsurl
        }, cb);
      });
    };

    // joins and stores a wallet
    root.joinWallet = function(opts, cb) {
      var walletClient = bwcService.getClient();
      $log.debug('Joining Wallet:', opts);

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

        // check if exist
        if (lodash.find(root.profile.credentials, {
            'walletId': walletData.walletId
          })) {
          return cb(gettextCatalog.getString('Cannot join the same wallet more that once'));
        }
      } catch (ex) {
        $log.debug(ex);
        return cb(gettextCatalog.getString('Bad wallet invitation'));
      }
      opts.networkName = walletData.network;
      $log.debug('Joining Wallet:', opts);

      seedWallet(opts, function(err, walletClient) {
        if (err) return cb(err);

        walletClient.joinWallet(opts.secret, opts.myName || 'me', {}, function(err) {
          if (err) return bwcError.cb(err, gettextCatalog.getString('Could not join wallet'), cb);
          addAndBindWalletClient(walletClient, {
            bwsurl: opts.bwsurl
          }, cb);
        });
      });
    };

    root.getWallet = function(walletId) {
      return root.wallet[walletId];
    };


    root.deleteWalletClient = function(client, cb) {
      var walletId = client.credentials.walletId;

      var config = configService.getSync();

      $log.debug('Deleting Wallet:', client.credentials.walletName);
      client.removeAllListeners();

      root.profile.deleteWallet(walletId);

      delete root.wallet[walletId];

      storageService.removeAllWalletData(walletId, function(err) {
        if (err) $log.warn(err);
      });

      storageService.storeProfile(root.profile, function(err) {
        if (err) return cb(err);
        return cb();
      });
    };

    root.setMetaData = function(walletClient, addressBook, cb) {
      storageService.getAddressbook(walletClient.credentials.network, function(err, localAddressBook) {
        var localAddressBook1 = {};
        try {
          localAddressBook1 = JSON.parse(localAddressBook);
        } catch (ex) {
          $log.warn(ex);
        }
        var mergeAddressBook = lodash.merge(addressBook, localAddressBook1);
        storageService.setAddressbook(walletClient.credentials.network, JSON.stringify(addressBook), function(err) {
          if (err) return cb(err);
          return cb(null);
        });
      });
    }

    // Adds and bind a new client to the profile
    var addAndBindWalletClient = function(client, opts, cb) {
      if (!client || !client.credentials)
        return cb(gettextCatalog.getString('Could not access wallet'));

      var walletId = client.credentials.walletId

      if (!root.profile.addWallet(JSON.parse(client.export())))
        return cb(gettextCatalog.getString('Wallet already in Copay'));


      var skipKeyValidation = shouldSkipValidation(walletId);
      if (!skipKeyValidation)
        root.runValidation(client);

      root.bindWalletClient(client);

      var saveBwsUrl = function(cb) {
        var defaults = configService.getDefaults();
        var bwsFor = {};
        bwsFor[walletId] = opts.bwsurl || defaults.bws.url;

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

        configService.set({
          bwsFor: bwsFor,
        }, function(err) {
          if (err) $log.warn(err);
          return cb();
        });
      };

      saveBwsUrl(function() {
        storageService.storeProfile(root.profile, function(err) {
          return cb(err, client);
        });
      });
    };

    root.storeProfileIfDirty = function(cb) {
      if (root.profile.dirty) {
        storageService.storeProfile(root.profile, function(err) {
          $log.debug('Saved modified Profile');
          if (cb) return cb(err);
        });
      } else {
        if (cb) return cb();
      };
    };

    root.importWallet = function(str, opts, cb) {

      var walletClient = bwcService.getClient(null, opts);

      $log.debug('Importing Wallet:', opts);

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

        if (c.xPrivKey && c.xPrivKeyEncrypted) {
          $log.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 cb(gettextCatalog.getString('Could not import. Check input file and spending password'));
      }

      str = JSON.parse(str);

      if (!str.n) {
        return cb("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");
      }

      var addressBook = str.addressBook || {};

      addAndBindWalletClient(walletClient, {
        bwsurl: opts.bwsurl
      }, function(err, walletId) {
        if (err) return cb(err);
        root.setMetaData(walletClient, addressBook, function(error) {
          if (error) $log.warn(error);
          return cb(err, walletClient);
        });
      });
    };

    root.importExtendedPrivateKey = function(xPrivKey, opts, cb) {
      var walletClient = bwcService.getClient(null, opts);
      $log.debug('Importing Wallet xPrivKey');

      walletClient.importFromExtendedPrivateKey(xPrivKey, opts, function(err) {
        if (err) {
          if (err instanceof errors.NOT_AUTHORIZED)
            return cb(err);

          return bwcError.cb(err, gettextCatalog.getString('Could not import'), cb);
        }

        addAndBindWalletClient(walletClient, {
          bwsurl: opts.bwsurl
        }, cb);
      });
    };

    root._normalizeMnemonic = function(words) {
      if (!words || !words.indexOf) return words;
      var isJA = words.indexOf('\u3000') > -1;
      var wordList = words.split(/[\u3000\s]+/);

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

    root.importMnemonic = function(words, opts, cb) {
      var walletClient = bwcService.getClient(null, opts);

      $log.debug('Importing Wallet Mnemonic');

      words = root._normalizeMnemonic(words);
      walletClient.importFromMnemonic(words, {
        network: opts.networkName,
        passphrase: opts.passphrase,
        account: opts.account || 0,
      }, function(err) {
        if (err) {
          if (err instanceof errors.NOT_AUTHORIZED)
            return cb(err);

          return bwcError.cb(err, gettextCatalog.getString('Could not import'), cb);
        }

        addAndBindWalletClient(walletClient, {
          bwsurl: opts.bwsurl
        }, cb);
      });
    };

    root.importExtendedPublicKey = function(opts, cb) {
      var walletClient = bwcService.getClient(null, opts);
      $log.debug('Importing Wallet XPubKey');

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

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

          return bwcError.cb(err, gettextCatalog.getString('Could not import'), cb);
        }

        addAndBindWalletClient(walletClient, {
          bwsurl: opts.bwsurl
        }, cb);
      });
    };

    root.createProfile = function(cb) {
      $log.info('Creating profile');
      var defaults = configService.getDefaults();

      configService.get(function(err) {
        if (err) $log.debug(err);

        var p = Profile.create();
        storageService.storeNewProfile(p, function(err) {
          if (err) return cb(err);
          root.bindProfile(p, function(err) {
            // ignore NONAGREEDDISCLAIMER
            if (err && err.toString().match('NONAGREEDDISCLAIMER')) return cb();
            return cb(err);
          });
        });
      });
    };

    root.createDefaultWallet = function(cb) {
      var opts = {};
      opts.m = 1;
      opts.n = 1;
      opts.networkName = 'livenet';
      root.createWallet(opts, cb);
    };

    root.setDisclaimerAccepted = function(cb) {
      root.profile.disclaimerAccepted = true;
      storageService.storeProfile(root.profile, function(err) {
        return cb(err);
      });
    };

    root.isDisclaimerAccepted = function(cb) {
      var disclaimerAccepted = root.profile && root.profile.disclaimerAccepted;
      if (disclaimerAccepted)
        return cb(true);

      // OLD flag
      storageService.getCopayDisclaimerFlag(function(err, val) {
        if (val) {
          root.profile.disclaimerAccepted = true;
          return cb(true);
        } else {
          return cb();
        }
      });
    };

    root.updateCredentials = function(credentials, cb) {
      root.profile.updateWallet(credentials);
      storageService.storeProfile(root.profile, cb);
    };

    root.getLastKnownBalance = function(wid, cb) {
      storageService.getBalanceCache(wid, cb);
    };

    root.addLastKnownBalance = function(wallet, cb) {
      var now = Math.floor(Date.now() / 1000);
      var showRange = 600; // 10min;

      root.getLastKnownBalance(wallet.id, function(err, data) {
        if (data) {
          data = JSON.parse(data);
          wallet.cachedBalance = data.balance;
          wallet.cachedBalanceUpdatedOn = (data.updatedOn < now - showRange) ? data.updatedOn : null;
        }
        return cb();
      });
    };

    root.setLastKnownBalance = function(wid, balance, cb) {
      storageService.setBalanceCache(wid, {
        balance: balance,
        updatedOn: Math.floor(Date.now() / 1000),
      }, cb);
    };

    root.getWallets = function(opts) {

      if (opts && !lodash.isObject(opts))
        throw "bad argument";

      opts = opts || {};

      var ret = lodash.values(root.wallet);

      if (opts.network) {
        ret = lodash.filter(ret, function(x) {
          return (x.credentials.network == opts.network);
        });
      }

      if (opts.n) {
        ret = lodash.filter(ret, function(w) {
          return (w.credentials.n == opts.n);
        });
      }

      if (opts.m) {
        ret = lodash.filter(ret, function(w) {
          return (w.credentials.m == opts.m);
        });
      }

      if (opts.hasFunds) {
        ret = lodash.filter(ret, function(w) {
          return (w.status.availableBalanceSat > 0);
        });
      }

      if (opts.minAmount) {
        ret = lodash.filter(ret, function(w) {
          return (w.status.availableBalanceSat > opts.minAmount);
        });
      }

      if (opts.onlyComplete) {
        ret = lodash.filter(ret, function(w) {
          return w.isComplete();
        });
      } else {}

      // Add cached balance async
      lodash.each(ret, function(x) {
        root.addLastKnownBalance(x, function() {});
      });


      return lodash.sortBy(ret, [

        function(x) {
          return x.isComplete();
        }, 'createdOn'
      ]);
    };

    root.toggleHideBalanceFlag = function(walletId, cb) {
      root.wallet[walletId].balanceHidden = !root.wallet[walletId].balanceHidden;
      storageService.setHideBalanceFlag(walletId, root.wallet[walletId].balanceHidden.toString(), cb);
    };

    root.getNotifications = function(opts, cb) {
      opts = opts || {};

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

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

      var w = root.getWallets();
      if (lodash.isEmpty(w)) return cb();

      var l = w.length,
        j = 0,
        notifications = [];


      function isActivityCached(wallet) {
        return wallet.cachedActivity && wallet.cachedActivity.isValid;
      };


      function updateNotifications(wallet, cb2) {
        if (isActivityCached(wallet) && !opts.force) return cb2();

        wallet.getNotifications({
          timeSpan: TIME_STAMP,
          includeOwn: true,
        }, function(err, n) {
          if (err) return cb2(err);

          wallet.cachedActivity = {
            n: n.slice(-MAX),
            isValid: true,
          };

          return cb2();
        });
      };

      function process(notifications) {
        if (!notifications) return [];

        var shown = lodash.sortBy(notifications, 'createdOn').reverse();

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

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

          if (x.data && x.data.amount)
            x.amountStr = txFormatService.formatAmountStr(x.data.amount);

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

        var finale = shown; // GROUPING DISABLED!

        var finale = [],
          prev;


        // Item grouping... DISABLED.

        // REMOVE (if we want 1-to-1 notification) ????
        lodash.each(shown, function(x) {
          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 = lodash.assign(prev.data, x.data);
            prev.txid = prev.txid || x.txid;
            prev.amountStr = prev.amountStr || x.amountStr;
            prev.creatorName = prev.creatorName || x.creatorName;
          } else {
            finale.push(x);
            prev = x;
          }
        });

        var u = bwcService.getUtils();
        lodash.each(finale, function(x) {
          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;
      };

      lodash.each(w, function(wallet) {
        updateNotifications(wallet, function(err) {
          j++;
          if (err) {
            $log.warn('Error updating notifications:' + err);
          } else {

            var n;

            n = lodash.filter(wallet.cachedActivity.n, function(x) {
              return typeFilter[x.type];
            });

            var idToName = {};
            if (wallet.cachedStatus) {
              lodash.each(wallet.cachedStatus.wallet.copayers, function(c) {
                idToName[c.id] = c.name;
              });
            }

            lodash.each(n, function(x) {
              x.wallet = wallet;
              if (x.creatorId && wallet.cachedStatus) {
                x.creatorName = idToName[x.creatorId];
              };
            });

            notifications.push(n);
          }
          if (j == l) {
            notifications = lodash.sortBy(notifications, 'createdOn');
            notifications = lodash.compact(lodash.flatten(notifications)).slice(0, MAX);
            var total = notifications.length;
            return cb(null, process(notifications), total);
          };
        });
      });
    };


    root.getTxps = function(opts, cb) {
      var MAX = 100;
      opts = opts || {};

      var w = root.getWallets();
      if (lodash.isEmpty(w)) return cb();

      var txps = [];

      lodash.each(w, function(x) {
        if (x.pendingTxps)
          txps = txps.concat(x.pendingTxps);
      });
      var n = txps.length;
      txps = lodash.sortBy(txps, 'pendingForUs', 'createdOn');
      txps = lodash.compact(lodash.flatten(txps)).slice(0, opts.limit || MAX);
      return cb(null, txps, n);
    };

    return root;
  });
