/* eslint-disable react-hooks/immutability --
   reanimated shared values mutate via .value inside worklets, which
   this rule cannot model */
import {SymbolView} from 'expo-symbols'
import {useEffect, type ReactNode} from 'react'
import {View} from 'react-native'
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import Animated, {
  runOnJS,
  useAnimatedReaction,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
  type SharedValue,
} from 'react-native-reanimated'

import {hapticSelection} from '@/lib/haptics'
import {atoms as a, useTheme} from '@/sprinkles'

// id → row index, updated on the ui thread while a drag is live
type Positions = Record<string, number>

const toPositions = (ids: string[]): Positions =>
  Object.fromEntries(ids.map((id, i) => [id, i]))

const movedPositions = (
  positions: Positions,
  id: string,
  to: number,
): Positions => {
  'worklet'
  const from = positions[id] ?? 0
  const out: Positions = {}
  for (const key in positions) {
    const i = positions[key] ?? 0
    if (key === id) out[key] = to
    else if (from < to && i > from && i <= to) out[key] = i - 1
    else if (to < from && i >= to && i < from) out[key] = i + 1
    else out[key] = i
  }
  return out
}

const clampIndex = (value: number, count: number): number => {
  'worklet'
  return Math.max(0, Math.min(value, count - 1))
}

const DRAG_ACTIVATE_MS = 200

function DragSortRow({
  id,
  index,
  count,
  rowHeight,
  positions,
  onSettle,
  onDragActive,
  children,
}: {
  id: string
  index: number
  count: number
  rowHeight: number
  positions: SharedValue<Positions>
  onSettle: (id: string, index: number) => void
  onDragActive: (active: boolean) => void
  children: ReactNode
}) {
  const t = useTheme()
  const isActive = useSharedValue(false)
  const startIndex = useSharedValue(0)
  const startTop = useSharedValue(0)
  const top = useSharedValue(index * rowHeight)

  // rows not being dragged glide to their (possibly reassigned) slot
  useAnimatedReaction(
    () => positions.value[id],
    (index) => {
      if (index == null || isActive.value) return
      top.value = withTiming(index * rowHeight)
    },
  )

  const pan = Gesture.Pan()
    // press-and-hold to lift, so the surrounding scroll view keeps
    // plain swipes
    .activateAfterLongPress(DRAG_ACTIVATE_MS)
    .onStart(() => {
      const index = positions.value[id] ?? 0
      isActive.value = true
      startIndex.value = index
      startTop.value = index * rowHeight
      top.value = startTop.value
      runOnJS(onDragActive)(true)
      runOnJS(hapticSelection)()
    })
    .onUpdate((event) => {
      top.value = Math.max(
        0,
        Math.min(
          startTop.value + event.translationY,
          (count - 1) * rowHeight,
        ),
      )
      const next = clampIndex(
        Math.round(top.value / rowHeight),
        count,
      )
      if (next !== positions.value[id]) {
        positions.value = movedPositions(positions.value, id, next)
        runOnJS(hapticSelection)()
      }
    })
    .onEnd(() => {
      const index = positions.value[id] ?? 0
      top.value = withTiming(index * rowHeight)
      if (index !== startIndex.value) runOnJS(onSettle)(id, index)
    })
    .onFinalize(() => {
      isActive.value = false
      runOnJS(onDragActive)(false)
    })

  const animatedStyle = useAnimatedStyle(() => ({
    top: top.value,
    zIndex: isActive.value ? 1 : 0,
    transform: [{scale: withTiming(isActive.value ? 1.02 : 1)}],
  }))

  const moveTo = (index: number) => {
    const clamped = clampIndex(index, count)
    if (clamped === (positions.value[id] ?? 0)) return
    onSettle(id, clamped)
  }

  return (
    <Animated.View
      style={[
        a.absolute,
        {left: 0, right: 0, height: rowHeight},
        animatedStyle,
      ]}
    >
      <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}>
        <View style={a.flex_1}>{children}</View>
        <GestureDetector gesture={pan}>
          <View
            hitSlop={12}
            accessible
            accessibilityRole="adjustable"
            accessibilityLabel="Reorder"
            accessibilityActions={[
              {name: 'increment', label: 'Move down'},
              {name: 'decrement', label: 'Move up'},
            ]}
            onAccessibilityAction={({nativeEvent}) =>
              moveTo(
                (positions.value[id] ?? 0) +
                  (nativeEvent.actionName === 'increment' ? 1 : -1),
              )
            }
          >
            <SymbolView
              name="line.3.horizontal"
              size={18}
              weight="medium"
              tintColor={t.atoms.text_contrast_low.color}
            />
          </View>
        </GestureDetector>
      </View>
    </Animated.View>
  )
}

// fixed-height rows sortable by dragging their handle. onMove fires once
// per settled drag with the item's new index — the same insertion slot
// moveItem in @crumbs/core/collections takes as `to`, so callers can feed
// it straight through. onDragActiveChange lets the host pause its own
// scrolling while a row is lifted
export const DragSortList = <T extends {id: string}>({
  items,
  rowHeight,
  renderItem,
  onMove,
  onDragActiveChange,
}: {
  items: T[]
  rowHeight: number
  renderItem: (item: T) => ReactNode
  onMove: (item: T, index: number) => void
  onDragActiveChange?: (active: boolean) => void
}) => {
  const positions = useSharedValue(
    toPositions(items.map((item) => item.id)),
  )

  useEffect(() => {
    positions.value = toPositions(items.map((item) => item.id))
  }, [items, positions])

  const settle = (id: string, index: number) => {
    const item = items.find((it) => it.id === id)
    if (item) onMove(item, index)
  }

  return (
    <View style={{height: items.length * rowHeight}}>
      {items.map((item, index) => (
        <DragSortRow
          key={item.id}
          id={item.id}
          index={index}
          count={items.length}
          rowHeight={rowHeight}
          positions={positions}
          onSettle={settle}
          onDragActive={onDragActiveChange ?? (() => {})}
        >
          {renderItem(item)}
        </DragSortRow>
      ))}
    </View>
  )
}
