const _ = require('lodash/fp')
const jsonRpc = require('../../common/json-rpc')

const BN = require('../../../bn')
const E = require('../../../error')
const logger = require('../../../logger')
const { utils: coinUtils } = require('@lamassu/coins')

const cryptoRec = coinUtils.getCryptoCurrency('BTC')
const unitScale = cryptoRec.unitScale

const rpcConfig = jsonRpc.rpcConfig(cryptoRec)

const SUPPORTS_BATCHING = true

function fetch (method, params) {
  return jsonRpc.fetch(rpcConfig, method, params)
}

function errorHandle (e) {
  const err = JSON.parse(e.message)
  switch (err.code) {
    case -4:
      return loadWallet()
    case -5:
      return logger.error(`${err}`)
    case -6:
      throw new E.InsufficientFundsError()
    case -18:
      return createWallet()
    case -35:
      // Wallet is already loaded, just return
      return
    default:
      throw e
  }
}

function checkCryptoCode (cryptoCode) {
  if (cryptoCode !== 'BTC') return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
  return Promise.resolve().then(loadWallet)
}

function createWallet () {
  return fetch('createwallet', ['wallet'])
    .then(loadWallet)
}

function loadWallet () {
  return fetch('loadwallet', ['wallet', true])
    // Catching the error here to suppress error code -35
    // This improves UX on the initial wallet load and serves as error sink
    // for wallet creation/loading related issues before actual business logic runs
    .catch(errorHandle)
}

function accountBalance (cryptoCode) {
  return checkCryptoCode(cryptoCode)
    .then(() => fetch('getwalletinfo'))
    .then(({ balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
    .catch(errorHandle)
}

function accountUnconfirmedBalance (cryptoCode) {
  return checkCryptoCode(cryptoCode)
    .then(() => fetch('getwalletinfo'))
    .then(({ unconfirmed_balance: balance }) => new BN(balance).shiftedBy(unitScale).decimalPlaces(0))
    .catch(errorHandle)
}

// We want a balance that includes all spends (0 conf) but only deposits that
// have at least 1 confirmation. getbalance does this for us automatically.
function balance (account, cryptoCode, settings, operatorId) {
  return accountBalance(cryptoCode)
}

function estimateFee () {
  return fetch('estimatesmartfee', [6, 'unset'])
    .then(result => BN(result.feerate))
    .catch(() => {})
}

function calculateFeeDiscount (feeMultiplier) {
  // 0 makes bitcoind do automatic fee selection
  const AUTOMATIC_FEE = 0
  if (!feeMultiplier || feeMultiplier.eq(1)) return AUTOMATIC_FEE
  return estimateFee()
    .then(estimatedFee => {
      if (!estimatedFee) return AUTOMATIC_FEE
      const newFee = estimatedFee.times(feeMultiplier)
      if (newFee.lt(0.00001) || newFee.gt(0.1)) return AUTOMATIC_FEE
      return newFee
    })
}

function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
  const { toAddress, cryptoAtoms, cryptoCode } = tx
  const coins = cryptoAtoms.shiftedBy(-unitScale).toFixed(8)

  return checkCryptoCode(cryptoCode)
    .then(() => calculateFeeDiscount(feeMultiplier))
    .then(newFee => fetch('settxfee', [newFee]))
    .then(() => fetch('sendtoaddress', [toAddress, coins]))
    .then((txId) => fetch('gettransaction', [txId]))
    .then((res) => _.pick(['fee', 'txid'], res))
    .then((pickedObj) => {
      return {
        fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
        txid: pickedObj.txid
      }
    })
    .catch(errorHandle)
}

function sendCoinsBatch (account, txs, cryptoCode, feeMultiplier) {
  return checkCryptoCode(cryptoCode)
    .then(() => calculateFeeDiscount(feeMultiplier))
    .then(newFee => fetch('settxfee', [newFee]))
    .then(() => _.reduce((acc, value) => ({
      ...acc,
      [value.toAddress]: _.isNil(acc[value.toAddress])
        ? BN(value.cryptoAtoms).shiftedBy(-unitScale).toFixed(8)
        : BN(acc[value.toAddress]).plus(BN(value.cryptoAtoms).shiftedBy(-unitScale).toFixed(8))
    }), {}, txs))
    .then((obj) => fetch('sendmany', ['', obj]))
    .then((txId) => fetch('gettransaction', [txId]))
    .then((res) => _.pick(['fee', 'txid'], res))
    .then((pickedObj) => ({
      fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
      txid: pickedObj.txid
    }))
    .catch(errorHandle)
}

function newAddress (account, info, tx, settings, operatorId) {
  return checkCryptoCode(info.cryptoCode)
    .then(() => fetch('getnewaddress'))
    .catch(errorHandle)
}

function addressBalance (address, confs) {
  return fetch('getreceivedbyaddress', [address, confs])
    .then(r => new BN(r).shiftedBy(unitScale).decimalPlaces(0))
    .catch(errorHandle)
}

function confirmedBalance (address, cryptoCode) {
  return checkCryptoCode(cryptoCode)
    .then(() => addressBalance(address, 1))
}

function pendingBalance (address, cryptoCode) {
  return checkCryptoCode(cryptoCode)
    .then(() => addressBalance(address, 0))
}

function getStatus (account, tx, requested, settings, operatorId) {
  const { toAddress, cryptoCode } = tx
  return checkCryptoCode(cryptoCode)
    .then(() => confirmedBalance(toAddress, cryptoCode))
    .then(confirmed => {
      if (confirmed.gte(requested)) return { receivedCryptoAtoms: confirmed, status: 'confirmed' }

      return pendingBalance(toAddress, cryptoCode)
        .then(pending => {
          if (pending.gte(requested)) return { receivedCryptoAtoms: pending, status: 'authorized' }
          if (pending.gt(0)) return { receivedCryptoAtoms: pending, status: 'insufficientFunds' }
          return { receivedCryptoAtoms: pending, status: 'notSeen' }
        })
    })
}

function newFunding (account, cryptoCode, settings, operatorId) {
  return checkCryptoCode(cryptoCode)
    .then(() => {
      const promises = [
        accountUnconfirmedBalance(cryptoCode),
        accountBalance(cryptoCode),
        newAddress(account, { cryptoCode })
      ]

      return Promise.all(promises)
    })
    .then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
      fundingPendingBalance,
      fundingConfirmedBalance,
      fundingAddress
    }))
    .catch(errorHandle)
}

function cryptoNetwork (account, cryptoCode, settings, operatorId) {
  return checkCryptoCode(cryptoCode)
    .then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main')
}

function fetchRBF (txId) {
  return fetch('getmempoolentry', [txId])
    .then((res) => {
      return [txId, res['bip125-replaceable']]
    })
    .catch(err => {
      errorHandle(err)
      return [txId, true]
    })
}

function checkBlockchainStatus (cryptoCode) {
  return checkCryptoCode(cryptoCode)
    .then(() => fetch('getblockchaininfo'))
    .then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
}

module.exports = {
  balance,
  sendCoins,
  newAddress,
  getStatus,
  newFunding,
  cryptoNetwork,
  fetchRBF,
  estimateFee,
  sendCoinsBatch,
  checkBlockchainStatus,
  SUPPORTS_BATCHING
}
