import { useCallback, useEffect, useRef, useState } from 'react'

export interface AsyncResource<T> {
  data: T | null
  loading: boolean
  error: Error | null
  reload: () => void
}

/**
 * Seed-aware, cancel-safe read lifecycle (§0/§3). Mirrors the auth-react
 * useState+useEffect+cancel-flag pattern (no react-query/SWR), but is
 * StrictMode-safe — the seeded no-mount-fetch guarantee holds under React's
 * dev double-invoke (setup→cleanup→setup).
 * - seed.initialData !== undefined  → seed state, loading:false, NO mount fetch
 *   (a `null` initialData is a valid seed: SSR rendered "not found")
 * - otherwise                       → loading:true, fetch on mount
 * - reload() or `key` change        → cancel any in-flight, refetch via `fetcher`
 * `key` is a stable string derived from the hook's args so arg-changes refetch.
 *
 * The seeded mount is skipped via a SETTLED-KEY ref (the `key\u0000reloadToken`
 * tag we already hold data for), NOT a one-shot boolean — a boolean is flipped
 * on the first dev setup, so StrictMode's second setup would fetch over seeded
 * data (a silent §0/§3 break). Chesterton's Fence: do not simplify this back to
 * a flag.
 */
export function useAsyncResource<T>(
  fetcher: () => Promise<T>,
  key: string,
  seed: { initialData?: T } | undefined,
): AsyncResource<T> {
  const hasSeed = seed?.initialData !== undefined
  const [data, setData] = useState<T | null>(hasSeed ? (seed?.initialData ?? null) : null)
  const [loading, setLoading] = useState<boolean>(!hasSeed)
  const [error, setError] = useState<Error | null>(null)
  const [reloadToken, setReloadToken] = useState(0)

  const fetcherRef = useRef(fetcher)
  fetcherRef.current = fetcher

  // The (key, reloadToken) tag we already hold data for — the seeded mount OR
  // the last-settled fetch. A seeded mount inits it to the seeded tag, so the
  // mount (and any StrictMode dev re-invoke of it) skips the fetch; a real
  // key/reload change yields a different tag → fetch. NUL separator never
  // appears in a hook `key`, so the join is unambiguous.
  const settledTagRef = useRef<string | null>(hasSeed ? `${key}\u0000${reloadToken}` : null)

  const reload = useCallback(() => setReloadToken((t) => t + 1), [])

  useEffect(() => {
    const tag = `${key}\u0000${reloadToken}`
    if (settledTagRef.current === tag) {
      // Already hold data for this (key, reloadToken) — seeded mount or a
      // StrictMode dev re-invoke. No fetch; no cleanup to register.
      return
    }
    let cancelled = false
    setLoading(true)
    setError(null)
    void fetcherRef.current()
      .then((result) => {
        if (!cancelled) {
          settledTagRef.current = tag
          setData(result)
          setLoading(false)
        }
      })
      .catch((e: unknown) => {
        if (!cancelled) {
          settledTagRef.current = tag
          setError(e instanceof Error ? e : new Error(String(e)))
          setLoading(false)
        }
      })
    return () => {
      cancelled = true
    }
  }, [key, reloadToken])

  return { data, loading, error, reload }
}
