#!/usr/bin/env node

'use strict';

const Config = require('../lib/node/config');
const util = require('../lib/utils/util');
const Client = require('../lib/http/client');
const Wallet = require('../lib/http/wallet');

const ANTIREPLAY = ''
  + '6a2e426974636f696e3a204120506565722d746f2d5065657'
  + '220456c656374726f6e696320436173682053797374656d';

function CLI() {
  this.config = new Config('bcoin');

  this.config.load({
    argv: true,
    env: true
  });

  this.config.open('bcoin.conf');

  this.argv = this.config.argv;
  this.client = null;
  this.wallet = null;
}

CLI.prototype.log = function log(json) {
  if (typeof json === 'string')
    return console.log.apply(console, arguments);
  return console.log(JSON.stringify(json, null, 2));
};

CLI.prototype.getInfo = async function getInfo() {
  const info = await this.client.getInfo();
  this.log(info);
};

CLI.prototype.getWallets = async function getWallets() {
  const wallets = await this.client.getWallets();
  this.log(wallets);
};

CLI.prototype.createWallet = async function createWallet() {
  const options = {
    id: this.config.str([0, 'id']),
    type: this.config.str('type'),
    master: this.config.str('master'),
    mnemonic: this.config.str('mnemonic'),
    m: this.config.uint('m'),
    n: this.config.uint('n'),
    witness: this.config.bool('witness'),
    passphrase: this.config.str('passphrase'),
    watchOnly: false,
    accountKey: null
  };

  if (this.config.has('watch')) {
    options.watchOnly = true;
    options.accountKey = this.config.str('watch');
  }

  const wallet = await this.client.createWallet(options);

  this.log(wallet);
};

CLI.prototype.getMaster = async function getMaster() {
  const master = await this.wallet.getMaster();
  this.log(master);
};

CLI.prototype.getKey = async function getKey() {
  const address = this.config.str(0);
  const key = await this.wallet.getKey(address);
  this.log(key);
};

CLI.prototype.getWIF = async function getWIF() {
  const address = this.config.str(0);
  const passphrase = this.config.str('passphrase');
  const key = await this.wallet.getWIF(address, passphrase);
  if (!key) {
    this.log('Key not found.');
    return;
  }
  this.log(key.privateKey);
};

CLI.prototype.addSharedKey = async function addSharedKey() {
  const key = this.config.str(0);
  const account = this.config.str('account');
  await this.wallet.addSharedKey(account, key);
  this.log('Added key.');
};

CLI.prototype.removeSharedKey = async function removeSharedKey() {
  const key = this.config.str(0);
  const account = this.config.str('account');
  await this.wallet.removeSharedKey(account, key);
  this.log('Removed key.');
};

CLI.prototype.getSharedKeys = async function getSharedKeys() {
  const acct = this.config.str([0, 'account']);
  const account = await this.wallet.getAccount(acct);
  if (!account) {
    this.log('Account not found.');
    return;
  }
  this.log(account.keys);
};

CLI.prototype.getAccount = async function getAccount() {
  const acct = this.config.str([0, 'account']);
  const account = await this.wallet.getAccount(acct);
  this.log(account);
};

CLI.prototype.createAccount = async function createAccount() {
  const name = this.config.str([0, 'name']);

  const options = {
    type: this.config.str('type'),
    m: this.config.uint('m'),
    n: this.config.uint('n'),
    witness: this.config.bool('witness'),
    accountKey: this.config.str('watch')
  };

  const account = await this.wallet.createAccount(name, options);

  this.log(account);
};

CLI.prototype.createAddress = async function createAddress() {
  const account = this.config.str([0, 'account']);
  const addr = await this.wallet.createAddress(account);
  this.log(addr);
};

CLI.prototype.createChange = async function createChange() {
  const account = this.config.str([0, 'account']);
  const addr = await this.wallet.createChange(account);
  this.log(addr);
};

