import { NextApiRequest, NextApiResponse } from 'next'
import getRawBody from 'raw-body'
import crypto from 'crypto'
import dayjs from 'dayjs'

import {
  BtcPayGetRatesRes,
  BtcPayGetPaymentMethodsRes,
  DonationMetadata,
  DonationCryptoPayments,
} from '../../../server/types'
import { btcpayApi as _btcpayApi, btcpayApi, prisma } from '../../../server/services'
import { env } from '../../../env.mjs'
import { givePointsToUser } from '../../../server/utils/perks'
import { sendDonationConfirmationEmail } from '../../../server/utils/mailing'
import { NET_DONATION_AMOUNT_WITH_POINTS_RATE, POINTS_PER_USD } from '../../../config'
import { getDonationAttestation, getMembershipAttestation } from '../../../server/utils/attestation'
import { addUserToPgMembersGroup } from '../../../utils/pg-forum-connection'
import { log } from '../../../utils/logging'
import {
  getBtcPayInvoice,
  getBtcPayInvoicePaymentMethods,
} from '../../../server/utils/btcpayserver'

export const config = {
  api: {
    bodyParser: false,
  },
}

type WebhookBody = Record<string, any> & {
  manuallyMarked: boolean
  deliveryId: string
  webhookId: string
  originalDeliveryId: string
  isRedelivery: boolean
  type: string
  timestamp: number
  storeId: string
  invoiceId: string
  metadata?: DonationMetadata
  paymentMethod: string
}

async function handleFundingRequiredApiDonation(body: WebhookBody) {
  if (!body.metadata || JSON.stringify(body.metadata) === '{}') return

  const existingDonation = await prisma.donation.findFirst({
    where: { btcPayInvoiceId: body.invoiceId },
  })

  if (existingDonation) {
    log(
      'warn',
      `[BTCPay webhook] Attempted to process already processed invoice ${body.invoiceId}.`
    )
    return
  }

  // Handle payment methods like "BTC-LightningNetwork" if added in the future
  const cryptoCode = body.paymentMethod.includes('-')
    ? body.paymentMethod.split('-')[0]
    : body.paymentMethod

  const { data: rates } = await btcpayApi.get<BtcPayGetRatesRes>(
    `/rates?currencyPair=${cryptoCode}_USD`
  )

  const cryptoRate = Number(rates[0].rate)
  const cryptoAmount = Number(body.payment.value)
  const fiatAmount = Number((cryptoAmount * cryptoRate).toFixed(2))

  await prisma.donation.create({
    data: {
      userId: null,
      btcPayInvoiceId: body.invoiceId,
      projectName: body.metadata.projectName,
      projectSlug: body.metadata.projectSlug,
      fundSlug: body.metadata.fundSlug,
      grossFiatAmount: fiatAmount,
      netFiatAmount: fiatAmount,
      showDonorNameOnLeaderboard: body.metadata.showDonorNameOnLeaderboard === 'true',
      donorName: body.metadata.donorName,
    },
  })

  log('info', `[BTCPay webhook] Successfully processed invoice ${body.invoiceId}!`)
}

