import { useCallback } from 'react'
import { useSendWallets } from '@/wallets'
import { formatSats } from '@/lib/format'
import useInvoice from '@/components/use-invoice'
import { FAST_POLL_INTERVAL, WALLET_SEND_PAYMENT_TIMEOUT_MS } from '@/lib/constants'
import {
  WalletsNotAvailableError, WalletSenderError, WalletAggregateError, WalletPaymentAggregateError,
  WalletNotEnabledError, WalletSendNotConfiguredError, WalletPaymentError, WalletError, WalletReceiverError
} from '@/wallets/errors'
import { canSend } from './common'
import { useWalletLoggerFactory } from './logger'
import { timeoutSignal, withTimeout } from '@/lib/time'

export function useWalletPayment () {
  const wallets = useSendWallets()
  const sendPayment = useSendPayment()
  const loggerFactory = useWalletLoggerFactory()
  const invoiceHelper = useInvoice()

  return useCallback(async (invoice, { waitFor, updateOnFallback }) => {
    let aggregateError = new WalletAggregateError([])
    let latestInvoice = invoice

    // throw a special error that caller can handle separately if no payment was attempted
    if (wallets.length === 0) {
      throw new WalletsNotAvailableError()
    }

    for (let i = 0; i < wallets.length; i++) {
      const wallet = wallets[i]
      const logger = loggerFactory(wallet)

      const { bolt11 } = latestInvoice
      const controller = invoiceController(latestInvoice, invoiceHelper.isInvoice)

      const walletPromise = sendPayment(wallet, logger, latestInvoice)
      const pollPromise = controller.wait(waitFor)

      try {
        return await new Promise((resolve, reject) => {
          // can't await wallet payments since we might pay hold invoices and thus payments might not settle immediately.
          // that's why we separately check if we received the payment with the invoice controller.
          walletPromise.catch(reject)
          pollPromise.then(resolve).catch(reject)
        })
      } catch (err) {
        let paymentError = err
        const message = `payment failed: ${paymentError.reason ?? paymentError.message}`

        if (!(paymentError instanceof WalletError)) {
          // payment failed for some reason unrelated to wallets (ie invoice expired or was canceled).
          // bail out of attempting wallets.
          logger.error(message, { bolt11 })
          throw paymentError
        }

        // at this point, paymentError is always a wallet error,
        // we just need to distinguish between receiver and sender errors

        try {
          // we need to poll one more time to check for failed forwards since sender wallet errors
          // can be caused by them which we want to handle as receiver errors, not sender errors.
          await invoiceHelper.isInvoice(latestInvoice, waitFor)
        } catch (err) {
          if (err instanceof WalletError) {
            paymentError = err
          }
        }

        if (paymentError instanceof WalletReceiverError) {
          // if payment failed because of the receiver, use the same wallet again
          // and log this as info, not error
          logger.info('failed to forward payment to receiver, retrying with new invoice', { bolt11 })
          i -= 1
        } else if (paymentError instanceof WalletPaymentError) {
          // only log payment errors, not configuration errors
          logger.error(message, { bolt11 })
        }

        if (paymentError instanceof WalletPaymentError) {
          // if a payment was attempted, cancel invoice to make sure it cannot be paid later and create new invoice to retry.
          await invoiceHelper.cancel(latestInvoice)
        }

        // only create a new invoice if we will try to pay with a wallet again
        const retry = paymentError instanceof WalletReceiverError || i < wallets.length - 1
        if (retry) {
          latestInvoice = await invoiceHelper.retry(latestInvoice, { update: updateOnFallback })
        }

        aggregateError = new WalletAggregateError([aggregateError, paymentError], latestInvoice)

        continue
      } finally {
        controller.stop()
      }
    }

    // if we reach this line, no wallet payment succeeded
    throw new WalletPaymentAggregateError([aggregateError], latestInvoice)
  }, [wallets, invoiceHelper, sendPayment])
}

function invoiceController (inv, isInvoice) {
  const controller = new AbortController()
  const signal = controller.signal
  controller.wait = async (waitFor = inv => inv?.actionState === 'PAID') => {
    return await new Promise((resolve, reject) => {
      let updatedInvoice, paid
      const interval = setInterval(async () => {
        try {
          ({ invoice: updatedInvoice, check: paid } = await isInvoice(inv, waitFor))
          if (paid) {
            resolve(updatedInvoice)
            clearInterval(interval)
            signal.removeEventListener('abort', abort)
          } else {
            console.info(`invoice #${inv.id}: waiting for payment ...`)
          }
        } catch (err) {
          reject(err)
          clearInterval(interval)
          signal.removeEventListener('abort', abort)
        }
      }, FAST_POLL_INTERVAL)

      const abort = () => {
        console.info(`invoice #${inv.id}: stopped waiting`)
        resolve(updatedInvoice)
        clearInterval(interval)
        signal.removeEventListener('abort', abort)
      }
      signal.addEventListener('abort', abort)
    })
  }

  controller.stop = () => controller.abort()

  return controller
}

function useSendPayment () {
  return useCallback(async (wallet, logger, invoice) => {
    if (!wallet.config.enabled) {
      throw new WalletNotEnabledError(wallet.def.name)
    }

    if (!canSend(wallet)) {
      throw new WalletSendNotConfiguredError(wallet.def.name)
    }

    const { bolt11, satsRequested } = invoice

    logger.info(`↗ sending payment: ${formatSats(satsRequested)}`, { bolt11 })
    try {
      const preimage = await withTimeout(
        wallet.def.sendPayment(bolt11, wallet.config, {
          logger,
          signal: timeoutSignal(WALLET_SEND_PAYMENT_TIMEOUT_MS)
        }),
        WALLET_SEND_PAYMENT_TIMEOUT_MS)
      logger.ok(`↗ payment sent: ${formatSats(satsRequested)}`, { bolt11, preimage })
    } catch (err) {
      // we don't log the error here since we want to handle receiver errors separately
      const message = err.message || err.toString?.()
      throw new WalletSenderError(wallet.def.name, invoice, message)
    }
  }, [])
}