CLI.prototype.createNested = async function createNested() {
  const account = this.config.str([0, 'account']);
  const addr = await this.wallet.createNested(account);
  this.log(addr);
};

CLI.prototype.getAccounts = async function getAccounts() {
  const accounts = await this.wallet.getAccounts();
  this.log(accounts);
};

CLI.prototype.getWallet = async function getWallet() {
  const info = await this.wallet.getInfo();
  this.log(info);
};

CLI.prototype.getTX = async function getTX() {
  const hash = this.config.str(0);

  if (util.isBase58(hash)) {
    const txs = await this.client.getTXByAddress(hash);
    this.log(txs);
    return;
  }

  const tx = await this.client.getTX(hash);

  if (!tx) {
    this.log('TX not found.');
    return;
  }

  this.log(tx);
};

CLI.prototype.getBlock = async function getBlock() {
  let hash = this.config.str(0);

  if (hash.length !== 64)
    hash = parseInt(hash, 10);

  const block = await this.client.getBlock(hash);

  if (!block) {
    this.log('Block not found.');
    return;
  }

  this.log(block);
};

CLI.prototype.getCoin = async function getCoin() {
  const hash = this.config.str(0);
  const index = this.config.uint(1);

  if (util.isBase58(hash)) {
    const coins = await this.client.getCoinsByAddress(hash);
    this.log(coins);
    return;
  }

  const coin = await this.client.getCoin(hash, index);

  if (!coin) {
    this.log('Coin not found.');
    return;
  }

  this.log(coin);
};

CLI.prototype.getWalletHistory = async function getWalletHistory() {
  const account = this.config.str('account');
  const txs = await this.wallet.getHistory(account);
  this.log(txs);
};

CLI.prototype.getWalletPending = async function getWalletPending() {
  const account = this.config.str('account');
  const txs = await this.wallet.getPending(account);
  this.log(txs);
};

CLI.prototype.getWalletCoins = async function getWalletCoins() {
  const account = this.config.str('account');
  const coins = await this.wallet.getCoins(account);
  this.log(coins);
};

CLI.prototype.listenWallet = async function listenWallet() {
  await this.wallet.open();

  this.wallet.on('tx', (details) => {
    this.log('TX:');
    this.log(details);
  });

  this.wallet.on('confirmed', (details) => {
    this.log('TX confirmed:');
    this.log(details);
  });

  this.wallet.on('unconfirmed', (details) => {
    this.log('TX unconfirmed:');
    this.log(details);
  });

  this.wallet.on('conflict', (details) => {
    this.log('TX conflict:');
    this.log(details);
  });

  this.wallet.on('address', (receive) => {
    this.log('New addresses allocated:');
    this.log(receive);
  });

  this.wallet.on('balance', (balance) => {
    this.log('Balance:');
    this.log(balance);
  });

  return await this.wallet.onDisconnect();
};

CLI.prototype.getBalance = async function getBalance() {
  const account = this.config.str('account');
  const balance = await this.wallet.getBalance(account);
  this.log(balance);
};

CLI.prototype.getMempool = async function getMempool() {
  const txs = await this.client.getMempool();
  this.log(txs);
};

CLI.prototype.sendTX = async function sendTX() {
  const outputs = [];

  if (this.config.has('script')) {
    outputs.push({
      script: this.config.str('script'),
      value: this.config.ufixed([0, 'value'], 8)
    });
  } else {
    outputs.push({
      address: this.config.str([0, 'address']),
      value: this.config.ufixed([1, 'value'], 8)
    });
  }

  if (this.config.bool('no-replay')) {
    outputs.push({
      script: ANTIREPLAY,
      value: 0
    });
  }

  const options = {
    account: this.config.str('account'),
    passphrase: this.config.str('passphrase'),
    outputs: outputs,
    smart: this.config.bool('smart'),
    rate: this.config.ufixed('rate', 8),
    subtractFee: this.config.bool('subtract-fee')
  };

  const tx = await this.wallet.send(options);

  this.log(tx);
};