// This handles both donations and memberships.
async function handleDonationOrMembership(body: WebhookBody) {
  if (!body.metadata || JSON.stringify(body.metadata) === '{}') return

  const existingDonation = await prisma.donation.findFirst({
    where: { btcPayInvoiceId: body.invoiceId },
  })

  if (existingDonation) {
    log(
      'warn',
      `[BTCPay webhook] Attempted to process already processed invoice ${body.invoiceId}.`
    )
    return
  }

  const termToMembershipExpiresAt = {
    monthly: dayjs().add(1, 'month').toDate(),
    annually: dayjs().add(1, 'year').toDate(),
  } as const

  let membershipExpiresAt = null

  if (body.metadata.isMembership === 'true' && body.metadata.membershipTerm) {
    membershipExpiresAt = termToMembershipExpiresAt[body.metadata.membershipTerm]
  }

  const cryptoPayments: DonationCryptoPayments = []
  const paymentMethods = await getBtcPayInvoicePaymentMethods(body.invoiceId)
  const shouldGivePointsBack = body.metadata.givePointsBack === 'true'

  // Get how much was paid for each crypto
  paymentMethods.forEach((paymentMethod) => {
    if (!body.metadata) return

    const cryptoRate = Number(paymentMethod.rate)
    const grossCryptoAmount = Number(paymentMethod.paymentMethodPaid)

    // Deduct 10% of amount if donator wants points
    const netCryptoAmount = shouldGivePointsBack
      ? grossCryptoAmount * NET_DONATION_AMOUNT_WITH_POINTS_RATE
      : grossCryptoAmount

    // Move on if amound paid with current method is 0
    if (!grossCryptoAmount) return

    cryptoPayments.push({
      cryptoCode: paymentMethod.currency,
      grossAmount: grossCryptoAmount,
      netAmount: netCryptoAmount,
      rate: cryptoRate,
    })
  })

  // Handle marked paid invoice
  if (body.manuallyMarked) {
    const invoice = await getBtcPayInvoice(body.invoiceId)

    const amountPaidFiat = cryptoPayments.reduce(
      (total, paymentMethod) => total + paymentMethod.grossAmount * paymentMethod.rate,
      0
    )

    const invoiceAmountFiat = Number(invoice.amount)
    const amountDueFiat = invoiceAmountFiat - amountPaidFiat

    if (amountDueFiat > 0) {
      cryptoPayments.push({
        cryptoCode: 'MANUAL',
        grossAmount: amountDueFiat,
        netAmount: shouldGivePointsBack
          ? amountDueFiat * NET_DONATION_AMOUNT_WITH_POINTS_RATE
          : amountDueFiat,
        rate: 1,
      })
    }
  }

  const grossFiatAmount = cryptoPayments.reduce(
    (total, paymentMethod) => total + paymentMethod.grossAmount * paymentMethod.rate,
    0
  )

  const netFiatAmount = cryptoPayments.reduce(
    (total, paymentMethod) => total + paymentMethod.netAmount * paymentMethod.rate,
    0
  )

  const pointsToGive = shouldGivePointsBack ? Math.floor(grossFiatAmount / POINTS_PER_USD) : 0

  const donation = await prisma.donation.create({
    data: {
      userId: body.metadata.userId,
      btcPayInvoiceId: body.invoiceId,
      projectName: body.metadata.projectName,
      projectSlug: body.metadata.projectSlug,
      fundSlug: body.metadata.fundSlug,
      cryptoPayments,
      grossFiatAmount: Number(grossFiatAmount.toFixed(2)),
      netFiatAmount: Number(netFiatAmount.toFixed(2)),
      pointsAdded: pointsToGive,
      membershipExpiresAt,
      membershipTerm: body.metadata.membershipTerm || null,
      showDonorNameOnLeaderboard: body.metadata.showDonorNameOnLeaderboard === 'true',
      donorName: body.metadata.donorName,
    },
  })

  // Add points
  if (shouldGivePointsBack && body.metadata.userId) {
    try {
      await givePointsToUser({ pointsToGive, donation })
    } catch (error) {
      log(
        'error',
        `[Stripe webhook] Failed to give points for invoice ${body.invoiceId}. Rolling back.`
      )
      await prisma.donation.delete({ where: { id: donation.id } })
      throw error
    }
  }

  // Add PG forum user to membership group
  if (
    body.metadata.isMembership &&
    body.metadata.fundSlug === 'privacyguides' &&
    body.metadata.userId
  ) {
    try {
      await addUserToPgMembersGroup(body.metadata.userId)
    } catch (error) {
      log(
        'warn',
        `[BTCPay webhook] Could not add user ${body.metadata.userId} to PG forum members group. Invoice: ${body.invoiceId}. NOT rolling back. Continuing... Cause:`
      )
      console.error(error)
    }
  }

  if (body.metadata.donorEmail && body.metadata.donorName) {
    let attestationMessage = ''
    let attestationSignature = ''

    if (body.metadata.isMembership === 'true' && body.metadata.membershipTerm) {
      const attestation = await getMembershipAttestation({
        donorName: body.metadata.donorName,
        donorEmail: body.metadata.donorEmail,
        totalAmountToDate: grossFiatAmount,
        donation,
      })

      attestationMessage = attestation.message
      attestationSignature = attestation.signature
    }

    if (body.metadata.isMembership === 'false') {
      const attestation = await getDonationAttestation({
        donorName: body.metadata.donorName,
        donorEmail: body.metadata.donorEmail,
        donation,
      })

      attestationMessage = attestation.message
      attestationSignature = attestation.signature
    }

    try {
      await sendDonationConfirmationEmail({
        to: body.metadata.donorEmail,
        donorName: body.metadata.donorName,
        donation,
        attestationMessage,
        attestationSignature,
      })
    } catch (error) {
      log(
        'warn',
        `[BTCPay webhook] Failed to send donation confirmation email for invoice ${body.invoiceId}. NOT rolling back. Cause:`
      )
      console.error(error)
    }
  }

  log('info', `[BTCPay webhook] Successfully processed invoice ${body.invoiceId}!`)
}

async function handleBtcpayWebhook(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    res.setHeader('Allow', ['POST'])
    res.status(405).end(`Method ${req.method} Not Allowed`)
    return
  }

  if (typeof req.headers['btcpay-sig'] !== 'string') {
    res.status(400).json({ success: false })
    return
  }

  const rawBody = await getRawBody(req)
  const body: WebhookBody = JSON.parse(Buffer.from(rawBody).toString('utf8'))

  const expectedSigHash = crypto
    .createHmac('sha256', env.BTCPAY_WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex')

  const incomingSigHash = (req.headers['btcpay-sig'] as string).split('=')[1]

  if (expectedSigHash !== incomingSigHash) {
    console.error('Invalid signature')
    res.status(401).json({ success: false })
    return
  }

  if (!body.metadata) {
    return res.status(200).json({ success: true })
  }

  if (body.type === 'InvoicePaymentSettled') {
    // Handle payments to funding required API invoices ONLY
    if (body.metadata.staticGeneratedForApi === 'false') {
      return res.status(200).json({ success: true })
    }

    await handleFundingRequiredApiDonation(body)
  }

  if (body.type === 'InvoiceSettled') {
    // If this is a funding required API invoice, let InvoiceReceivedPayment handle it instead
    if (body.metadata.staticGeneratedForApi === 'true') {
      return res.status(200).json({ success: true })
    }

    await handleDonationOrMembership(body)
  }

  res.status(200).json({ success: true })
}

export default handleBtcpayWebhook
