/**
 * Globale Scroll-Wiederherstellung (einmal im AppShell gemountet, gilt für ALLE Seiten/Listen).
 *
 * WICHTIG: Gescrollt wird NICHT das Fenster, sondern der Inhaltsbereich `.app-main`
 * (`.app-shell` ist height:100dvh/overflow:hidden). Daher arbeiten wir auf diesem Container.
 *
 * Erhält die Scrollposition je Route über:
 *  - Zurück/Vorwärts-Navigation (POP) → gemerkte Position wiederherstellen,
 *  - Bildschirm-Sperre / App-Hintergrund (visibilitychange, pagehide/pageshow) → Position
 *    sichern und beim Zurückkommen wiederherstellen.
 * Neue Navigation (PUSH/REPLACE) startet oben.
 *
 * EVENT-basiert (zuverlässiger als ein Timer): ein MutationObserver auf dem Inhalt springt
 * erneut zur Zielposition, sobald die Liste (nach-)lädt und höher wird — bricht aber sofort ab,
 * sobald die Nutzerin selbst scrollt. Deep-Links (?focus=, #anchor) haben Vorrang.
 */
import { useCallback, useEffect, useRef } from 'react'
import { useLocation, useNavigationType } from 'react-router-dom'

const PREFIX = 'scroll:'
// Sicherheitsnetz: Inhalts-Beobachtung nach dieser Zeit beenden (kein Dauer-Listener).
const RESTORE_OBSERVE_CAP_MS = 15000

function getScroller(): HTMLElement | null {
  return document.querySelector<HTMLElement>('.app-main')
}
function getTop(el: HTMLElement | null): number {
  return el ? el.scrollTop : window.scrollY
}
function setTop(el: HTMLElement | null, y: number): void {
  if (el) el.scrollTo(0, y)
  else window.scrollTo(0, y)
}