CLI.prototype.createTX = async function createTX() {
  let output;

  if (this.config.has('script')) {
    output = {
      script: this.config.str('script'),
      value: this.config.ufixed([0, 'value'], 8)
    };
  } else {
    output = {
      address: this.config.str([0, 'address']),
      value: this.config.ufixed([1, 'value'], 8)
    };
  }

  const options = {
    account: this.config.str('account'),
    passphrase: this.config.str('passphrase'),
    outputs: [output],
    smart: this.config.bool('smart'),
    rate: this.config.ufixed('rate', 8),
    subtractFee: this.config.bool('subtract-fee')
  };

  const tx = await this.wallet.createTX(options);

  this.log(tx);
};

CLI.prototype.signTX = async function signTX() {
  const passphrase = this.config.str('passphrase');
  const raw = this.config.str([0, 'tx']);
  const tx = await this.wallet.sign(raw, { passphrase });

  this.log(tx);
};

CLI.prototype.zapWallet = async function zapWallet() {
  const age = this.config.uint([0, 'age'], 72 * 60 * 60);
  await this.wallet.zap(this.config.str('account'), age);
  this.log('Zapped!');
};

CLI.prototype.broadcast = async function broadcast() {
  const raw = this.config.str([0, 'tx']);
  const tx = await this.client.broadcast(raw);
  this.log('Broadcasted:');
  this.log(tx);
};

CLI.prototype.viewTX = async function viewTX() {
  const raw = this.config.str([0, 'tx']);
  const tx = await this.wallet.fill(raw);
  this.log(tx);
};

CLI.prototype.getDetails = async function getDetails() {
  const hash = this.config.str(0);
  const details = await this.wallet.getTX(hash);
  this.log(details);
};

CLI.prototype.getWalletBlocks = async function getWalletBlocks() {
  const blocks = await this.wallet.getBlocks();
  this.log(blocks);
};

CLI.prototype.getWalletBlock = async function getWalletBlock() {
  const height = this.config.uint(0);
  const block = await this.wallet.getBlock(height);
  this.log(block);
};

CLI.prototype.retoken = async function retoken() {
  const result = await this.wallet.retoken();
  this.log(result);
};

CLI.prototype.rescan = async function rescan() {
  const height = this.config.uint(0);
  await this.client.rescan(height);
  this.log('Rescanning...');
};

CLI.prototype.reset = async function reset() {
  let hash = this.config.str(0);

  if (hash.length !== 64)
    hash = parseInt(hash, 10);

  await this.client.reset(hash);

  this.log('Chain has been reset.');
};

CLI.prototype.resend = async function resend() {
  await this.client.resend();
  this.log('Resending...');
};

CLI.prototype.resendWallet = async function resendWallet() {
  await this.wallet.resend();
  this.log('Resending...');
};

CLI.prototype.backup = async function backup() {
  const path = this.config.str(0);

  await this.client.backup(path);

  this.log('Backup complete.');
};

CLI.prototype.importKey = async function importKey() {
  const key = this.config.str(0);
  const account = this.config.str('account');

  if (!key)
    throw new Error('No key for import.');

  if (util.isBase58(key)) {
    await this.wallet.importPrivate(account, key);
    this.log('Imported private key.');
    return;
  }

  if (util.isHex(key)) {
    await this.wallet.importPublic(account, key);
    this.log('Imported public key.');
    return;
  }

  throw new Error('Bad key for import.');
};

CLI.prototype.importAddress = async function importAddress() {
  const address = this.config.str(0);
  const account = this.config.str('account');
  await this.wallet.importAddress(account, address);
  this.log('Imported address.');
};

CLI.prototype.lock = async function lock() {
  await this.wallet.lock();
  this.log('Locked.');
};

