'use client'
import { useGSAP } from '@gsap/react'
import gsap from 'gsap'
import { SplitText } from 'gsap/SplitText'
import { ScrollTrigger, GSDevTools } from 'gsap/all'
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, type ReactNode } from 'react'

gsap.registerPlugin(useGSAP)
gsap.registerPlugin(ScrollTrigger)
gsap.registerPlugin(SplitText)
gsap.registerPlugin(GSDevTools)

// iOS Safari shows/hides its address bar at the scroll extremes, which resizes
// the viewport and makes ScrollTrigger refresh + fire spurious onLeave/onEnter
// toggles (text animating out at the bottom and not coming back). Ignoring those
// mobile-toolbar resizes keeps the real enter/leave reverse behavior intact.
ScrollTrigger.config({ ignoreMobileResize: true })

// Flip to true to draw ScrollTrigger start/end markers on every animated
// element and mount the GSDevTools timeline scrubber. Handy for seeing exactly
// where each card's enter/exit lines sit relative to the viewport.
export const GSAP_DEBUG = false

export function nearestScroller(el: Element): Element | Window {
  let node: Element | null = el.parentElement
  while (node) {
    if (node.getAttribute('data-slot') === 'scroll-area-viewport') {
      const viewport = node as HTMLElement
      const rect = viewport.getBoundingClientRect()
      const hasUsableBox = rect.width > 0 && rect.height > 0
      const canScroll =
        viewport.scrollHeight > viewport.clientHeight ||
        viewport.scrollWidth > viewport.clientWidth

      if (hasUsableBox && canScroll) return viewport
    }
    node = node.parentElement
  }
  return window
}

const GsapContext = createContext<{
  // Add a real animation (with its own duration) to the entrance timeline.
  addAnimation: (
    animation: gsap.core.TimelineChild,
    position: gsap.Position
  ) => void,
  // Schedule a zero-duration callback at `position` — used to *start* an
  // independent reveal tween so the timeline only orchestrates timing.
  schedule: (fn: () => void, position: gsap.Position) => void,
  // Run `cb` once the entrance is done (timeline complete or first user scroll).
  onReady: (cb: () => void) => void,
  // Re-measure all ScrollTriggers (throttled to once per frame). Call it
  // whenever an animation changes content height so trigger positions stay
  // aligned with the real layout.
  requestRefresh: () => void,
  resetTimeline: () => void,
  resumeTimeline: () => void,
  getScroller: () => Element | Window | null
} | null>(null)

export function useGsapContext() {
  return useContext(GsapContext)
}

export const useTimeLine = (dep:any,all?:boolean) => {
  const gsapContext = useGsapContext()
    useEffect(() => {
      if (GSAP_DEBUG) {
        console.log("[cv-debug][useTimeLine:effect]", {
          hasDep: !!dep,
          isArray: dep instanceof Array,
          length: dep instanceof Array ? dep.length : undefined,
          all,
        })
      }
      if (dep instanceof Array && all) {
        let acc = true;
        let allDepsSatisfied = dep.reduce((p,c) => c !== undefined && p ,acc )
        if (allDepsSatisfied) {
          if (GSAP_DEBUG) console.log("[cv-debug][useTimeLine:resume-all]")
          gsapContext?.resumeTimeline()
        }
      } else {
        if (dep) {
          if (GSAP_DEBUG) console.log("[cv-debug][useTimeLine:resume]")
          gsapContext?.resumeTimeline()
        }
      }
    },[dep])
  useLayoutEffect(() => {
    return () => {
      gsapContext?.resetTimeline()
    }
  },[])
}

