import { GraphQLError } from 'graphql'
import retry from 'async-retry'
import Prisma from '@prisma/client'
import { settleHodlInvoice } from 'ln-service'
import { createHmac } from './wallet'
import { msatsToSats } from '../../lib/format'

export default async function serialize (models, ...calls) {
  return await retry(async bail => {
    try {
      const [, ...result] = await models.$transaction(
        [models.$executeRaw`SELECT ASSERT_SERIALIZED()`, ...calls],
        { isolationLevel: Prisma.TransactionIsolationLevel.Serializable })
      return calls.length > 1 ? result : result[0]
    } catch (error) {
      console.log(error)
      if (error.message.includes('SN_INSUFFICIENT_FUNDS')) {
        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 must not cause balance to exceed 1m sats'))
      }
      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: 100,
    factor: 1.1,
    retries: 5
  })
}

export async function serializeInvoicable (queries, { models, lnd, hash, hmac, me, enforceFee }) {
  if (!me && !hash) {
    throw new Error('you must be logged in or pay')
  }

  let trx = Array.isArray(queries) ? queries : [queries]
  // save offset to the first query in arguments relative to queries that we will run
  let queryOffset = 0

  let invoice
  if (hash) {
    invoice = await checkInvoice(models, hash, hmac, enforceFee)
    trx = [
      models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${invoice.user.id}`,
      ...trx,
      models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } })
    ]
    // first query in arguments is now at index 1
    queryOffset = 1
  }

  const results = await serialize(models, ...trx)
  const result = trx.length > 1 ? results[queryOffset][0] : results[0]

  if (invoice?.isHeld) await settleHodlInvoice({ secret: invoice.preimage, lnd })

  return result
}

export async function checkInvoice (models, hash, hmac, fee) {
  if (!hash) {
    throw new GraphQLError('hash required', { extensions: { code: 'BAD_INPUT' } })
  }
  if (!hmac) {
    throw new GraphQLError('hmac required', { extensions: { code: 'BAD_INPUT' } })
  }
  const hmac2 = createHmac(hash)
  if (hmac !== hmac2) {
    throw new GraphQLError('bad hmac', { extensions: { code: 'FORBIDDEN' } })
  }

  const invoice = await models.invoice.findUnique({
    where: { hash },
    include: {
      user: true
    }
  })

  if (!invoice) {
    throw new GraphQLError('invoice not found', { extensions: { code: 'BAD_INPUT' } })
  }

  const expired = new Date(invoice.expiresAt) <= new Date()
  if (expired) {
    throw new GraphQLError('invoice expired', { extensions: { code: 'BAD_INPUT' } })
  }
  if (invoice.confirmedAt) {
    throw new GraphQLError('invoice already used', { extensions: { code: 'BAD_INPUT' } })
  }

  if (invoice.cancelled) {
    throw new GraphQLError('invoice was canceled', { extensions: { code: 'BAD_INPUT' } })
  }

  if (!invoice.msatsReceived) {
    throw new GraphQLError('invoice was not paid', { extensions: { code: 'BAD_INPUT' } })
  }
  if (fee && msatsToSats(invoice.msatsReceived) < fee) {
    throw new GraphQLError('invoice amount too low', { extensions: { code: 'BAD_INPUT' } })
  }

  return invoice
}
