const bitcoindService = require('services/bitcoind.js');
const BitcoindError = require('models/errors.js').BitcoindError;

async function getBlockCount() {
  const blockCount = await bitcoindService.getBlockCount();

  return {blockCount: blockCount.result};
}

async function getConnectionsCount() {
  const peerInfo = await bitcoindService.getPeerInfo();

  var outBoundConnections = 0;
  var inBoundConnections = 0;
  var clearnetConnections = 0;
  var torConnections = 0;
  var i2pConnections = 0;

  peerInfo.result.forEach(function(peer) {
    if (peer.inbound === false) {
      outBoundConnections++;
    } else {
      inBoundConnections++;
    }

    if (peer.network === "onion") {
      torConnections++;
    } else if (peer.network === "i2p") {
      i2pConnections++;
    } else {
      // ipv4 and ipv6 are clearnet
      clearnetConnections++;
    }   
  });

  const connections = {
    total: inBoundConnections + outBoundConnections,
    inbound: inBoundConnections,
    outbound: outBoundConnections,
    clearnet: clearnetConnections,
    tor: torConnections,
    i2p: i2pConnections
  };

  return connections;
}

async function getStatus() {
  try {
    await bitcoindService.help();

    return {operational: true};
  } catch (error) {
    if (error instanceof BitcoindError) {
      return {operational: false};
    }

    throw error;
  }
}

// Return the max synced header for all connected peers or -1 if no data is available.
async function getMaxSyncHeader() {
  const peerInfo = (await bitcoindService.getPeerInfo()).result;

  if (peerInfo.length === 0) {
    return -1;
  }

  const maxPeer = peerInfo.reduce(function(prev, current) {
    return prev.syncedHeaders > current.syncedHeaders ? prev : current;
  });

  return maxPeer.syncedHeaders;
}

async function getMempoolInfo() {
  return await bitcoindService.getMempoolInfo();
}

async function getLocalSyncInfo() {
  const info = await bitcoindService.getBlockChainInfo();

  var blockChainInfo = info.result;
  var chain = blockChainInfo.chain;
  var blockCount = blockChainInfo.blocks;
  var headerCount = blockChainInfo.headers;
  var percent = blockChainInfo.verificationprogress;
  var pruned = blockChainInfo.pruned;
  var pruneTargetSize = blockChainInfo.pruneTargetSize;

  return {
    chain,
    percent,
    currentBlock: blockCount,
    headerCount: headerCount, // eslint-disable-line object-shorthand,
    pruned,
    pruneTargetSize
  };
}

async function getSyncStatus() {
  const maxPeerHeader = await getMaxSyncHeader();
  const localSyncInfo = await getLocalSyncInfo();

  if (maxPeerHeader > localSyncInfo.headerCount) {
    localSyncInfo.headerCount = maxPeerHeader;
  }

  return localSyncInfo;
}

// TODO - consider using getNetworkInfo for info on proxy for ipv4 and ipv6
async function getVersion() {
  const networkInfo = await bitcoindService.getNetworkInfo();
  const unformattedVersion = networkInfo.result.subversion;

  // Remove all non-digits or decimals.
  const version = unformattedVersion.replace(/[^\d.]/g, '');

  return {version: version}; // eslint-disable-line object-shorthand
}

async function getTransaction(txid) {
  const transactionObj = await bitcoindService.getTransaction(txid);

  return {
    txid,
    timestamp: transactionObj.result.time,
    confirmations: transactionObj.result.confirmations,
    blockhash: transactionObj.result.blockhash,
    size: transactionObj.result.size,
    input: transactionObj.result.vin.txid,
    utxo: transactionObj.result.vout,
    rawtx: transactionObj.result.hex
  };
}

async function getNetworkInfo() {
  const networkInfo = await bitcoindService.getNetworkInfo();

  return networkInfo.result; // eslint-disable-line object-shorthand
}

async function getBlock(hash) {
  const blockObj = await bitcoindService.getBlock(hash);

  return {
    block: hash,
    confirmations: blockObj.result.confirmations,
    size: blockObj.result.size,
    height: blockObj.result.height,
    blocktime: blockObj.result.time,
    prevblock: blockObj.result.previousblockhash,
    nextblock: blockObj.result.nextblockhash,
    transactions: blockObj.result.tx
  };
}

