import * as fs from "fs";
import pify from "pify";
import pMap from "p-map";
import { flatten, range } from "lodash";
import { util } from "@zilliqa-js/account";
import { isValidChecksumAddress, sign } from "@zilliqa-js/crypto";
import { BN, Long, units, bytes } from "@zilliqa-js/util";
import { Zilliqa } from "@zilliqa-js/zilliqa";

export class ZilliqaService {
  accounts: string[] = [];
  zil: Zilliqa;

  constructor(api: string, mnemonics: { [mnemonic: string]: number }) {
    const zilliqa = new Zilliqa(api);
    this.zil = zilliqa;

    // you can use one or more mnemonics to manage/generate a large number of accounts
    for (let m in mnemonics) {
      const num = mnemonics[m];
      range(num).forEach((i) => {
        const address = this.zil.wallet.addByMnemonic(m, i);
        this.accounts.push(address);
      });
    }
  }

  addAccount(privateKey: string) {
    // this returns the address of the newly-added account.
    return this.zil.wallet.addByPrivateKey(privateKey);
  }

  async addKeystoreFile(path: string, passphrase: string) {
    const buf = await pify(fs.readFile)(path);
    const json = buf.toString();
    const address = await this.zil.wallet.addByKeystore(json, passphrase);

    return address;
  }

  createAccount(mnemonic: string, index: number) {
    // this returns the address of the freshly-created account.
    return this.zil.wallet.addByMnemonic(mnemonic, index);
  }

  async getTxBlock() {
    const res = await this.zil.blockchain.getLatestTxBlock();

    return parseInt(res.result!.header.BlockNum, 10);
  }

  async getDeposits(addresses: string[], block: number) {
    const res = await this.zil.blockchain.getTransactionsForTxBlock(block);

    if (res.error) {
      // in this case, there are no transactions in the block; we can return
      // an empty array.
      if (res.error.code === -1) {
        return [];
      }

      // you may not wish to throw, depending on the response.
      throw res.error;
    }

    // if there is no error, result will definitely exist.
    const transactions = await pMap(flatten(res.result), (tx) =>
      this.zil.blockchain.getTransaction(tx)
    );

    // filter out everything that isn't in the list of deposit addresses we
    // are interested in.
    return transactions.filter(
      (tx) => addresses.indexOf(tx.txParams.toAddr.toLowerCase()) !== -1
    );
  }

  async withdraw(from: string, to: string, amount: BN) {
    try {
      // always check whether the to minimally passes the checksum.
      // this prevents users from trying to withdraw native ZILs to an
      // ethereum address, and subsequently being unable to recover them!
      if (!isValidChecksumAddress(to)) {
        throw new Error(`${to} is not a valid Zilliqa checksum address!`);
      }

      if (!this.zil.wallet.accounts[from]) {
        throw new Error(`We do not control the ${from} address!`);
      }

      const fromPubKey = this.zil.wallet.accounts[from].publicKey;

      // can also be sent using this.zil.blockchain.createTransaction
      // use the manual way here to demonstrate lower-level functions
      const tx = await this.zil.wallet.signWith(
        this.zil.transactions.new({
          version: bytes.pack(2, 1),
          amount,
          gasLimit: Long.fromNumber(50), // normal (non-contract) transactions cost 50 gas
          gasPrice: new BN(units.toQa(2000, units.Units.Li)), // the minimum gas price is 2,000 li
          toAddr: to, // toAddr is self-explanatory
          pubKey: fromPubKey, // this determines which account is used to send the tx
        }),
        from
      );

      const res = await this.zil.provider.send(
        "CreateTransaction",
        tx.txParams
      );

      if (res.error) {
        throw res.error;
      }

      tx.confirm(res.result.TranID, 33, 1000).then((tx) => {
        // at this point, the tx has been confirmed
        // we don't wait for the confirmation before resonding to the request,
        // but we can use the final result to update the database, etc.
        console.log(`Transaction successfully confirmed.`);

        if (tx.isRejected()) {
          // you may not wish to throw. instead, you may wish to log this
          // failed confirmation to some database, or retry.
          throw new Error("Transaction was rejected");
        }
      });

      return res.result;
    } catch (err) {
      console.log(err);
      // handle the error
      // can be:
      // - timeout
      // - insufficient balance
      // - wrong nonce
      // - did not pass checksum validation
      // - js error
    }
  }
}
