#!/usr/bin/env node

'use strict';

/**
 * Module imports
 */



const Config = require('bcfg');
const { NodeClient, WalletClient } = require('hs-client');
const { hd, MTX, Network } = require('hsd');
const { Rules } = require('hsd/lib/covenants');
const { hashName, types } = Rules;
const { util, HID, LedgerHSD, LedgerChange, LedgerCovenant } = require('..');
const { Device } = HID;
// Import from renewal.js the functions to fetch domains and create a batch
const { fetchDomains, createBatch } = require('./renewal.js');


/**
 * Global constants
 */

const VALID_CMDS = [
  'createwallet',
  'createaccount',
  'createaddress',
  'sendtoaddress',
  'getwallets',
  'getaccounts',
  'getaccount',
  'getbalance',
  'sendraw',
  'signraw',
  'renew'
];

const VERSION = require('../package').version;

async function createWallet(client, config, ledger, args) {
  if (args.length !== 1)
    throw new Error('Invalid arguments');

  const id = args[0];
  const index = config.uint('account-index');
  const xpub = await ledger.getAccountXPUB(index);
  const wallet = await client.createWallet(id, {
    watchOnly: true,
    accountKey: xpub.xpubkey(config.str('network'))
  });

  console.log(`Created wallet (id=hsd-ledger, wid=${wallet.wid}).`);
  console.log(`Created account (name=default, account-index=${index}').`);
}

async function createAccount(client, config, ledger, args) {
  if (args.length !== 2)
    throw new Error('Invalid arguments');

  const id = config.str('wallet-id');
  const name = args[0];
  const index = parseInt(args[1], 10);
  const xpub = await ledger.getAccountXPUB(index);
  await client.createAccount(id, name, {
    watchOnly: true,
    accountKey: xpub.xpubkey(config.str('network'))
  });

  console.log(`Created account (name=${name}, account-index=${index}').`);
}

async function createAddress(client, config, ledger, args) {
  if (args.length !== 0)
    throw new Error('Invalid arguments');

  const id = config.str('wallet-id');
  const name = config.str('account-name');
  const addr = await client.createAddress(id, name);
  const acct = await client.getAccount(id, name);
  const xpub = hd.PublicKey.fromBase58(acct.accountKey, config.str('network'));
  const account = (xpub.childIndex ^ hd.common.HARDENED) >>> 0;

  console.log(`Verify address on Ledger device: ${addr.address}`);

  await ledger.getAddress(account, addr.branch, addr.index, { confirm: true });
}

