#!/usr/bin/env node

require('../lib/environment-helper')

const hdkey = require('ethereumjs-wallet/hdkey')
const hkdf = require('futoin-hkdf')
const crypto = require('crypto')
const path = require('path')
const pify = require('pify')
const fs = pify(require('fs'))
const _ = require('lodash/fp')
const { BigNumber } = require('bignumber.js')
const coins = require('@lamassu/coins')
const Web3 = require('web3')
const web3 = new Web3()
const Tx = require('ethereumjs-tx')

const mnemonicHelpers = require('../lib/mnemonic-helpers')
const settingsLoader = require('../lib/new-settings-loader')
const BN = require('../lib/bn')
const ph = require('../lib/plugin-helper')
const configManager = require('../lib/new-config-manager')
const walletI = require('../lib/wallet')

const LOCKFILE_PATH = '/var/lock/lamassu-eth-pending-sweep'
const defaultPrefixPath = "m/44'/60'/1'/0'"
let lastUsedNonces = {}

const hex = bigNum => '0x' + bigNum.integerValue(BN.ROUND_DOWN).toString(16)
const MNEMONIC_PATH = process.env.MNEMONIC_PATH

function writeNewMnemonic (mnemonic) {
  return fs.writeFile(`${MNEMONIC_PATH}-new-temp`, mnemonic)
    .then(() => `${MNEMONIC_PATH}-new-temp`)
}

function renameNewMnemonic () {
  return fs.rename(`${MNEMONIC_PATH}-new-temp`, `${MNEMONIC_PATH}`)
    .then(() => MNEMONIC_PATH)
}

function backupMnemonic () {
  const folderPath = path.dirname(MNEMONIC_PATH)
  const fileName = path.resolve(folderPath, `mnemonic-${Date.now()}.txt`)
  return fs.copyFile(MNEMONIC_PATH, fileName)
    .then(() => fileName)
}

function computeSeed (seed) {
  const masterSeed = mnemonicHelpers.toEntropyBuffer(seed)
  return hkdf(masterSeed, 32, { salt: 'lamassu-server-salt', info: 'wallet-seed' })
}

function computeOperatorId (masterSeed) {
  return hkdf(masterSeed, 16, { salt: 'lamassu-server-salt', info: 'operator-id' }).toString('hex')
}

function generateRandomSeed () {
  const seed = crypto
    .randomBytes(32)
    .toString('hex')

  return Buffer.from(seed, 'hex')
}

function generateNewMnemonic (newSeed) {
  return mnemonicHelpers.fromSeed(newSeed)
}

function defaultWallet (seed) {
  return defaultHdNode(seed).deriveChild(0).getWallet()
}

function defaultWalletAcc (account) {
  return defaultHdNodeAcc(account).deriveChild(0).getWallet()
}

function defaultAddress (seed) {
  return defaultWallet(seed).getChecksumAddressString()
}

function defaultHdNode (seed) {
  const key = hdkey.fromMasterSeed(seed)
  return key.derivePath(defaultPrefixPath)
}

function defaultHdNodeAcc (account) {
  const key = hdkey.fromMasterSeed(account.seed)
  return key.derivePath(defaultPrefixPath)
}

function getAllBalance () {
  return settingsLoader.loadLatest()
    .then(settings => walletI.balance(settings, 'ETH'))
    .then(r => r.balance)
}

function isInfuraRunning (settings) {
  const isInfuraSelected = settings.config.wallets_ETH_wallet === 'infura'
  const isInfuraConfigured =
    !_.isNil(settings.accounts.infura) 
    && !_.isNil(settings.accounts.infura.apiKey)
    && !_.isNil(settings.accounts.infura.apiSecret)
    && !_.isNil(settings.accounts.infura.endpoint)

  return isInfuraSelected && isInfuraConfigured
}

function isGethRunning (settings) {
  return walletI.checkBlockchainStatus(settings, 'ETH')
    .then(res => res === 'ready')
    .catch(() => false)
}

function connect (url) {
  if (!web3.isConnected()) {
    web3.setProvider(new web3.providers.HttpProvider(url))
  }
}

function sendCoins (account, tx, settings, operatorId, feeMultiplier, _opts) {
  const { toAddress, cryptoAtoms, cryptoCode } = tx
  const opts = { ..._opts, includesFee: _.defaultTo(false, _opts?.includesFee) }
  return generateTx(toAddress, defaultWalletAcc(account), cryptoAtoms, cryptoCode, opts)
    .then(pify(web3.eth.sendRawTransaction))
    .then(txid => {
      return pify(web3.eth.getTransaction)(txid)
        .then(tx => {
          if (!tx) return { txid }

          const fee = new BN(tx.gas).times(new BN(tx.gasPrice)).decimalPlaces(0)

          return { txid, fee }
        })
    })
}

