import React, {useEffect} from 'react';
import {LayoutChangeEvent, View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import Animated, {
  cancelAnimation,
  runOnJS,
  useAnimatedReaction,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

import {scrubberStyles as styles} from './styles';

interface Props {
  currentTime: number;
  duration: number;
  /** Seconds of video downloaded ahead of the playhead. Drives the
   *  lighter `buffered` fill behind the solid progress fill. */
  bufferedPosition: number;
  /** Pause playback + enable scrubbing mode when the drag begins so the
   *  player doesn't fight the user's seeks. */
  onSeekStart?: () => void;
  /** Fires during the drag so the visible frame can track the thumb.
   *  Callers should throttle internally — this is dispatched on every
   *  gesture frame. */
  onSeekUpdate?: (time: number) => void;
  /** Final commit on drag end (or a tap on the track). */
  onSeek: (time: number) => void;
}

const clampRatio = (n: number): number => {
  'worklet';
  if (!Number.isFinite(n)) return 0;
  if (n < 0) return 0;
  if (n > 1) return 1;
  return n;
};

export function Scrubber({
  currentTime,
  duration,
  bufferedPosition,
  onSeekStart,
  onSeekUpdate,
  onSeek,
}: Props) {
  // Compute the initial ratio from the props handed in on mount so the
  // very first commit paints with the thumb already at the playhead.
  // The common trigger of this bug is the user exiting fullscreen: the
  // Scrubber re-mounts mid-playback, and if we initialized to 0 the
  // reaction-driven sync would animate from 0 → real position. Reading
  // the props at mount eliminates that frame entirely.
  const initialRatio = duration > 0 ? clampRatio(currentTime / duration) : 0;

  const trackWidth = useSharedValue(0);
  const isDragging = useSharedValue(false);
  const draggedRatio = useSharedValue(0);
  const playbackRatio = useSharedValue(initialRatio);
  // Mirror duration + currentTime as SharedValues so the
  // `useAnimatedReaction` worklet below — which owns the snap-vs-tween
  // decision — can read them on the UI thread. The previous version
  // ran the decision inside a `useEffect` on the JS thread; reading
  // `playbackRatio.value` from JS could be stale relative to the UI
  // runtime's actual value, which made the snap-on-rewind miss its
  // own condition and left the thumb parked at duration even after
  // `setCurrentTime(0)` propagated.
  const durationSV = useSharedValue(duration);
  const currentTimeSV = useSharedValue(currentTime);
  // First-sync flag also lives on UI thread so the reaction owns it.
  const hasSnappedSV = useSharedValue(duration > 0);

  useEffect(() => {
    durationSV.value = duration;
  }, [duration, durationSV]);
  useEffect(() => {
    currentTimeSV.value = currentTime;
  }, [currentTime, currentTimeSV]);

  // All "where should the thumb be?" decisions run here on the UI
  // thread, so every read of `playbackRatio.value` is by definition
  // current. The reaction fires whenever any of the tracked
  // SharedValues change.
  useAnimatedReaction(
    () => ({
      ct: currentTimeSV.value,
      dur: durationSV.value,
      dragging: isDragging.value,
    }),
    curr => {
      'worklet';
      // The gesture owns playbackRatio while the user is dragging.
      if (curr.dragging) return;
      // Duration not yet known — park at zero and reset the snap flag
      // so the first "duration > 0" frame snaps unconditionally.
      if (curr.dur === 0) {
        cancelAnimation(playbackRatio);
        playbackRatio.value = 0;
        hasSnappedSV.value = false;
        return;
      }
      const target = clampRatio(curr.ct / curr.dur);
      // First sync after we got a real duration → snap.
      if (!hasSnappedSV.value) {
        hasSnappedSV.value = true;
        cancelAnimation(playbackRatio);
        playbackRatio.value = target;
        return;
      }
      // Rewind to start (e.g. `playToEnd`) → hard snap. The thumb
      // must never animate from end back to start.
      if (target < 0.001) {
        cancelAnimation(playbackRatio);
        playbackRatio.value = 0;
        return;
      }
      // Large jump (fullscreen exit, scrub commit, source reload) →
      // snap rather than tween over 200 ms.
      if (Math.abs(target - playbackRatio.value) * curr.dur > 1) {
        cancelAnimation(playbackRatio);
        playbackRatio.value = target;
        return;
      }
      // Normal playback tick — smoothly bridge the 250 ms `timeUpdate`
      // interval so the thumb glides instead of stepping.
      playbackRatio.value = withTiming(target, {duration: 200});
    },
  );

  const onLayout = (e: LayoutChangeEvent): void => {
    trackWidth.value = e.nativeEvent.layout.width;
  };

  const commitSeek = (ratio: number): void => {
    if (!duration) return;
    onSeek(ratio * duration);
  };

  const notifySeekStart = (): void => {
    onSeekStart?.();
  };

  const liveSeek = (time: number): void => {
    onSeekUpdate?.(time);
  };

  const pan = Gesture.Pan()
    .minDistance(0)
    .onBegin(e => {
      isDragging.value = true;
      runOnJS(notifySeekStart)();
      const w = trackWidth.value;
      const r = clampRatio(w > 0 ? e.x / w : 0);
      draggedRatio.value = r;
      const d = durationSV.value;
      if (d > 0) runOnJS(liveSeek)(r * d);
    })
    .onUpdate(e => {
      const w = trackWidth.value;
      const r = clampRatio(w > 0 ? e.x / w : 0);
      draggedRatio.value = r;
      const d = durationSV.value;
      if (d > 0) runOnJS(liveSeek)(r * d);
    })
    .onEnd(() => {
      const r = draggedRatio.value;
      isDragging.value = false;
      playbackRatio.value = r;
      runOnJS(commitSeek)(r);
    })
    .onFinalize(() => {
      // Cancel cleanly if the gesture is interrupted (touch cancel,
      // navigation transition, etc.) — clears the drag-lock so the
      // animation tracks playback again.
      isDragging.value = false;
    });

  const progressStyle = useAnimatedStyle(() => {
    const r = isDragging.value ? draggedRatio.value : playbackRatio.value;
    return {width: `${r * 100}%`};
  });

  // Buffered fraction comes from the player's `bufferedPosition` (an
  // absolute timestamp up to which content is downloaded). Plain
  // per-render value — `bufferedPosition` doesn't change fast enough
  // to need worklet interpolation.
  const bufferedPct =
    duration > 0
      ? Math.min(100, Math.max(0, (bufferedPosition / duration) * 100))
      : 0;

  return (
    <GestureDetector gesture={pan}>
      <View style={styles.touchArea} onLayout={onLayout}>
        <View style={styles.track}>
          <View style={[styles.buffered, {width: `${bufferedPct}%`}]} />
          <Animated.View style={[styles.progress, progressStyle]} />
        </View>
      </View>
    </GestureDetector>
  );
}
