import { useCallback, useEffect, useMemo, useReducer, useRef, type ReactElement } from 'react'
import {
  CheckoutContext,
  checkoutReducer,
  initialState,
  type CheckoutContextValue,
} from './context.js'
import type { CheckoutClient, CheckoutStartInput } from './client.js'
import { ensureKey, persistRecord, clearRecord, loadRecord } from './idempotency.js'
import {
  isCheckoutWireError,
  isCheckoutValidationError,
  isOrderIdempotencyConflictError,
} from './errors.js'

const POLL_BACKOFF_MS = [1000, 2000, 4000, 8000] as const
const POLL_BUDGET_MS = 120_000

const TERMINAL_SUCCESS = new Set(['paid', 'fulfilled', 'completed'])
const TERMINAL_FAILURE = new Set(['failed', 'cancelled', 'unfulfillable'])

export function CheckoutProvider(props: {
  client: CheckoutClient
  cartId: string
  cartVersion?: string | number
  makeIdempotencyKey?: () => string
  children: React.ReactNode
}): ReactElement {
  const { client, cartId, cartVersion, makeIdempotencyKey, children } = props
  const [state, dispatch] = useReducer(checkoutReducer, initialState)
  const submitting = useRef(false)
  const cartVersionRef = useRef(cartVersion)
  const attemptEpoch = useRef(0)

  useEffect(() => {
    cartVersionRef.current = cartVersion
  })

  // Resume on mount: a persisted record + a non-terminal order → re-enter settling.
  useEffect(() => {
    const rec = loadRecord(cartId)
    if (rec?.orderId) {
      dispatch({ type: 'SETTLING', orderId: rec.orderId })
    }
    // mount-only resume; cartId is stable per provider instance
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  const submit = useCallback(
    async (input: Omit<CheckoutStartInput, 'idempotencyKey'>): Promise<void> => {
      if (submitting.current) return
      submitting.current = true
      const epoch = ++attemptEpoch.current
      const mintVersion = cartVersion
      dispatch({ type: 'SUBMIT_START', cartVersion })
      try {
        const idempotencyKey = ensureKey(cartId, makeIdempotencyKey)
        const res = await client.start({ ...input, idempotencyKey })
        if (attemptEpoch.current !== epoch) return
        if (res.clientSecret) {
          if (
            cartVersionRef.current !== undefined &&
            cartVersionRef.current !== mintVersion
          ) {
            clearRecord(cartId)
            dispatch({ type: 'CART_CHANGED' })
          } else {
            persistRecord(cartId, {
              idempotencyKey,
              orderId: res.orderId,
              clientSecret: res.clientSecret,
              mintCartVersion: mintVersion,
            })
            dispatch({ type: 'AWAIT_PAYMENT', orderId: res.orderId, clientSecret: res.clientSecret })
          }
        } else {
          persistRecord(cartId, { idempotencyKey, orderId: res.orderId, mintCartVersion: mintVersion })
          dispatch({ type: 'SETTLING', orderId: res.orderId })
        }
      } catch (e) {
        if (attemptEpoch.current !== epoch) return
        if (isCheckoutWireError(e) && e.stale) {
          dispatch({ type: 'STALE', stale: e.stale })
        } else if (isCheckoutValidationError(e) && e.reason === 'STALE_ITEMS' && e.stale) {
          dispatch({ type: 'STALE', stale: e.stale })
        } else if (isOrderIdempotencyConflictError(e)) {
          clearRecord(cartId) // cart diverged under a live key → next submit's ensureKey mints fresh
          dispatch({ type: 'CART_CHANGED' })
        } else {
          dispatch({ type: 'FAILED', error: isCheckoutWireError(e) ? e : null })
        }
      } finally {
        submitting.current = false
      }
    },
    [client, cartId, cartVersion, makeIdempotencyKey],
  )

  const reset = useCallback(() => {
    attemptEpoch.current++
    clearRecord(cartId)
    dispatch({ type: 'RESET' })
  }, [cartId])

  const retry = useCallback(() => {
    if (state.phase !== 'settle_timeout') return
    const rec = loadRecord(cartId)
    if (!rec?.orderId) return
    dispatch({ type: 'SETTLING', orderId: rec.orderId })
  }, [cartId, state.phase])

  // Proactive cart-change guard (§0.1 layer a + a.1): unmount stale PaymentElement when cart diverges.
  // awaiting_payment ONLY — invalidating during confirming would CART_CHANGED while confirm() is
  // in flight at the PSP (charge committed, un-abortable); SETTLING is then rejected → double charge.
  useEffect(() => {
    if (
      state.clientSecret &&
      state.phase === 'awaiting_payment' &&
      cartVersion !== undefined &&
      state.mintCartVersion !== null &&
      state.mintCartVersion !== cartVersion
    ) {
      clearRecord(cartId)
      dispatch({ type: 'CART_CHANGED' })
    }
  }, [cartVersion, state.clientSecret, state.phase, state.mintCartVersion, cartId])

  // THE single settle-poll effect — lives here, in the provider singleton, never in a hook.
  // Reacts to phase==='settling'; polls getStatus with backoff to a terminal status.
  useEffect(() => {
    if (state.phase !== 'settling' || !state.orderId) return
    let cancelled = false
    let attempt = 0
    const started = Date.now()
    const orderId = state.orderId

    const tick = async (): Promise<void> => {
      if (cancelled) return
      try {
        const res = await client.getStatus(orderId)
        if (cancelled) return
        if (TERMINAL_SUCCESS.has(res.status)) {
          clearRecord(cartId)
          dispatch({ type: 'PAID' })
          return
        }
        if (TERMINAL_FAILURE.has(res.status)) {
          dispatch({ type: 'FAILED', error: null })
          return
        }
        if (res.clientSecret) {
          // requires_action surfaced again → back to awaiting_payment
          dispatch({ type: 'AWAIT_PAYMENT', orderId, clientSecret: res.clientSecret })
          return
        }
      } catch {
        // transient transport error — keep polling within the budget
      }
      if (Date.now() - started >= POLL_BUDGET_MS) {
        if (!cancelled) dispatch({ type: 'SETTLE_TIMEOUT' })
        return
      }
      const delay = POLL_BACKOFF_MS[Math.min(attempt, POLL_BACKOFF_MS.length - 1)]
      attempt += 1
      timer = setTimeout(() => void tick(), delay)
    }

    let timer = setTimeout(() => void tick(), 0)
    return () => {
      cancelled = true
      clearTimeout(timer)
    }
  }, [state.phase, state.orderId, client, cartId])

  const value = useMemo<CheckoutContextValue>(
    () => ({ state, client, cartId, makeIdempotencyKey, dispatch, submit, reset, retry }),
    [state, client, cartId, makeIdempotencyKey, submit, reset, retry],
  )

  return <CheckoutContext.Provider value={value}>{children}</CheckoutContext.Provider>
}