import { type ReactNode, useEffect, useMemo, useState } from 'react'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, {
  runOnJS,
  type SharedValue,
  useAnimatedReaction,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated'
import { haptic } from 'src/lib/haptics'
import { View } from 'tamagui'

// Drag-to-reorder list primitive. Built on `Gesture.Pan().activateAfterLongPress`
// so a tap or short-press still bubbles to a child Pressable; only a held
// finger starts a drag. Designed for fixed-row-height lists of <50 items —
// each row is absolutely positioned within a parent of total height
// `items.length * rowHeight`, with translateY animated via reanimated shared
// values. Rows shift via `withSpring` as the dragged item crosses their
// midpoint; the dragged row lifts (scale 1.02) until release.
//
// Why no `react-native-draggable-flatlist`: that library is built on
// Reanimated 2; the project ships Reanimated 4.3. Compatibility is unclear
// and the surface (≤50 fixed-height rows) doesn't need virtualization or
// the library's larger API.

type ItemBase = { id: string }

type Props<T extends ItemBase> = {
  items: T[]
  rowHeight?: number
  onReorder: (newIds: string[]) => void
  renderItem: (item: T, opts: { isActive: boolean }) => ReactNode
}

const SPRING = { damping: 22, stiffness: 220, mass: 0.6 } as const
const LIFT_SCALE = 1.02
const LONG_PRESS_MS = 180

export function SortableList<T extends ItemBase>({
  items,
  rowHeight = 64,
  onReorder,
  renderItem,
}: Props<T>) {
  const originalIndices = useMemo(
    () => Object.fromEntries(items.map((it, i) => [it.id, i])) as Record<string, number>,
    [items]
  )

  const positions = useSharedValue<Record<string, number>>({ ...originalIndices })
  const activeId = useSharedValue<string | null>(null)

  // Reset positions when the items array changes (parent committed a reorder
  // or the server returned a different list set). Outside of those resets,
  // positions is mutated only on the worklet thread during drag.
  useEffect(() => {
    positions.value = { ...originalIndices }
  }, [originalIndices, positions])

  const commitOrder = () => {
    const ordered = Object.entries(positions.value)
      .sort((a, b) => a[1] - b[1])
      .map(([id]) => id)
    onReorder(ordered)
  }

  return (
    <View height={items.length * rowHeight} position="relative">
      {items.map((item) => (
        <SortableRow
          key={item.id}
          id={item.id}
          rowHeight={rowHeight}
          originalIndex={originalIndices[item.id]}
          totalItems={items.length}
          positions={positions}
          activeId={activeId}
          onCommit={commitOrder}
        >
          {(isActive) => renderItem(item, { isActive })}
        </SortableRow>
      ))}
    </View>
  )
}

function SortableRow({
  id,
  rowHeight,
  originalIndex,
  totalItems,
  positions,
  activeId,
  onCommit,
  children,
}: {
  id: string
  rowHeight: number
  originalIndex: number
  totalItems: number
  positions: SharedValue<Record<string, number>>
  activeId: SharedValue<string | null>
  onCommit: () => void
  children: (isActive: boolean) => ReactNode
}) {
  const translateY = useSharedValue(0)
  const scale = useSharedValue(1)
  const zIndex = useSharedValue(0)
  const [isActive, setIsActive] = useState(false)

  // Non-active rows spring to their target Y (derived from positions[id])
  // whenever the dragged item crosses their midpoint. The dragged row's
  // translateY is driven directly by the gesture handler — the reaction
  // skips it.
  useAnimatedReaction(
    () => ({ pos: positions.value[id] ?? originalIndex, active: activeId.value }),
    (curr) => {
      if (curr.active === id) return
      const targetY = (curr.pos - originalIndex) * rowHeight
      if (translateY.value !== targetY) {
        translateY.value = withSpring(targetY, SPRING)
      }
    }
  )

  const setActiveJS = (v: boolean) => setIsActive(v)
  const triggerHaptic = () => haptic.selection()

  const pan = Gesture.Pan()
    .activateAfterLongPress(LONG_PRESS_MS)
    .onStart(() => {
      activeId.value = id
      zIndex.value = 10
      scale.value = withSpring(LIFT_SCALE, SPRING)
      runOnJS(setActiveJS)(true)
      runOnJS(triggerHaptic)()
    })
    .onUpdate((e) => {
      if (activeId.value !== id) return
      translateY.value = e.translationY

      const center = originalIndex * rowHeight + e.translationY + rowHeight / 2
      let target = Math.floor(center / rowHeight)
      if (target < 0) target = 0
      if (target > totalItems - 1) target = totalItems - 1

      const currentPos = positions.value[id] ?? originalIndex
      if (target !== currentPos) {
        const next = { ...positions.value }
        if (target > currentPos) {
          for (const otherId in next) {
            if (otherId === id) continue
            if (next[otherId] > currentPos && next[otherId] <= target) {
              next[otherId] = next[otherId] - 1
            }
          }
        } else {
          for (const otherId in next) {
            if (otherId === id) continue
            if (next[otherId] >= target && next[otherId] < currentPos) {
              next[otherId] = next[otherId] + 1
            }
          }
        }
        next[id] = target
        positions.value = next
      }
    })
    .onEnd(() => {
      if (activeId.value !== id) return
      const finalPos = positions.value[id] ?? originalIndex
      const finalY = (finalPos - originalIndex) * rowHeight
      translateY.value = withSpring(finalY, SPRING)
      scale.value = withSpring(1, SPRING)
      zIndex.value = 0
      activeId.value = null
      runOnJS(setActiveJS)(false)
      runOnJS(onCommit)()
    })
    .onFinalize((_e, success) => {
      // If the gesture is cancelled before activation (short press, etc.),
      // make sure we never leave a stale active flag behind. `success`
      // distinguishes natural ends (already cleaned up by onEnd) from
      // cancellations.
      if (success) return
      if (activeId.value === id) activeId.value = null
      scale.value = withSpring(1, SPRING)
      zIndex.value = 0
      runOnJS(setActiveJS)(false)
    })

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: translateY.value }, { scale: scale.value }],
    zIndex: zIndex.value,
  }))

  return (
    <GestureDetector gesture={pan}>
      <Animated.View
        style={[
          {
            position: 'absolute',
            top: originalIndex * rowHeight,
            left: 0,
            right: 0,
            height: rowHeight,
          },
          animatedStyle,
        ]}
      >
        {children(isActive)}
      </Animated.View>
    </GestureDetector>
  )
}
