#!/usr/bin/env node

'use strict';

var config = require('../lib/node/config');
var util = require('../lib/utils/util');
var co = require('../lib/utils/co');
var Client = require('../lib/http/client');
var Wallet = require('../lib/http/wallet');
var Amount = require('../lib/btc/amount');
var main;

function CLI() {
  this.config = config.parseRaw({
    config: true,
    arg: true,
    env: true,
    network: 'main'
  });
  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);
  console.log(JSON.stringify(json, null, 2));
};

CLI.prototype.getInfo = co(function* getInfo() {
  var info = yield this.client.getInfo();
  this.log(info);
});

CLI.prototype.getWallets = co(function* getWallets() {
  var wallets = yield this.client.getWallets();
  this.log(wallets);
});

CLI.prototype.createWallet = co(function* createWallet() {
  var options = { id: this.argv[0] };
  var wallet;

  if (this.config.type)
    options.type = this.config.type;

  if (this.config.master)
    options.master = this.config.master;

  if (this.config.mnemonic)
    options.master = this.config.mnemonic;

  if (this.config.m)
    options.m = this.config.m >>> 0;

  if (this.config.n)
    options.n = this.config.n >>> 0;

  if (this.config.witness != null)
    options.witness = !!this.config.witness;

  if (this.config.passphrase)
    options.passphrase = this.config.passphrase;

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

  wallet = yield this.client.createWallet(options);
  this.log(wallet);
});

CLI.prototype.getMaster = co(function* getMaster() {
  var master = yield this.wallet.getMaster();
  this.log(master);
});

CLI.prototype.getKey = co(function* getKey() {
  var address = this.argv[0];
  var key = yield this.wallet.getKey(address);
  this.log(key);
});

CLI.prototype.getWIF = co(function* getWIF() {
  var address = this.argv[0];
  var key = yield this.wallet.getWIF(address, this.config.passphrase);
  this.log(key.privateKey);
});

CLI.prototype.addSharedKey = co(function* addSharedKey() {
  var key = this.argv[0];
  yield this.wallet.addSharedKey(this.config.account, key);
  this.log('Added key.');
});

CLI.prototype.removeSharedKey = co(function* removeSharedKey() {
  var key = this.argv[0];
  yield this.wallet.removeSharedKey(this.config.account, key);
  this.log('Removed key.');
});

CLI.prototype.getSharedKeys = co(function* getSharedKeys() {
  var acct = this.argv[0] || this.config.account;
  var account = yield this.wallet.getAccount(acct);
  this.log(account.keys);
});

CLI.prototype.getAccount = co(function* getAccount() {
  var acct = this.argv[0] || this.config.account;
  var account = yield this.wallet.getAccount(acct);
  this.log(account);
});

CLI.prototype.createAccount = co(function* createAccount() {
  var options = { name: this.argv[0] };
  var account;

  if (this.config.type)
    options.type = this.config.type;

  if (this.config.m)
    options.m = this.config.m >>> 0;

  if (this.config.n)
    options.n = this.config.n >>> 0;

  if (this.config.witness != null)
    options.witness = !!this.config.witness;

  if (this.config.watch)
    options.accountKey = this.config.watch;

  account = yield this.wallet.createAccount(options);

  this.log(account);
});

CLI.prototype.createAddress = co(function* createAddress() {
  var account = this.argv[0];
  var addr = yield this.wallet.createAddress(account);
  this.log(addr);
});

CLI.prototype.createChange = co(function* createChange() {
  var account = this.argv[0];
  var addr = yield this.wallet.createChange(account);
  this.log(addr);
});

CLI.prototype.createNested = co(function* createNested() {
  var account = this.argv[0];
  var addr = yield this.wallet.createNested(account);
  this.log(addr);
});

CLI.prototype.getAccounts = co(function* getAccounts() {
  var accounts = yield this.wallet.getAccounts();
  this.log(accounts);
});

CLI.prototype.getWallet = co(function* getWallet() {
  var info = yield this.wallet.getInfo();
  this.log(info);
});

CLI.prototype.getTX = co(function* getTX() {
  var hash = this.argv[0];
  var txs, tx;

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

  tx = yield this.client.getTX(hash);

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

  this.log(tx);
});

CLI.prototype.getBlock = co(function* getBlock() {
  var hash = this.argv[0];
  var block;

  if (hash.length !== 64)
    hash = +hash;

  block = yield this.client.getBlock(hash);

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

  this.log(block);
});

CLI.prototype.getCoin = co(function* getCoin() {
  var hash = this.argv[0];
  var index = this.argv[1];
  var coins, coin;

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

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

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

  this.log(coin);
});