CLI.prototype.unlock = async function unlock() {
  const passphrase = this.config.str(0);
  const timeout = this.config.uint(1);
  await this.wallet.unlock(passphrase, timeout);
  this.log('Unlocked.');
};

CLI.prototype.rpc = async function rpc() {
  const method = this.argv.shift();
  const params = [];

  for (const arg of this.argv) {
    let param;
    try {
      param = JSON.parse(arg);
    } catch (e) {
      param = arg;
    }
    params.push(param);
  }

  let result;
  try {
    result = await this.client.rpc.execute(method, params);
  } catch (e) {
    if (e.type === 'RPCError') {
      this.log(e.message);
      return;
    }
    throw e;
  }

  this.log(result);
};

CLI.prototype.handleWallet = async function handleWallet() {
  this.wallet = new Wallet({
    uri: this.config.str(['url', 'uri']),
    apiKey: this.config.str('api-key'),
    network: this.config.str('network'),
    id: this.config.str('id', 'primary'),
    token: this.config.str('token')
  });

  switch (this.argv.shift()) {
    case 'listen':
      await this.listenWallet();
      break;
    case 'get':
      await this.getWallet();
      break;
    case 'master':
      await this.getMaster();
      break;
    case 'shared':
      if (this.argv[0] === 'add') {
        this.argv.shift();
        await this.addSharedKey();
        break;
      }
      if (this.argv[0] === 'remove') {
        this.argv.shift();
        await this.removeSharedKey();
        break;
      }
      if (this.argv[0] === 'list')
        this.argv.shift();
      await this.getSharedKeys();
      break;
    case 'balance':
      await this.getBalance();
      break;
    case 'history':
      await this.getWalletHistory();
      break;
    case 'pending':
      await this.getWalletPending();
      break;
    case 'coins':
      await this.getWalletCoins();
      break;
    case 'account':
      if (this.argv[0] === 'list') {
        this.argv.shift();
        await this.getAccounts();
        break;
      }
      if (this.argv[0] === 'create') {
        this.argv.shift();
        await this.createAccount();
        break;
      }
      if (this.argv[0] === 'get')
        this.argv.shift();
      await this.getAccount();
      break;
    case 'address':
      await this.createAddress();
      break;
    case 'change':
      await this.createChange();
      break;
    case 'nested':
      await this.createNested();
      break;
    case 'retoken':
      await this.retoken();
      break;
    case 'sign':
      await this.signTX();
      break;
    case 'mktx':
      await this.createTX();
      break;
    case 'send':
      await this.sendTX();
      break;
    case 'zap':
      await this.zapWallet();
      break;
    case 'tx':
      await this.getDetails();
      break;
    case 'blocks':
      await this.getWalletBlocks();
      break;
    case 'block':
      await this.getWalletBlock();
      break;
    case 'view':
      await this.viewTX();
      break;
    case 'import':
      await this.importKey();
      break;
    case 'watch':
      await this.importAddress();
      break;
    case 'key':
      await this.getKey();
      break;
    case 'dump':
      await this.getWIF();
      break;
    case 'lock':
      await this.lock();
      break;
    case 'unlock':
      await this.unlock();
      break;
    case 'resend':
      await this.resendWallet();
      break;
    default:
      this.log('Unrecognized command.');
      this.log('Commands:');
      this.log('  $ listen: Listen for events.');
      this.log('  $ get: View wallet.');
      this.log('  $ master: View wallet master key.');
      this.log('  $ shared add [xpubkey]: Add key to wallet.');
      this.log('  $ shared remove [xpubkey]: Remove key from wallet.');
      this.log('  $ balance: Get wallet balance.');
      this.log('  $ history: View TX history.');
      this.log('  $ pending: View pending TXs.');
      this.log('  $ coins: View wallet coins.');
      this.log('  $ account list: List account names.');
      this.log('  $ account create [account-name]: Create account.');
      this.log('  $ account get [account-name]: Get account details.');
      this.log('  $ address: Derive new address.');
      this.log('  $ change: Derive new change address.');
      this.log('  $ nested: Derive new nested address.');
      this.log('  $ retoken: Create new api key.');
      this.log('  $ send [address] [value]: Send transaction.');
      this.log('  $ mktx [address] [value]: Create transaction.');
      this.log('  $ sign [tx-hex]: Sign transaction.');
      this.log('  $ zap [age?]: Zap pending wallet TXs.');
      this.log('  $ tx [hash]: View transaction details.');
      this.log('  $ blocks: List wallet blocks.');
      this.log('  $ block [height]: View wallet block.');
      this.log('  $ view [tx-hex]: Parse and view transaction.');
      this.log('  $ import [wif|hex]: Import private or public key.');
      this.log('  $ watch [address]: Import an address.');
      this.log('  $ key [address]: Get wallet key by address.');
      this.log('  $ dump [address]: Get wallet key WIF by address.');
      this.log('  $ lock: Lock wallet.');
      this.log('  $ unlock [passphrase] [timeout?]: Unlock wallet.');
      this.log('  $ resend: Resend pending transactions.');
      this.log('Other Options:');
      this.log('  --passphrase [passphrase]: For signing & account creation.');
      this.log('  --account [account-name]: Account name.');
      break;
  }
};

