'use strict';

angular.module('copayApp.services').factory('walletService', function($log, $timeout, lodash, trezor, ledger, intelTEE, storageService, configService, rateService, uxLanguage, $filter, gettextCatalog, bwcError, $ionicPopup, fingerprintService, ongoingProcess, gettext, $rootScope, txFormatService, $ionicModal, $state, bwcService, bitcore, popupService) {
  // `wallet` is a decorated version of client.

  var root = {};

  root.externalSource = {
    ledger: ledger.description,
    trezor: trezor.description,
    intelTEE: intelTEE.description
  }

  root.WALLET_STATUS_MAX_TRIES = 7;
  root.WALLET_STATUS_DELAY_BETWEEN_TRIES = 1.4 * 1000;
  root.SOFT_CONFIRMATION_LIMIT = 12;
  root.SAFE_CONFIRMATIONS = 6;

  var errors = bwcService.getErrors();

  var _signWithLedger = function(wallet, txp, cb) {
    $log.info('Requesting Ledger Chrome app to sign the transaction');

    ledger.signTx(txp, wallet.credentials.account, function(result) {
      $log.debug('Ledger response', result);
      if (!result.success)
        return cb(result.message || result.error);

      txp.signatures = lodash.map(result.signatures, function(s) {
        return s.substring(0, s.length - 2);
      });
      return wallet.signTxProposal(txp, cb);
    });
  };

  var _signWithTrezor = function(wallet, txp, cb) {
    $log.info('Requesting Trezor  to sign the transaction');

    var xPubKeys = lodash.pluck(wallet.credentials.publicKeyRing, 'xPubKey');
    trezor.signTx(xPubKeys, txp, wallet.credentials.account, function(err, result) {
      if (err) return cb(err);

      $log.debug('Trezor response', result);
      txp.signatures = result.signatures;
      return wallet.signTxProposal(txp, cb);
    });
  };

  var _signWithIntelTEE = function(wallet, txp, cb) {
    $log.info('Requesting Intel TEE to sign the transaction');

    intelTEE.signTx(wallet.credentials.hwInfo.id, txp, function(err, result) {
      if (err) return cb(err);

      $log.debug('Intel TEE response', result);
      txp.signatures = result.Signatures;
      return wallet.signTxProposal(txp, cb);
    });
  };

  root.showMneumonicFromHardware = function(wallet, cb) {
    switch (wallet.getPrivKeyExternalSourceName()) {
      case root.externalSource.intelTEE.id:
        return intelTEE.showMneumonic(wallet.credentials.hwInfo.id, cb);
        break;
      default:
        cb('Error: unrecognized external source');
        break;
    }
  };

  root.showReceiveAddressFromHardware = function(wallet, address, cb) {
    switch (wallet.getPrivKeyExternalSourceName()) {
      case root.externalSource.intelTEE.id:
        root.getAddressObj(wallet, address, function(err, addrObj) {
          if (err) return cb(err);
          return intelTEE.showReceiveAddress(wallet.credentials.hwInfo.id, addrObj, cb);
        });
        break;
      default:
        cb('Error: unrecognized external source');
        break;
    }
  };

  root.invalidateCache = function(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;
  };

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


    function processPendingTxps(status) {
      var txps = status.pendingTxps;
      var now = Math.floor(Date.now() / 1000);

      /* To test multiple outputs...
      var txp = {
        message: 'test multi-output',
        fee: 1000,
        createdOn: new Date() / 1000,
        outputs: []
      };
      function addOutput(n) {
        txp.outputs.push({
          amount: 600,
          toAddress: '2N8bhEwbKtMvR2jqMRcTCQqzHP6zXGToXcK',
          message: 'output #' + (Number(n) + 1)
        });
      };
      lodash.times(150, addOutput);
      txps.push(txp);
      */

      lodash.each(txps, function(tx) {

        tx = txFormatService.processTx(tx);

        // no future transactions...
        if (tx.createdOn > now)
          tx.createdOn = now;

        tx.wallet = wallet;

        if (!tx.wallet) {
          $log.error("no wallet at txp?");
          return;
        }

        var action = lodash.find(tx.actions, {
          copayerId: tx.wallet.copayerId
        });

        if (!action && tx.status == 'pending') {
          tx.pendingForUs = true;
        }

        if (action && action.type == 'accept') {
          tx.statusForUs = 'accepted';
        } else if (action && action.type == 'reject') {
          tx.statusForUs = 'rejected';
        } else {
          tx.statusForUs = 'pending';
        }

        if (!tx.deleteLockTime)
          tx.canBeRemoved = true;
      });

      wallet.pendingTxps = txps;
    };


    function get(cb) {
      wallet.getStatus({
        twoStep: true
      }, function(err, ret) {
        if (err) {
          if (err instanceof errors.NOT_AUTHORIZED) {
            return cb('WALLET_NOT_REGISTERED');
          }
          return cb(err);
        }

        return cb(null, ret);
      });
    };

    function cacheBalance(wallet, balance) {
      if (!balance) return;

      var config = configService.getSync().wallet;

      var cache = wallet.cachedStatus;

      // Address with Balance
      cache.balanceByAddress = balance.byAddress;

      // Total wallet balance is same regardless of 'spend unconfirmed funds' setting.
      cache.totalBalanceSat = balance.totalAmount;

      // Spend unconfirmed funds
      if (config.spendUnconfirmed) {
        cache.lockedBalanceSat = balance.lockedAmount;
        cache.availableBalanceSat = balance.availableAmount;
        cache.totalBytesToSendMax = balance.totalBytesToSendMax;
        cache.pendingAmount = 0;
        cache.spendableAmount = balance.totalAmount - balance.lockedAmount;
      } else {
        cache.lockedBalanceSat = balance.lockedConfirmedAmount;
        cache.availableBalanceSat = balance.availableConfirmedAmount;
        cache.totalBytesToSendMax = balance.totalBytesToSendConfirmedMax;
        cache.pendingAmount = balance.totalAmount - balance.totalConfirmedAmount;
        cache.spendableAmount = balance.totalConfirmedAmount - balance.lockedAmount;
      }

      // Selected unit
      cache.unitToSatoshi = config.settings.unitToSatoshi;
      cache.satToUnit = 1 / cache.unitToSatoshi;
      cache.unitName = config.settings.unitName;

      //STR
      cache.totalBalanceStr = txFormatService.formatAmount(cache.totalBalanceSat) + ' ' + cache.unitName;
      cache.lockedBalanceStr = txFormatService.formatAmount(cache.lockedBalanceSat) + ' ' + cache.unitName;
      cache.availableBalanceStr = txFormatService.formatAmount(cache.availableBalanceSat) + ' ' + cache.unitName;
      cache.spendableBalanceStr = txFormatService.formatAmount(cache.spendableAmount) + ' ' + cache.unitName;
      cache.pendingBalanceStr = txFormatService.formatAmount(cache.pendingAmount) + ' ' + cache.unitName;

      cache.alternativeName = config.settings.alternativeName;
      cache.alternativeIsoCode = config.settings.alternativeIsoCode;

      // Check address
      root.isAddressUsed(wallet, balance.byAddress, function(err, used) {
        if (used) {
          $log.debug('Address used. Creating new');
          // Force new address
          root.getAddress(wallet, true, function(err, addr) {
            $log.debug('New address: ', addr);
          });
        }
      });

      rateService.whenAvailable(function() {

        var totalBalanceAlternative = rateService.toFiat(cache.totalBalanceSat, cache.alternativeIsoCode);
        var pendingBalanceAlternative = rateService.toFiat(cache.pendingAmount, cache.alternativeIsoCode);
        var lockedBalanceAlternative = rateService.toFiat(cache.lockedBalanceSat, cache.alternativeIsoCode);
        var spendableBalanceAlternative = rateService.toFiat(cache.spendableAmount, cache.alternativeIsoCode);
        var alternativeConversionRate = rateService.toFiat(100000000, cache.alternativeIsoCode);

        cache.totalBalanceAlternative = $filter('formatFiatAmount')(totalBalanceAlternative);
        cache.pendingBalanceAlternative = $filter('formatFiatAmount')(pendingBalanceAlternative);
        cache.lockedBalanceAlternative = $filter('formatFiatAmount')(lockedBalanceAlternative);
        cache.spendableBalanceAlternative = $filter('formatFiatAmount')(spendableBalanceAlternative);
        cache.alternativeConversionRate = $filter('formatFiatAmount')(alternativeConversionRate);

        cache.alternativeBalanceAvailable = true;
        cache.isRateAvailable = true;
      });
    };

    function isStatusCached() {
      return wallet.cachedStatus && wallet.cachedStatus.isValid;
    };

    function cacheStatus(status) {
      wallet.cachedStatus = status ||  {};
      var cache = wallet.cachedStatus;
      cache.statusUpdatedOn = Date.now();
      cache.isValid = true;
      cache.email = status.preferences ? status.preferences.email : null;
      cacheBalance(wallet, status.balance);
    };

    function walletStatusHash(status) {
      return status ? status.balance.totalAmount : wallet.totalBalanceSat;
    };

    function _getStatus(initStatusHash, tries, cb) {
      if (isStatusCached() && !opts.force) {
        $log.debug('Wallet status cache hit:' + wallet.id);
        cacheStatus(wallet.cachedStatus);
        processPendingTxps(wallet.cachedStatus);
        return cb(null, wallet.cachedStatus);
      };

      tries = tries || 0;

      $log.debug('Updating Status:', wallet.credentials.walletName, tries);
      get(function(err, status) {
        if (err) return cb(err);

        var currentStatusHash = walletStatusHash(status);
        $log.debug('Status update. hash:' + currentStatusHash + ' Try:' + tries);
        if (opts.untilItChanges &&
          initStatusHash == currentStatusHash &&
          tries < root.WALLET_STATUS_MAX_TRIES &&
          walletId == wallet.credentials.walletId) {
          return $timeout(function() {
            $log.debug('Retrying update... ' + walletId + ' Try:' + tries)
            return _getStatus(initStatusHash, ++tries, cb);
          }, root.WALLET_STATUS_DELAY_BETWEEN_TRIES * tries);
        }

        processPendingTxps(status);

        $log.debug('Got Wallet Status for:' + wallet.credentials.walletName);

        cacheStatus(status);

        return cb(null, status);
      });
    };

    _getStatus(walletStatusHash(), 0, cb);
  };

  var getSavedTxs = function(walletId, cb) {
    storageService.getTxHistory(walletId, function(err, txs) {
      if (err) return cb(err);

      var localTxs = [];

      if (!txs) {
        return cb(null, localTxs);
      }

      try {
        localTxs = JSON.parse(txs);
      } catch (ex) {
        $log.warn(ex);
      }
      return cb(null, lodash.compact(localTxs));
    });
  };

  var getTxsFromServer = function(wallet, skip, endingTxid, limit, cb) {
    var res = [];

    wallet.getTxHistory({
      skip: skip,
      limit: limit
    }, function(err, txsFromServer) {
      if (err) return cb(err);

      if (!txsFromServer.length)
        return cb();

      var res = lodash.takeWhile(txsFromServer, function(tx) {
        return tx.txid != endingTxid;
      });

      return cb(null, res, res.length >= limit);
    });
  };

  var removeAndMarkSoftConfirmedTx = function(txs) {
    return lodash.filter(txs, function(tx) {
      if (tx.confirmations >= root.SOFT_CONFIRMATION_LIMIT)
        return tx;
      tx.recent = true;
    });
  }

  var processNewTxs = function(wallet, txs) {
    var config = configService.getSync().wallet.settings;
    var now = Math.floor(Date.now() / 1000);
    var txHistoryUnique = {};
    var ret = [];
    wallet.hasUnsafeConfirmed = false;

    lodash.each(txs, function(tx) {
      tx = txFormatService.processTx(tx);

      // no future transactions...
      if (tx.time > now)
        tx.time = now;

      if (tx.confirmations >= root.SAFE_CONFIRMATIONS) {
        tx.safeConfirmed = root.SAFE_CONFIRMATIONS + '+';
      } else {
        tx.safeConfirmed = false;
        wallet.hasUnsafeConfirmed = true;
      }

      if (tx.note) {
        delete tx.note.encryptedEditedByName;
        delete tx.note.encryptedBody;
      }

      if (!txHistoryUnique[tx.txid]) {
        ret.push(tx);
        txHistoryUnique[tx.txid] = true;
      } else {
        $log.debug('Ignoring duplicate TX in history: ' + tx.txid)
      }
    });

    return ret;
  };

  var updateLocalTxHistory = function(wallet, opts, cb) {
    var FIRST_LIMIT = 5;
    var LIMIT = 50;
    var requestLimit = FIRST_LIMIT;
    var walletId = wallet.credentials.walletId;
    var config = configService.getSync().wallet.settings;

    var opts = opts || {};
    var progressFn = opts.progressFn || function() {};
    var foundLimitTx = false;

    var fixTxsUnit = function(txs) {
      if (!txs || !txs[0] || !txs[0].amountStr) return;

      var cacheUnit = txs[0].amountStr.split(' ')[1];

      if (cacheUnit == config.unitName)
        return;

      var name = ' ' + config.unitName;

      $log.debug('Fixing Tx Cache Unit to:' + name)
      lodash.each(txs, function(tx) {

        tx.amountStr = txFormatService.formatAmount(tx.amount) + name;
        tx.feeStr = txFormatService.formatAmount(tx.fees) + name;
      });
    };

    getSavedTxs(walletId, function(err, txsFromLocal) {
      if (err) return cb(err);

      fixTxsUnit(txsFromLocal);

      var confirmedTxs = removeAndMarkSoftConfirmedTx(txsFromLocal);
      var endingTxid = confirmedTxs[0] ? confirmedTxs[0].txid : null;
      var endingTs = confirmedTxs[0] ? confirmedTxs[0].time : null;

      // First update
      progressFn(txsFromLocal, 0);
      wallet.completeHistory = txsFromLocal;

      function getNewTxs(newTxs, skip, next) {
        getTxsFromServer(wallet, skip, endingTxid, requestLimit, function(err, res, shouldContinue) {
          if (err) {
            $log.warn(bwcError.msg(err, 'Server Error')); //TODO
            if (err instanceof errors.CONNECTION_ERROR || (err.message && err.message.match(/5../))) {
              $log.info('Retrying history download in 5 secs...');
              return $timeout(function() {
                return getNewTxs(newTxs, skip, next);
              }, 5000);
            };
            return next(err);
          }

          newTxs = newTxs.concat(processNewTxs(wallet, lodash.compact(res)));

          progressFn(newTxs.concat(txsFromLocal), newTxs.length);

          skip = skip + requestLimit;

          $log.debug('Syncing TXs. Got:' + newTxs.length + ' Skip:' + skip, ' EndingTxid:', endingTxid, ' Continue:', shouldContinue);

          // TODO Dirty <HACK>
          // do not sync all history, just looking for a single TX.
          if (opts.limitTx) {

            foundLimitTx = lodash.find(newTxs, {
              txid: opts.limitTx,
            });

            if (foundLimitTx) {
              $log.debug('Found limitTX: ' + opts.limitTx);
              return next(null, newTxs);
            }
          }
          // </HACK>


          if (!shouldContinue) {
            $log.debug('Finished Sync: New / soft confirmed Txs: ' + newTxs.length);
            return next(null, newTxs);
          }

          requestLimit = LIMIT;
          getNewTxs(newTxs, skip, next);
        });
      };

      getNewTxs([], 0, function(err, txs) {
        if (err) return cb(err);

        var newHistory = lodash.uniq(lodash.compact(txs.concat(confirmedTxs)), function(x) {
          return x.txid;
        });


        function updateNotes(cb2) {
          if (!endingTs) return cb2();

          $log.debug('Syncing notes from: ' + endingTs);
          wallet.getTxNotes({
            minTs: endingTs
          }, function(err, notes) {
            if (err) {
              $log.warn(err);
              return cb2();
            };
            lodash.each(notes, function(note) {
              $log.debug('Note for ' + note.txid);
              lodash.each(newHistory, function(tx) {
                if (tx.txid == note.txid) {
                  $log.debug('...updating note for ' + note.txid);
                  tx.note = note;
                }
              });
            });
            return cb2();
          });
        }

        updateNotes(function() {

          // <HACK>
          if (foundLimitTx) {
            $log.debug('Tx history read until limitTx: ' + opts.limitTx);
            return cb(null, newHistory);
          }
          // </HACK>

          var historyToSave = JSON.stringify(newHistory);

          lodash.each(txs, function(tx) {
            tx.recent = true;
          })

          $log.debug('Tx History synced. Total Txs: ' + newHistory.length);

          // Final update
          if (walletId == wallet.credentials.walletId) {
            wallet.completeHistory = newHistory;
          }

          return storageService.setTxHistory(historyToSave, walletId, function() {
            $log.debug('Tx History saved.');

            return cb();
          });
        });
      });
    });
  };

  root.getTxNote = function(wallet, txid, cb) {
    wallet.getTxNote({
      txid: txid
    }, function(err, note) {
      if (err) return cb(err);
      return cb(null, note);
    });
  };

  root.editTxNote = function(wallet, args, cb) {
    wallet.editTxNote(args, function(err, res) {
      return cb(err, res);
    });
  };

  root.getTxp = function(wallet, txpid, cb) {
    wallet.getTx(txpid, function(err, txp) {
      if (err) return cb(err);
      return cb(null, txp);
    });
  };

  root.getTx = function(wallet, txid, cb) {

    function finish(list){
      var tx = lodash.find(list, {
          txid: txid
      });

      if (!tx) return cb('Could not get transaction');
      return cb(null, tx);
    };

    if (wallet.completeHistory && wallet.completeHistory.isValid) {
      finish(wallet.completeHistory);
    } else {
      root.getTxHistory(wallet, {
        limitTx: txid
      }, function(err, txHistory) {
        if (err) return cb(err);

        finish(txHistory);
      });
    }
  };


  root.clearTxHistory = function(wallet, cb) {
    root.invalidateCache(wallet);

    storageService.removeTxHistory(wallet.id, function(err) {
      if (err) {
        $log.error(err);
        return cb(err);
      }
      return cb();
    });
  };

 

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

    var walletId = wallet.credentials.walletId;

    if (!wallet.isComplete()) return cb();

    function isHistoryCached() {
      return wallet.completeHistory && wallet.completeHistory.isValid;
    };

    if (isHistoryCached() && !opts.force) return cb(null, wallet.completeHistory);

    $log.debug('Updating Transaction History');

    updateLocalTxHistory(wallet, opts, function(err, txs) {
      if (err) return cb(err);

      if (opts.limitTx) {
        return cb(err, txs);
      }

      wallet.completeHistory.isValid = true;
      return cb(err, wallet.completeHistory);
    });
  };

  root.isEncrypted = function(wallet) {
    if (lodash.isEmpty(wallet)) return;
    var isEncrypted = wallet.isPrivKeyEncrypted();
    if (isEncrypted) $log.debug('Wallet is encrypted');
    return isEncrypted;
  };

  root.createTx = function(wallet, txp, cb) {
    if (lodash.isEmpty(txp) || lodash.isEmpty(wallet))
      return cb('MISSING_PARAMETER');

    wallet.createTxProposal(txp, function(err, createdTxp) {
      if (err) return cb(err);
      else {
        $log.debug('Transaction created');
        return cb(null, createdTxp);
      }
    });
  };

  root.publishTx = function(wallet, txp, cb) {
    if (lodash.isEmpty(txp) || lodash.isEmpty(wallet))
      return cb('MISSING_PARAMETER');

    wallet.publishTxProposal({
      txp: txp
    }, function(err, publishedTx) {
      if (err) return cb(err);
      else {
        $log.debug('Transaction published');
        return cb(null, publishedTx);
      }
    });
  };

  root.signTx = function(wallet, txp, password, cb) {
    if (!wallet || !txp || !cb)
      return cb('MISSING_PARAMETER');

    if (wallet.isPrivKeyExternal()) {
      switch (wallet.getPrivKeyExternalSourceName()) {
        case root.externalSource.ledger.id:
          return _signWithLedger(wallet, txp, cb);
        case root.externalSource.trezor.id:
          return _signWithTrezor(wallet, txp, cb);
        case root.externalSource.intelTEE.id:
          return _signWithIntelTEE(wallet, txp, cb);
        default:
          var msg = 'Unsupported External Key:' + wallet.getPrivKeyExternalSourceName();
          $log.error(msg);
          return cb(msg);
      }
    } else {

      try {
        wallet.signTxProposal(txp, password, function(err, signedTxp) {
          $log.debug('Transaction signed err:' + err);
          return cb(err, signedTxp);
        });
      } catch (e) {
        $log.warn('Error at signTxProposal:', e);
        return cb(e);
      }
    }
  };

  root.broadcastTx = function(wallet, txp, cb) {
    if (lodash.isEmpty(txp) || lodash.isEmpty(wallet))
      return cb('MISSING_PARAMETER');

    if (txp.status != 'accepted')
      return cb('TX_NOT_ACCEPTED');

    wallet.broadcastTxProposal(txp, function(err, broadcastedTxp, memo) {
      if (err)
        return cb(err);

      $log.debug('Transaction broadcasted');
      if (memo) $log.info(memo);

      return cb(null, broadcastedTxp);
    });
  };

  root.rejectTx = function(wallet, txp, cb) {
    if (lodash.isEmpty(txp) || lodash.isEmpty(wallet))
      return cb('MISSING_PARAMETER');

    wallet.rejectTxProposal(txp, null, function(err, rejectedTxp) {
      $log.debug('Transaction rejected');
      return cb(err, rejectedTxp);
    });
  };

  root.removeTx = function(wallet, txp, cb) {
    if (lodash.isEmpty(txp) || lodash.isEmpty(wallet))
      return cb('MISSING_PARAMETER');

    wallet.removeTxProposal(txp, function(err) {
      $log.debug('Transaction removed');

      root.invalidateCache(wallet);
      $rootScope.$emit('Local/TxAction', wallet.id);

      return cb(err);
    });
  };

  root.updateRemotePreferences = function(clients, prefs, cb) {
    prefs = prefs || {};
    cb = cb || function() {};

    if (!lodash.isArray(clients))
      clients = [clients];

    function updateRemotePreferencesFor(clients, prefs, next) {
      var wallet = clients.shift();
      if (!wallet) return next();
      $log.debug('Saving remote preferences', wallet.credentials.walletName, prefs);

      wallet.savePreferences(prefs, function(err) {

        if (err) {
          popupService.showAlert(bwcError.msg(err, gettextCatalog.getString('Could not save preferences on the server')));
          return next(err);
        }

        updateRemotePreferencesFor(clients, prefs, next);
      });
    };

    // Update this JIC.
    var config = configService.getSync().wallet.settings;

    //prefs.email  (may come from arguments)
    prefs.language = uxLanguage.getCurrentLanguage();
    prefs.unit = config.unitCode;

    updateRemotePreferencesFor(lodash.clone(clients), prefs, function(err) {
      if (err) return cb(err);

      $log.debug('Remote preferences saved for' + lodash.map(clients, function(x) {
        return x.credentials.walletId;
      }).join(','));

      lodash.each(clients, function(c) {
        c.preferences = lodash.assign(prefs, c.preferences);
      });
      return cb();
    });
  };

  root.recreate = function(wallet, cb) {
    $log.debug('Recreating wallet:', wallet.id);
    ongoingProcess.set('recreating', true);
    wallet.recreateWallet(function(err) {
      wallet.notAuthorized = false;
      ongoingProcess.set('recreating', false);
      return cb(err);
    });
  };

  root.startScan = function(wallet, cb) {
    cb = cb || function() {};

    $log.debug('Scanning wallet ' + wallet.id);
    if (!wallet.isComplete()) return;

    wallet.updating = true;
    ongoingProcess.set('scanning', true);
    wallet.startScan({
      includeCopayerBranches: true,
    }, function(err) {
      wallet.updating = false;
      ongoingProcess.set('scanning', false);
      return cb(err);
    });
  };


  root.expireAddress = function(wallet, cb) {
    $log.debug('Cleaning Address ' + wallet.id);
    storageService.clearLastAddress(wallet.id, function(err) {
      return cb(err);
    });
  };

  // Check address
  root.isAddressUsed = function(wallet, byAddress, cb) {
    storageService.getLastAddress(wallet.id, function(err, addr) {
      var used = lodash.find(byAddress, {
        address: addr
      });
      return cb(err, used);
    });
  };

  var createAddress = function(wallet, cb) {
    $log.debug('Creating address for wallet:', wallet.id);

    wallet.createAddress({}, function(err, addr) {
      if (err) {
        var prefix = gettextCatalog.getString('Could not create address');
        if (err.error && err.error.match(/locked/gi)) {
          $log.debug(err.error);
          return $timeout(function() {
            createAddress(wallet, cb);
          }, 5000);
        } else if (err.message && err.message == 'MAIN_ADDRESS_GAP_REACHED') {
          $log.warn(err.message);
          prefix = null;
          wallet.getMainAddresses({
            reverse: true,
            limit: 1
          }, function(err, addr) {
            if (err) return cb(err);
            return cb(null, addr[0].address);
          });
        }
        return bwcError.cb(err, prefix, cb);
      }
      return cb(null, addr.address);
    });
  };

  root.getMainAddresses = function(wallet, opts, cb) {
    opts = opts || {};
    opts.reverse = true;
    wallet.getMainAddresses(opts, function(err, addresses) {
      return cb(err, addresses);
    });
  };

  root.getBalance = function(wallet, opts, cb) {
    opts = opts || {};
    wallet.getBalance(opts, function(err, resp) {
      return cb(err, resp);
    });
  };

  root.getAddress = function(wallet, forceNew, cb) {
    storageService.getLastAddress(wallet.id, function(err, addr) {
      if (err) return cb(err);

      if (!forceNew && addr) return cb(null, addr);

      if (!wallet.isComplete())
        return cb('WALLET_NOT_COMPLETE');

      createAddress(wallet, function(err, _addr) {
        if (err) return cb(err, addr);
        storageService.storeLastAddress(wallet.id, _addr, function() {
          if (err) return cb(err);
          return cb(null, _addr);
        });
      });
    });
  };

  root.getAddressObj = function(wallet, address, cb) {
    wallet.getMainAddresses({
      reverse: true
    }, function(err, addr) {
      if (err) return cb(err);
      var addrObj = lodash.find(addr, function(a) {
        return a.address == address;
      });
      var err = null;
      if (!addrObj) {
        err = 'Error: specified address not in wallet';
      }
      return cb(err, addrObj);
    });
  };

  root.isReady = function(wallet, cb) {
    if (!wallet.isComplete())
      return cb('WALLET_NOT_COMPLETE');

    if (wallet.needsBackup)
      return cb('WALLET_NEEDS_BACKUP');
    return cb();
  };


  // An alert dialog
  var askPassword = function(name, title, cb) {
    var opts = {
      inputType: 'password',
      forceHTMLPrompt: true,
      class: 'text-warn'
    };
    popupService.showPrompt(title, name, opts, function(res) {
      if (!res) return cb();
      if (res) return cb(res)
    });
  };


  root.encrypt = function(wallet, cb) {
    var title = gettextCatalog.getString('Enter new spending password');
    var warnMsg = gettextCatalog.getString('Your wallet key will be encrypted. The Spending Password cannot be recovered. Be sure to write it down.');
    askPassword(warnMsg, title, function(password) {
      if (!password) return cb('no password');
      title = gettextCatalog.getString('Confirm your new spending password');
      askPassword(warnMsg, title, function(password2) {
        if (!password2 || password != password2)
          return cb('password mismatch');

        wallet.encryptPrivateKey(password);
        return cb();
      });
    });
  };


  root.decrypt = function(wallet, cb) {
    $log.debug('Disabling private key encryption for' + wallet.name);
    askPassword(null, gettextCatalog.getString('Enter Spending Password'), function(password) {
      if (!password) return cb('no password');

      try {
        wallet.decryptPrivateKey(password);
      } catch (e) {
        return cb(e);
      }
      return cb();
    });
  };

  root.handleEncryptedWallet = function(wallet, cb) {
    if (!root.isEncrypted(wallet)) return cb();

    askPassword(wallet.name, gettextCatalog.getString('Enter Spending Password'), function(password) {
      if (!password) return cb('No password');
      if (!wallet.checkPassword(password)) return cb('Wrong password');

      return cb(null, password);
    });
  };


  root.reject = function(wallet, txp, cb) {
    ongoingProcess.set('rejectTx', true);
    root.rejectTx(wallet, txp, function(err, txpr) {
      root.invalidateCache(wallet);
      ongoingProcess.set('rejectTx', false);

      if (err) return cb(err);

      $rootScope.$emit('Local/TxAction', wallet.id);
      return cb(null, txpr);
    });
  };


  root.onlyPublish = function(wallet, txp, cb, customStatusHandler) {
    ongoingProcess.set('sendingTx', true, customStatusHandler);
    root.publishTx(wallet, txp, function(err, publishedTxp) {
      root.invalidateCache(wallet);
      ongoingProcess.set('sendingTx', false, customStatusHandler);
      if (err) return cb(bwcError.msg(err));
      $rootScope.$emit('Local/TxAction', wallet.id);
      return cb();
    });
  };


  root.prepare = function(wallet, cb) {
    fingerprintService.check(wallet, function(err) {
      if (err) return cb(err);

      root.handleEncryptedWallet(wallet, function(err, password) {
        if (err) return cb(err);

        return cb(null, password);
      });
    });
  };

  root.publishAndSign = function(wallet, txp, cb, customStatusHandler) {

    var publishFn = root.publishTx;

    // Already published?
    if (txp.status == 'pending') {
      publishFn = function(wallet, txp, cb) {
        return cb(null, txp);
      };
    }

    root.prepare(wallet, function(err, password) {
      if (err) return cb(bwcError.msg(err));

      ongoingProcess.set('sendingTx', true, customStatusHandler);

      publishFn(wallet, txp, function(err, publishedTxp) {
        ongoingProcess.set('sendingTx', false, customStatusHandler);
        if (err) return cb(bwcError.msg(err));

        ongoingProcess.set('signingTx', true, customStatusHandler);
        root.signTx(wallet, publishedTxp, password, function(err, signedTxp) {
          ongoingProcess.set('signingTx', false, customStatusHandler);
          root.invalidateCache(wallet);


          if (err) {
            $log.warn('sign error:' + err);
            var msg = err && err.message ?
              err.message :
              gettextCatalog.getString('The payment was created but could not be completed. Please try again from home screen');

            $rootScope.$emit('Local/TxAction', wallet.id);
            return cb(msg);
          }

          if (signedTxp.status == 'accepted') {
            ongoingProcess.set('broadcastingTx', true, customStatusHandler);
            root.broadcastTx(wallet, signedTxp, function(err, broadcastedTxp) {
              ongoingProcess.set('broadcastingTx', false, customStatusHandler);
              if (err) return cb(bwcError.msg(err));

              $rootScope.$emit('Local/TxAction', wallet.id);
              return cb(null, broadcastedTxp);
            });
          } else {
            $rootScope.$emit('Local/TxAction', wallet.id);
            return cb(null, signedTxp);
          }
        });
      });
    });
  };

  root.getEncodedWalletInfo = function(wallet, password, cb) {
    var derivationPath = wallet.credentials.getBaseAddressDerivationPath();
    var encodingType = {
      mnemonic: 1,
      xpriv: 2,
      xpub: 3
    };
    var info;

    // not supported yet
    if (wallet.credentials.derivationStrategy != 'BIP44' || !wallet.canSign())
      return cb(gettextCatalog.getString('Exporting via QR not supported for this wallet'));

    var keys = root.getKeysWithPassword(wallet, password);

    if (keys.mnemonic) {
      info = {
        type: encodingType.mnemonic,
        data: keys.mnemonic,
      }
    } else {
      info = {
        type: encodingType.xpriv,
        data: keys.xPrivKey
      }
    }

    return cb(null, info.type + '|' + info.data + '|' + wallet.credentials.network.toLowerCase() + '|' + derivationPath + '|' + (wallet.credentials.mnemonicHasPassphrase));
  };

  root.setTouchId = function(wallet, enabled, cb) {

    var opts = {
      touchIdFor: {}
    };
    opts.touchIdFor[wallet.id] = enabled;

    fingerprintService.check(wallet, function(err) {
      if (err) {
        opts.touchIdFor[wallet.id] = !enabled;
        $log.debug('Error with fingerprint:' + err);
        return cb(err);
      }
      configService.set(opts, cb);
    });
  };

  root.getKeys = function(wallet, cb) {
    root.prepare(wallet, function(err, password) {
      if (err) return cb(err);
      var keys;

      try {
        keys = wallet.getKeys(password);
      } catch (e) {
        return cb(e);
      }

      return cb(null, keys);
    });
  };

  root.getKeysWithPassword = function(wallet, password) {
    try {
      return wallet.getKeys(password);
    } catch (e) {}
  }

  root.getSendMaxInfo = function(wallet, opts, cb) {
    opts = opts || {};
    wallet.getSendMaxInfo(opts, function(err, res) {
      return cb(err, res);
    });
  };

  return root;
});