function generateTx (_toAddress, wallet, amount, cryptoCode, opts) {
  const fromAddress = '0x' + wallet.getAddress().toString('hex')

  const isErc20Token = coins.utils.isErc20Token(cryptoCode)
  const toAddress = isErc20Token ? coins.utils.getErc20Token(cryptoCode).contractAddress : _toAddress.toLowerCase()

  let contract, contractData
  if (isErc20Token) {
    contract = web3.eth.contract(ABI.ERC20).at(toAddress)
    contractData = isErc20Token && contract.transfer.getData(_toAddress.toLowerCase(), hex(toSend))
  }

  const txTemplate = {
    from: fromAddress,
    to: toAddress,
    value: amount.toString()
  }

  if (isErc20Token) txTemplate.data = contractData

  const promises = [
    pify(web3.eth.estimateGas)(txTemplate),
    pify(web3.eth.getGasPrice)(),
    pify(web3.eth.getTransactionCount)(fromAddress)
  ]

  return Promise.all(promises)
    .then(([gas, gasPrice, txCount]) => [
      BN(gas),
      BN(gasPrice),
      _.max([0, txCount, lastUsedNonces[fromAddress] + 1])
    ])
    .then(([gas, gasPrice, txCount]) => {
      lastUsedNonces[fromAddress] = txCount

      const toSend = opts.includesFee
        ? amount.minus(gasPrice.times(gas))
        : amount

      const rawTx = {
        chainId: _.defaultTo(1, opts?.chainId),
        nonce: _.defaultTo(txCount, opts?.nonce),
        gasPrice: hex(gasPrice),
        gasLimit: hex(gas),
        to: toAddress,
        from: fromAddress,
        value: isErc20Token ? hex(BN(0)) : hex(toSend)
      }

      if (isErc20Token) {
        rawTx.data = contractData
      }

      const tx = new Tx(rawTx)
      const privateKey = wallet.getPrivateKey()

      tx.sign(privateKey)

      return '0x' + tx.serialize().toString('hex')
    })
}

function fetchWallet (settings, cryptoCode) {
  return fs.readFile(MNEMONIC_PATH, 'utf8')
    .then(mnemonic => {
      const computeSeed = masterSeed =>
        hkdf(masterSeed, 32, { salt: 'lamassu-server-salt', info: 'wallet-seed' })

      const masterSeed = mnemonicHelpers.toEntropyBuffer(mnemonic)
      const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet
      const wallet = ph.load(ph.WALLET, plugin)
      const rawAccount = settings.accounts[plugin]
      const account = _.set('seed', computeSeed(masterSeed), rawAccount)
      if (_.isFunction(wallet.run)) wallet.run(account)
      const operatorId = computeOperatorId(masterSeed)
      return { wallet, account, operatorId }
    })
}

fs.exists(LOCKFILE_PATH, function(exists) {
  if (!exists) {
    console.log('Couldn\'t find the lamassu-eth-pending-sweep lock file, exiting...')
    process.exit(1)
  }
})

const seed = generateRandomSeed()
const mnemonic = generateNewMnemonic(seed)
const mnemonicSeed = computeSeed(mnemonic)
const newAddress = defaultAddress(mnemonicSeed)

settingsLoader.loadLatest()
  .then(settings => Promise.all([isInfuraRunning(settings), isGethRunning(settings), settings]))
  .then(([infuraIsRunning, gethIsRunning, settings]) => {
    if (!infuraIsRunning && !gethIsRunning) {
      console.log('Neither geth nor Infura are running, so the script cannot be executed.')
      process.exit(2)
    }

    console.log(`Backing up old mnemonic...`)
    return Promise.all([backupMnemonic(), infuraIsRunning, settings])
  })
  .then(([fileName, infuraIsRunning, settings]) => {
    console.log(`Successfully backed up the old mnemonic, new location is ${fileName}`)
    return Promise.all([writeNewMnemonic(mnemonic), infuraIsRunning, settings])
  })
  .then(([tempMnemonicFileName, infuraIsRunning, settings]) => {
    console.log(`New mnemonic stored temporarily in ${tempMnemonicFileName}`)
    console.log(`Starting funds transfer...`)
    return Promise.all([infuraIsRunning, settings])
  })
  .then(([infuraIsRunning, settings]) => {
    if (infuraIsRunning) {
      const endpoint = _.startsWith('https://')(settings.accounts.infura.endpoint)
        ? settings.accounts.infura.endpoint
        : `https://${settings.accounts.infura.endpoint}`
      connect(endpoint)
    } else {
      connect(`http://localhost:${coins.utils.getCryptoCurrency('ETH').defaultPort}`)
    }

    return Promise.all([getAllBalance(), settings, fetchWallet(settings, 'ETH')])
  })
  .then(([balance, settings, { account, operatorId }]) => {
    const tx = {
      cryptoCode: 'ETH',
      toAddress: newAddress,
      cryptoAtoms: BN(balance.times(0.99999).toFixed(0, BigNumber.ROUND_DOWN))
    }

    const opts = {
      chainId: 1,
      nonce: 0,
      includesFee: true
    }

    return sendCoins(account, tx, settings, operatorId, null, opts)
  })
  .then(resTx => {
    console.log('Successfully moved funds from the old wallet to the new one.')
    console.log('Information about the transaction', resTx)
    console.log('Moving the current mnemonic to the default file...')
    return renameNewMnemonic()
  })
  .then(() => {
    console.log('New mnemonic stored successfully! All your funds (minus the transaction fee) should be available in the next few minutes.')
    return fs.rmdir(LOCKFILE_PATH)
  })
  .then(() => {
    console.log('lamassu-eth-pending-sweep lock file successfully removed')
    return fs.mkdir(`${LOCKFILE_PATH}-finished`)
  })
  .then(() => {
    console.log('lamassu-eth-pending-sweep-finished lock file successfully created, this will automatically be deleted once the upgrade script finishes running')
    console.log('Process finished successfully! You may now execute the upgrade script again')
    process.exit(0)
  })
  .catch(err => {
    console.error(err)
    process.exit(1)
  })