async function sendToAddress(wclient, nclient, config, ledger, args) {
  if (args.length !== 2)
    throw new Error('Invalid arguments');

  const network = Network.get(config.str('network'));
  const id = config.str('wallet-id');
  const acct = config.str('account-name');
  const address = args[0];
  const amount = parseFloat(args[1]);

  await wclient.execute('selectwallet', [id]);

  const params = [address, amount, '', '', false, acct];
  const json = await wclient.execute('createsendtoaddress', params);
  const mtx = MTX.fromJSON(json);
  let i, key;

  for (i = mtx.outputs.length - 1; i >= 0; i--) {
    const output = mtx.outputs[i];
    const addr = output.address.toString(network.type);
    key = await wclient.getKey(id, addr);

    if (key && key.branch)
      break;
  }

  if (!key || !key.branch)
    throw new Error('Expected change address.');

  const { account, branch, index } = key;
  const coinType = network.keyPrefix.coinType;
  const options = {
    change: new LedgerChange({
      path: `m/44'/${coinType}'/${account}'/${branch}/${index}`,
      index: i,
      version: 0
    })
  };

  util.displayDetails(console, network, mtx, options);

  const signed = await ledger.signTransaction(mtx, options);
  const rawtx = signed.encode().toString('hex');
  const txid = await nclient.execute('sendrawtransaction', [rawtx]);

  console.log(`Submitted TXID: ${txid}`);
}
async function sendRaw(wclient, nclient, config, ledger, args) { // Create a function to sign raw transactions
  if (args.length !== 2) // Make sure there are two arguments (batch, names)
    throw new Error('Invalid arguments'); // Throw an error if there are not two arguments

  const network = Network.get(config.str('network')); // Get the network
  const id = config.str('wallet-id'); // Get the wallet id
  const acct = config.str('account-name'); // Get the account name
  // Log the arguments to the console (for debugging)

  const batch = JSON.parse(args[0]); // Get the batch
  const names = JSON.parse(args[1]); // Get the names
  await wclient.execute('selectwallet', [id]); // Select the wallet

  try {
    const mtx = MTX.fromJSON(batch.result); // Create a new MTX from the JSON
    const hashes = {}; // Create an empty object to store the hashes
    for (const name of names) { // Loop through the names
      const hash = hashName(name); // Hash the name
      hashes[hash] = name; // Add the hash to the hashes object to use later
    }


    let i, key; // Create variables to use later
    const options = []; // Create an empty array to store the options
    for (i = mtx.outputs.length - 1; i >= 0; i--) { // Loop through the outputs
      const output = mtx.outputs[i]; // Get the output
      const addr = output.address.toString(network.type); // Get the address
      key = await wclient.getKey(id, addr); // Get the key
      if (!key) // If there is no key
        continue; // Continue to the next output
      if (key.branch === 1) { // If the key is a change address
        if (options.change) // If there is already a change address
          throw new Error('Transaction should only have one change output.'); // Throw an error
        const path = `m/44'/${network.keyPrefix.coinType}'/${key.account}'/${key.branch}/${key.index}`; // Create the derivation path
        options.change = new LedgerChange({ path: `m/44'/${network.keyPrefix.coinType}'/${key.account}'/${key.branch}/${key.index}`, index: i, version: 0 }); // Add the change address to the options
      }
      const { account, branch, index } = key; // Get the account, branch, and index from the key
      const coinType = network.keyPrefix.coinType; // Get the coin type from the network
      switch (output.covenant.type) { 
        case types.NONE:
        case types.BID:
        case types.FINALIZE:
          break;
        case types.OPEN:
        case types.REVEAL:
        case types.REDEEM:
        case types.REGISTER:
        case types.UPDATE:
        case types.RENEW:
        case types.TRANSFER:
        case types.REVOKE: { // If the covenant type is any of REVEAL, REDEEM, REGISTER, UPDATE, RENEW, TRANSFER, or REVOKE
          if (options.covenants == null) // If there are no covenants
            options.covenants = []; // Create an empty array for the covenants
          const hash = output.covenant.items[0]; // Get the hash from the covenant
          const name = hashes[hash]; // Get the name from the hashes object (is needed for SPV nodes)
          if (name == undefined) { // If the name is not found
            console.log("Name not found in file"); // Log that the name was not found
            console.log(hash); // Log the hash (for debugging)
          }
          options.covenants.push(new LedgerCovenant({ index: i, name })); // Add the covenant to the options

          break;
        }
        default:
          throw new Error('Unrecognized covenant type.');

      }

    } // end for loop
    util.displayDetails(console, network, mtx, options); // Display the details to the log for user verification
    const signed = await ledger.signTransaction(mtx, options); // Sign the transaction with the ledger
    const rawtx = signed.encode().toString('hex'); // Encode the transaction as hex
    const txid = await nclient.execute('sendrawtransaction', [rawtx]); // Send the transaction to the network
    console.log(`Submitted TXID: ${txid}`); // Log the TXID to the console to view the transaction on a block explorer

  } catch (err) { // Catch any errors
    console.error(err); // Log the error to the console
  }
}