const memoizedGetFormattedBlock = () => {
  const cache = {};

  return async blockHeight => {
    // cache cleanup
    // 6 blocks/hr * 24 hrs/day * 7 days = 1008 blocks over 7 days
    // plus some wiggle room in case weird difficulty adjustment or period of faster blocks
    const CACHE_LIMIT = 1100;
    while(Object.keys(cache).length > CACHE_LIMIT) {
      const cacheItemToDelete = Object.keys(cache)[0];
      delete cache[cacheItemToDelete];
    }
    
    if (blockHeight in cache) {
      return cache[blockHeight];
    } else {
      let blockHash;
      try {
        ({result: blockHash} = await bitcoindService.getBlockHash(blockHeight));
      } catch (error) {
        if (error instanceof BitcoindError) {
          return error;
        }
        throw error;
      }

      const {result: block} = await bitcoindService.getBlock(blockHash);

      cache[blockHeight] = {
        hash: block.hash,
        height: block.height,
        numTransactions: block.tx.length,
        confirmations: block.confirmations,
        time: block.time,
        size: block.size,
        previousblockhash: block.previousblockhash
      };

      return cache[blockHeight];
    }
  };
};

const initializedMemoizedGetFormattedBlock = memoizedGetFormattedBlock();


async function getBlockRangeTransactionChunks(fromHeight, toHeight, blocksPerChunk) {
  const {blocks} = await getBlocks(fromHeight, toHeight);
  const chunks = [];
  blocks.forEach((block, index) => {
    const chunkIndex = Math.floor(index / blocksPerChunk);
    if (!chunks[chunkIndex]) {
      chunks[chunkIndex] = {
        time: block.time,
        numTransactions: 0,
      };
    }
    chunks[chunkIndex].numTransactions += block.numTransactions;
  });

  return chunks;
}

async function getBlocks(fromHeight, toHeight) {
  const blocks = [];

  // loop from 'to height' till 'from Height'
  for (let currentHeight = toHeight; currentHeight >= fromHeight; currentHeight--) {
    // terminate loop if we reach the genesis block
    if (currentHeight === 0) {
      break;
    }

    try {
      const formattedBlock = await initializedMemoizedGetFormattedBlock(currentHeight);
      blocks.push(formattedBlock);
    } catch(e) {
      console.error('Error memoizing formatted blocks')
    }
  }

  return {blocks};
}

async function getBlockHash(height) {
  const getBlockHashObj = await bitcoindService.getBlockHash(height);

  return {
    hash: getBlockHashObj.result
  };
}

async function nodeStatusDump() {
  const blockchainInfo = await bitcoindService.getBlockChainInfo();
  const networkInfo = await bitcoindService.getNetworkInfo();
  const mempoolInfo = await bitcoindService.getMempoolInfo();
  const miningInfo = await bitcoindService.getMiningInfo();

  return {
    blockchain_info: blockchainInfo.result,
    network_info: networkInfo.result,
    mempool: mempoolInfo.result,
    mining_info: miningInfo.result
  };
}

async function nodeStatusSummary() {
  const blockchainInfo = await bitcoindService.getBlockChainInfo();
  const networkInfo = await bitcoindService.getNetworkInfo();
  const mempoolInfo = await bitcoindService.getMempoolInfo();
  const miningInfo = await bitcoindService.getMiningInfo();

  return {
    difficulty: blockchainInfo.result.difficulty,
    size: blockchainInfo.result.sizeOnDisk,
    mempool: mempoolInfo.result.usage,
    connections: networkInfo.result.connections,
    networkhashps: miningInfo.result.networkhashps
  };
}

async function stop() {
  const stopResponse = await bitcoindService.stop();
  return {stopResponse};
}

async function restartBitcoindWithRetries(MAX_TRIES = 60) {
  let tries = 0;

  while (tries < MAX_TRIES) {
    try {
      await stop();
      break;
    } catch (error) {
      console.error(error);
      console.log(`Failed to stop bitcoind. Retrying in 1 second. (Try ${tries + 1} of ${MAX_TRIES})`);
      tries++;
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }
}

module.exports = {
  getBlockHash,
  getTransaction,
  getBlock,
  getBlockCount,
  getBlocks,
  getBlockRangeTransactionChunks,
  getConnectionsCount,
  getNetworkInfo,
  getMempoolInfo,
  getStatus,
  getSyncStatus,
  getVersion,
  nodeStatusDump,
  nodeStatusSummary,
  stop,
  restartBitcoindWithRetries
};
