import Feather from '@expo/vector-icons/Feather'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEvent } from 'expo'
import { Image } from 'expo-image'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { type VideoSource, VideoView } from 'expo-video'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ActivityIndicator, Pressable, StatusBar, StyleSheet } from 'react-native'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, {
  cancelAnimation,
  Easing,
  runOnJS,
  type SharedValue,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import UserAvatar from 'src/components/common/UserAvatar'
import { listScopeKeys, useCacheMutation } from 'src/hooks/mutations/cacheMutation'
import { PlaybackScope, useVideo } from 'src/hooks/playback/PlaybackScope'
import { useScopedPlayer } from 'src/hooks/playback/useScopedPlayer'
import { useRequirePixelfed } from 'src/hooks/useRequirePixelfed'
import { postStoryViewed } from 'src/lib/api'
import { storyQueries } from 'src/lib/queries/stories'
import { normalizeCarousel, type StoryGroup, type StoryNode } from 'src/lib/story-types'
import { useActiveProfile } from 'src/state/AuthProvider'
import { timeAgo } from 'src/utils'
import { Text, View, XStack, YStack } from 'tamagui'

const DEFAULT_PHOTO_DURATION_S = 5

function nodeSrc(n: StoryNode): string {
  const raw = n as unknown as Record<string, unknown>
  return String(raw.src ?? raw.url ?? '')
}

function nodeType(n: StoryNode): 'photo' | 'video' {
  return n.type === 'video' ? 'video' : 'photo'
}

function nodeDuration(n: StoryNode): number {
  if (typeof n.duration === 'number' && n.duration > 0) return n.duration
  return DEFAULT_PHOTO_DURATION_S
}

export default function StoryViewerRoute() {
  // Mastodon Pass Phase 4 §5.3 — Stories are Pixelfed-only. The pixelfed
  // gate runs in the inner component (after the scope mounts) so the
  // hooks order stays stable on early-return.
  return (
    <PlaybackScope name="story">
      <StoryViewer />
    </PlaybackScope>
  )
}

function StoryViewer() {
  useRequirePixelfed()
  const { pid } = useLocalSearchParams<{ pid: string }>()
  const router = useRouter()
  const { profile: user } = useActiveProfile()
  const queryClient = useQueryClient()
  const insets = useSafeAreaInsets()
  // fullScreenModal presentation ignores the system status bar by default on
  // iOS, which puts the progress bars under the dynamic island. Pad by the
  // top inset (min 12) and reserve the home-indicator region on the bottom.
  const topPad = Math.max(insets.top, 12)
  const bottomPad = Math.max(insets.bottom, 8)

  const { data, isLoading } = useQuery(storyQueries.carousel())
  const carousel = useMemo(() => normalizeCarousel(data), [data])

  const group: StoryGroup | null = useMemo(() => {
    if (!pid) return null
    if (user?.id && String(user.id) === String(pid)) {
      return {
        pid: String(user.id),
        username: user.username,
        avatar: user.avatar,
        nodes: carousel.self,
        seen: false,
      }
    }
    return carousel.nodes.find((g) => String(g.pid) === String(pid)) ?? null
  }, [pid, carousel, user])

  const nodes = group?.nodes ?? []
  const firstUnseenIndex = Math.max(
    0,
    nodes.findIndex((n) => !(n.seen || (n as any).viewed))
  )
  const [nodeIndex, setNodeIndex] = useState(firstUnseenIndex)
  const [paused, setPaused] = useState(false)
  const progress = useSharedValue(0)
  const closeTY = useSharedValue(0)

  // Reset to the right start index once carousel loads.
  useEffect(() => {
    if (nodes.length > 0) setNodeIndex(firstUnseenIndex)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [group?.pid, nodes.length])

  const currentNode = nodes[nodeIndex]
  const isVideo = currentNode ? nodeType(currentNode) === 'video' : false
  const src = currentNode ? nodeSrc(currentNode) : ''
  // P3a — stories are ephemeral by design and re-watches inside a single
  // session ride the in-memory `useVideoPlayer` buffer; persistent disk cache
  // would just keep already-expired story media on the device.
  const videoSource: VideoSource | null = useMemo(
    () => (isVideo && src ? { uri: src, useCaching: false } : null),
    [isVideo, src]
  )

  const segmentVideoId = currentNode ? `story:${currentNode.id}` : 'story:none'
  const { playVideo } = useVideo()
  const { player } = useScopedPlayer({
    source: videoSource,
    videoId: segmentVideoId,
    mode: 'story',
    audible: true,
  })

  // Claim the audio slot for the active video segment so the home feed
  // (or any other scope holding it) yields and pauses. Releases the slot
  // for photo segments — they're not videos, but expo-audio still has
  // category state pinned globally so silent doesn't equal yielded.
  useEffect(() => {
    if (isVideo && currentNode) playVideo(segmentVideoId)
    else playVideo(null)
  }, [isVideo, currentNode, segmentVideoId, playVideo])

  // Optimistically marks the node as seen in the carousel cache so the story
  // ring updates immediately (and stays quiet if the request fails — the
  // factory restores the prior cache on error).
  const markSeen = useCacheMutation<unknown, string>({
    mutationFn: (sid) => postStoryViewed({ sid }),
    snapshotKeys: () => listScopeKeys(storyQueries.carousel().queryKey),
    applyOptimistic: (qc, sid) => {
      qc.setQueryData<any>(storyQueries.carousel().queryKey, (prev: any) => {
        if (!prev) return prev
        const markInNodes = (nodes: any[] | undefined) =>
          Array.isArray(nodes)
            ? nodes.map((n) =>
                String(n?.id) === String(sid) ? { ...n, seen: true, viewed: true } : n
              )
            : nodes
        // The carousel shape has varied across server versions; cover the two
        // common layouts without asserting on their exact types here.
        const next: any = { ...prev }
        if (Array.isArray(prev.self)) next.self = markInNodes(prev.self)
        if (Array.isArray(prev.nodes)) {
          next.nodes = prev.nodes.map((g: any) => ({
            ...g,
            nodes: markInNodes(g.nodes),
          }))
        }
        if (Array.isArray(prev.data)) {
          next.data = prev.data.map((g: any) => ({
            ...g,
            nodes: markInNodes(g.nodes),
          }))
        }
        return next
      })
    },
  })

  const seenRef = useRef<Set<string>>(new Set())
  useEffect(() => {
    if (!currentNode) return
    const id = String(currentNode.id)
    if (seenRef.current.has(id)) return
    seenRef.current.add(id)
    markSeen.mutate(id)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [currentNode?.id])

  const handleClose = useCallback(() => {
    queryClient.invalidateQueries({ queryKey: storyQueries.carousel().queryKey })
    router.back()
  }, [queryClient, router])

  const handleNext = useCallback(() => {
    if (nodeIndex < nodes.length - 1) {
      setNodeIndex((i) => i + 1)
    } else {
      handleClose()
    }
  }, [nodeIndex, nodes.length, handleClose])

  const handlePrev = useCallback(() => {
    if (nodeIndex > 0) setNodeIndex((i) => i - 1)
    else progress.value = 0
  }, [nodeIndex, progress])

  // Drive progress. For photos: time-based. For videos: the timeUpdate event
  // moves the bar so duration reflects the actual clip.
  const durationS = currentNode ? nodeDuration(currentNode) : DEFAULT_PHOTO_DURATION_S

  useEffect(() => {
    cancelAnimation(progress)
    progress.value = 0
    if (!currentNode) return
    if (isVideo) return // video event loop drives the bar
    if (paused) return
    progress.value = withTiming(
      1,
      { duration: durationS * 1000, easing: Easing.linear },
      (finished) => {
        if (finished) runOnJS(handleNext)()
      }
    )
    return () => cancelAnimation(progress)
  }, [currentNode?.id, isVideo, paused, durationS, handleNext, progress])

  // Video playback lifecycle. Source recycling is handled by
  // useScopedPlayer (`replaceAsync` on URI change); only the long-press
  // pause/resume drives play state from this layer.
  useEffect(() => {
    if (!isVideo) return
    if (paused) player.pause()
    else player.play()
  }, [paused, isVideo, player])

  const { currentTime } = useEvent(player, 'timeUpdate', {
    currentTime: 0,
    currentLiveTimestamp: null,
    currentOffsetFromLive: 0,
    bufferedPosition: 0,
  })

  useEffect(() => {
    if (!isVideo) return
    const total = player.duration || durationS
    if (total <= 0) return
    const ratio = Math.min(1, Math.max(0, currentTime / total))
    progress.value = ratio
    if (!paused && ratio >= 0.999) handleNext()
  }, [currentTime, isVideo, durationS, paused, progress, handleNext, player.duration])

  // Gestures. Tap splits by horizontal position; long-press pauses; vertical
  // pan closes if dragged down far enough.
  const tap = Gesture.Tap()
    .maxDuration(250)
    .onEnd((e) => {
      const x = e.x
      const w = e.absoluteX + (e.x - e.absoluteX)
      const third = w / 3
      if (x < third) runOnJS(handlePrev)()
      else runOnJS(handleNext)()
    })

  const longPress = Gesture.LongPress()
    .minDuration(180)
    .onStart(() => {
      runOnJS(setPaused)(true)
    })
    .onFinalize(() => {
      runOnJS(setPaused)(false)
    })

  const pan = Gesture.Pan()
    .activeOffsetY(15)
    .failOffsetX([-25, 25])
    .onUpdate((e) => {
      if (e.translationY > 0) closeTY.value = e.translationY
    })
    .onEnd((e) => {
      if (e.translationY > 100) {
        runOnJS(handleClose)()
      } else {
        closeTY.value = withTiming(0, { duration: 180 })
      }
    })

  const composed = Gesture.Race(longPress, pan, tap)

  const closeStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: closeTY.value }],
    opacity: Math.max(0, 1 - closeTY.value / 400),
  }))

  if (isLoading || !group) {
    return (
      <View flex={1} backgroundColor="#000" paddingTop={topPad} paddingBottom={bottomPad}>
        <StatusBar barStyle="light-content" />
        <YStack flex={1} alignItems="center" justifyContent="center">
          <ActivityIndicator color="white" />
        </YStack>
      </View>
    )
  }

  if (!currentNode) {
    return (
      <View flex={1} backgroundColor="#000" paddingTop={topPad} paddingBottom={bottomPad}>
        <StatusBar barStyle="light-content" />
        <YStack flex={1} alignItems="center" justifyContent="center" gap="$3">
          <Feather name="cloud-off" size={42} color="white" />
          <Text color="white">No active stories.</Text>
          <Pressable onPress={handleClose}>
            <Text color="white" opacity={0.8}>
              Tap to close
            </Text>
          </Pressable>
        </YStack>
      </View>
    )
  }

  return (
    <View flex={1} backgroundColor="#000">
      <StatusBar barStyle="light-content" />
      <GestureDetector gesture={composed}>
        <Animated.View
          style={[
            StyleSheet.absoluteFill,
            { paddingTop: topPad, paddingBottom: bottomPad },
            closeStyle,
          ]}
        >
          <View flex={1}>
            <YStack px="$3" pt="$2" gap="$2">
              <XStack gap={4}>
                {nodes.map((_, i) => (
                  <ProgressSegment
                    key={nodes[i].id}
                    isActive={i === nodeIndex}
                    isDone={i < nodeIndex}
                    progress={progress}
                  />
                ))}
              </XStack>
              <XStack alignItems="center" gap="$3" py="$1">
                <UserAvatar url={group.avatar} initial={group.username} size="sm" />
                <YStack flex={1}>
                  <Text color="white" fontWeight="700" fontSize="$4">
                    {group.username ?? 'You'}
                  </Text>
                  {currentNode.created_at ? (
                    <Text color="white" opacity={0.7} fontSize="$2">
                      {timeAgo(currentNode.created_at)} ago
                    </Text>
                  ) : null}
                </YStack>
                <Pressable onPress={handleClose} hitSlop={12}>
                  <Feather name="x" size={24} color="white" />
                </Pressable>
              </XStack>
            </YStack>

            <View flex={1} justifyContent="center" alignItems="center">
              {isVideo && videoSource ? (
                <VideoView
                  player={player}
                  style={{ width: '100%', height: '100%' }}
                  contentFit="contain"
                  nativeControls={false}
                />
              ) : (
                <Image
                  source={{ uri: src }}
                  style={{ width: '100%', height: '100%' }}
                  contentFit="contain"
                  cachePolicy="memory-disk"
                />
              )}
            </View>

            <XStack
              px="$3"
              pb="$2"
              pt="$3"
              gap="$2"
              alignItems="center"
              justifyContent="flex-end"
            >
              {user?.id && String(user.id) === String(group.pid) ? (
                <Pressable
                  onPress={() =>
                    router.push({
                      pathname: '/story/viewers/[id]',
                      params: { id: String(currentNode.id) },
                    })
                  }
                  hitSlop={10}
                >
                  <XStack alignItems="center" gap="$2" opacity={0.85}>
                    <Feather name="eye" size={16} color="white" />
                    <Text color="white" fontSize="$3">
                      Viewers
                    </Text>
                  </XStack>
                </Pressable>
              ) : null}
            </XStack>
          </View>
        </Animated.View>
      </GestureDetector>
    </View>
  )
}

function ProgressSegment({
  isActive,
  isDone,
  progress,
}: {
  isActive: boolean
  isDone: boolean
  progress: SharedValue<number>
}) {
  const style = useAnimatedStyle(() => {
    const value = isDone ? 1 : isActive ? progress.value : 0
    return { width: `${value * 100}%` }
  })
  return (
    <View
      flex={1}
      height={2.5}
      borderRadius={2}
      backgroundColor="rgba(255,255,255,0.35)"
      overflow="hidden"
    >
      <Animated.View style={[{ height: '100%', backgroundColor: 'white' }, style]} />
    </View>
  )
}
