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

const DEFAULT_REFRESH_PULL_THRESHOLD = 200
const ANIMATION_DURATION = 400

export interface UsePullToRefreshOptions {
  onRefresh?: () => Promise<void>
  threshold?: number
  indicatorThreshold?: number
  enabled?: boolean
}

export interface PullToRefreshState {
  isPulling: boolean
  pullDistance: number
  canRefresh: boolean
}

export function usePullToRefresh({
  onRefresh,
  threshold = DEFAULT_REFRESH_PULL_THRESHOLD,
  indicatorThreshold = 0,
  enabled = false,
}: UsePullToRefreshOptions) {
  const [state, setState] = useState<PullToRefreshState>({
    isPulling: false,
    pullDistance: 0,
    canRefresh: false,
  })

  const startY = useRef(0)
  const currentY = useRef(0)
  const containerRef = useRef<HTMLElement | null>(null)
  const animationRef = useRef<number | null>(null)
  const isRefreshingRef = useRef(false)

  const handleTouchStart = useCallback(
    (event: TouchEvent) => {
      if (!enabled || !containerRef.current || isRefreshingRef.current) return

      if (animationRef.current) {
        cancelAnimationFrame(animationRef.current)
        animationRef.current = null
      }

      setState(prev => {
        const container = containerRef.current
        if (!container) return prev

        const isAtTop = container.scrollTop <= 0

        if (isAtTop) {
          const touch = event.touches[0]
          startY.current = touch.clientY
        } else {
          startY.current = 0
          return {
            ...prev,
            isPulling: false,
            pullDistance: 0,
            canRefresh: false,
          }
        }

        return prev
      })
    },
    [enabled]
  )

  const handleMove = useCallback(
    (clientY: number, preventDefault?: () => void) => {
      if (!enabled || !containerRef.current || startY.current === 0) {
        return
      }

      setState(prev => {
        if (isRefreshingRef.current) {
          return prev
        }

        const container = containerRef.current
        if (!container) return prev

        const isAtTop = container.scrollTop <= 0
        currentY.current = clientY
        const deltaY = currentY.current - startY.current
        const isValidPull = isAtTop && deltaY > 0

        if (isValidPull) {
          const pullDistance = deltaY
          const shouldShowIndicator = pullDistance >= indicatorThreshold

          if (shouldShowIndicator && preventDefault) {
            preventDefault()
          }

          if (shouldShowIndicator || prev.isPulling) {
            const elasticDistance =
              pullDistance > threshold
                ? threshold + (pullDistance - threshold) * 0.5
                : pullDistance

            return {
              ...prev,
              isPulling: shouldShowIndicator,
              pullDistance: elasticDistance,
              canRefresh: pullDistance >= threshold,
            }
          }

          return prev
        } else if (prev.isPulling) {
          return {
            ...prev,
            isPulling: false,
            pullDistance: 0,
            canRefresh: false,
          }
        }

        return prev
      })
    },
    [threshold, indicatorThreshold, enabled]
  )

  const handleTouchMove = useCallback(
    (event: TouchEvent) => {
      if (isRefreshingRef.current) return

      const touch = event.touches[0]
      if (!touch) return

      if (startY.current !== 0) {
        handleMove(touch.clientY, () => event.preventDefault())
      }
    },
    [handleMove]
  )

  const resetRefreshingState = useCallback(() => {
    isRefreshingRef.current = false
  }, [])

  const animatePullDistanceToZero = useCallback((fromDistance?: number) => {
    if (animationRef.current) {
      cancelAnimationFrame(animationRef.current)
      animationRef.current = null
    }

    let startDistance = fromDistance
    if (startDistance === undefined) {
      setState(prev => {
        startDistance = prev.pullDistance
        return prev
      })
    }

    if (!startDistance || startDistance <= 0) {
      setState(prev => ({
        ...prev,
        pullDistance: 0,
        canRefresh: false,
        isPulling: false,
      }))
      return
    }

    const duration = ANIMATION_DURATION
    const startTime = Date.now()

    const animate = () => {
      const elapsed = Date.now() - startTime
      const progress = Math.min(elapsed / duration, 1)

      const easeOut = 1 - (1 - progress) ** 3
      const currentDistance = startDistance! * (1 - easeOut)

      setState(prev => ({
        ...prev,
        pullDistance: currentDistance,
        canRefresh: progress < 1 ? prev.canRefresh : false,
      }))

      if (progress < 1) {
        animationRef.current = requestAnimationFrame(animate)
      } else {
        animationRef.current = null
        setState(prev => ({
          ...prev,
          pullDistance: 0,
          canRefresh: false,
          isPulling: false,
        }))
      }
    }

    animationRef.current = requestAnimationFrame(animate)
  }, [])

  const handleEnd = useCallback(async () => {
    if (isRefreshingRef.current) {
      startY.current = 0
      return
    }

    startY.current = 0

    if (animationRef.current) {
      cancelAnimationFrame(animationRef.current)
      animationRef.current = null
    }

    let shouldRefresh = false
    let currentPullDistance = 0
    const wasRefreshing = isRefreshingRef.current

    setState(prev => {
      shouldRefresh = prev.canRefresh && !wasRefreshing
      currentPullDistance = prev.pullDistance

      return {
        ...prev,
        isPulling: false,
      }
    })

    if (currentPullDistance <= 0) {
      setState(prev => ({
        ...prev,
        pullDistance: 0,
        canRefresh: false,
      }))
      return
    }

    if (shouldRefresh && onRefresh) {
      isRefreshingRef.current = true
      try {
        await onRefresh()
      } catch (error) {
        console.error('Pull to refresh failed:', error)
      }
      resetRefreshingState()
    }

    animatePullDistanceToZero(currentPullDistance)
  }, [onRefresh, animatePullDistanceToZero, resetRefreshingState])

  const bindToElement = useCallback(
    (element: HTMLElement | null) => {
      if (!element) return

      containerRef.current = element

      element.addEventListener('touchstart', handleTouchStart, {passive: false})
      element.addEventListener('touchmove', handleTouchMove, {passive: false})
      element.addEventListener('touchend', handleEnd)
      element.addEventListener('touchcancel', handleEnd)

      return () => {
        element.removeEventListener('touchstart', handleTouchStart)
        element.removeEventListener('touchmove', handleTouchMove)
        element.removeEventListener('touchend', handleEnd)
        element.removeEventListener('touchcancel', handleEnd)
      }
    },
    [handleTouchStart, handleTouchMove, handleEnd]
  )

  useEffect(() => {
    return () => {
      if (animationRef.current) {
        cancelAnimationFrame(animationRef.current)
        animationRef.current = null
      }
    }
  }, [])

  return {
    state,
    bindToElement,
    containerRef,
  }
}
