import { useRef, useState, type ReactNode } from 'react'
import {
  DndContext,
  DragOverlay,
  KeyboardSensor,
  PointerSensor,
  closestCenter,
  useSensor,
  useSensors,
  type CollisionDetection,
  type DragEndEvent,
  type DragStartEvent,
  type KeyboardCoordinateGetter,
} from '@dnd-kit/core'
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
import { Box, Typography } from '@mui/material'
import {
  getLegendStore,
  selectLayersByGroup,
  selectOrderedGroups,
  useLegendId,
  useLegendShallow,
  useLegendStore,
} from '../../stores'
import { LegendSortableContext } from './contexts'
import { parseSortableId, toSortableId } from './sortable-ids'
import { styles } from './styles'

export interface LegendSortableProps {
  /** Panel content (`Legend.Group` / ungrouped `Legend.Row` children). */
  children?: ReactNode
}

/**
 * Opt-in DnD reordering for legend groups and layers (within bucket only).
 *
 * @experimental This API is new and may change in a future release.
 */
export function LegendSortable({ children }: LegendSortableProps) {
  const id = useLegendId()
  const [activeId, setActiveId] = useState<string | null>(null)
  // Cached on drag start — avoid re-reading groupId on every collision candidate.
  const activeLayerGroupIdRef = useRef<string | undefined>(undefined)

  const topLevelIds = useLegendShallow(id, (s) => [
    ...selectOrderedGroups(s).map((g) => toSortableId('group', g.id)),
    ...selectLayersByGroup(s, undefined).map((l) =>
      toSortableId('layer', l.id),
    ),
  ])

  // Same-kind + same-bucket legality — shared by collision detection and keyboard moves.
  const isLegalTarget = (activeId: string, candidateId: string): boolean => {
    const from = parseSortableId(activeId)
    const to = parseSortableId(candidateId)
    if (!from || to?.kind !== from.kind) return false
    if (from.kind === 'layer') {
      const state = getLegendStore(id).getState()
      return state.layers[to.id]?.groupId === activeLayerGroupIdRef.current
    }
    return true
  }

  // Stock keyboard getter steps through nested member rows inside a group's rect.
  const keyboardCoordinates: KeyboardCoordinateGetter = (
    event,
    { context },
  ) => {
    const { active, collisionRect, droppableRects, droppableContainers } =
      context
    if (!active || !collisionRect) return undefined
    if (event.code !== 'ArrowDown' && event.code !== 'ArrowUp') return undefined
    event.preventDefault()

    const down = event.code === 'ArrowDown'
    const candidates = droppableContainers
      .getEnabled()
      .filter(
        (container) =>
          container.id !== active.id &&
          isLegalTarget(String(active.id), String(container.id)),
      )
      .flatMap((container) => {
        const rect = droppableRects.get(container.id)
        return rect ? [rect] : []
      })
      .filter((rect) =>
        down ? rect.top > collisionRect.top : rect.top < collisionRect.top,
      )
      .sort((a, b) => (down ? a.top - b.top : b.top - a.top))

    const target = candidates[0]
    return target ? { x: target.left, y: target.top } : undefined
  }

  const sensors = useSensors(
    useSensor(PointerSensor),
    useSensor(KeyboardSensor, { coordinateGetter: keyboardCoordinates }),
  )

  // Nested droppables: filter to legal targets before closestCenter.
  const collisionDetection: CollisionDetection = (args) => {
    const droppableContainers = args.droppableContainers.filter((container) =>
      isLegalTarget(String(args.active.id), String(container.id)),
    )
    return closestCenter({ ...args, droppableContainers })
  }

  const handleDragStart = (event: DragStartEvent) => {
    const nextId = String(event.active.id)
    setActiveId(nextId)
    const parsed = parseSortableId(nextId)
    activeLayerGroupIdRef.current =
      parsed?.kind === 'layer'
        ? getLegendStore(id).getState().layers[parsed.id]?.groupId
        : undefined
  }

  const handleDragEnd = (event: DragEndEvent) => {
    setActiveId(null)
    const { active, over } = event
    if (!over || active.id === over.id) return
    const from = parseSortableId(String(active.id))
    const to = parseSortableId(String(over.id))
    if (!from || from.kind !== to?.kind) return

    const state = getLegendStore(id).getState()
    if (from.kind === 'group') {
      const index = selectOrderedGroups(state).findIndex((g) => g.id === to.id)
      if (index !== -1) state.moveGroup(from.id, index)
      return
    }
    const fromLayer = state.layers[from.id]
    const toLayer = state.layers[to.id]
    if (!fromLayer || !toLayer || fromLayer.groupId !== toLayer.groupId) return
    const bucket = selectLayersByGroup(state, fromLayer.groupId)
    const index = bucket.findIndex((l) => l.id === to.id)
    if (index !== -1) state.moveLayer(from.id, index)
  }

  return (
    <LegendSortableContext.Provider value={true}>
      <DndContext
        sensors={sensors}
        collisionDetection={collisionDetection}
        onDragStart={handleDragStart}
        onDragEnd={handleDragEnd}
        onDragCancel={() => setActiveId(null)}
      >
        <SortableContext
          items={topLevelIds}
          strategy={verticalListSortingStrategy}
        >
          {children}
        </SortableContext>
        <DragOverlay>
          {activeId && <OverlayCard activeId={activeId} />}
        </DragOverlay>
      </DndContext>
    </LegendSortableContext.Provider>
  )
}

function OverlayCard({ activeId }: { activeId: string }) {
  const id = useLegendId()
  const parsed = parseSortableId(activeId)
  const label = useLegendStore(id, (s) =>
    parsed
      ? parsed.kind === 'group'
        ? s.groups[parsed.id]?.label
        : s.layers[parsed.id]?.name
      : undefined,
  )
  const groupIcon = useLegendStore(id, (s) =>
    parsed?.kind === 'group' ? s.groups[parsed.id]?.icon : undefined,
  )

  if (!parsed || label === undefined) return null

  return (
    <Box sx={styles.overlay}>
      {parsed.kind === 'group' && groupIcon != null && (
        <Box sx={styles.overlayIcon}>{groupIcon}</Box>
      )}
      <Typography
        variant='button'
        fontWeight={parsed.kind === 'group' ? 'bold' : undefined}
        color='text.primary'
        sx={styles.overlayLabel}
      >
        {label}
      </Typography>
    </Box>
  )
}
