import { GraphQLError } from 'graphql'
import retry from 'async-retry'
import Prisma from '@prisma/client'
import { msatsToSats, numWithUnits } from '@/lib/format'
import { BALANCE_LIMIT_MSATS } from '@/lib/constants'

export default async function serialize (trx, { models, lnd }) {
  // wrap first argument in array if not array already
  const isArray = Array.isArray(trx)
  if (!isArray) trx = [trx]

  // conditional queries can be added inline using && syntax
  // we filter any falsy value out here
  trx = trx.filter(q => !!q)

  const results = await retry(async bail => {
    try {
      const [, ...results] = await models.$transaction(
        [models.$executeRaw`SELECT ASSERT_SERIALIZED()`, ...trx],
        { isolationLevel: Prisma.TransactionIsolationLevel.Serializable })
      return results
    } catch (error) {
      console.log(error)
      // two cases where we get insufficient funds:
      // 1. plpgsql function raises
      // 2. constraint violation via a prisma call
      // XXX prisma does not provide a way to distinguish these cases so we
      // have to check the error message
      if (error.message.includes('SN_INSUFFICIENT_FUNDS') ||
        error.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"')) {
        bail(new GraphQLError('insufficient funds', { extensions: { code: 'BAD_INPUT' } }))
      }
      if (error.message.includes('SN_NOT_SERIALIZABLE')) {
        bail(new Error('wallet balance transaction is not serializable'))
      }
      if (error.message.includes('SN_CONFIRMED_WITHDRAWL_EXISTS')) {
        bail(new Error('withdrawal invoice already confirmed (to withdraw again create a new invoice)'))
      }
      if (error.message.includes('SN_PENDING_WITHDRAWL_EXISTS')) {
        bail(new Error('withdrawal invoice exists and is pending'))
      }
      if (error.message.includes('SN_INELIGIBLE')) {
        bail(new Error('user ineligible for gift'))
      }
      if (error.message.includes('SN_UNSUPPORTED')) {
        bail(new Error('unsupported action'))
      }
      if (error.message.includes('SN_DUPLICATE')) {
        bail(new Error('duplicate not allowed'))
      }
      if (error.message.includes('SN_REVOKED_OR_EXHAUSTED')) {
        bail(new Error('faucet has been revoked or is exhausted'))
      }
      if (error.message.includes('SN_INV_PENDING_LIMIT')) {
        bail(new Error('too many pending invoices'))
      }
      if (error.message.includes('SN_INV_EXCEED_BALANCE')) {
        bail(new Error(`pending invoices and withdrawals must not cause balance to exceed ${numWithUnits(msatsToSats(BALANCE_LIMIT_MSATS))}`))
      }
      if (error.message.includes('40001') || error.code === 'P2034') {
        throw new Error('wallet balance serialization failure - try again')
      }
      if (error.message.includes('23514') || ['P2002', 'P2003', 'P2004'].includes(error.code)) {
        bail(new Error('constraint failure'))
      }
      bail(error)
    }
  }, {
    minTimeout: 10,
    maxTimeout: 100,
    retries: 10
  })

  // if first argument was not an array, unwrap the result
  return isArray ? results : results[0]
}