import { useCallback, useMemo, useRef, useState } from 'react'
import { cartReduce, type Cart, type CartAction, type PriceSnapshot } from '@platform-modules/commerce-cart'
import type { CartClient } from './client.js'

export interface CartMutations {
  addLine(input: { variantId: string; qty: number; price: PriceSnapshot; vendorId?: string | null }): Promise<Cart>
  setQty(lineId: string, qty: number): Promise<Cart>
  removeLine(lineId: string): Promise<Cart>
  clear(): Promise<Cart>
  merge(guestCartId: string): Promise<Cart>
  pending: boolean
  error: Error | null
}

interface Deps {
  client: CartClient
  cartRef: React.MutableRefObject<Cart | null>
  setCart: (c: Cart) => void
}

export function useCartMutations({ client, cartRef, setCart }: Deps): CartMutations {
  const [pending, setPending] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  const clientRef = useRef(client)
  clientRef.current = client
  const setCartRef = useRef(setCart)
  setCartRef.current = setCart

  const queueRef = useRef<Promise<unknown>>(Promise.resolve())
  const seqRef = useRef(0)
  const latestSeqRef = useRef(0)
  const inFlightRef = useRef(0)

  const toErr = (e: unknown) => (e instanceof Error ? e : new Error(String(e)))

  // Serialize ONLY the network call. fn runs after the previous settles. inFlight drives `pending`.
  const enqueue = useCallback(<T,>(fn: () => Promise<T>): Promise<T> => {
    const wasIdle = inFlightRef.current === 0
    inFlightRef.current += 1
    setPending(true)
    const run = wasIdle ? fn() : queueRef.current.then(fn, fn)
    queueRef.current = run.then(
      () => { inFlightRef.current -= 1; if (inFlightRef.current === 0) setPending(false) },
      () => { inFlightRef.current -= 1; if (inFlightRef.current === 0) setPending(false) },
    )
    return run
  }, [])

  // Optimistic action (setQty/removeLine/clear): optimism is SYNCHRONOUS at call time;
  // ONLY the persist + seq-gated reconcile/rollback are enqueued. Putting setCart inside the
  // queue would defer each queued tap behind the prior round-trip (the await-every-tap defect).
  const optimistic = useCallback((action: CartAction): Promise<Cart> => {
    const prev = cartRef.current
    if (prev === null) {
      // No base to reduce → pessimistic fallback (seed UI from the server result).
      const mySeq = (latestSeqRef.current = ++seqRef.current)
      return enqueue(async () => {
        try {
          const server = await clientRef.current.apply(action)
          if (mySeq === latestSeqRef.current) {
            setCartRef.current(server)
            cartRef.current = server
          }
          return server
        } catch (e) {
          setError(toErr(e))
          throw e
        }
      })
    }
    // SYNCHRONOUS optimism — UI updates instantly even with a prior persist in flight.
    // A cartReduce throw here (setQty/removeLine on an absent lineId) is CORRECT + UI-unreachable:
    // addLine is pessimistic, so a line is never editable until its server lineId reconciles in.
    let next: Cart
    try {
      next = cartReduce(prev, action)
    } catch (e) {
      setError(toErr(e))
      return Promise.reject(e)
    }
    setCartRef.current(next)
    cartRef.current = next // keep the live ref fresh for a same-tick chained action
    const mySeq = (latestSeqRef.current = ++seqRef.current)
    return enqueue(async () => {
      try {
        const server = await clientRef.current.apply(action)
        if (mySeq === latestSeqRef.current) {
          setCartRef.current(server)
          cartRef.current = server
        }
        return server
      } catch (e) {
        setError(toErr(e))
        if (mySeq === latestSeqRef.current) {
          setCartRef.current(prev) // rollback only if still latest
          cartRef.current = prev
        }
        throw e
      }
    })
  }, [enqueue, cartRef])

  // Pessimistic persist (addLine/merge): no optimistic apply; reconcile-if-latest.
  const pessimistic = useCallback((run: () => Promise<Cart>): Promise<Cart> => {
    const mySeq = (latestSeqRef.current = ++seqRef.current)
    return enqueue(async () => {
      try {
        const server = await run()
        if (mySeq === latestSeqRef.current) {
          setCartRef.current(server)
          cartRef.current = server
        }
        return server
      } catch (e) {
        setError(toErr(e))
        throw e
      }
    })
  }, [enqueue])

  const addLine = useCallback(
    (input: { variantId: string; qty: number; price: PriceSnapshot; vendorId?: string | null }) =>
      pessimistic(() => clientRef.current.apply({ type: 'addLine', ...input })),
    [pessimistic],
  )
  const setQty = useCallback((lineId: string, qty: number) => optimistic({ type: 'setQty', lineId, qty }), [optimistic])
  const removeLine = useCallback((lineId: string) => optimistic({ type: 'removeLine', lineId }), [optimistic])
  const clear = useCallback(() => optimistic({ type: 'clear' }), [optimistic])
  const merge = useCallback((guestCartId: string) => pessimistic(() => clientRef.current.merge(guestCartId)), [pessimistic])

  return useMemo(
    () => ({ addLine, setQty, removeLine, clear, merge, pending, error }),
    [addLine, setQty, removeLine, clear, merge, pending, error],
  )
}