text: import {
    verify,
    decodeBase58Check,
    assertedType
  } from '../utils/crypto'
  import { encode } from '../tx/builder/helpers'
  
  import BigNumber from 'bignumber.js'
  import {
    BASE_VERIFICATION_SCHEMA,
    CONTRACT_VERIFICATION_SCHEMA,
    MIN_GAS_PRICE,
    NAME_CLAIM_VERIFICATION_SCHEMA,
    OBJECT_ID_TX_TYPE,
    OBJECT_TAG_SIGNED_TRANSACTION,
    PROTOCOL_VM_ABI,
    SIGNATURE_VERIFICATION_SCHEMA,
    TX_TYPE
  } from './builder/schema'
  import { buildTxHash, calculateFee, unpackTx } from './builder'
  import { NodePool } from '../node-pool'
  
  /**
   * Transaction validator
   * @module @aeternity/aepp-sdk/es/tx/validator
   * @export TransactionValidator
   * @example import TransactionValidator from '@aeternity/aepp-sdk/es/tx/validator'
   */
  
  const VALIDATORS = {
    // VALIDATE SIGNATURE
    signature ({ rlpEncoded, signature, ownerPublicKey, networkId = 'ae_mainnet' }) {
      const txWithNetworkId = Buffer.concat([Buffer.from(networkId), rlpEncoded])
      const txHashWithNetworkId = Buffer.concat([Buffer.from(networkId), buildTxHash(rlpEncoded, { raw: true })])
      const decodedPub = decodeBase58Check(assertedType(ownerPublicKey, 'ak'))
      return verify(txWithNetworkId, signature, decodedPub) || verify(txHashWithNetworkId, signature, decodedPub)
    },
    // VALIDATE IF ENOUGH FEE
    insufficientFee ({ minFee, fee }) {
      return BigNumber(minFee).lte(BigNumber(fee))
    },
    // VALIDATE IF TTL VALID
    expiredTTL ({ ttl, height }) {
      return BigNumber(ttl).eq(0) || BigNumber(ttl).gte(BigNumber(height))
    },
    // Insufficient Balance for Amount plus Fee
    insufficientBalanceForAmountFee ({ balance, amount = 0, fee }) {
      return BigNumber(balance).gte(BigNumber(amount).plus(fee))
    },
    // Insufficient Balance for Amount
    insufficientBalanceForAmount ({ balance, amount = 0 }) {
      return BigNumber(balance).gte(BigNumber(amount))
    },
    // IF NONCE USED
    nonceUsed ({ accountNonce, nonce }) {
      return BigNumber(nonce).gt(BigNumber(accountNonce))
    },
    // IF NONCE TO HIGH
    nonceHigh ({ accountNonce, nonce }) {
      return !(BigNumber(nonce).gt(BigNumber(accountNonce).plus(1)))
    },
    minGasPrice ({ gasPrice }) {
      return isNaN(gasPrice) || BigNumber(gasPrice).gte(BigNumber(MIN_GAS_PRICE))
    },
    // VM/ABI version validation based on consensus protocol version
    vmAndAbiVersion ({ ctVersion, abiVersion, consensusProtocolVersion, txType }) {
      // If not contract tx
      if (!ctVersion) ctVersion = { abiVersion }
      const supportedProtocol = PROTOCOL_VM_ABI[consensusProtocolVersion]
      // If protocol not implemented
      if (!supportedProtocol) return true
      // If protocol for tx type not implemented
      const txProtocol = supportedProtocol[txType]
  
      return !Object.entries(ctVersion)
        .reduce((acc, [key, value]) =>
          [...acc, value === undefined ? true : txProtocol[key].includes(parseInt(value))],
        []).includes(false)
    },
    insufficientBalanceForFeeNameFee ({ nameFee, fee, balance, VSN }) {
      return VSN === 1 || BigNumber(balance).gte(BigNumber(nameFee).plus(fee))
    }
  }
  
  const resolveDataForBase = async (chain, { ownerPublicKey }) => {
    let accountNonce = 0
    let accountBalance = 0
    try {
      const { nonce, balance } = await chain.api.getAccountByPubkey(ownerPublicKey)
      accountNonce = nonce
      accountBalance = balance
    } catch (e) { console.log('We can not get info about this publicKey') }
    return {
      height: (await chain.api.getCurrentKeyBlockHeight()).height,
      balance: accountBalance,
      accountNonce,
      ownerPublicKey,
      ...chain.getNodeInfo()
    }
  }
  
  // Verification using SCHEMA
  const verifySchema = (schema, data) => {
    // Verify through schema
    return schema.reduce(
      (acc, [msg, validatorKey, { key, type, txKey }]) => {
        if (!VALIDATORS[validatorKey](data)) acc.push({ msg: msg(data), txKey, type })
        return acc
      },
      []
    )
  }
  
  /**
   * Unpack and verify transaction (verify nonce, ttl, fee, signature, account balance)
   * @function
   * @alias module:@aeternity/aepp-sdk/es/tx/validator
   *
   * @param {String} txHash Base64Check transaction hash
   * @param {Object} [options={}] Options
   * @param {String} [options.networkId] networkId Use in signature verification
   * @return {Promise<Object>} Object with verification errors and warnings
   */
  async function unpackAndVerify (txHash, { networkId } = {}) {
    const { tx: unpackedTx, rlpEncoded, txType } = unpackTx(txHash)
  
    if (+unpackedTx.tag === OBJECT_TAG_SIGNED_TRANSACTION) {
      const { txType, tx } = unpackedTx.encodedTx
      const signatures = unpackedTx.signatures.map(raw => ({ raw, hash: encode(raw, 'sg') }))
      const rlpEncodedTx = unpackedTx.encodedTx.rlpEncoded
  
      return {
        validation: await this.verifyTx({ tx, signatures, rlpEncoded: rlpEncodedTx }, networkId),
        tx,
        signatures,
        txType
      }
    }
    return {
      validation: await this.verifyTx({ tx: unpackedTx, rlpEncoded }, networkId),
      tx: unpackedTx,
      txType
    }
  }
  
  const getOwnerPublicKey = (tx) =>
    tx[['senderId', 'accountId', 'ownerId', 'callerId', 'oracleId', 'fromId', 'initiator', 'gaId'].find(key => tx[key])].replace('ok_', 'ak_')
  
  /**
   * Verify transaction (verify nonce, ttl, fee, signature, account balance)
   * @function
   * @alias module:@aeternity/aepp-sdk/es/tx/validator
   *
   * @param {Object} [data={}] data TX data object
   * @param {String} [data.tx] tx Transaction hash
   * @param {Array} [data.signatures] signatures Transaction signature's
   * @param {Array} [data.rlpEncoded] rlpEncoded RLP encoded transaction
   * @param {String} networkId networkId Use in signature verification
   * @return {Promise<Array>} Object with verification errors and warnings
   */
  async function verifyTx ({ tx, signatures, rlpEncoded }, networkId) {
    networkId = networkId || this.getNetworkId() || 'ae_mainnet'
    // Fetch data for verification
    const ownerPublicKey = getOwnerPublicKey(tx)
    const gas = Object.prototype.hasOwnProperty.call(tx, 'gas') ? +tx.gas : 0
    const txType = OBJECT_ID_TX_TYPE[+tx.tag]
    const resolvedData = {
      minFee: calculateFee(0, txType, { gas, params: tx, showWarning: false }),
      ...(await resolveDataForBase(this, { ownerPublicKey })),
      ...tx,
      txType
    }
    const signatureVerification = signatures && signatures.length
      ? verifySchema(SIGNATURE_VERIFICATION_SCHEMA, {
        rlpEncoded,
        signature: signatures[0].raw,
        ownerPublicKey,
        networkId
      })
      : []
    const baseVerification = verifySchema(BASE_VERIFICATION_SCHEMA, resolvedData)
  
    return [
      ...baseVerification,
      ...signatureVerification,
      ...customVerification(txType, resolvedData)
    ]
  }
  
  /**
   * Verification for speciific txType
   * @param txType
   * @param data
   * @return {Array}
   */
  function customVerification (txType, data) {
    switch (txType) {
      case TX_TYPE.contractCreate:
      case TX_TYPE.contractCall:
      case TX_TYPE.oracleRegister:
        return verifySchema(CONTRACT_VERIFICATION_SCHEMA, data)
      case TX_TYPE.nameClaim:
        return verifySchema(NAME_CLAIM_VERIFICATION_SCHEMA, data)
      default:
        return []
    }
  }
  
  /**
   * Transaction Validator Stamp
   * This stamp give us possibility to unpack and validate some of transaction properties,
   * to make sure we can post it to the chain
   * @function
   * @alias module:@aeternity/aepp-sdk/es/tx/validator
   * @rtype Stamp
   * @param {Object} [options={}] - Initializer object
   * @param {Object} [options.url] - Node url
   * @param {Object} [options.internalUrl] - Node internal url
   * @return {Object} Transaction Validator instance
   * @example TransactionValidator({url: 'https://testnet.aeternity.io'})
   */
  const TransactionValidator = NodePool.compose({
    methods: {
      verifyTx,
      unpackAndVerify
    }
  })
  
  export default TransactionValidator
  