async function renew(wclient, nclient, config, ledger, args) { // Create a function to sign raw transactions
  if (args.length !== 1) // Make sure there are two arguments (batch, names)
    throw new Error('Invalid arguments'); // Throw an error if there are not two arguments

  const network = Network.get(config.str('network')); // Get the network
  const id = config.str('wallet-id'); // Get the wallet id
  const acct = config.str('account-name'); // Get the account name
  // Log the arguments to the console (for debugging)

  const type = args[0]; // Get the batch type
  var batch; // Create a variable to store the batch
  var names
  if (type === 'all') {
    names = await fetchDomains(id,0); // Fetch the domains
    console.log(names.length);
    if (names.length === 0) {
      console.log("No domains to renew");
      return;
    }
  }
  else {
    const days = parseInt(type, 10); // Get the number of days
    names = await fetchDomains(id,days); // Fetch the domains
  }

  console.log("Renewing domains: ", names.length);
  if (names.length === 0) {
    console.log("No domains to renew");
    return;
  }
  batch = await createBatch(names); // Create the batch

  await wclient.execute('selectwallet', [id]); // Select the wallet

  try {
    const mtx = MTX.fromJSON(batch.result); // Create a new MTX from the JSON
    const hashes = {}; // Create an empty object to store the hashes
    for (const name of names) { // Loop through the names
      const hash = hashName(name); // Hash the name
      hashes[hash] = name; // Add the hash to the hashes object to use later
    }


    let i, key; // Create variables to use later
    const options = []; // Create an empty array to store the options
    for (i = mtx.outputs.length - 1; i >= 0; i--) { // Loop through the outputs
      const output = mtx.outputs[i]; // Get the output
      const addr = output.address.toString(network.type); // Get the address
      key = await wclient.getKey(id, addr); // Get the key
      if (!key) // If there is no key
        continue; // Continue to the next output
      if (key.branch === 1) { // If the key is a change address
        if (options.change) // If there is already a change address
          throw new Error('Transaction should only have one change output.'); // Throw an error
        const path = `m/44'/${network.keyPrefix.coinType}'/${key.account}'/${key.branch}/${key.index}`; // Create the derivation path
        options.change = new LedgerChange({ path: `m/44'/${network.keyPrefix.coinType}'/${key.account}'/${key.branch}/${key.index}`, index: i, version: 0 }); // Add the change address to the options
      }
      const { account, branch, index } = key; // Get the account, branch, and index from the key
      const coinType = network.keyPrefix.coinType; // Get the coin type from the network
      switch (output.covenant.type) { 
        case types.NONE:
        case types.BID:
        case types.FINALIZE:
          break;
        case types.OPEN:
        case types.REVEAL:
        case types.REDEEM:
        case types.REGISTER:
        case types.UPDATE:
        case types.RENEW:
        case types.TRANSFER:
        case types.REVOKE: { // If the covenant type is any of REVEAL, REDEEM, REGISTER, UPDATE, RENEW, TRANSFER, or REVOKE
          if (options.covenants == null) // If there are no covenants
            options.covenants = []; // Create an empty array for the covenants
          const hash = output.covenant.items[0]; // Get the hash from the covenant
          const name = hashes[hash]; // Get the name from the hashes object (is needed for SPV nodes)
          if (name == undefined) { // If the name is not found
            console.log("Name not found in file"); // Log that the name was not found
            console.log(hash); // Log the hash (for debugging)
          }
          options.covenants.push(new LedgerCovenant({ index: i, name })); // Add the covenant to the options

          break;
        }
        default:
          throw new Error('Unrecognized covenant type.');

      }

    } // end for loop
    util.displayDetails(console, network, mtx, options); // Display the details to the log for user verification
    const signed = await ledger.signTransaction(mtx, options); // Sign the transaction with the ledger
    const rawtx = signed.encode().toString('hex'); // Encode the transaction as hex
    const txid = await nclient.execute('sendrawtransaction', [rawtx]); // Send the transaction to the network
    console.log(`Submitted TXID: ${txid}`); // Log the TXID to the console to view the transaction on a block explorer

  } catch (err) { // Catch any errors
    console.error(err); // Log the error to the console
  }
}

