import { Elements, useElements, useStripe } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js'
import type {
  Stripe,
  StripeElementLocale,
  StripeElementsOptions,
  Appearance,
} from '@stripe/stripe-js'
import type { ChargeResult } from '@platform-modules/billing'
import { useCallback, useEffect, useMemo, useState, type ReactElement, type ReactNode } from 'react'
import { PaymentUiContext, type ConfirmPaymentOptions, type PaymentConfirmResult } from './context'
import { PaymentConfirmError, PaymentProviderError } from './errors'
import { assertClientSecretShape, assertPublishableKey } from './guards'

export interface StripePaymentProviderProps {
  /** Stripe publishable key (pk_test_… / pk_live_…). PUBLIC — safe in the client bundle. */
  publishableKey: string
  /**
   * Per-PaymentIntent client secret from checkout start (billing ChargeResult.requires_client_action).
   * Client-safe. This is the PaymentIntent IDENTITY: the provider internally keys the Elements group
   * to it, so passing a NEW clientSecret auto-remounts the card field for the new intent — pass it
   * as a normal prop, no manual `key=` needed. A change clears the card field (correct: card data
   * lives in Stripe's iframe and is intrinsically unpreservable across a new intent).
   */
  clientSecret: Extract<ChargeResult, { kind: 'requires_client_action' }>['clientSecret']
  /** Optional Elements appearance (token-driven, see spec §6). */
  appearance?: Appearance
  /** Optional Elements locale. */
  locale?: StripeElementLocale
  /** TEST-ONLY seam — a resolved/stub Stripe so jsdom tests never hit the network. */
  stripeOverride?: PromiseLike<Stripe | null>
  children: ReactNode
}

function StripePaymentUiBridge({
  children,
  stripePromise,
}: {
  children: ReactNode
  stripePromise: PromiseLike<Stripe | null>
}): ReactElement {
  const stripe = useStripe()
  const elements = useElements()
  const [initError, setInitError] = useState<PaymentProviderError | null>(null)

  useEffect(() => {
    void Promise.resolve(stripePromise).then((s) => {
      if (s === null) {
        setInitError(new PaymentProviderError('billing-react: Stripe failed to load.'))
      }
    })
  }, [stripePromise])

  const ready = !!stripe && !!elements

  const confirm = useCallback(
    async (opts: ConfirmPaymentOptions): Promise<PaymentConfirmResult> => {
      if (!stripe || !elements) {
        throw new PaymentProviderError('billing-react: Stripe is not ready.')
      }
      const result = await stripe.confirmPayment({
        elements,
        confirmParams: { return_url: opts.returnUrl },
        redirect: 'if_required',
      })
      if (result.error) {
        return {
          kind: 'error',
          error: new PaymentConfirmError(
            result.error.message ?? 'payment failed',
            result.error.code ?? result.error.type ?? 'payment_failed',
          ),
        }
      }
      if (result.paymentIntent?.status === 'succeeded') {
        return { kind: 'succeeded' }
      }
      return { kind: 'requires_redirect' }
    },
    [stripe, elements],
  )

  const value = useMemo(
    () => ({ ready, confirm, error: initError }),
    [ready, confirm, initError],
  )

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

export function StripePaymentProvider({
  publishableKey,
  clientSecret,
  appearance,
  locale,
  stripeOverride,
  children,
}: StripePaymentProviderProps): ReactElement {
  // Fail closed at the trust boundary BEFORE any Stripe call (spec §5).
  assertPublishableKey(publishableKey)
  assertClientSecretShape(clientSecret)

  // Stable Stripe instance keyed to publishableKey — a new instance per render remounts Elements.
  const stripe = useMemo<PromiseLike<Stripe | null>>(
    () => stripeOverride ?? loadStripe(publishableKey),
    [publishableKey, stripeOverride],
  )

  const options = useMemo<StripeElementsOptions>(
    () => ({ clientSecret, appearance, locale }),
    [clientSecret, appearance, locale],
  )

  return (
    <Elements stripe={stripe} options={options} key={clientSecret}>
      <StripePaymentUiBridge stripePromise={stripe}>{children}</StripePaymentUiBridge>
    </Elements>
  )
}