import React, {
  useCallback,
  useRef,
  useMemo,
  useState,
  forwardRef,
  useImperativeHandle,
  useEffect,
  useLayoutEffect,
} from "react";
import {
  View,
  RefreshControl,
  type NativeSyntheticEvent,
  type NativeScrollEvent,
  type LayoutChangeEvent,
  type ViewStyle,
} from "react-native";
import Animated, {
  useAnimatedStyle,
  useAnimatedReaction,
  useSharedValue,
} from "react-native-reanimated";
import { Gesture } from "react-native-gesture-handler";
import { scheduleOnRN } from "react-native-worklets";
import {
  FlashList,
  type ListRenderItemInfo,
  type FlashListRef,
  type FlashListProps,
} from "@shopify/flash-list";
import { DragStateProvider, useDragState } from "./contexts/DragStateContext";
import { useOverlayCrossfade } from "./hooks/drag/useOverlayCrossfade";
import { ListAnimationProvider } from "./contexts/ListAnimationContext";
import { AnimatedFlashListItem } from "./AnimatedFlashListItem";
import { DEFAULT_DRAG_CONFIG } from "./constants/drag";
import type {
  AnimatedListItem,
  AnimatedFlashListProps,
  AnimatedFlashListRef,
  HapticFeedbackType,
} from "./types";

/**
 * Error boundary that catches JS errors in animation hooks and falls back
 * to rendering children without animation wrappers.
 * @internal
 */
interface AnimationErrorBoundaryState {
  hasError: boolean;
}

class AnimationErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback: React.ReactNode },
  AnimationErrorBoundaryState