async function signRaw(wclient, nclient, config, ledger, args) { // Create a function to sign raw transactions
  if (args.length !== 2) // Make sure there are two arguments (batch, names)
    throw new Error('Invalid arguments'); // Throw an error if there are not two arguments

  const network = Network.get(config.str('network')); // Get the network
  const id = config.str('wallet-id'); // Get the wallet id
  const acct = config.str('account-name'); // Get the account name
  // Log the arguments to the console (for debugging)

  const batch = JSON.parse(args[0]); // Get the batch
  const names = JSON.parse(args[1]); // Get the names
  await wclient.execute('selectwallet', [id]); // Select the wallet

  try {
    const mtx = MTX.fromJSON(batch.result); // Create a new MTX from the JSON
    const hashes = {}; // Create an empty object to store the hashes
    for (const name of names) { // Loop through the names
      const hash = hashName(name); // Hash the name
      hashes[hash] = name; // Add the hash to the hashes object to use later
    }


    let i, key; // Create variables to use later
    const options = []; // Create an empty array to store the options
    for (i = mtx.outputs.length - 1; i >= 0; i--) { // Loop through the outputs
      const output = mtx.outputs[i]; // Get the output
      const addr = output.address.toString(network.type); // Get the address
      key = await wclient.getKey(id, addr); // Get the key
      if (!key) // If there is no key
        continue; // Continue to the next output
      if (key.branch === 1) { // If the key is a change address
        if (options.change) // If there is already a change address
          throw new Error('Transaction should only have one change output.'); // Throw an error
        const path = `m/44'/${network.keyPrefix.coinType}'/${key.account}'/${key.branch}/${key.index}`; // Create the derivation path
        options.change = new LedgerChange({ path: `m/44'/${network.keyPrefix.coinType}'/${key.account}'/${key.branch}/${key.index}`, index: i, version: 0 }); // Add the change address to the options
      }
      const { account, branch, index } = key; // Get the account, branch, and index from the key
      const coinType = network.keyPrefix.coinType; // Get the coin type from the network
      switch (output.covenant.type) { 
        case types.NONE:
        case types.BID:
        case types.FINALIZE:
          break;
        case types.OPEN:
        case types.REVEAL:
        case types.REDEEM:
        case types.REGISTER:
        case types.UPDATE:
        case types.RENEW:
        case types.TRANSFER:
        case types.REVOKE: { // If the covenant type is any of REVEAL, REDEEM, REGISTER, UPDATE, RENEW, TRANSFER, or REVOKE
          if (options.covenants == null) // If there are no covenants
            options.covenants = []; // Create an empty array for the covenants
          const hash = output.covenant.items[0]; // Get the hash from the covenant
          const name = hashes[hash]; // Get the name from the hashes object (is needed for SPV nodes)
          if (name == undefined) { // If the name is not found
            console.log("Name not found in file"); // Log that the name was not found
            console.log(hash); // Log the hash (for debugging)
          }
          options.covenants.push(new LedgerCovenant({ index: i, name })); // Add the covenant to the options

          break;
        }
        default:
          throw new Error('Unrecognized covenant type.');

      }

    } // end for loop
    util.displayDetails(console, network, mtx, options); // Display the details to the log for user verification
    const signed = await ledger.signTransaction(mtx, options); // Sign the transaction with the ledger
    console.log(signed); // Log the TX to the console

  } catch (err) { // Catch any errors
    console.error(err); // Log the error to the console
  }
}

async function getWallets(client, args) {
  if (args.length)
    throw new Error('Too many arguments');

  const wallets = await client.getWallets();

  console.log(wallets);
}

async function getAccounts(client, config, args) {
  if (args.length)
    throw new Error('Too many arguments');

  const id = config.str('wallet-id');
  const accounts = await client.getAccounts(id);

  console.log(accounts);
}

async function getAccount(client, config, args) {
  if (args.length !== 1)
    throw new Error('Invalid arguments');

  const id = config.str('wallet-id');
  const name = args[0];
  const account = await client.getAccount(id, name);

  console.log(account);
}

async function getBalance(client, config, args) {
  if (args.length !== 0)
    throw new Error('Invalid arguments');

  const id = config.str('wallet-id');
  const name = config.str('account-name');
  const balance = await client.getBalance(id, name);

  console.log(balance);
}

