// frontend\src\components\ui\SwipeCard.tsx

'use client'

import { useEffect, useRef, useState, useCallback, type ReactNode, forwardRef, useImperativeHandle } from 'react'
import { FireIcon as FireSolidIcon } from '@heroicons/react/24/solid'
import { createRoot } from 'react-dom/client'

function cn(...parts: Array<string | false | null | undefined>) {
  return parts.filter(Boolean).join(' ')
}

function getGlobalFxLayer(): HTMLDivElement | null {
  if (typeof document === 'undefined') return null

  const ID = '__swipecard_hot_fx_layer__'
  let el = document.getElementById(ID) as HTMLDivElement | null

  if (!el) {
    el = document.createElement('div')
    el.id = ID
    el.style.position = 'fixed'
    el.style.inset = '0'
    el.style.pointerEvents = 'none'
    el.style.zIndex = '2147483647'
    ;(el.style as any).contain = 'layout style paint'
    document.body.appendChild(el)
  }

  return el
}

export type SwipeAction = {
  label: ReactNode
  className?: string
}

export type SwipeCardProps = {
  children: ReactNode
  enabled?: boolean
  disabled?: boolean
  onTap?: () => void
  onSwipeLeft: () => boolean | void | Promise<boolean | void>
  onSwipeRight: () => boolean | void | Promise<boolean | void>
  className?: string
  leftAction?: SwipeAction
  rightAction?: SwipeAction
  thresholdPx?: number
  thresholdRatio?: number
  snapMs?: number
  commitMs?: number
  ignoreFromBottomPx?: number
  ignoreSelector?: string
  tapIgnoreSelector?: string
  onDoubleTap?: () => boolean | void | Promise<boolean | void>
  hotTargetSelector?: string
  doubleTapMs?: number
  doubleTapMaxMovePx?: number
  pressDelayMs?: number
  enablePressHold?: boolean
  onPressStart?: (info: { pointerType: string; button: number }) => void
  onPressEnd?: () => void
}

export type SwipeCardHandle = {
  swipeLeft: (opts?: { runAction?: boolean }) => Promise<boolean>
  swipeRight: (opts?: { runAction?: boolean }) => Promise<boolean>
  reset: () => void
}

const HORIZONTAL_START_PX = 10
const LOCK_RATIO = 1.15
const FLICK_VELOCITY = 0.55
const MAX_OVERSWIPE_PX = 64
const ROTATE_MAX_DEG = 5
const SCALE_MIN = 0.992

function clamp(v: number, min: number, max: number) {
  return Math.max(min, Math.min(max, v))
}

function rubberBand(delta: number, width: number) {
  if (delta === 0) return 0

  const sign = Math.sign(delta)
  const abs = Math.abs(delta)
  const limit = width * 0.9

  if (abs <= limit) return delta

  const overflow = abs - limit
  const resisted = limit + overflow * 0.18
  return sign * Math.min(resisted, width + MAX_OVERSWIPE_PX)
}

