import { useCallback, useEffect, useRef, useState } from 'react';
import {
  Dimensions,
  KeyboardAvoidingView,
  Modal,
  Platform,
  Pressable,
  View,
} from 'react-native';
import {
  initialWindowMetrics,
  SafeAreaProvider,
  SafeAreaView,
} from 'react-native-safe-area-context';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  Easing,
  Extrapolation,
  interpolate,
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from 'react-native-reanimated';

const SCREEN_HEIGHT = Dimensions.get('window').height;
const ANIMATION_MS = 240;

interface BottomSheetProps {
  open: boolean;
  onClose: () => void;
  /**
   * Fires after the close animation completes and the underlying Modal has
   * unmounted. Use this to chain a follow-up sheet/Alert without stacking
   * iOS Modals — presenting a second Modal while another is still mounted
   * silently fails on iOS and leaves the app looking frozen.
   */
  onClosed?: () => void;
  /** When true, wrap content in KeyboardAvoidingView so the sheet floats above the keyboard. */
  avoidKeyboard?: boolean;
  /**
   * Cap on the sheet's height. Defaults to 85%; pass a string like "60%" or
   * a number of px when content has a stable footprint.
   */
  maxHeight?: number | `${number}%`;
  children: React.ReactNode;
}

/**
 * Shared modal sheet shell. Handles slide-in animation, backdrop fade,
 * drag-to-dismiss, and modal-safe SafeAreaProvider seeding so iOS modals get
 * correct insets on the first frame. The drag handle at the top is rendered
 * here too, so callers don't need to draw it themselves.
 */
export function BottomSheet({
  open,
  onClose,
  onClosed,
  avoidKeyboard = false,
  maxHeight = '85%',
  children,
}: BottomSheetProps) {
  // Mount slightly past `open` so the slide-in animation has its starting
  // position rendered, and the slide-out animation can play before unmount.
  const [mounted, setMounted] = useState(false);
  const translateY = useSharedValue(SCREEN_HEIGHT);

  // Mount as soon as we open; the close path unmounts after the exit animation.
  if (open && !mounted) setMounted(true);

  // Latest onClosed in a ref so the worklet→JS bridge always invokes the
  // current callback even if the parent re-rendered with a new closure.
  const onClosedRef = useRef(onClosed);
  useEffect(() => {
    onClosedRef.current = onClosed;
  }, [onClosed]);

  const handleClosed = useCallback(() => {
    setMounted(false);
    onClosedRef.current?.();
  }, []);

  useEffect(() => {
    if (open) {
      requestAnimationFrame(() => {
        translateY.value = withSpring(0, {
          damping: 24,
          stiffness: 260,
          mass: 0.7,
        });
      });
    } else if (mounted) {
      translateY.value = withTiming(
        SCREEN_HEIGHT,
        { duration: ANIMATION_MS, easing: Easing.in(Easing.cubic) },
        (finished) => {
          if (finished) runOnJS(handleClosed)();
        },
      );
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open]);

  const sheetPan = Gesture.Pan()
    .activeOffsetY(10)
    .failOffsetX([-25, 25])
    .onUpdate((e) => {
      'worklet';
      // Reanimated shared values are mutated by design; react-hooks/immutability
      // doesn't model worklets, so the mutations below are flagged spuriously.
      // eslint-disable-next-line react-hooks/immutability
      translateY.value = Math.max(0, e.translationY);
    })
    .onEnd((e) => {
      'worklet';
      if (e.translationY > 120 || e.velocityY > 800) {
        runOnJS(onClose)();
      } else {
        // eslint-disable-next-line react-hooks/immutability
        translateY.value = withSpring(0, {
          damping: 24,
          stiffness: 260,
          mass: 0.7,
        });
      }
    });

  const sheetStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: translateY.value }],
  }));

  const backdropStyle = useAnimatedStyle(() => {
    const opacity = interpolate(
      translateY.value,
      [0, SCREEN_HEIGHT * 0.7],
      [0.55, 0],
      Extrapolation.CLAMP,
    );
    return { opacity };
  });

  if (!mounted) return null;

  const Wrapper = avoidKeyboard ? KeyboardAvoidingView : View;
  const wrapperProps = avoidKeyboard
    ? { behavior: Platform.OS === 'ios' ? ('padding' as const) : undefined }
    : {};

  return (
    <Modal
      visible={mounted}
      transparent
      animationType="none"
      onRequestClose={onClose}
    >
      <SafeAreaProvider initialMetrics={initialWindowMetrics}>
        <View style={{ flex: 1 }}>
          <Animated.View
            pointerEvents={open ? 'auto' : 'none'}
            style={[
              { position: 'absolute', inset: 0, backgroundColor: 'black' },
              backdropStyle,
            ]}
          >
            <Pressable style={{ flex: 1 }} onPress={onClose} />
          </Animated.View>

          <Wrapper
            {...wrapperProps}
            style={{ flex: 1, justifyContent: 'flex-end' }}
            pointerEvents="box-none"
          >
            <GestureDetector gesture={sheetPan}>
              <Animated.View
                style={[
                  {
                    backgroundColor: '#1c1c1c',
                    borderTopLeftRadius: 22,
                    borderTopRightRadius: 22,
                    overflow: 'hidden',
                    maxHeight,
                  },
                  sheetStyle,
                ]}
              >
                <SafeAreaView edges={['bottom']}>
                  <View className="px-5 pt-3 items-center">
                    <View className="bg-white/25 h-1 w-12 rounded-full mb-3" />
                  </View>
                  {children}
                </SafeAreaView>
              </Animated.View>
            </GestureDetector>
          </Wrapper>
        </View>
      </SafeAreaProvider>
    </Modal>
  );
}
