import type { ChargeRequest, RefundRequest } from '../index.js'
import type { CreateSubscriptionInput, Subscription } from '../subscriptions/index.js'

const MAX_DECIMAL_DIGITS = 21
const GENERIC_PAYPAL_CHARGE_KEY = /^[A-Za-z0-9._-]+$/
const PAYPAL_SUBSCRIPTION_STATUSES = new Set([
  'APPROVAL_PENDING',
  'APPROVED',
  'ACTIVE',
  'SUSPENDED',
  'CANCELLED',
  'EXPIRED',
])

export type PaypalCreds = {
  clientId: string
  clientSecret: string
  webhookId: string
  apiBaseUrl?: string
  fetch?: typeof fetch
}

export type PaypalMoney = {
  currency_code?: string
  currency?: string
  value?: string
  total?: string
}

export type PaypalLink = {
  href?: string
  rel?: string
  method?: string
}

export type PaypalOrder = {
  id?: string
  status?: string
  links?: PaypalLink[]
  purchase_units?: Array<{
    payments?: {
      captures?: Array<{
        id?: string
        status?: string
        amount?: PaypalMoney
      }>
    }
  }>
}

export type PaypalCapture = {
  id?: string
  amount?: PaypalMoney
}

export type PaypalRefund = {
  id?: string
  status?: string
  amount?: PaypalMoney
}

export type PaypalSubscriptionRecord = {
  id?: string
  status?: string
  plan_id?: string
  custom_id?: string
  start_time?: string
}

export type PaypalWebhookHeaders = {
  authAlgo: string
  certUrl: string
  signature: string
  transmissionId: string
  transmissionTime: string
}

export type PaypalWebhookEvent = {
  id?: string
  event_type?: string
  resource?: Record<string, unknown>
}

export class PaypalApiError extends Error {
  override readonly name = 'PaypalApiError'

  constructor(
    readonly path: string,
    readonly status: number,
    readonly body: unknown,
  ) {
    super(`PayPal ${path} failed with status ${status}`)
  }
}

export class PaypalPayloadError extends Error {
  override readonly name = 'PaypalPayloadError'

  constructor(message: string) {
    super(message)
  }
}

export function getPaypalFetch(creds: PaypalCreds): typeof fetch {
  return creds.fetch ?? fetch
}

export function getPaypalApiBaseUrl(creds: PaypalCreds): string {
  return creds.apiBaseUrl ?? 'https://api-m.paypal.com'
}

export function normalizeMinorUnits(value: bigint): number {
  const amount = Number(value)
  if (!Number.isSafeInteger(amount)) {
    throw new PaypalPayloadError(`PayPal amount exceeds Number safe integer range: ${value}`)
  }
  return amount
}

export function assertPositiveMinorUnits(amount: number, field: string): void {
  if (!Number.isSafeInteger(amount) || amount <= 0) {
    throw new PaypalPayloadError(`PayPal ${field} must be a positive safe integer minor-unit amount`)
  }
}

export function decimalToMinorUnits(value: string): bigint {
  const trimmed = value.trim()
  if (!/^-?\d+(\.\d+)?$/.test(trimmed)) {
    throw new PaypalPayloadError(`invalid PayPal decimal amount "${value}"`)
  }

  const negative = trimmed.startsWith('-')
  const unsigned = negative ? trimmed.slice(1) : trimmed
  const [wholePart = '', fractionPart = ''] = unsigned.split('.')
  const digitCount = wholePart.length + fractionPart.length
  if (digitCount > MAX_DECIMAL_DIGITS) {
    throw new PaypalPayloadError(`PayPal decimal amount has too many digits: ${value}`)
  }
  if (fractionPart.length > 2 && /[^0]/.test(fractionPart.slice(2))) {
    throw new PaypalPayloadError(`unsupported sub-minor-unit precision in "${value}"`)
  }

  const normalizedFraction = (fractionPart + '00').slice(0, 2)
  const whole = BigInt(wholePart || '0')
  const fraction = BigInt(normalizedFraction)
  const minorUnits = whole * 100n + fraction
  return negative ? -minorUnits : minorUnits
}

export function minorUnitsToDecimal(amount: number): string {
  if (!Number.isInteger(amount)) {
    throw new PaypalPayloadError(`PayPal amount must be integer minor units, got ${amount}`)
  }

  const negative = amount < 0
  const absolute = Math.abs(amount)
  const whole = Math.trunc(absolute / 100)
  const fraction = absolute % 100
  const sign = negative ? '-' : ''
  return `${sign}${whole}.${String(fraction).padStart(2, '0')}`
}