const SwipeCard = forwardRef<SwipeCardHandle, SwipeCardProps>(function SwipeCard(
  {
    children,
    enabled = true,
    disabled = false,
    onTap,
    onSwipeLeft,
    onSwipeRight,
    className,
    thresholdPx = 140,
    thresholdRatio = 0.28,
    ignoreFromBottomPx = 72,
    ignoreSelector = '[data-swipe-ignore]',
    snapMs = 220,
    commitMs = 220,
    tapIgnoreSelector = 'button,a,input,textarea,select,video[controls],video[controls] *,[data-tap-ignore]',
    onDoubleTap,
    hotTargetSelector = '[data-hot-target]',
    doubleTapMs = 360,
    doubleTapMaxMovePx = 48,
    pressDelayMs = 220,
    enablePressHold = true,
    onPressStart,
    onPressEnd,
  },
  ref
) {
  const cardRef = useRef<HTMLDivElement | null>(null)
  const outerRef = useRef<HTMLDivElement | null>(null)

  const doubleTapBusyRef = useRef(false)
  const pressTimerRef = useRef<number | null>(null)
  const longPressActiveRef = useRef(false)

  const rafRef = useRef<number | null>(null)
  const dxRef = useRef(0)
  const thresholdRef = useRef(0)
  const widthRef = useRef(360)

  const tapTimerRef = useRef<number | null>(null)
  const lastTapRef = useRef<{ t: number; x: number; y: number } | null>(null)

  const velocityXRef = useRef(0)
  const lastMoveXRef = useRef(0)
  const lastMoveTRef = useRef(0)

  const pointer = useRef<{
    id: number | null
    startX: number
    startY: number
    dragging: boolean
    captured: boolean
    tapIgnored: boolean
    noSwipe: boolean
    axisLocked: 'x' | 'y' | null
  }>({
    id: null,
    startX: 0,
    startY: 0,
    dragging: false,
    captured: false,
    tapIgnored: false,
    noSwipe: false,
    axisLocked: null,
  })

  const [dx, setDx] = useState(0)
  const [armedDir, setArmedDir] = useState<null | 'left' | 'right'>(null)
  const [animMs, setAnimMs] = useState(0)

  const clearTapTimer = useCallback(() => {
    if (tapTimerRef.current != null) {
      window.clearTimeout(tapTimerRef.current)
      tapTimerRef.current = null
    }
  }, [])

  const clearPressTimer = useCallback(() => {
    if (pressTimerRef.current != null) {
      window.clearTimeout(pressTimerRef.current)
      pressTimerRef.current = null
    }
  }, [])

  const endLongPress = useCallback(() => {
    clearPressTimer()

    if (!longPressActiveRef.current) return

    longPressActiveRef.current = false
    onPressEnd?.()
  }, [clearPressTimer, onPressEnd])

  const flushDx = useCallback((nextDx: number) => {
    dxRef.current = nextDx
    if (rafRef.current != null) return
    rafRef.current = requestAnimationFrame(() => {
      rafRef.current = null
      setDx(dxRef.current)
    })
  }, [])

  const reset = useCallback(() => {
    clearPressTimer()
    longPressActiveRef.current = false

    if (rafRef.current != null) {
      cancelAnimationFrame(rafRef.current)
      rafRef.current = null
    }

    dxRef.current = 0
    velocityXRef.current = 0

    if (cardRef.current) {
      cardRef.current.style.touchAction = 'pan-y'
    }

    setAnimMs(snapMs)
    setDx(0)
    setArmedDir(null)

    window.setTimeout(() => {
      setAnimMs(0)
    }, snapMs)
  }, [snapMs, clearPressTimer])

  const commit = useCallback(
    async (dir: 'left' | 'right', runAction: boolean) => {
      if (rafRef.current != null) {
        cancelAnimationFrame(rafRef.current)
        rafRef.current = null
      }

      if (cardRef.current) {
        cardRef.current.style.touchAction = 'pan-y'
      }

      const w = cardRef.current?.offsetWidth || widthRef.current || 360
      const outDx = dir === 'right' ? w + 56 : -(w + 56)

      setAnimMs(commitMs)
      setArmedDir(dir)
      dxRef.current = outDx
      setDx(outDx)

      const animPromise = new Promise<void>((resolve) => {
        window.setTimeout(resolve, commitMs)
      })

      // Wichtig: erst rausanimieren lassen, dann Aktion starten
      await animPromise

      let ok: boolean | void = true
      if (runAction) {
        ok = await Promise.resolve(
          dir === 'right' ? onSwipeRight() : onSwipeLeft()
        ).catch(() => false)
      }

      if (ok === false) {
        setAnimMs(snapMs)
        setArmedDir(null)
        setDx(0)
        dxRef.current = 0
        window.setTimeout(() => setAnimMs(0), snapMs)
        return false
      }

      return true
    },
    [commitMs, onSwipeLeft, onSwipeRight, snapMs]
  )

  const softResetForTap = useCallback(() => {
    if (rafRef.current != null) {
      cancelAnimationFrame(rafRef.current)
      rafRef.current = null
    }

    dxRef.current = 0
    setAnimMs(0)
    setDx(0)
    setArmedDir(null)

    try {
      const el = cardRef.current
      if (el) el.style.touchAction = 'pan-y'
    } catch {
      // ignore
    }
  }, [])

  const runHotFx = useCallback(
    (clientX?: number, clientY?: number) => {
      const outer = outerRef.current
      const card = cardRef.current
      if (!outer || !card) return

      const layer = getGlobalFxLayer()
      if (!layer) return

      let startX = typeof clientX === 'number' ? clientX : window.innerWidth / 2
      let startY = typeof clientY === 'number' ? clientY : window.innerHeight / 2

      const targetEl = hotTargetSelector
        ? ((outerRef.current?.querySelector(hotTargetSelector) as HTMLElement | null) ??
            (card.querySelector(hotTargetSelector) as HTMLElement | null))
        : null

      let endX = startX
      let endY = startY
      if (targetEl) {
        const tr = targetEl.getBoundingClientRect()
        endX = tr.left + tr.width / 2
        endY = tr.top + tr.height / 2
      }

      const dx = endX - startX
      const dy = endY - startY

      const flame = document.createElement('div')
      flame.style.position = 'absolute'
      flame.style.left = `${startX}px`
      flame.style.top = `${startY}px`
      flame.style.transform = 'translate(-50%, -50%)'
      flame.style.pointerEvents = 'none'
      flame.style.willChange = 'transform, opacity'
      flame.style.zIndex = '2147483647'
      flame.style.lineHeight = '1'
      flame.style.userSelect = 'none'
      flame.style.filter = 'drop-shadow(0 10px 16px rgba(0,0,0,0.22))'

      const inner = document.createElement('div')
      inner.style.width = '30px'
      inner.style.height = '30px'
      inner.style.color = '#f59e0b'
      inner.style.display = 'block'

      flame.appendChild(inner)
      layer.appendChild(flame)

      const root = createRoot(inner)
      root.render(<FireSolidIcon className="w-full h-full" aria-hidden="true" />)

      void flame.getBoundingClientRect()

      const popMs = 200
      const holdMs = 500
      const flyMs = 400
      const duration = popMs + holdMs + flyMs

      const tPopEnd = popMs / duration
      const tHoldEnd = (popMs + holdMs) / duration

      const anim = flame.animate(
        [
          { transform: 'translate(-50%, -50%) scale(0.15)', opacity: 0, offset: 0.0 },
          { transform: 'translate(-50%, -50%) scale(1.25)', opacity: 1, offset: tPopEnd * 0.55 },
          { transform: 'translate(-50%, -50%) scale(1.00)', opacity: 1, offset: tPopEnd },
          { transform: 'translate(-50%, -50%) scale(1.00)', opacity: 1, offset: tHoldEnd },
          {
            transform: `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px)) scale(0.85)`,
            opacity: 0.95,
            offset: tHoldEnd + (1 - tHoldEnd) * 0.75,
          },
          {
            transform: `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px)) scale(0.55)`,
            opacity: 0,
            offset: 1.0,
          },
        ],
        {
          duration,
          easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)',
          fill: 'forwards',
        }
      )

      if (targetEl) {
        window.setTimeout(() => {
          try {
            targetEl.animate(
              [
                { transform: 'translateZ(0) scale(1)', filter: 'brightness(1)' },
                { transform: 'translateZ(0) scale(1.10)', filter: 'brightness(1.25)', offset: 0.35 },
                { transform: 'translateZ(0) scale(1)', filter: 'brightness(1)' },
              ],
              { duration: 260, easing: 'cubic-bezier(0.2, 0.9, 0.2, 1)' }
            )
          } catch {
            // ignore
          }
        }, popMs + holdMs + Math.round(flyMs * 0.75))
      }

      anim.onfinish = () => {
        try {
          root.unmount()
        } catch {
          // ignore
        }
        flame.remove()
      }
    },
    [hotTargetSelector]
  )

  useEffect(() => {
    return () => {
      clearTapTimer()
      clearPressTimer()
    }
  }, [clearTapTimer, clearPressTimer])

  useImperativeHandle(
    ref,
    () => ({
      swipeLeft: (opts) => commit('left', opts?.runAction ?? true),
      swipeRight: (opts) => commit('right', opts?.runAction ?? true),
      reset: () => reset(),
    }),
    [commit, reset]
  )

  const absDx = Math.abs(dx)
  const swipeDir: 'left' | 'right' | null = dx === 0 ? null : dx > 0 ? 'right' : 'left'
  const activeThreshold =
    thresholdRef.current || Math.min(thresholdPx, (cardRef.current?.offsetWidth || 360) * thresholdRatio)

  const reveal = clamp(absDx / Math.max(1, activeThreshold), 0, 1)
  const revealSoft = clamp(absDx / Math.max(1, activeThreshold * 1.35), 0, 1)

  const tiltDeg = clamp(dx / 34, -ROTATE_MAX_DEG, ROTATE_MAX_DEG)
  const dragScale = dx === 0 ? 1 : Math.max(SCALE_MIN, 1 - revealSoft * 0.008)

  return (
    <div ref={outerRef} className={cn('relative isolate overflow-visible rounded-lg', className)}>
      <div
        ref={cardRef}
        className="relative"
        style={{
          transform:
            dx !== 0
              ? `translate3d(${dx}px,0,0) rotate(${tiltDeg}deg) scale(${dragScale})`
              : undefined,
          transition: animMs
            ? `transform ${animMs}ms cubic-bezier(0.22, 1, 0.36, 1)`
            : undefined,
          touchAction: 'pan-y',
          willChange: dx !== 0 ? 'transform' : undefined,
          borderRadius: dx !== 0 ? '12px' : undefined,
          filter: dx !== 0 ? `saturate(${1 + reveal * 0.06}) brightness(${1 + reveal * 0.015})` : undefined,
        }}
        onPointerDown={(e) => {
          if (!enabled || disabled) return

          const target = e.target as HTMLElement | null

          let tapIgnored = Boolean(tapIgnoreSelector && target?.closest?.(tapIgnoreSelector))

          if (ignoreSelector && target?.closest?.(ignoreSelector)) return

          let noSwipe = false

          const root = e.currentTarget as HTMLElement
          const videos = Array.from(root.querySelectorAll('video')) as HTMLVideoElement[]
          const ctlVideo = videos.find((v) => v.controls)

          if (ctlVideo) {
            const vr = ctlVideo.getBoundingClientRect()

            const inVideo =
              e.clientX >= vr.left &&
              e.clientX <= vr.right &&
              e.clientY >= vr.top &&
              e.clientY <= vr.bottom

            if (inVideo) {
              const fromBottomVideo = vr.bottom - e.clientY
              const scrubZonePx = 72

              if (fromBottomVideo <= scrubZonePx) {
                noSwipe = true
                tapIgnored = true
              } else {
                const edgeZonePx = 64
                const xFromLeft = e.clientX - vr.left
                const xFromRight = vr.right - e.clientX
                const inEdge = xFromLeft <= edgeZonePx || xFromRight <= edgeZonePx

                if (!inEdge) {
                  noSwipe = true
                  tapIgnored = true
                }
              }
            }
          }

          const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
          const fromBottom = rect.bottom - e.clientY
          if (ignoreFromBottomPx && fromBottom <= ignoreFromBottomPx) {
            noSwipe = true
          }

          endLongPress()

          // Long-Press nur im Teaser-Modus, nicht im Inline-Playback / Controls / gesperrten Zonen
          if (enablePressHold && !noSwipe && !tapIgnored && onPressStart) {
            const pressInfo = {
              pointerType: e.pointerType,
              button: e.button,
            }

            const pointerId = e.pointerId

            pressTimerRef.current = window.setTimeout(() => {
              pressTimerRef.current = null

              if (pointer.current.id !== pointerId) return
              if (pointer.current.dragging) return
              if (pointer.current.noSwipe) return
              if (pointer.current.tapIgnored) return

              longPressActiveRef.current = true
              onPressStart(pressInfo)
            }, pressDelayMs)
          }

          const w = cardRef.current?.offsetWidth || 360
          widthRef.current = w
          thresholdRef.current = Math.min(thresholdPx, w * thresholdRatio)

          pointer.current = {
            id: e.pointerId,
            startX: e.clientX,
            startY: e.clientY,
            dragging: false,
            captured: false,
            tapIgnored,
            noSwipe,
            axisLocked: null,
          }

          lastMoveXRef.current = e.clientX
          lastMoveTRef.current = performance.now()
          velocityXRef.current = 0
          dxRef.current = 0
        }}
        onPointerMove={(e) => {
          if (!enabled || disabled) return
          if (pointer.current.id !== e.pointerId) return
          if (pointer.current.noSwipe) return
          if (longPressActiveRef.current) return

          const ddx = e.clientX - pointer.current.startX
          const ddy = e.clientY - pointer.current.startY
          const absX = Math.abs(ddx)
          const absY = Math.abs(ddy)

          if (!pointer.current.axisLocked) {
            if (absX < HORIZONTAL_START_PX && absY < HORIZONTAL_START_PX) return

            if (absY > absX * LOCK_RATIO) {
              clearPressTimer()
              pointer.current.axisLocked = 'y'
              pointer.current.id = null
              return
            }

            if (absX > absY * LOCK_RATIO) {
              pointer.current.axisLocked = 'x'
            } else {
              return
            }
          }

          if (pointer.current.axisLocked !== 'x') return

          if (!pointer.current.dragging) {
            clearPressTimer()
            pointer.current.dragging = true
            ;(e.currentTarget as HTMLElement).style.touchAction = 'none'
            setAnimMs(0)

            try {
              ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
              pointer.current.captured = true
            } catch {
              pointer.current.captured = false
            }
          }

          const now = performance.now()
          const dt = Math.max(1, now - lastMoveTRef.current)
          const vx = (e.clientX - lastMoveXRef.current) / dt
          velocityXRef.current = vx
          lastMoveXRef.current = e.clientX
          lastMoveTRef.current = now

          const next = rubberBand(ddx, widthRef.current)
          flushDx(next)

          const threshold = thresholdRef.current
          const nextDir = next > threshold ? 'right' : next < -threshold ? 'left' : null

          setArmedDir((prev) => {
            if (prev === nextDir) return prev
            if (nextDir) {
              try {
                navigator.vibrate?.(10)
              } catch {
                // ignore
              }
            }
            return nextDir
          })
        }}
        onPointerUp={(e) => {
          if (!enabled || disabled) return
          if (pointer.current.id !== e.pointerId) return

          const threshold = thresholdRef.current || Math.min(thresholdPx, widthRef.current * thresholdRatio)

          const wasDragging = pointer.current.dragging
          const wasCaptured = pointer.current.captured
          const wasTapIgnored = pointer.current.tapIgnored

          const wasLongPress = longPressActiveRef.current
          clearPressTimer()

          pointer.current.id = null
          pointer.current.dragging = false
          pointer.current.captured = false
          pointer.current.axisLocked = null

          if (wasCaptured) {
            try {
              ;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId)
            } catch {
              // ignore
            }
          }

          ;(e.currentTarget as HTMLElement).style.touchAction = 'pan-y'

          if (wasLongPress) {
            longPressActiveRef.current = false
            onPressEnd?.()
            return
          }

          if (!wasDragging) {
            const now = Date.now()
            const last = lastTapRef.current

            const isNear =
              last &&
              Math.hypot(e.clientX - last.x, e.clientY - last.y) <= doubleTapMaxMovePx

            const isDouble = Boolean(onDoubleTap) && last && now - last.t <= doubleTapMs && isNear

            if (isDouble) {
              lastTapRef.current = null
              clearTapTimer()

              if (doubleTapBusyRef.current) return
              doubleTapBusyRef.current = true

              try {
                runHotFx(e.clientX, e.clientY)
              } catch {
                // ignore
              }

              requestAnimationFrame(() => {
                ;(async () => {
                  try {
                    await onDoubleTap?.()
                  } finally {
                    doubleTapBusyRef.current = false
                  }
                })()
              })

              return
            }

            if (wasTapIgnored) {
              lastTapRef.current = { t: now, x: e.clientX, y: e.clientY }
              clearTapTimer()
              tapTimerRef.current = window.setTimeout(() => {
                tapTimerRef.current = null
                lastTapRef.current = null
              }, onDoubleTap ? doubleTapMs : 0)
              return
            }

            softResetForTap()

            lastTapRef.current = { t: now, x: e.clientX, y: e.clientY }
            clearTapTimer()

            tapTimerRef.current = window.setTimeout(() => {
              tapTimerRef.current = null
              lastTapRef.current = null
              onTap?.()
            }, onDoubleTap ? doubleTapMs : 0)

            return
          }

          if (rafRef.current != null) {
            cancelAnimationFrame(rafRef.current)
            rafRef.current = null
          }

          const finalDx = dxRef.current
          const abs = Math.abs(finalDx)
          const vx = velocityXRef.current
          const fastRight = vx > FLICK_VELOCITY
          const fastLeft = vx < -FLICK_VELOCITY

          dxRef.current = 0

          if (finalDx > threshold || (fastRight && abs >= threshold * 0.5)) {
            void commit('right', true)
            return
          }

          if (finalDx < -threshold || (fastLeft && abs >= threshold * 0.5)) {
            void commit('left', true)
            return
          }

          reset()
        }}
        onPointerCancel={(e) => {
          if (!enabled || disabled) return
          endLongPress()
          clearPressTimer()

          if (pointer.current.captured && pointer.current.id != null) {
            try {
              ;(e.currentTarget as HTMLElement).releasePointerCapture(pointer.current.id)
            } catch {
              // ignore
            }
          }

          pointer.current = {
            id: null,
            startX: 0,
            startY: 0,
            dragging: false,
            captured: false,
            tapIgnored: false,
            noSwipe: false,
            axisLocked: null,
          }

          if (rafRef.current != null) {
            cancelAnimationFrame(rafRef.current)
            rafRef.current = null
          }

          dxRef.current = 0
          velocityXRef.current = 0

          try {
            ;(e.currentTarget as HTMLElement).style.touchAction = 'pan-y'
          } catch {
            // ignore
          }

          reset()
        }}
      >
        <div className="relative">
          <div className="relative z-10">{children}</div>

          <div
            className="absolute inset-0 z-20 pointer-events-none rounded-lg transition-opacity duration-100"
            style={{
              opacity: dx === 0 ? 0 : 0.08 + revealSoft * 0.16,
              background:
                swipeDir === 'right'
                  ? 'linear-gradient(90deg, rgba(16,185,129,0.16) 0%, rgba(16,185,129,0.05) 42%, rgba(0,0,0,0) 100%)'
                  : swipeDir === 'left'
                    ? 'linear-gradient(270deg, rgba(244,63,94,0.16) 0%, rgba(244,63,94,0.05) 42%, rgba(0,0,0,0) 100%)'
                    : 'transparent',
            }}
          />

          <div
            className="absolute inset-0 z-20 pointer-events-none rounded-lg transition-opacity duration-100"
            style={{
              opacity: armedDir ? 1 : 0,
              boxShadow:
                armedDir === 'right'
                  ? 'inset 0 0 0 1px rgba(16,185,129,0.40), inset 0 0 28px rgba(16,185,129,0.10)'
                  : armedDir === 'left'
                    ? 'inset 0 0 0 1px rgba(244,63,94,0.40), inset 0 0 28px rgba(244,63,94,0.10)'
                    : 'none',
            }}
          />
        </div>
      </div>
    </div>
  )
})

export default SwipeCard