async function main() {
  const device = await Device.requestDevice();
  device.set({
    timeout: 300000 // 5 minutes.
  });

  await device.open();

  const config = new Config('hsd', {
    suffix: 'network',
    fallback: 'main',
    alias: {
      'h': 'help',
      'i': 'accountindex',
      'a': 'accountname',
      'n': 'network',
      'p': 'projectsponsor',
      'q': 'qrcode',
      'k': 'showkey',
      'w': 'walletid'
    }
  });

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

  config.inject({
    accountIndex: 0,
    accountName: 'default',
    network: 'main',
    walletId: 'hsd-ledger',
    token: ''
  });

  const argv = config.argv;
  const cmd = argv.shift();
  const args = argv;
  const type = config.str('network');
  const network = Network.get(type);
  const id = config.str('wallet-id');
  const token = config.str('token');

  if (config.str('help') && argv.length === 0) {
    usage();
    process.exit(0);
  }

  if (config.str('version') && argv.length === 0) {
    version();
    process.exit(0);
  }

  const ledger = new LedgerHSD({
    device: device,
    network: type
  });

  const nclient = new NodeClient({
    url: config.str('url') || config.str('node-url'),
    apiKey: config.str('api-key') || config.str('node-api-key'),
    ssl: config.bool('ssl') || config.str('node-ssl'),
    host: config.str('http-host') || config.str('node-http-host'),
    port: config.uint('node-http-port') || network.rpcPort
  });

  const wclient = new WalletClient({
    url: config.str('url') || config.str('wallet-url'),
    apiKey: config.str('api-key') || config.str('wallet-api-key'),
    ssl: config.bool('ssl') || config.bool('wallet-ssl'),
    host: config.str('http-host') || config.str('wallet-http-host'),
    port: config.uint('wallet-http-port') || network.walletPort,
    token
  });

  await nclient.open();
  await wclient.open();

  try {
    const wallets = await wclient.getWallets();

    if (!wallets.includes(id)) {
      if (id !== 'hsd-ledger')
        throw new Error(`Wallet "${id}" does not exist.`);

      console.log('Default hsd-ledger wallet not detected.');

      await createWallet(wclient, config, ledger, [id]);
    }

    switch (cmd) {
      case VALID_CMDS[0]:
        await createWallet(wclient, config, ledger, args);
        break;

      case VALID_CMDS[1]:
        await createAccount(wclient, config, ledger, args);
        break;

      case VALID_CMDS[2]:
        await createAddress(wclient, config, ledger, args);
        break;

      case VALID_CMDS[3]:
        await sendToAddress(wclient, nclient, config, ledger, args);
        break;

      case VALID_CMDS[4]:
        await getWallets(wclient, args);
        break;

      case VALID_CMDS[5]:
        await getAccounts(wclient, config, args);
        break;

      case VALID_CMDS[6]:
        await getAccount(wclient, config, args);
        break;

      case VALID_CMDS[7]:
        await getBalance(wclient, config, args);
        break;

      case VALID_CMDS[8]:
        await sendRaw(wclient, nclient, config, ledger, args);
        break;
      case VALID_CMDS[9]:
        await signRaw(wclient, nclient, config, ledger, args);
        break;
      case VALID_CMDS[10]:
        await renew(wclient, nclient, config, ledger, args);
        break;
      default:
        usage(new Error('Must provide valid command.'));
        process.exit(1);
        break;
    }
  } catch (e) {
    throw (e);
  } finally {
    await wclient.close();
    await nclient.close();
    await device.close();
  }

  process.exit(0);
}

/**
 * Displays application version.
 */

function version() {
  console.log(`hsd-ledger v${VERSION}`);
  console.log('');
}

/**
 * Displays usage or error message.
 * @param {String|Error} err - the error message or object
 */

function usage(err) {
  if (err) {
    console.error(`${err.stack}`);
    console.error('');
    return;
  }

  console.log('usage:');
  console.log('  $ hsd-ledger createwallet <wallet-id>');
  console.log('  $ hsd-ledger createaccount <account-name> <account-index>');
  console.log('  $ hsd-ledger createaddress');
  console.log('  $ hsd-ledger sendtoaddress <address> <amount>');
  console.log('  $ hsd-ledger getwallets');
  console.log('  $ hsd-ledger getaccounts');
  console.log('  $ hsd-ledger getaccount <account-name>');
  console.log('  $ hsd-ledger getbalance');
  console.log('');
  console.log('options:');
  console.log('  --help');
  console.log('  --version');
  console.log('  -n, --network <id> (default "main")');
  console.log('  -w, --wallet-id <id> (default "hsd-ledger")');
  console.log('  -a, --account-name <name> (default "default")');
  console.log('  -i, --account-index <index> (default 0)');
  console.log('');
  console.log('The following options configure the node and wallet clients:');
  console.log('  --ssl');
  console.log('  --url <url>');
  console.log('  --api-key <api-key>');
  console.log('  --host <host> (default "localhost")');
  console.log('');
  console.log('The following options configure the node client only:');
  console.log('  --node-ssl');
  console.log('  --node-url <url>');
  console.log('  --node-api-key <api-key>');
  console.log('  --node-host <host> (default "localhost")');
  console.log('  --node-port <port> (default 14037)');
  console.log('');
  console.log('The following options configure the wallet client only:');
  console.log('  --wallet-ssl');
  console.log('  --wallet-url <url>');
  console.log('  --wallet-api-key <api-key>');
  console.log('  --wallet-host <host> (default "localhost")');
  console.log('  --wallet-port <port> (default 14039)');
  console.log('');
}

/*
 * Execute
 */

main().catch((err) => {
  usage(err);
  process.exit(1);
});