> {
  state: AnimationErrorBoundaryState = { hasError: false };

  static getDerivedStateFromError(): AnimationErrorBoundaryState {
    return { hasError: true };
  }

  componentDidCatch(error: Error) {
    if (__DEV__) {
      console.warn(
        "[AnimatedFlashList] Animation error caught by error boundary. " +
          "Falling back to non-animated list.",
        error,
      );
    }
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

interface DragOverlayProps<T extends AnimatedListItem> {
  data: T[];
  totalItemsRef: React.RefObject<number>;
  renderItem: AnimatedFlashListProps<T>["renderItem"];
}

const DragOverlay = React.memo(function DragOverlay<
  T extends AnimatedListItem,
>({ data, totalItemsRef, renderItem }: DragOverlayProps<T>) {
  const {
    overlayActive,
    overlayItemId,
    overlayBaseX,
    overlayBaseY,
    overlayWidth,
    overlayHeight,
    overlayTranslateY,
    overlayReady,
    overlayVisible,
    scrollOffset,
    dragStartScrollOffset,
    draggedScale,
    isDragging,
    isDropping,
    overlayTouchOffset,
    overlayAbsoluteY,
    overlayContainerY,
    dragOverlayOpacity,
    overlayFrozenItemId,
    overlayRenderedOnUI,
  } = useDragState();

  // Drive overlay crossfade animation on UI thread
  // This ensures smooth visibility transitions without JS scheduling races
  useOverlayCrossfade();

  const [overlayId, setOverlayId] = useState<string | null>(null);
  const [frozenId, setFrozenId] = useState<string | null>(null);

  // O(1) lookup map for items by ID
  const itemMap = useMemo(() => {
    return new Map(data.map(item => [item.id, item]));
  }, [data]);

  const setOverlayIdSafe = useCallback((nextId: string | null) => {
    setOverlayId(nextId);
  }, []);

  const setFrozenIdSafe = useCallback((nextId: string | null) => {
    setFrozenId(nextId);
  }, []);

  useAnimatedReaction(
    () => overlayItemId.value,
    (current, prev) => {
      "worklet";
      if (current === prev) return;
      scheduleOnRN(setOverlayIdSafe, current || null);
    },
    [setOverlayIdSafe],
  );

  // Track frozen ID for rendering during fade-out
  useAnimatedReaction(
    () => overlayFrozenItemId.value,
    (current, prev) => {
      "worklet";
      if (current === prev) return;
      scheduleOnRN(setFrozenIdSafe, current || null);
    },
    [setFrozenIdSafe],
  );

  useEffect(() => {
    overlayVisible.value = Boolean(overlayId);
  }, [overlayId, overlayVisible]);

  // Use frozenId to keep overlay rendered during fade-out
  // Uses O(1) Map lookup instead of O(n) find()
  const overlayItem = useMemo(() => {
    const id = frozenId || overlayId;
    if (!id) return null;
    return itemMap.get(id) ?? null;
  }, [itemMap, frozenId, overlayId]);

  // Create disabled gesture and false isDragging for overlay rendering
  // This allows renderItem to show drag handle UI without functional gesture
  const disabledGesture = useMemo(() => Gesture.Pan().enabled(false), []);
  const falseIsDragging = useSharedValue(false);

  const overlayStyle = useAnimatedStyle(() => {
    // Use frozenItemId so overlay stays renderable during fade-out
    const itemId = overlayFrozenItemId.value || overlayItemId.value;

    // Keep rendering if frozenItemId is set (crossfade in progress) even if overlayReady is false.
    // This prevents the overlay from becoming invisible before the crossfade completes,
    // which would cause a brief moment where both overlay AND original are invisible.
    // FIXED: Also require overlayVisible to ensure JS-side overlayItem exists before fading.
    // overlayVisible is only cleared AFTER fade-out completes (in useOverlayCrossfade),
    // so this doesn't break fade-out.
    if (!itemId || (!overlayFrozenItemId.value && !overlayReady.value) || !overlayVisible.value) {
      // Mark overlay as NOT rendered when returning invisible style
      overlayRenderedOnUI.value = false;
      return { opacity: 0, position: "absolute" };
    }

    // Mark overlay as rendered on UI thread - this is the KEY fix for the vanish bug.
    // Setting this here (in useAnimatedStyle, which runs on UI thread) instead of
    // in useEffect (JS thread) ensures the original item knows the overlay has
    // actually rendered BEFORE the crossfade starts. This prevents the race condition
    // where the original starts fading out before the overlay appears.
    overlayRenderedOnUI.value = true;

    // Include isDropping so scale animates smoothly during drop phase
    // Without this, scale jumps from 1.03 to 1.0 instantly when isDragging becomes false,
    // causing a ~1-2px visual jump due to the scale transform's center pivot point.
    const scale = (isDragging.value || isDropping.value) ? draggedScale.value : 1;

    // During active drag, use absolute finger position for reliable overlay tracking.
    // This avoids lag from JS-thread scroll offset updates during simultaneous scroll+drag.
    // Formula: top = absoluteY - touchOffset - containerY
    // Where:
    // - absoluteY = finger's current screen Y position
    // - touchOffset = how far down in the item the finger started
    // - containerY = overlay container's screen Y position
    //
    // During drop animation (isDropping), switch back to slot-based positioning
    // since the finger is no longer tracking and we need to animate to final slot.
    let top: number;
    if (isDragging.value) {
      // Active drag: follow finger precisely
      top = overlayAbsoluteY.value - overlayTouchOffset.value - overlayContainerY.value;
    } else {
      // Drop animation: use slot-based positioning with scroll compensation
      // Note: scrollDelta is SUBTRACTED because overlayBaseY was measured at drag start
      // (when scroll was dragStartScrollOffset). As user scrolls down (scrollDelta > 0),
      // the target slot moves UP visually, so we subtract.
      const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
      top = overlayBaseY.value + overlayTranslateY.value - scrollDelta;
    }

    return {
      position: "absolute",
      left: overlayBaseX.value,
      top,
      width: overlayWidth.value,
      height: overlayHeight.value,
      zIndex: 1000,
      transform: [{ scale }],
      opacity: dragOverlayOpacity.value,  // Uses shared crossfade value
    };
  });

  // Only return null when BOTH overlayItem is null AND frozenId is empty
  // This keeps the Animated.View mounted during fade-out (while frozenId is set)
  if (!overlayItem && !frozenId) return null;

  const overlayIndex = overlayItem
    ? data.findIndex((item) => item.id === overlayItem.id)
    : -1;
  const totalItems = totalItemsRef.current ?? data.length;

  return (
    <Animated.View pointerEvents="none" style={overlayStyle}>
      {overlayItem &&
        renderItem({
          item: overlayItem,
          index: overlayIndex,
          totalItems,
          animatedStyle: {},
          dragHandleProps: {
            gesture: disabledGesture,
            isDragging: falseIsDragging,
          },
          isDragging: false,
          isDragEnabled: false,
          triggerExitAnimation: () => {},
          resetExitAnimation: () => {},
        })}
    </Animated.View>
  );
}) as <T extends AnimatedListItem>(
  props: DragOverlayProps<T>,
) => React.ReactElement | null;

/**
 * v40.1: Shifted item overlays - renders copies of shifted items at their visual positions
 * during the drop transition to mask the FlashList repositioning glitch.
 * Uses viewport-relative Y positions captured at drop start.
 */
interface ShiftedItemOverlaysProps<T extends AnimatedListItem> {
  data: T[];
  totalItemsRef: React.RefObject<number>;
  renderItem: AnimatedFlashListProps<T>["renderItem"];
}

const ShiftedItemOverlays = React.memo(function ShiftedItemOverlays<
  T extends AnimatedListItem,
>({ data, totalItemsRef, renderItem }: ShiftedItemOverlaysProps<T>) {
  const { shiftedOverlays, shiftedOverlaysActive, shiftedOverlaysRendered } =
    useDragState();

  // State to hold overlay items on JS thread
  const [overlayItems, setOverlayItems] = useState<
    Array<{
      item: T;
      position: { y: number; height: number };
    }>
  >([]);

  // O(1) lookup map for items by ID - stored in ref to break cascade dependency
  // This avoids O(n) data.find() for each overlay item during drop rendering
  const itemMapRef = useRef<Map<string, T>>(new Map());

  useEffect(() => {
    itemMapRef.current = new Map(data.map(item => [item.id, item]));
  }, [data]);

  // Callback to update overlay items from JS thread
  // v40.1: Y position is now viewport-relative (captured with scroll offset subtracted)
  // Uses itemMapRef for O(1) lookups and empty deps to break cascade dependency
  const updateOverlayItems = useCallback(
    (
      overlays: Record<
        string,
        {
          baseY: number;
          height: number;
          shift: number;
          scrollAtCapture: number;
        }
      >,
      active: boolean,
    ) => {
      if (active && Object.keys(overlays).length > 0) {
        const itemMap = itemMapRef.current;
        const items = Object.entries(overlays)
          .map(([id, pos]) => ({
            item: itemMap.get(id),  // O(1) lookup instead of O(n) find()
            position: { y: pos.baseY, height: pos.height },
          }))
          .filter(
            (x): x is { item: T; position: { y: number; height: number } } =>
              x.item !== undefined,
          );
        setOverlayItems(items);
      } else {
        setOverlayItems([]);
      }
    },
    [],  // Empty deps - uses ref to break cascade
  );

  // Watch for overlay changes
  useAnimatedReaction(
    () => ({
      active: shiftedOverlaysActive.value,
      overlays: shiftedOverlays.value,
    }),
    (state, prev) => {
      "worklet";
      // Only update when state changes
      if (
        prev !== null &&
        state.active === prev.active &&
        Object.keys(state.overlays).length === Object.keys(prev.overlays).length
      ) {
        return;
      }
      scheduleOnRN(updateOverlayItems, state.overlays, state.active);
    },
    [updateOverlayItems],
  );

  // v41.6: Use useLayoutEffect for earlier signal (runs after commit, before paint)
  // This reduces the timing gap between overlay render and originals being hidden
  useLayoutEffect(() => {
    shiftedOverlaysRendered.value = overlayItems.length > 0;
  }, [overlayItems.length, shiftedOverlaysRendered]);

  // Create disabled gesture and false isDragging for overlay rendering
  const disabledGesture = useMemo(() => Gesture.Pan().enabled(false), []);
  const falseIsDragging = useSharedValue(false);

  if (overlayItems.length === 0) return null;

  const totalItems = totalItemsRef.current ?? data.length;

  return (
    <>
      {overlayItems.map(({ item, position }) => (
        <View
          key={`shifted-overlay-${item.id}`}
          style={{
            position: "absolute",
            top: position.y,
            left: 0,
            right: 0,
            height: position.height,
            zIndex: 999,
          }}
          pointerEvents="none"
        >
          {renderItem({
            item,
            index: data.findIndex((d) => d.id === item.id),
            totalItems,
            animatedStyle: {},
            dragHandleProps: {
              gesture: disabledGesture,
              isDragging: falseIsDragging,
            },
            isDragging: false,
            isDragEnabled: false,
            triggerExitAnimation: () => {},
            resetExitAnimation: () => {},
          })}
        </View>
      ))}
    </>
  );
}) as <T extends AnimatedListItem>(
  props: ShiftedItemOverlaysProps<T>,
) => React.ReactElement | null;

const DragContainer = React.memo(function DragContainer({
  children,
}: {
  children: React.ReactNode;
}) {
  const {
    overlayContainerX,
    overlayContainerY,
    overlayContainerYLayout,
    overlayContainerReady,
    dragContainerRef,
  } = useDragState();

  const handleLayout = useCallback(() => {
    const node = dragContainerRef.current;
    if (!node) return;
    (node as unknown as View).measureInWindow((x: number, y: number) => {
      overlayContainerX.value = x;
      overlayContainerY.value = y;
      overlayContainerYLayout.value = y; // Keep measureInWindow value for shifted overlay calculations
      overlayContainerReady.value = true;
    });
  }, [
    overlayContainerX,
    overlayContainerY,
    overlayContainerYLayout,
    overlayContainerReady,
    dragContainerRef,
  ]);

  return (
    <Animated.View
      ref={dragContainerRef}
      style={containerStyle}
      onLayout={handleLayout}
    >
      {children}
    </Animated.View>
  );
});

/**
 * Item wrapper component for FlashList
 * Uses ref for totalItems to avoid renderItem callback recreation
 */
interface ItemWrapperProps<T extends AnimatedListItem> {
  item: T;
  index: number;
  totalItemsRef: React.RefObject<number>;
  renderItem: AnimatedFlashListProps<T>["renderItem"];
  canDrag?: (item: T, index: number) => boolean;
  dragEnabled: boolean;
  onReorderByDelta?: (itemId: string, delta: number) => void;
  onHapticFeedback?: (type: HapticFeedbackType) => void;
}

const ItemWrapper = React.memo(function ItemWrapper<
  T extends AnimatedListItem,
>({
  item,
  index,
  totalItemsRef,
  renderItem,
  canDrag,
  dragEnabled,
  onReorderByDelta,
  onHapticFeedback,
}: ItemWrapperProps<T>) {
  const isDragEnabled = dragEnabled && (canDrag ? canDrag(item, index) : true);

  return (
    <AnimatedFlashListItem
      item={item}
      index={index}
      totalItems={totalItemsRef.current ?? 0}
      isDragEnabled={isDragEnabled}
      renderItem={renderItem}
      onReorderByDelta={onReorderByDelta}
      onHapticFeedback={onHapticFeedback}
    />
  );
}) as <T extends AnimatedListItem>(
  props: ItemWrapperProps<T>,
) => React.ReactElement;

/**
 * Inner FlashList component that uses DragStateContext for scroll tracking
 */
interface InnerFlashListProps<T extends AnimatedListItem> {
  data: T[];
  totalItemsRef: React.RefObject<number>;
  flashListRef: React.RefObject<FlashListRef<T> | null>;
  renderItem: AnimatedFlashListProps<T>["renderItem"];
  keyExtractor: (item: T, index: number) => string;
  canDrag?: (item: T, index: number) => boolean;
  dragEnabled: boolean;
  onReorderByDelta?: (itemId: string, delta: number) => void;
  onHapticFeedback?: (type: HapticFeedbackType) => void;
  itemHeight: number;
  ListFooterComponent?: AnimatedFlashListProps<T>["ListFooterComponent"];
  ListEmptyComponent?: FlashListProps<T>["ListEmptyComponent"];
  onEndReached?: () => void;
  onEndReachedThreshold?: number;
  onRefresh?: () => void | Promise<void>;
  refreshing?: boolean;
  refreshTintColor?: string;
  contentContainerStyle?: AnimatedFlashListProps<T>["contentContainerStyle"];
  drawDistance?: number;
  showsVerticalScrollIndicator?: boolean;
  extraData?: FlashListProps<T>["extraData"];
  onCommitLayoutEffect?: FlashListProps<T>["onCommitLayoutEffect"];
}

function InnerFlashList<T extends AnimatedListItem>({
  data,
  totalItemsRef,
  flashListRef,
  renderItem,
  keyExtractor,
  canDrag,
  dragEnabled,
  onReorderByDelta,
  onHapticFeedback,
  itemHeight,
  ListFooterComponent,
  ListEmptyComponent,
  onEndReached,
  onEndReachedThreshold,
  onRefresh,
  refreshing,
  refreshTintColor,
  contentContainerStyle,
  drawDistance = 500,
  showsVerticalScrollIndicator = true,
  extraData,
  onCommitLayoutEffect,
}: InnerFlashListProps<T>): React.ReactElement {
  // Get drag state context for scroll tracking
  const {
    scrollOffset,
    contentHeight,
    visibleHeight,
    listTopY,
    listLeftX,
    setListRef,
    totalItems,
    notifyLayoutCommit,
    isDragging,
    isDropping,
    itemIndexRegistry,
  } = useDragState();

  // v40.1: Disable scroll while dropping to prevent overlay drift
  // FlashList needs scrollEnabled as a regular boolean, not a SharedValue
  const [scrollEnabled, setScrollEnabled] = useState(true);
  const setScrollEnabledSafe = useCallback((enabled: boolean) => {
    setScrollEnabled(enabled);
  }, []);

  useAnimatedReaction(
    () => isDragging.value || isDropping.value,
    (draggingOrDropping, prevDraggingOrDropping) => {
      "worklet";
      if (draggingOrDropping !== prevDraggingOrDropping) {
        scheduleOnRN(setScrollEnabledSafe, !draggingOrDropping);
      }
    },
    [setScrollEnabledSafe],
  );

  // Callback ref that notifies DragStateContext when FlashList is available.
  // This fires immediately when FlashList sets the ref, unlike useEffect which
  // would run after render with flashListRef.current potentially still null.
  const handleFlashListRef = useCallback(
    (instance: FlashListRef<T> | null) => {
      // Update the ref object for other uses (scrollToOffset, prepareForLayoutAnimationRender, etc.)
      (flashListRef as React.MutableRefObject<FlashListRef<T> | null>).current =
        instance;
      // Notify DragStateContext - this triggers native layout observation setup
      setListRef(instance as FlashListRef<unknown> | null);
    },
    [flashListRef, setListRef],
  );

  // Sync totalItems SharedValue with data.length for worklet access
  useEffect(() => {
    totalItems.value = data.length;
  }, [data.length, totalItems]);

  // Prune stale entries from itemIndexRegistry when data changes.
  // Skip during active drag/drop to avoid corrupting indices mid-operation.
  useEffect(() => {
    if (isDragging.value || isDropping.value) return;

    const currentIds = new Set(data.map((item) => item.id));
    const registry = itemIndexRegistry.value;
    const staleIds = Object.keys(registry).filter((id) => !currentIds.has(id));

    if (staleIds.length > 0) {
      const pruned = { ...registry };
      for (const id of staleIds) {
        delete pruned[id];
      }
      itemIndexRegistry.value = pruned;
    }
  }, [data, itemIndexRegistry, isDragging, isDropping]);

  // Update scroll offset on scroll
  const handleScroll = useCallback(
    (event: NativeSyntheticEvent<NativeScrollEvent>) => {
      scrollOffset.value = event.nativeEvent.contentOffset.y;
    },
    [scrollOffset],
  );

  // Update content height when list content changes
  const handleContentSizeChange = useCallback(
    (_width: number, height: number) => {
      contentHeight.value = height;
    },
    [contentHeight],
  );

  // Update visible height and list position when layout changes
  const handleLayout = useCallback(
    (event: LayoutChangeEvent) => {
      visibleHeight.value = event.nativeEvent.layout.height;
      // Get native scroll ref and validate it has measureInWindow before using
      const nativeRef = flashListRef.current?.getNativeScrollRef?.();
      if (
        nativeRef &&
        typeof nativeRef === "object" &&
        "measureInWindow" in nativeRef &&
        typeof nativeRef.measureInWindow === "function"
      ) {
        nativeRef.measureInWindow((x: number, y: number) => {
          listTopY.value = y;
          listLeftX.value = x;
        });
      }
    },
    [visibleHeight, listTopY, listLeftX, flashListRef],
  );

  const handleCommitLayoutEffect = useCallback(() => {
    notifyLayoutCommit();
    onCommitLayoutEffect?.();
  }, [notifyLayoutCommit, onCommitLayoutEffect]);

  // Render item for FlashList
  const flashListRenderItem = useCallback(
    ({ item, index }: ListRenderItemInfo<T>) => (
      <ItemWrapper
        item={item}
        index={index}
        totalItemsRef={totalItemsRef}
        renderItem={renderItem}
        canDrag={canDrag}
        dragEnabled={dragEnabled}
        onReorderByDelta={onReorderByDelta}
        onHapticFeedback={onHapticFeedback}
      />
    ),
    [
      totalItemsRef,
      renderItem,
      canDrag,
      dragEnabled,
      onReorderByDelta,
      onHapticFeedback,
    ],
  );

  // getItemType for FlashList recycling optimization
  const getItemType = useCallback(() => "animated-item", []);

  // Override item layout for consistent drag calculations
  // Note: We cast the layout to include size for drag calculations
  const overrideItemLayout = useCallback(
    (layout: { span?: number }, _item: T, _index: number) => {
      // FlashList v2 uses this for span, but we extend for size in drag calculations
      (layout as { size?: number }).size = itemHeight;
    },
    [itemHeight],
  );

  return (
    <FlashList
      ref={handleFlashListRef}
      data={data}
      renderItem={flashListRenderItem}
      keyExtractor={keyExtractor}
      getItemType={getItemType}
      overrideItemLayout={overrideItemLayout}
      onScroll={handleScroll}
      onContentSizeChange={handleContentSizeChange}
      onLayout={handleLayout}
      scrollEventThrottle={16}
      drawDistance={drawDistance}
      maintainVisibleContentPosition={{ disabled: true }}
      showsVerticalScrollIndicator={showsVerticalScrollIndicator}
      onCommitLayoutEffect={handleCommitLayoutEffect}
      extraData={extraData}
      contentContainerStyle={contentContainerStyle}
      ListFooterComponent={ListFooterComponent ?? undefined}
      ListEmptyComponent={ListEmptyComponent ?? undefined}
      onEndReached={onEndReached}
      onEndReachedThreshold={onEndReachedThreshold}
      scrollEnabled={scrollEnabled}
      refreshControl={
        onRefresh ? (
          <RefreshControl
            refreshing={refreshing ?? false}
            onRefresh={onRefresh}
            tintColor={refreshTintColor}
            colors={refreshTintColor ? [refreshTintColor] : undefined}
          />
        ) : undefined
      }
    />
  );
}

/**
 * AnimatedFlashList - High-performance animated list with drag-to-reorder
 *
 * A wrapper around @shopify/flash-list that provides:
 * - Smooth drag-to-reorder with autoscroll
 * - Entry animations for new items
 * - Exit animations for removed items
 * - Full TypeScript generics support
 *
 * @example
 * ```tsx
 * <AnimatedFlashList<MyItem>
 *   data={items}
 *   keyExtractor={(item) => item.id}
 *   renderItem={({ item, animatedStyle, dragHandleProps }) => (
 *     <Animated.View style={animatedStyle}>
 *       <MyItem item={item} />
 *       {dragHandleProps && (
 *         <GestureDetector gesture={dragHandleProps.gesture}>
 *           <DragHandle />
 *         </GestureDetector>
 *       )}
 *     </Animated.View>
 *   )}
 *   dragEnabled
 *   onReorder={(itemId, from, to) => reorderItems(itemId, from, to)}
 * />
 * ```
 */
function AnimatedFlashListInner<T extends AnimatedListItem>(
  props: AnimatedFlashListProps<T>,
  ref: React.ForwardedRef<AnimatedFlashListRef>,
): React.ReactElement | null {
  const {
    data,
    keyExtractor,
    renderItem,
    dragEnabled = false,
    onReorder,
    onReorderByNeighbors,
    canDrag,
    onHapticFeedback,
    config,
    onPrepareLayoutAnimation,
    ListFooterComponent,
    ListEmptyComponent,
    onRefresh,
    refreshing = false,
    onEndReached,
    onEndReachedThreshold = 0.5,
    contentContainerStyle,
    ...flashListProps
  } = props;

  const {
    extraData: userExtraData,
    onCommitLayoutEffect: userOnCommitLayoutEffect,
    ...restFlashListProps
  } = flashListProps;

  // Merge config with defaults
  // Note: estimatedItemSize was removed in FlashList v2
  // Users should configure itemHeight via config.drag.itemHeight
  const dragConfig = useMemo(
    () => ({
      ...DEFAULT_DRAG_CONFIG,
      ...config?.drag,
    }),
    [config?.drag],
  );

  // Ref to FlashList
  const flashListRef = useRef<FlashListRef<T>>(null);

  // Expose methods to parent via ref
  useImperativeHandle(ref, () => ({
    prepareForLayoutAnimation: () => {
      flashListRef.current?.prepareForLayoutAnimationRender();
      onPrepareLayoutAnimation?.();
    },
    scrollToOffset: (offset: number, animated = true) => {
      flashListRef.current?.scrollToOffset({ offset, animated });
    },
    scrollToIndex: (index: number, animated = true) => {
      flashListRef.current?.scrollToIndex({ index, animated });
    },
  }));

  // Keep valid items in ref for reorder callback
  const dataRef = useRef<T[]>([]);
  dataRef.current = data;

  // Force FlashList to re-render visible cells after reorder to avoid
  // 1-frame gaps when internal recycling repositions without a React render.
  const [layoutVersion, setLayoutVersion] = useState(0);
  const bumpLayoutVersion = useCallback(() => {
    setLayoutVersion((version) => version + 1);
  }, []);

  // Handle reorder by index delta - converts to various callback formats
  // NOTE: No LayoutAnimation needed - layout commit compensation handles visual transitions
  const handleReorderByDelta = useCallback(
    (itemId: string, indexDelta: number) => {
      if (indexDelta === 0) return;

      const currentItems = dataRef.current;
      const currentIndex = currentItems.findIndex((item) => item.id === itemId);
      if (currentIndex === -1) return;

      const newIndex = Math.max(
        0,
        Math.min(currentItems.length - 1, currentIndex + indexDelta),
      );
      if (newIndex === currentIndex) return;

      // Call onReorder if provided
      if (onReorder) {
        onReorder(itemId, currentIndex, newIndex);
      }

      // Call onReorderByNeighbors if provided (for fractional indexing)
      if (onReorderByNeighbors) {
        let afterItemId: string | null = null;
        let beforeItemId: string | null = null;

        if (indexDelta > 0) {
          afterItemId = currentItems[newIndex]?.id ?? null;
          beforeItemId =
            newIndex < currentItems.length - 1
              ? (currentItems[newIndex + 1]?.id ?? null)
              : null;
        } else {
          afterItemId =
            newIndex > 0 ? (currentItems[newIndex - 1]?.id ?? null) : null;
          beforeItemId = currentItems[newIndex]?.id ?? null;
        }

        onReorderByNeighbors(itemId, afterItemId, beforeItemId);
      }

      bumpLayoutVersion();
    },
    [onReorder, onReorderByNeighbors, bumpLayoutVersion],
  );

  const combinedExtraData = useMemo(
    () => [userExtraData, layoutVersion],
    [userExtraData, layoutVersion],
  );

  // Use ref for totalItems to avoid renderItem callback recreation
  const totalItemsRef = useRef(data.length);
  totalItemsRef.current = data.length;

  // Early return for empty data
  if (!data || !Array.isArray(data) || data.length === 0) {
    const emptyContent = ListEmptyComponent
      ? React.isValidElement(ListEmptyComponent)
        ? ListEmptyComponent
        : React.createElement(ListEmptyComponent as React.ComponentType)
      : null;
    const footerContent = ListFooterComponent
      ? React.isValidElement(ListFooterComponent)
        ? ListFooterComponent
        : React.createElement(ListFooterComponent as React.ComponentType)
      : null;

    if (emptyContent || footerContent) {
      return (
        <View style={containerStyle}>
          {emptyContent}
          {footerContent}
        </View>
      );
    }
    return null;
  }

  const plainFlashListFallback = (
    <View style={containerStyle}>
      <FlashList
        ref={flashListRef as React.RefObject<FlashListRef<T>>}
        data={data}
        renderItem={({ item, index }) =>
          renderItem({
            item,
            index,
            totalItems: data.length,
            animatedStyle: {},
            dragHandleProps: null,
            isDragging: false,
            isDragEnabled: false,
            triggerExitAnimation: () => {},
            resetExitAnimation: () => {},
          })
        }
        keyExtractor={keyExtractor}
        contentContainerStyle={contentContainerStyle}
        ListFooterComponent={ListFooterComponent ?? undefined}
        onEndReached={onEndReached}
        onEndReachedThreshold={onEndReachedThreshold}
      />
    </View>
  );

  return (
    <AnimationErrorBoundary fallback={plainFlashListFallback}>
      <ListAnimationProvider>
        <DragStateProvider config={dragConfig}>
          <DragContainer>
            <InnerFlashList
              {...restFlashListProps}
              data={data}
              totalItemsRef={totalItemsRef}
              flashListRef={flashListRef}
              renderItem={renderItem}
              keyExtractor={keyExtractor}
              canDrag={canDrag}
              dragEnabled={dragEnabled}
              onReorderByDelta={
                onReorder || onReorderByNeighbors
                  ? handleReorderByDelta
                  : undefined
              }
              onHapticFeedback={onHapticFeedback}
              itemHeight={dragConfig.itemHeight}
              ListFooterComponent={ListFooterComponent}
              ListEmptyComponent={ListEmptyComponent}
              onEndReached={onEndReached}
              onEndReachedThreshold={onEndReachedThreshold}
              onRefresh={onRefresh}
              refreshing={refreshing}
              contentContainerStyle={contentContainerStyle}
              extraData={combinedExtraData}
              onCommitLayoutEffect={userOnCommitLayoutEffect}
            />
            <DragOverlay
              data={data}
              totalItemsRef={totalItemsRef}
              renderItem={renderItem}
            />
            <ShiftedItemOverlays
              data={data}
              totalItemsRef={totalItemsRef}
              renderItem={renderItem}
            />
          </DragContainer>
        </DragStateProvider>
      </ListAnimationProvider>
    </AnimationErrorBoundary>
  );
}

const containerStyle: ViewStyle = {
  flex: 1,
  overflow: 'hidden',
};

// Forward ref with generic support
export const AnimatedFlashList = forwardRef(AnimatedFlashListInner) as <
  T extends AnimatedListItem,
>(
  props: AnimatedFlashListProps<T> & {
    ref?: React.ForwardedRef<AnimatedFlashListRef>;
  },
) => React.ReactElement | null;