CLI.prototype.getWalletHistory = co(function* getWalletHistory() {
  var txs = yield this.wallet.getHistory(this.config.account);
  this.log(txs);
});

CLI.prototype.getWalletPending = co(function* getWalletPending() {
  var txs = yield this.wallet.getPending(this.config.account);
  this.log(txs);
});

CLI.prototype.getWalletCoins = co(function* getWalletCoins() {
  var coins = yield this.wallet.getCoins(this.config.account);
  this.log(coins);
});

CLI.prototype.listenWallet = co(function* listenWallet() {
  var self = this;

  yield this.wallet.open();

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

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

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

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

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

  this.wallet.on('balance', function(balance) {
    self.log('Balance:');
    self.log(balance);
  });

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

CLI.prototype.getBalance = co(function* getBalance() {
  var balance = yield this.wallet.getBalance(this.config.account);
  this.log(balance);
});

CLI.prototype.getMempool = co(function* getMempool() {
  var txs = yield this.client.getMempool();
  this.log(txs);
});

CLI.prototype.sendTX = co(function* sendTX() {
  var output = {};
  var options, tx;

  if (this.config.script) {
    output.script = this.config.script;
    output.value = Amount.value(this.config.value || this.argv[0]);
  } else {
    output.address = this.config.address || this.argv[0];
    output.value = Amount.value(this.config.value || this.argv[1]);
  }

  options = {
    account: this.config.account,
    passphrase: this.config.passphrase,
    outputs: [output],
    rate: this.config.rate
  };

  tx = yield this.wallet.send(options);

  this.log(tx);
});

CLI.prototype.createTX = co(function* createTX() {
  var output = {};
  var options, tx;

  if (this.config.script) {
    output.script = this.config.script;
    output.value = Amount.value(this.config.value || this.argv[0]);
  } else {
    output.address = this.config.address || this.argv[0];
    output.value = Amount.value(this.config.value || this.argv[1]);
  }

  options = {
    account: this.config.account,
    passphrase: this.config.passphrase,
    outputs: [output]
  };

  tx = yield this.wallet.createTX(options);

  this.log(tx);
});

CLI.prototype.signTX = co(function* signTX() {
  var options = { passphrase: this.config.passphrase };
  var raw = options.tx || this.argv[0];
  var tx = yield this.wallet.sign(raw, options);
  this.log(tx);
});

CLI.prototype.zapWallet = co(function* zapWallet() {
  var age = (this.config.age >>> 0) || 72 * 60 * 60;
  yield this.wallet.zap(this.config.account, age);
  this.log('Zapped!');
});

CLI.prototype.broadcast = co(function* broadcast() {
  var raw = this.argv[0] || this.config.tx;
  var tx = yield this.client.broadcast(raw);
  this.log('Broadcasted:');
  this.log(tx);
});

CLI.prototype.viewTX = co(function* viewTX() {
  var raw = this.argv[0] || this.config.tx;
  var tx = yield this.wallet.fill(raw);
  this.log(tx);
});

CLI.prototype.getDetails = co(function* getDetails() {
  var hash = this.argv[0];
  var details = yield this.wallet.getTX(hash);
  this.log(details);
});

CLI.prototype.getWalletBlocks = co(function* getWalletBlocks() {
  var blocks = yield this.wallet.getBlocks();
  this.log(blocks);
});

CLI.prototype.getWalletBlock = co(function* getWalletBlock() {
  var height = this.argv[0] | 0;
  var block = yield this.wallet.getBlock(height);
  this.log(block);
});

CLI.prototype.retoken = co(function* retoken() {
  var result = yield this.wallet.retoken();
  this.log(result);
});

CLI.prototype.rescan = co(function* rescan() {
  var height = this.argv[0];

  if (!util.isUInt32(height))
    height = null;

  yield this.client.rescan(height);

  this.log('Rescanning...');
});

CLI.prototype.reset = co(function* reset() {
  var hash = this.argv[0];

  if (hash.length !== 64)
    hash = +hash;

  yield this.client.reset(hash);

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

CLI.prototype.resend = co(function* resend() {
  yield this.client.resend();
  this.log('Resending...');
});

CLI.prototype.resendWallet = co(function* resendWallet() {
  yield this.wallet.resend();
  this.log('Resending...');
});

CLI.prototype.backup = co(function* backup() {
  var path = this.argv[0];

  yield this.client.backup(path);

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

CLI.prototype.importKey = co(function* importKey() {
  var key = this.argv[0];
  var account = this.config.account;

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

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

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

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

CLI.prototype.importAddress = co(function* importKey() {
  var address = this.argv[0];
  var account = this.config.account;
  yield this.wallet.importAddress(account, address);
  this.log('Imported address.');
});

CLI.prototype.lock = co(function* lock() {
  yield this.wallet.lock();
  this.log('Locked.');
});

CLI.prototype.unlock = co(function* unlock() {
  var passphrase = this.argv[0];
  var timeout = +this.argv[1] || null;
  yield this.wallet.unlock(passphrase, timeout);
  this.log('Unlocked.');
});

CLI.prototype.rpc = co(function* rpc() {
  var method = this.argv.shift();
  var params = [];
  var i, arg, param, result;

  for (i = 0; i < this.argv.length; i++) {
    arg = this.argv[i];
    try {
      param = JSON.parse(arg);
    } catch (e) {
      param = arg;
    }
    params.push(param);
  }

  try {
    result = yield 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 = co(function* handleWallet() {
  this.wallet = new Wallet({
    uri: this.config.url || this.config.uri,
    apiKey: this.config.apikey,
    network: this.config.network,
    id: this.config.id || 'primary',
    token: this.config.token
  });

  switch (this.argv.shift()) {
    case 'listen':
      return yield this.listenWallet();
    case 'get':
      return yield this.getWallet();
    case 'master':
      return yield this.getMaster();
    case 'shared':
      if (this.argv[0] === 'add') {
        this.argv.shift();
        return yield this.addSharedKey();
      }
      if (this.argv[0] === 'remove') {
        this.argv.shift();
        return yield this.removeSharedKey();
      }
      if (this.argv[0] === 'list')
        this.argv.shift();
      return yield this.getSharedKeys();
    case 'balance':
      return yield this.getBalance();
    case 'history':
      return yield this.getWalletHistory();
    case 'pending':
      return yield this.getWalletPending();
    case 'coins':
      return yield this.getWalletCoins();
    case 'account':
      if (this.argv[0] === 'list') {
        this.argv.shift();
        return yield this.getAccounts();
      }
      if (this.argv[0] === 'create') {
        this.argv.shift();
        return yield this.createAccount();
      }
      if (this.argv[0] === 'get')
        this.argv.shift();
      return yield this.getAccount();
    case 'address':
      return yield this.createAddress();
    case 'change':
      return yield this.createChange();
    case 'nested':
      return yield this.createNested();
    case 'retoken':
      return yield this.retoken();
    case 'sign':
      return yield this.signTX();
    case 'mktx':
      return yield this.createTX();
    case 'send':
      return yield this.sendTX();
    case 'zap':
      return yield this.zapWallet();
    case 'tx':
      return yield this.getDetails();
    case 'blocks':
      return yield this.getWalletBlocks();
    case 'block':
      return yield this.getWalletBlock();
    case 'view':
      return yield this.viewTX();
    case 'import':
      return yield this.importKey();
    case 'watch':
      return yield this.importAddress();
    case 'key':
      return yield this.getKey();
    case 'dump':
      return yield this.getWIF();
    case 'lock':
      return yield this.lock();
    case 'unlock':
      return yield this.unlock();
    case 'resend':
      return yield this.resendWallet();
    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 and account creation.');
      this.log('  --account [account-name]: Account name.');
      return;
  }
});

CLI.prototype.handleNode = co(function* handleNode() {
  this.client = new Client({
    uri: this.config.url || this.config.uri,
    apiKey: this.config.apikey,
    network: this.config.network
  });

  switch (this.argv.shift()) {
    case 'info':
      return yield this.getInfo();
    case 'wallets':
      return yield this.getWallets();
    case 'mkwallet':
      return yield this.createWallet();
    case 'broadcast':
      return yield this.broadcast();
    case 'mempool':
      return yield this.getMempool();
    case 'tx':
      return yield this.getTX();
    case 'coin':
      return yield this.getCoin();
    case 'block':
      return yield this.getBlock();
    case 'rescan':
      return yield this.rescan();
    case 'reset':
      return yield this.reset();
    case 'resend':
      return yield this.resend();
    case 'backup':
      return yield this.backup();
    case 'rpc':
      return yield this.rpc();
    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.');
      return;
  }
});

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

CLI.prototype.destroy = function destroy() {
  if (this.wallet)
    this.wallet.client.destroy();
  if (this.client)
    this.client.destroy();
  return Promise.resolve();
};

main = co(function* main() {
  var cli = new CLI();
  yield cli.open();
  yield cli.destroy();
});

main().then(process.exit).catch(function(err) {
  console.error(err.stack + '');
  return process.exit(1);
});