export function parsePaypalMoney(money: PaypalMoney | undefined): {
  amountMinor: bigint
  currency: string | null
} {
  if (!money) {
    throw new PaypalPayloadError('missing PayPal money payload')
  }

  const rawValue = money.value ?? money.total
  if (typeof rawValue !== 'string') {
    throw new PaypalPayloadError('missing PayPal money value')
  }

  const currency =
    typeof money.currency_code === 'string'
      ? money.currency_code
      : typeof money.currency === 'string'
        ? money.currency
        : null

  return {
    amountMinor: decimalToMinorUnits(rawValue),
    currency,
  }
}

export function parsePositivePaypalMoney(money: PaypalMoney | undefined): {
  amountMinor: bigint
  currency: string
} {
  const parsed = parsePaypalMoney(money)
  if (parsed.amountMinor <= 0n) {
    throw new PaypalPayloadError('PayPal amount must be positive minor units')
  }
  if (!parsed.currency) {
    throw new PaypalPayloadError('missing PayPal money currency')
  }
  return {
    amountMinor: parsed.amountMinor,
    currency: parsed.currency,
  }
}

export function mapSubscriptionRecord(record: PaypalSubscriptionRecord): Subscription {
  const id = requireString(record.id, 'subscription.id')
  const planId = requireString(record.plan_id, 'subscription.plan_id')
  const currentPeriod = resolveSubscriptionPeriod(record)
  const status = requireString(record.status, 'subscription.status')
  if (!PAYPAL_SUBSCRIPTION_STATUSES.has(status)) {
    throw new PaypalPayloadError(`unsupported PayPal subscription status "${status}"`)
  }

  return {
    id,
    status,
    currentPeriod,
    planId,
  }
}

export function buildSubscriptionCreateBody(input: CreateSubscriptionInput) {
  return {
    plan_id: input.planId,
    custom_id: input.currentPeriod,
  }
}

export function buildOrderCreateBody(req: ChargeRequest) {
  assertPositiveMinorUnits(req.amount, 'charge amount')
  if (!GENERIC_PAYPAL_CHARGE_KEY.test(req.chargeKey)) {
    throw new PaypalPayloadError(
      'PayPal chargeKey must contain only letters, numbers, ".", "_", or "-"',
    )
  }

  return {
    intent: 'CAPTURE',
    purchase_units: [
      {
        invoice_id: req.chargeKey,
        custom_id: req.chargeKey,
        amount: {
          currency_code: req.currency.toUpperCase(),
          value: minorUnitsToDecimal(req.amount),
        },
      },
    ],
  }
}

export function requireString(value: unknown, field: string): string {
  if (typeof value !== 'string' || value.trim() === '') {
    throw new PaypalPayloadError(`missing PayPal field "${field}"`)
  }
  return value
}

export function resolveSubscriptionPeriod(record: {
  custom_id?: string
  start_time?: string
}): string {
  if (typeof record.custom_id === 'string' && record.custom_id.trim() !== '') {
    return validatePeriod(record.custom_id, 'subscription.custom_id')
  }

  const normalized = normalizePeriod(record.start_time)
  if (normalized) {
    return normalized
  }

  throw new PaypalPayloadError('missing PayPal subscription current period')
}

export function validatePeriod(value: string, field: string): string {
  if (!/^\d{4}-\d{2}$/.test(value)) {
    throw new PaypalPayloadError(`invalid PayPal ${field} period "${value}"`)
  }
  return value
}

export function normalizePeriod(value: unknown): string | null {
  if (typeof value !== 'string' || value.trim() === '') return null
  if (/^\d{4}-\d{2}$/.test(value)) return value

  const date = new Date(value)
  if (Number.isNaN(date.getTime())) return null

  const year = date.getUTCFullYear()
  const month = String(date.getUTCMonth() + 1).padStart(2, '0')
  return `${year}-${month}`
}

export function resolveChargeKey(
  subscriptionId: string | null,
  period: string | null,
  fallback: string,
): string {
  if (subscriptionId && period) {
    return `${subscriptionId}:${period}`
  }
  return fallback
}

export function extractApproveUrl(order: PaypalOrder): string | null {
  for (const link of order.links ?? []) {
    if (link.rel === 'approve' && typeof link.href === 'string' && link.href.trim() !== '') {
      return link.href
    }
  }
  return null
}

export function extractSettledCapture(order: PaypalOrder): {
  captureId: string
  amountMinor: bigint
  currency: string
} | null {
  const capture = order.purchase_units?.[0]?.payments?.captures?.[0]
  if (!capture || capture.status !== 'COMPLETED') return null

  const captureId = requireString(capture.id, 'order.purchase_units[0].payments.captures[0].id')
  const { amountMinor, currency } = parsePositivePaypalMoney(capture.amount)
  return { captureId, amountMinor, currency }
}

export function extractCaptureId(refund: RefundRequest): string {
  if (refund.refundId.trim() !== '') {
    return refund.refundId
  }
  throw new PaypalPayloadError('PayPal refund requires refundId to carry the capture id')
}