export default function GsapProvider({ children }: { children: ReactNode }) {
  const tl = useRef<gsap.core.Timeline | null>(null)
  const scrollerRef = useRef<Element | Window | null>(null)
  const getScroller = useCallback(() => {
    // const cached = scrollerRef.current
    // if (!cached || (cached instanceof Element && !document.contains(cached))) {
      let scrollers = document.querySelectorAll('[data-slot="scroll-area-viewport"]')
      if (scrollers.length < 1) {
        scrollerRef.current = window
      } else {
        let scrollerArray = Array.from(scrollers.values()).sort((a,b) => {
          const s1 = a as HTMLDivElement;
          const s2 = b as HTMLDivElement;
          // using bitwise not (~~) to coerce NaN values to 0
          const aPriority = ~~Number(s1.dataset?.scrollerPriority)
          const bPriority = ~~Number(s2.dataset?.scrollerPriority)
          return aPriority - bPriority;
        })
        let prioScroller = scrollerArray.pop();
        scrollerRef.current = prioScroller || window;
      }

    // }
    return scrollerRef.current
  }, [])
  const devToolsCreated = useRef(false)
  useGSAP(() => {
    if (!tl.current) {
      tl.current = gsap.timeline({ paused: true })
    }
    if (GSAP_DEBUG && tl.current && !devToolsCreated.current) {
      devToolsCreated.current = true
      GSDevTools.create({ animation: tl.current })
    }
    return () => { if (GSAP_DEBUG) console.log("gsap cleanup") }
  })

  // Handoff: fire registered callbacks once, when the entrance finishes.
  const readyFired = useRef(false)
  const readyCbs = useRef<Array<() => void>>([])
  const fireReady = useCallback(() => {
    if (readyFired.current) return
    if (GSAP_DEBUG) {
      console.log("[cv-debug][gsap:ready]", {
        callbacks: readyCbs.current.length,
        duration: tl.current?.duration(),
        progress: tl.current?.progress(),
      })
    }
    readyFired.current = true
    readyCbs.current.forEach((cb) => cb())
    readyCbs.current = []
  },[])
  const onReady = useCallback((cb: () => void) => {
    if (GSAP_DEBUG) console.log("[cv-debug][gsap:onReady]", { readyFired: readyFired.current })
    if (readyFired.current) cb()
    else readyCbs.current.push(cb)
  },[])

  const addAnimation = useCallback((animation: gsap.core.TimelineChild, position: gsap.Position) => {
    // Content can mount in waves (e.g. nested queries resolving after the
    // entrance already played). Parking a tween in a finished, paused timeline
    // would freeze it at its from-state, so once the entrance is done let the
    // (live) tween play on its own instead.
    if (GSAP_DEBUG) {
      console.log("[cv-debug][gsap:addAnimation]", {
        position,
        readyFired: readyFired.current,
        durationBefore: tl.current?.duration(),
      })
    }
    if (readyFired.current) return
    tl.current?.add(animation, position);
    if (GSAP_DEBUG) {
      console.log("[cv-debug][gsap:addAnimation:done]", {
        position,
        durationAfter: tl.current?.duration(),
        children: tl.current?.getChildren(false, true, true).length,
      })
    }
  },[])
  const schedule = useCallback((fn: () => void, position: gsap.Position) => {
    // Same late-arrival case: a callback added past the playhead never fires, so
    // run the reveal immediately once the entrance has finished.
    if (GSAP_DEBUG) {
      console.log("[cv-debug][gsap:schedule]", {
        position,
        readyFired: readyFired.current,
        durationBefore: tl.current?.duration(),
        childrenBefore: tl.current?.getChildren(false, true, true).length,
      })
    }
    if (readyFired.current) { fn(); return }
    tl.current?.add(fn, position)
    if (GSAP_DEBUG) {
      console.log("[cv-debug][gsap:schedule:done]", {
        position,
        durationAfter: tl.current?.duration(),
        childrenAfter: tl.current?.getChildren(false, true, true).length,
      })
    }
  },[])

  // Throttle ScrollTrigger.refresh() to once per frame so the ResizeObserver
  // can fire freely while content height animates.
  const refreshQueued = useRef(false)
  const scheduleRefresh = useCallback(() => {
    if (refreshQueued.current) return
    refreshQueued.current = true
    requestAnimationFrame(() => {
      refreshQueued.current = false
      ScrollTrigger.refresh()
    })
  },[])

  const scrollCleanup = useRef<(() => void) | null>(null)
  const resizeObserver = useRef<ResizeObserver | null>(null)

  const resetTimeline = useCallback(() => {
    if (GSAP_DEBUG) {
      console.log("[cv-debug][gsap:reset]", {
        duration: tl.current?.duration(),
        progress: tl.current?.progress(),
      })
    }
    tl.current?.kill()
    tl.current?.revert()
    ScrollTrigger.getAll().forEach(st => st.kill())
    resizeObserver.current?.disconnect()
    scrollCleanup.current?.()
    scrollCleanup.current = null
    readyFired.current = false
    readyCbs.current = []
    tl.current = gsap.timeline({paused:true})
  },[])

  const resumeTimeline = useCallback(() => {
    const t = tl.current
    if (!t) {
      if (GSAP_DEBUG) console.log("[cv-debug][gsap:resume:skip-no-timeline]")
      return
    }
    if (GSAP_DEBUG) {
      console.log("[cv-debug][gsap:resume:start]", {
        duration: t.duration(),
        progress: t.progress(),
        paused: t.paused(),
        readyFired: readyFired.current,
        children: t.getChildren(false, true, true).length,
      })
    }
    // When the orchestrated entrance finishes, hand off to scroll control and
    // realign triggers against the now-settled layout.
    t.eventCallback("onComplete", () => { fireReady(); ScrollTrigger.refresh() })

    const scroller = getScroller()
    // If the user scrolls before the entrance finishes, snap it to the end and
    // switch to scroll control so the timeline and ScrollTriggers never fight
    // over the same elements.
    scrollCleanup.current?.()
    const onFirstScroll = () => { t.progress(1); fireReady() }
    scroller?.addEventListener("scroll", onFirstScroll, { once: true, passive: true })
    scrollCleanup.current = () => scroller?.removeEventListener("scroll", onFirstScroll)

    // Continuously realign triggers while content height changes — entrance
    // growth, scroll-driven collapses, late-loading media.
    if (scroller instanceof Element) {
      const target = scroller.firstElementChild ?? scroller
      resizeObserver.current?.disconnect()
      resizeObserver.current = new ResizeObserver(scheduleRefresh)
      resizeObserver.current.observe(target)
    }

    t.resume()
    if (GSAP_DEBUG) {
      console.log("[cv-debug][gsap:resume:after]", {
        duration: t.duration(),
        progress: t.progress(),
        paused: t.paused(),
      })
    }
  },[getScroller, fireReady, scheduleRefresh])

  // Fonts/markdown/images loading also change content height after the triggers
  // were created; refresh so start/end stay aligned with the real card sizes.
  useEffect(() => {
    const refresh = () => ScrollTrigger.refresh()
    window.addEventListener("load", refresh)
    document.fonts?.ready.then(refresh).catch(() => {})
    return () => window.removeEventListener("load", refresh)
  }, [])
  return (
    <GsapContext.Provider value={{ addAnimation, schedule, onReady, requestRefresh: scheduleRefresh, resetTimeline, resumeTimeline, getScroller }}>
      {children}
    </GsapContext.Provider>
  )
}
