/**
 * Server-paginated infinite list (no react-query).
 *
 * `loader(page)` fetches ONE page; pages are accumulated into `items`. The list
 * never loads everything up front — `loadMore()` (wired to a scroll sentinel)
 * advances exactly one page at a time until `hasMore` is false.
 *
 * Changing `resetKey` (e.g. the filter/sort/search signature) clears the list and
 * reloads from page 1. `reload()` re-fetches page 1 on demand (e.g. retry).
 *
 * Design mirrors useApi (and satisfies the strict react-hooks rules):
 *  - the request signature lives in `req`; the query reset on `resetKey` change is
 *    an in-render state adjustment (not a setState-in-effect), so no cascade.
 *  - `loading` / `loadingMore` are DERIVED from resolved-vs-current request keys,
 *    never set synchronously inside the fetch effect.
 *  - the loader is read through a ref so the effect keys purely on the request id.
 */
import { useCallback, useEffect, useRef, useState } from 'react'
import { messageFor } from './useApi'
import type { Paged } from '../api/types'

export interface InfiniteListState<T> {
  items: T[]
  total: number
  /** Initial / reset load (page 1) in flight. */
  loading: boolean
  /** A subsequent page is being appended. */
  loadingMore: boolean
  error: string | null
  hasMore: boolean
  /** Advance one page; the scroll sentinel calls this. */
  loadMore: () => void
  /** Re-fetch from page 1 (retry). */
  reload: () => void
}

interface Req {
  key: string
  page: number
  nonce: number
}

interface Resolved<T> {
  reqId: string
  items: T[]
  total: number
  error: string | null
}

export function useInfiniteList<T>(
  loader: (page: number) => Promise<Paged<T>>,
  resetKey: string,
  /**
   * Optionaler stabiler Schlüssel je Liste/Route (z. B. "gerbils"). Wenn gesetzt, wird die
   * geladene Seitenzahl in der Session gemerkt und nach einer Zurück-Navigation wieder
   * aufgebaut — so ist die Liste hoch genug, dass die globale Scroll-Wiederherstellung greift.
   */
  restoreKey?: string,
): InfiniteListState<T> {
  const loaderRef = useRef(loader)
  useEffect(() => {
    loaderRef.current = loader
  })

  const storageKey = restoreKey ? `il-pages:${restoreKey}` : null

  // Gemerkte Zielseite EINMAL beim Mount lesen (nur wenn dieselbe Filter-/Sortier-Signatur):
  // bis hierhin werden nach einer Zurück-Navigation die Seiten wieder nachgeladen.
  const [restoreTarget, setRestoreTarget] = useState<number>(() => {
    if (!storageKey) return 0
    try {
      const saved = JSON.parse(sessionStorage.getItem(storageKey) || 'null')
      if (saved && saved.key === resetKey && typeof saved.page === 'number' && saved.page > 1) {
        return saved.page
      }
    } catch {
      /* ignorieren */
    }
    return 0
  })

  const [req, setReq] = useState<Req>({ key: resetKey, page: 1, nonce: 0 })
  // Adjust state during render when the query signature changes — the recommended
  // pattern for deriving state from changed inputs (no setState-in-effect cascade).
  if (req.key !== resetKey) {
    setReq({ key: resetKey, page: 1, nonce: 0 })
    setRestoreTarget(0) // Filter/Sortierung geändert → kein Seiten-Restore mehr
  }

  const queryId = `${req.nonce}:${req.key}`
  const reqId = `${queryId}#${req.page}`

  const [resolved, setResolved] = useState<Resolved<T>>({
    reqId: '',
    items: [],
    total: 0,
    error: null,
  })

  // Tracks an in-flight request synchronously so loadMore() can't advance (and
  // re-fetch/append) the same page twice if the scroll sentinel fires repeatedly.
  const inFlightRef = useRef(false)

  useEffect(() => {
    let cancelled = false
    const page = req.page
    const id = `${req.nonce}:${req.key}#${page}`
    inFlightRef.current = true
    loaderRef
      .current(page)
      .then((res) => {
        if (cancelled) return
        setResolved((prev) => ({
          reqId: id,
          // page 1 replaces (fresh query); later pages append.
          items: page === 1 ? res.items : [...prev.items, ...res.items],
          total: res.totalCount,
          error: null,
        }))
      })
      .catch((err) => {
        if (!cancelled) setResolved((prev) => ({ ...prev, reqId: id, error: messageFor(err) }))
      })
      .finally(() => {
        if (!cancelled) inFlightRef.current = false
      })
    return () => {
      cancelled = true
    }
  }, [req]) // req is a fresh object per change (key/page/nonce) → one fetch per change

  // Resolved data only counts if it belongs to the current query signature.
  const resolvedQueryId = resolved.reqId.slice(0, resolved.reqId.lastIndexOf('#'))
  const sameQuery = resolved.reqId !== '' && resolvedQueryId === queryId
  const inFlight = resolved.reqId !== reqId

  const items = sameQuery ? resolved.items : []
  const total = sameQuery ? resolved.total : 0
  const loading = !sameQuery || (inFlight && req.page === 1)
  const loadingMore = sameQuery && inFlight && req.page > 1
  const error = inFlight ? null : sameQuery ? resolved.error : null
  const hasMore = sameQuery && !inFlight && items.length < total

  const loadMore = useCallback(() => {
    if (inFlightRef.current) return
    setReq((r) => ({ ...r, page: r.page + 1 }))
  }, [])
  const reload = useCallback(() => setReq((r) => ({ key: r.key, page: 1, nonce: r.nonce + 1 })), [])

  // Geladene Seitenzahl je Query merken (für die Wiederherstellung nach Zurück-Navigation).
  useEffect(() => {
    if (!storageKey || !sameQuery || inFlight) return
    try {
      sessionStorage.setItem(storageKey, JSON.stringify({ key: req.key, page: req.page }))
    } catch {
      /* ignorieren */
    }
  }, [storageKey, sameQuery, inFlight, req.key, req.page])

  // Nach Zurück-Navigation die zuvor geladene Seitenzahl wieder aufbauen (eine Seite je
  // abgeschlossener Anfrage), damit die Liste ihre alte Höhe erreicht.
  useEffect(() => {
    if (restoreTarget <= req.page || !sameQuery || inFlight || !hasMore) return
    // eslint-disable-next-line react-hooks/set-state-in-effect -- kontrolliertes Nachladen bis zur gemerkten Seite
    setReq((r) => ({ ...r, page: r.page + 1 }))
  }, [restoreTarget, sameQuery, inFlight, hasMore, req.page])

  return { items, total, loading, loadingMore, error, hasMore, loadMore, reload }
}

/**
 * Wires an IntersectionObserver to a sentinel element that triggers loadMore() as
 * it scrolls into view. Render the returned ref on a small element shown only while
 * `list.hasMore`, e.g. `{list.hasMore && <div ref={sentinelRef} />}`. Re-subscribes
 * when the load state changes so it keeps paging while the sentinel stays in view.
 */
export function useInfiniteSentinel<T>(list: InfiniteListState<T>) {
  const ref = useRef<HTMLDivElement | null>(null)
  const { hasMore, loading, loadingMore, loadMore } = list
  useEffect(() => {
    const el = ref.current
    if (!el) return
    const io = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasMore && !loading && !loadingMore) loadMore()
      },
      { rootMargin: '300px 0px' },
    )
    io.observe(el)
    return () => io.disconnect()
  }, [hasMore, loading, loadingMore, loadMore])
  return ref
}
