text: 'use strict';
  
  var _ = require('lodash');
  var $ = require('preconditions').singleton();
  var log = require('npmlog');
  log.debug = log.verbose;
  var io = require('socket.io-client');
  var requestList = require('./request-list');
  
  function Insight(opts) {
    $.checkArgument(opts);
    $.checkArgument(_.contains(['livenet', 'testnet'], opts.network));
    $.checkArgument(opts.url);
  
    this.apiPrefix = opts.apiPrefix || '/simple-insight-api';
    this.network = opts.network || 'livenet';
    this.hosts = opts.url;
    this.userAgent = opts.userAgent || 'bws';
  };
  
  
  var _parseErr = function(err, res) {
    if (err) {
      log.warn('Insight error: ', err);
      return "Insight Error";
    }
    log.warn("Insight " + res.request.href + " Returned Status: " + res.statusCode);
    return "Error querying the blockchain";
  };
  
  Insight.prototype._doRequest = function(args, cb) {
    var opts = {
      hosts: this.hosts,
      headers: {
        'User-Agent': this.userAgent,
      }
    };
    requestList(_.defaults(args, opts), cb);
  };
  
  Insight.prototype.getConnectionInfo = function() {
    return 'Insight (' + this.network + ') @ ' + this.hosts;
  };
  
  /**
   * Retrieve a list of unspent outputs associated with an address or set of addresses
   */
  Insight.prototype.getUtxos = function(addresses, cb) {
    var url = this.url + this.apiPrefix + '/addrs/utxo';
    var args = {
      method: 'POST',
      path: this.apiPrefix + '/addrs/utxo',
      json: {
        addrs: [].concat(addresses).join(',')
      },
    };
  
    this._doRequest(args, function(err, res, unspent) {
      if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
      return cb(null, unspent);
    });
  };
  
  /**
   * Broadcast a transaction to the bitcoin network
   */
  Insight.prototype.broadcast = function(rawTx, cb) {
    var args = {
      method: 'POST',
      path: this.apiPrefix + '/tx/send',
      json: {
        rawtx: rawTx
      },
    };
  
    this._doRequest(args, function(err, res, body) {
      if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
      return cb(null, body ? body.txid : null);
    });
  };
  
  Insight.prototype.getTransaction = function(txid, cb) {
    var args = {
      method: 'GET',
      path: this.apiPrefix + '/tx/' + txid,
      json: true,
    };
  
    this._doRequest(args, function(err, res, tx) {
      if (res && res.statusCode == 404) return cb();
      if (err || res.statusCode !== 200)
        return cb(_parseErr(err, res));
  
      return cb(null, tx);
    });
  };
  
  Insight.prototype.getTransactions = function(addresses, from, to, cb) {
    var qs = [];
    var total;
    if (_.isNumber(from)) qs.push('from=' + from);
    if (_.isNumber(to)) qs.push('to=' + to);
  
    // Trim output
    qs.push('noAsm=1');
    qs.push('noScriptSig=1');
    qs.push('noSpent=1');
  
    var args = {
      method: 'POST',
      path: this.apiPrefix + '/addrs/txs' + (qs.length > 0 ? '?' + qs.join('&') : ''),
      json: {
        addrs: [].concat(addresses).join(',')
      },
      timeout: 120000,
    };
  
    this._doRequest(args, function(err, res, txs) {
      if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
  
      if (_.isObject(txs)) {
        if (txs.totalItems)
          total = txs.totalItems;
  
        if (txs.items)
          txs = txs.items;
      }
  
      // NOTE: Whenever Insight breaks communication with bitcoind, it returns invalid data but no error code.
      if (!_.isArray(txs) || (txs.length != _.compact(txs).length)) return cb(new Error('Could not retrieve transactions from blockchain. Request was:' + JSON.stringify(args)));
  
      return cb(null, txs, total);
    });
  };
  
  Insight.prototype.getAddressActivity = function(address, cb) {
    var self = this;
  
    var args = {
      method: 'GET',
      path: self.apiPrefix + '/addr/' + address,
      json: true,
    };
  
    this._doRequest(args, function(err, res, result) {
      if (res && res.statusCode == 404) return cb();
      if (err || res.statusCode !== 200)
        return cb(_parseErr(err, res));
  
      var nbTxs = result.unconfirmedTxApperances + result.txApperances;
      return cb(null, nbTxs > 0);
    });
  };
  
  Insight.prototype.estimateFee = function(nbBlocks, cb) {
    var path = this.apiPrefix + '/utils/estimatefee';
    if (nbBlocks) {
      path += '?nbBlocks=' + [].concat(nbBlocks).join(',');
    }
  
    var args = {
      method: 'GET',
      path: path,
      json: true,
    };
    this._doRequest(args, function(err, res, body) {
      if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
      return cb(null, body);
    });
  };
  
  Insight.prototype.getBlockchainHeight = function(cb) {
    var path = this.apiPrefix + '/sync';
  
    var args = {
      method: 'GET',
      path: path,
      json: true,
    };
    this._doRequest(args, function(err, res, body) {
      if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
      return cb(null, body.blockChainHeight);
    });
  };
  
  Insight.prototype.getTxidsInBlock = function(blockHash, cb) {
    var self = this;
  
    var args = {
      method: 'GET',
      path: this.apiPrefix + '/block/' + blockHash,
      json: true,
    };
  
    this._doRequest(args, function(err, res, body) {
      if (err || res.statusCode !== 200) return cb(_parseErr(err, res));
      return cb(null, body.tx);
    });
  };
  
  Insight.prototype.initSocket = function() {
  
    // sockets always use the first server on the pull
    var socket = io.connect(_.first([].concat(this.hosts)), {
      'reconnection': true,
    });
    return socket;
  };
  
  module.exports = Insight;
  