CLI.prototype.handleNode = async function handleNode() {
  this.client = new Client({
    uri: this.config.str(['url', 'uri']),
    apiKey: this.config.str('api-key'),
    network: this.config.str('network')
  });

  switch (this.argv.shift()) {
    case 'info':
      await this.getInfo();
      break;
    case 'wallets':
      await this.getWallets();
      break;
    case 'mkwallet':
      await this.createWallet();
      break;
    case 'broadcast':
      await this.broadcast();
      break;
    case 'mempool':
      await this.getMempool();
      break;
    case 'tx':
      await this.getTX();
      break;
    case 'coin':
      await this.getCoin();
      break;
    case 'block':
      await this.getBlock();
      break;
    case 'rescan':
      await this.rescan();
      break;
    case 'reset':
      await this.reset();
      break;
    case 'resend':
      await this.resend();
      break;
    case 'backup':
      await this.backup();
      break;
    case 'rpc':
      await this.rpc();
      break;
    default:
      this.log('Unrecognized command.');
      this.log('Commands:');
      this.log('  $ info: Get server info.');
      this.log('  $ wallets: List all wallets.');
      this.log('  $ wallet create [id]: Create wallet.');
      this.log('  $ broadcast [tx-hex]: Broadcast transaction.');
      this.log('  $ mempool: Get mempool snapshot.');
      this.log('  $ tx [hash/address]: View transactions.');
      this.log('  $ coin [hash+index/address]: View coins.');
      this.log('  $ block [hash/height]: View block.');
      this.log('  $ rescan [height]: Rescan for transactions.');
      this.log('  $ reset [height/hash]: Reset chain to desired block.');
      this.log('  $ resend: Resend pending transactions.');
      this.log('  $ backup [path]: Backup the wallet db.');
      this.log('  $ rpc [command] [args]: Execute RPC command.');
      break;
  }
};

CLI.prototype.open = async function open() {
  switch (this.argv[0]) {
    case 'w':
    case 'wallet':
      this.argv.shift();
      if (this.argv[0] === 'create') {
        this.argv[0] = 'mkwallet';
        await this.handleNode();
        break;
      }
      await this.handleWallet();
      break;
    default:
      await this.handleNode();
      break;
  }
};

CLI.prototype.destroy = function destroy() {
  if (this.wallet)
    this.wallet.client.destroy();

  if (this.client)
    this.client.destroy();

  return Promise.resolve();
};

(async () => {
  const cli = new CLI();
  await cli.open();
  await cli.destroy();
  process.exit(0);
})().catch((err) => {
  console.error(err.stack);
  process.exit(1);
});
