import {
  createContext,
  type ReactNode,
  useCallback,
  useContext,
  useMemo,
  useState,
} from 'react'
import { Dimensions } from 'react-native'
import { Gesture } from 'react-native-gesture-handler'
import Animated, {
  type AnimatedRef,
  measure,
  runOnJS,
  type SharedValue,
  scrollTo,
  useAnimatedRef,
  useAnimatedScrollHandler,
  useFrameCallback,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated'
import type { Account } from 'src/lib/api-types'
import { haptic } from 'src/lib/haptics'

// Cross-column drag-and-drop engine for the List Organizer (Surface E).
//
// Everything that has to live on the UI thread — the floating preview's
// transform, the token hit-test, the hovered-list reaction, auto-scroll — is
// driven by the shared values created here. The single React-tree owner is the
// screen, which mounts <OrganizerDragProvider>; rows and rail tokens reach the
// shared values through context (`useOrganizerDrag`) rather than prop-drilling
// across FlashList's `renderItem` boundary.
//
// Gesture composition uses the official "drag after long press" recipe from the
// Pan gesture docs: a Pan with `activateAfterLongPress` (held finger → drag),
// raced against a Tap for the quick-look via `Gesture.Exclusive(pan, tap)` — pan
// first, so the tap fires only when the pan fails (a quick press). Holding then
// moving starts a drag; a fast swipe fails the long-press window so the
// FlashList's own scroll wins by default (no `blocksExternalGesture` needed).
//
// NB: do NOT rewrite this as `Exclusive(longPress, manualActivation-pan)`. Per
// the gesture-state-manager docs, a manual `activate()` on a gesture that is
// exclusive-after another is delayed until the higher-priority gesture fails —
// and a long-press succeeds rather than fails, so the pan could never activate
// and dragging did nothing at all. `activateAfterLongPress` has no such trap and
// is the same primitive `SortableList` already ships on.

export type TokenFrame = {
  pageX: number
  pageY: number
  width: number
  height: number
}

export type DraggedRow = {
  account: Account
  listIds: string[]
  isMutual: boolean
}

const LONG_PRESS_MS = 220
const PREVIEW_WIDTH = 280
const PREVIEW_GRAB_Y = 28 // half the row's avatar-height, so the finger sits mid-card
const RAIL_WIDTH = 120
const EDGE_BAND = 110
const MAX_SCROLL_PER_FRAME = 18

type DragApi = {
  // Shared values (UI thread).
  draggedId: SharedValue<string | null>
  hoveredListId: SharedValue<string | null>
  tokenFrames: SharedValue<Record<string, TokenFrame>>
  tx: SharedValue<number>
  ty: SharedValue<number>
  scale: SharedValue<number>
  opacity: SharedValue<number>
  panActive: SharedValue<boolean>
  fingerX: SharedValue<number>
  fingerY: SharedValue<number>
  railScrollY: SharedValue<number>
  leftScrollY: SharedValue<number>
  // Page-coordinate origin of the body container. The preview is a child of
  // the body (which sits below the native header), but tx/ty are screen-page
  // coords (from measure() / finger absoluteX-Y, same space as the token
  // hit-test). The preview subtracts this origin to map back into its local
  // space so it lands under the finger, not header-height below it.
  containerX: SharedValue<number>
  containerY: SharedValue<number>
  // Refs + scroll handler the screen wires onto the left FlashList for
  // worklet-driven auto-scroll.
  leftListRef: AnimatedRef<Animated.FlatList<unknown>>
  leftScrollHandler: ReturnType<typeof useAnimatedScrollHandler>
  // JS-thread callbacks + preview state.
  onQuickLook: (account: Account) => void
  commitDrop: (accountId: string, listId: string) => void
  setDraggedRow: (row: DraggedRow | null) => void
  draggedRow: DraggedRow | null
  colorIndices: Record<string, number>
}

const DragContext = createContext<DragApi | null>(null)

export function useOrganizerDrag(): DragApi {
  const ctx = useContext(DragContext)
  if (!ctx) {
    throw new Error('useOrganizerDrag must be used within an <OrganizerDragProvider>')
  }
  return ctx
}

export function OrganizerDragProvider({
  onQuickLook,
  onCommitDrop,
  colorIndices,
  children,
}: {
  onQuickLook: (account: Account) => void
  onCommitDrop: (accountId: string, listId: string) => void
  colorIndices: Record<string, number>
  children: ReactNode
}) {
  const draggedId = useSharedValue<string | null>(null)
  const hoveredListId = useSharedValue<string | null>(null)
  const tokenFrames = useSharedValue<Record<string, TokenFrame>>({})
  const tx = useSharedValue(0)
  const ty = useSharedValue(0)
  const scale = useSharedValue(1)
  const opacity = useSharedValue(0)
  const panActive = useSharedValue(false)
  const fingerX = useSharedValue(0)
  const fingerY = useSharedValue(0)
  const railScrollY = useSharedValue(0)
  const leftScrollY = useSharedValue(0)
  const containerX = useSharedValue(0)
  const containerY = useSharedValue(0)
  const leftListRef = useAnimatedRef<Animated.FlatList<unknown>>()

  const [draggedRow, setDraggedRow] = useState<DraggedRow | null>(null)

  const leftScrollHandler = useAnimatedScrollHandler((e) => {
    leftScrollY.value = e.contentOffset.y
  })

  const { width: winW, height: winH } = Dimensions.get('window')
  const splitX = winW - RAIL_WIDTH
  const topEdge = 130
  const bottomEdge = winH - 150

  // Auto-scroll the left column when the dragged finger enters the top/bottom
  // edge band. scrollTo is a UI-thread API; the FlashList ref forwards to the
  // underlying scrollable. Only the (long) left column auto-scrolls — the rail
  // is short enough to keep every droppable token reachable without it.
  useFrameCallback(() => {
    'worklet'
    if (!panActive.value) return
    if (fingerX.value >= splitX) return
    let delta = 0
    if (fingerY.value < topEdge + EDGE_BAND) {
      const factor = Math.min(1, (topEdge + EDGE_BAND - fingerY.value) / EDGE_BAND)
      delta = -factor * MAX_SCROLL_PER_FRAME
    } else if (fingerY.value > bottomEdge - EDGE_BAND) {
      const factor = Math.min(1, (fingerY.value - (bottomEdge - EDGE_BAND)) / EDGE_BAND)
      delta = factor * MAX_SCROLL_PER_FRAME
    }
    if (delta !== 0) {
      const next = Math.max(0, leftScrollY.value + delta)
      scrollTo(leftListRef, 0, next, false)
      leftScrollY.value = next
    }
  })

  const api = useMemo<DragApi>(
    () => ({
      draggedId,
      hoveredListId,
      tokenFrames,
      tx,
      ty,
      scale,
      opacity,
      panActive,
      fingerX,
      fingerY,
      railScrollY,
      leftScrollY,
      containerX,
      containerY,
      leftListRef,
      leftScrollHandler,
      onQuickLook,
      commitDrop: onCommitDrop,
      setDraggedRow,
      draggedRow,
      colorIndices,
    }),
    [
      draggedId,
      hoveredListId,
      tokenFrames,
      tx,
      ty,
      scale,
      opacity,
      panActive,
      fingerX,
      fingerY,
      railScrollY,
      leftScrollY,
      containerX,
      containerY,
      leftListRef,
      leftScrollHandler,
      onQuickLook,
      onCommitDrop,
      draggedRow,
      colorIndices,
    ]
  )

  return <DragContext.Provider value={api}>{children}</DragContext.Provider>
}

/**
 * Builds the composed gesture + animated ref for one follow row. Called from
 * inside `OrganizerAccountRow` (so the per-row hooks obey the rules of hooks)
 * with the row's view-model; reads the shared drag state from context.
 */
export function useRowGesture(row: DraggedRow) {
  const ctx = useOrganizerDrag()
  const { account, listIds, isMutual } = row
  const rowRef = useAnimatedRef<Animated.View>()

  const {
    draggedId,
    hoveredListId,
    tokenFrames,
    tx,
    ty,
    scale,
    opacity,
    panActive,
    fingerX,
    fingerY,
    onQuickLook,
    commitDrop,
    setDraggedRow,
  } = ctx

  const onPickup = useCallback(() => {
    haptic.selection()
    setDraggedRow({ account, listIds, isMutual })
  }, [account, listIds, isMutual, setDraggedRow])

  const openQuickLook = useCallback(() => onQuickLook(account), [account, onQuickLook])
  const onDrop = useCallback(
    (listId: string) => commitDrop(account.id, listId),
    [account.id, commitDrop]
  )
  const onEnterToken = useCallback(() => haptic.selection(), [])

  const gesture = useMemo(() => {
    const accountId = account.id

    const tap = Gesture.Tap()
      .maxDuration(250)
      .onEnd((_e, success) => {
        if (success) runOnJS(openQuickLook)()
      })

    const pan = Gesture.Pan()
      // Hold in place for LONG_PRESS_MS to pick up; moving before that fails the
      // gesture so a quick swipe scrolls the FlashList instead of dragging.
      .activateAfterLongPress(LONG_PRESS_MS)
      .onStart((e) => {
        // Long-press elapsed with the finger still down → pickup. The preview
        // lifts from the row's measured frame, then onUpdate moves it to follow
        // the finger.
        panActive.value = true
        draggedId.value = accountId
        fingerX.value = e.absoluteX
        fingerY.value = e.absoluteY
        const m = measure(rowRef)
        if (m) {
          tx.value = m.pageX
          ty.value = m.pageY
        }
        scale.value = withTiming(1.04, { duration: 120 })
        opacity.value = withTiming(0.92, { duration: 120 })
        runOnJS(onPickup)()
      })
      .onUpdate((e) => {
        fingerX.value = e.absoluteX
        fingerY.value = e.absoluteY
        tx.value = e.absoluteX - PREVIEW_WIDTH / 2
        ty.value = e.absoluteY - PREVIEW_GRAB_Y
        // Hit-test the finger against every measured (on-screen) token frame.
        const frames = tokenFrames.value
        let hit: string | null = null
        for (const id in frames) {
          const f = frames[id]
          if (
            e.absoluteX >= f.pageX &&
            e.absoluteX <= f.pageX + f.width &&
            e.absoluteY >= f.pageY &&
            e.absoluteY <= f.pageY + f.height
          ) {
            hit = id
            break
          }
        }
        if (hit !== hoveredListId.value) {
          hoveredListId.value = hit
          if (hit !== null) runOnJS(onEnterToken)()
        }
      })
      .onEnd(() => {
        const target = hoveredListId.value
        if (target !== null) {
          opacity.value = withTiming(0, { duration: 120 })
          runOnJS(onDrop)(target)
        } else {
          // No drop target: fade + un-lift in place as the "didn't land" cue.
          opacity.value = withTiming(0, { duration: 180 })
          scale.value = withTiming(1, { duration: 180 })
        }
      })
      .onFinalize(() => {
        // Defensive reset on every termination (success, cancel, interrupt) so
        // no partial state leaks into the next drag.
        panActive.value = false
        hoveredListId.value = null
        draggedId.value = null
      })

    // Pan first: the tap waits for the pan to fail (a quick press) before it
    // opens the quick-look, so a hold-then-drag never also fires a tap.
    return Gesture.Exclusive(pan, tap)
  }, [
    account.id,
    rowRef,
    draggedId,
    hoveredListId,
    tokenFrames,
    tx,
    ty,
    scale,
    opacity,
    panActive,
    fingerX,
    fingerY,
    onPickup,
    openQuickLook,
    onDrop,
    onEnterToken,
  ])

  return { gesture, rowRef }
}