export function useScrollRestoration() {
  const { pathname } = useLocation()
  const navType = useNavigationType()
  const pathRef = useRef(pathname)
  const restoringRef = useRef(false)
  const rafSaveRef = useRef<number | null>(null)
  const lastHeightRef = useRef(0)
  const lastTopRef = useRef(0)

  const keyFor = (p: string) => PREFIX + p

  const persist = useCallback(() => {
    try {
      sessionStorage.setItem(keyFor(pathRef.current), String(getTop(getScroller())))
    } catch {
      /* sessionStorage nicht verfügbar */
    }
  }, [])

  const restore = useCallback((path: string, respectDeepLink: boolean) => {
    if (respectDeepLink) {
      if (window.location.hash) return
      try {
        if (new URLSearchParams(window.location.search).has('focus')) return
      } catch {
        /* ignorieren */
      }
    }
    let y = NaN
    try {
      y = Number(sessionStorage.getItem(keyFor(path)))
    } catch {
      /* ignorieren */
    }
    if (!Number.isFinite(y) || y <= 0) return

    // WICHTIG gegen Race: `restoringRef` bleibt aktiv, bis die Zielposition ERREICHT ist (oder
    // die Nutzerin selbst scrollt / die harte Obergrenze greift). So kann ein durch das
    // Re-Rendern/Nachladen ausgelöster Sprung nach oben (scroll→0) NICHT als neue Position
    // gespeichert werden und die gemerkte Position überschreiben.
    //
    // KEIN „Ruhe-/Settling-Timer": Listen, die ihre Inhalte erst nach einer kurzen Pause am Stück
    // rendern (z. B. die Tickets-Liste: ein Fetch, dann alle Karten auf einmal), würden sonst
    // vorzeitig freigegeben — die Position griffe dann ins Leere. Stattdessen warten wir per
    // MutationObserver auf JEDE Inhaltsänderung und springen erneut, bis die Position sitzt.
    restoringRef.current = true
    let done = false
    const stop = () => {
      if (done) return
      done = true
      restoringRef.current = false
      mo.disconnect()
      window.removeEventListener('wheel', onUser)
      window.removeEventListener('touchmove', onUser)
      window.removeEventListener('keydown', onUser)
      window.clearTimeout(safety)
    }
    // Sobald die Nutzerin selbst scrollt, brechen wir ab — nie gegen sie ankämpfen.
    const onUser = () => stop()
    const tryReach = () => {
      if (done) return
      const el = getScroller()
      setTop(el, y)
      if (Math.abs(getTop(el) - y) <= 2) stop() // Zielposition sitzt → fertig
    }
    // EVENT-basiert: jede DOM-Änderung im Inhalt (Liste lädt/rendert) → erneut zur Zielposition.
    const mo = new MutationObserver(() => requestAnimationFrame(tryReach))
    const passive = { passive: true } as AddEventListenerOptions
    window.addEventListener('wheel', onUser, passive)
    window.addEventListener('touchmove', onUser, passive)
    window.addEventListener('keydown', onUser)
    // Harte Obergrenze nur als Notausstieg (z. B. wenn die Zielposition nie erreichbar ist, weil
    // der Inhalt jetzt kürzer ist). Hält bis dahin nur den Speicher-Schutz — harmlos.
    const safety = window.setTimeout(stop, RESTORE_OBSERVE_CAP_MS)
    const scroller = getScroller()
    if (scroller) mo.observe(scroller, { childList: true, subtree: true })
    requestAnimationFrame(tryReach)
  }, [])

  // Auf Navigation reagieren: POP = wiederherstellen, sonst neue Seite oben starten.
  useEffect(() => {
    pathRef.current = pathname
    if (navType === 'POP') {
      restore(pathname, true)
    } else {
      restoringRef.current = true
      requestAnimationFrame(() => {
        setTop(getScroller(), 0)
        restoringRef.current = false
      })
    }
  }, [pathname, navType, restore])

  // Laufendes Mitschreiben (rAF-gedrosselt) am Scroll-Container + Sichern beim Ausblenden +
  // Wiederherstellen beim Wiederanzeigen (Handy entsperrt / aus dem Hintergrund).
  useEffect(() => {
    const onScroll = () => {
      const el = getScroller()
      if (el) {
        const prevTop = lastTopRef.current
        const prevHeight = lastHeightRef.current
        lastTopRef.current = el.scrollTop
        lastHeightRef.current = el.scrollHeight
        if (restoringRef.current || document.visibilityState !== 'visible') return
        // ENTSCHEIDEND: Beim Seitenwechsel wird der Inhalt aus-/eingehängt → die scrollHeight
        // SCHRUMPFT und der Container KLEMMT auf eine kleinere Position. Genau dieser Fall
        // (Position UND Höhe gleichzeitig gesunken) ist ein „unechter" Sprung — NICHT speichern,
        // sonst überschreibt er die gemerkte Stelle der Seite, die wir gerade verlassen. Eine
        // echte Nutzer-Scrollung verkleinert die scrollHeight nie.
        if (el.scrollTop < prevTop && el.scrollHeight < prevHeight) return
        if (el.scrollHeight <= el.clientHeight + 4) return // nicht scrollbar
      } else if (restoringRef.current || document.visibilityState !== 'visible') {
        return
      }
      if (rafSaveRef.current != null) return
      rafSaveRef.current = requestAnimationFrame(() => {
        rafSaveRef.current = null
        persist()
      })
    }
    const onVisibility = () => {
      if (document.visibilityState === 'hidden') persist()
      else restore(pathRef.current, false)
    }
    const onPageShow = () => restore(pathRef.current, false)

    const scroller = getScroller()
    lastHeightRef.current = scroller?.scrollHeight ?? 0
    lastTopRef.current = scroller?.scrollTop ?? 0
    const scrollTarget: HTMLElement | Window = scroller ?? window
    scrollTarget.addEventListener('scroll', onScroll, { passive: true })
    document.addEventListener('visibilitychange', onVisibility)
    window.addEventListener('pagehide', persist)
    window.addEventListener('pageshow', onPageShow)
    return () => {
      scrollTarget.removeEventListener('scroll', onScroll)
      document.removeEventListener('visibilitychange', onVisibility)
      window.removeEventListener('pagehide', persist)
      window.removeEventListener('pageshow', onPageShow)
      if (rafSaveRef.current != null) cancelAnimationFrame(rafSaveRef.current)
    }
  }, [persist, restore])

  // NUR Dev (Vite-HMR / React Fast Refresh): vor dem Hot-Update sichern, danach wiederherstellen.
  // In der Produktion ist import.meta.hot undefiniert → No-Op.
  useEffect(() => {
    const hot = import.meta.hot
    if (!hot) return
    const before = () => persist()
    const after = () => restore(pathRef.current, false)
    hot.on('vite:beforeUpdate', before)
    hot.on('vite:afterUpdate', after)
    hot.on('vite:beforeFullReload', before)
    return () => {
      hot.off?.('vite:beforeUpdate', before)
      hot.off?.('vite:afterUpdate', after)
      hot.off?.('vite:beforeFullReload', before)
    }
  }, [persist, restore])
}
