import {
  FlashList,
  type FlashListRef,
  type ListRenderItemInfo,
} from '@shopify/flash-list'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import {
  ActivityIndicator,
  Dimensions,
  type NativeScrollEvent,
  type NativeSyntheticEvent,
  RefreshControl,
} from 'react-native'
import Animated from 'react-native-reanimated'
import { EmptyState } from 'src/components/common/EmptyState'
import { ErrorState } from 'src/components/common/ErrorState'
import {
  type ScreenScrollChildProps,
  useAccentRefreshControlProps,
} from 'src/components/layout'
import { useMenuAnimatedScroll } from 'src/components/menu'
import FeedPost from 'src/components/post/FeedPost'
import { useVideo } from 'src/hooks/playback/PlaybackScope'
import type { LoginUserResponse, Status } from 'src/lib/api-types'
import { emit } from 'src/lib/tapestry/emit'
import { useTheme, View } from 'tamagui'

// Shared chronological-feed list. Owns FlashList + pagination + pull-to-refresh
// + scroll/padding hooks + video viewable handling. Consumers supply data,
// handlers, and empty state. Home, Explore/Federated are thin orchestrators
// over this. Like state is owned by FeedPost (useLikeMutation), so there is
// no onLike here by design.

export type TimelineHandlers = {
  onOpenComments: (id: string) => void
  onDeletePost?: (id: string) => void
  onShare?: (id: string) => void
}

type EmptyShape =
  | { icon?: string; title: string; subtitle?: string }
  | React.ReactElement
  | null

type Props = {
  data: Status[]
  user: LoginUserResponse
  handlers: TimelineHandlers
  isRefetching: boolean
  onRefresh: () => void
  hasNextPage?: boolean
  isFetchingNextPage?: boolean
  onEndReached?: () => void
  isError?: boolean
  errorMessage?: string
  empty?: EmptyShape
  listHeader?: React.ReactElement | null
  listRef?: React.Ref<FlashListRef<Status>>
  getItemType?: (item: Status) => string | undefined
  // Rendered after the last item once the list is fully paged in. Skipped
  // while the next page is loading or the list is still empty.
  endOfFeed?: React.ReactElement | null
  // Screen-shell props: contentInset, scrollIndicatorInsets, and padding
  // handed down from ScreenScrollContainer. Optional so this list can still
  // be used outside that wrapper (tests, legacy callers).
  scrollInsets?: ScreenScrollChildProps
}

// Raise from 50 → 60 so minor scroll jitter doesn't recompute the preload
// window on every frame the user drifts across a half-visible post.
const VIEW_CONFIG = { viewAreaCoveragePercentThreshold: 60 }

const keyExtractor = (item: Status) => item?.id || ''

// 1 keeps two videos warm around the visible one. Larger radii stall the
// Android codec pool and thrash the expo-video player cache without
// meaningful benefit on a chronological feed where the user rarely flips
// direction quickly.
const VIDEO_PRELOAD_RADIUS = 1

// FlashList v2 uses an adaptive drawDistance that factors velocity, but on a
// media feed with cells spanning 30-50% of the viewport, the default
// under-draws on fast flicks. Two screens of pre-render keeps upcoming cells
// warm without ballooning memory.
const SCREEN_HEIGHT = Dimensions.get('window').height
const DRAW_DISTANCE = SCREEN_HEIGHT * 2

// Wrap FlashList once at the module level so our animated scroll handler runs
// on the UI thread. Without this wrapper, Reanimated handlers silently fall
// back to JS-thread dispatch. Cast back to FlashList so consumers keep the
// original prop/ref typings — createAnimatedComponent's generic is written for
// FlatList-shaped components.
const AnimatedFlashList = Animated.createAnimatedComponent(
  FlashList as any
) as unknown as typeof FlashList

export function TimelineList({
  data,
  user,
  handlers,
  isRefetching,
  onRefresh,
  hasNextPage,
  isFetchingNextPage,
  onEndReached,
  isError,
  errorMessage,
  empty,
  listHeader,
  listRef,
  getItemType,
  endOfFeed,
  scrollInsets,
}: Props) {
  const theme = useTheme()
  const { playVideo, currentVideoId, setPreloadWindow } = useVideo()
  const onScroll = useMenuAnimatedScroll(true)
  const refreshProps = useAccentRefreshControlProps()

  // Tapestry scroll-segment tracker — emits one `s` letter per screen-height
  // of accumulated scroll distance. Lives on `onScrollEndDrag` /
  // `onMomentumScrollEnd` so the per-frame `onScroll` worklet stays
  // untouched; finger-rest fire matches the grammar's intent (one letter
  // per "I just read a chunk of feed") without firing on every velocity tick.
  const lastEmitYRef = useRef(0)
  const handleScrollSettle = useCallback((e: NativeSyntheticEvent<NativeScrollEvent>) => {
    const y = e.nativeEvent.contentOffset.y
    if (Math.abs(y - lastEmitYRef.current) >= SCREEN_HEIGHT) {
      lastEmitYRef.current = y
      emit('s')
    }
  }, [])

  useEffect(() => {
    return () => setPreloadWindow(null)
  }, [setPreloadWindow])

  // Read handlers through a ref so renderItem's identity stays stable across
  // feed mutations. Without this, the handlers object literal from the parent
  // rebuilds renderItem every render, FlashList invalidates cached cells, and
  // every memo'd child of FeedPost sees a fresh callback prop.
  const handlersRef = useRef(handlers)
  handlersRef.current = handlers
  const stableOnOpenComments = useCallback(
    (id: string) => handlersRef.current.onOpenComments(id),
    []
  )
  const stableOnDeletePost = useCallback(
    (id: string) => handlersRef.current.onDeletePost?.(id),
    []
  )
  const stableOnShare = useCallback((id: string) => handlersRef.current.onShare?.(id), [])

  const renderItem = useCallback(
    ({ item }: ListRenderItemInfo<Status>) => {
      if (!item?.id) return null
      return (
        <FeedPost
          post={item}
          user={user}
          onOpenComments={stableOnOpenComments}
          onDeletePost={stableOnDeletePost}
          onShare={stableOnShare}
        />
      )
    },
    [user, stableOnOpenComments, stableOnDeletePost, stableOnShare]
  )

  // Pagination append rebuilds `data` and, without a ref, would force FlashList
  // to rebind its viewability observer on every page. Keep the callback
  // identity stable by reading data through a ref instead.
  const dataRef = useRef(data)
  dataRef.current = data
  const currentVideoIdRef = useRef(currentVideoId)
  currentVideoIdRef.current = currentVideoId

  const onViewableItemsChanged = useCallback(
    ({ viewableItems }: any) => {
      const currentId = currentVideoIdRef.current
      if (!viewableItems?.length) {
        setPreloadWindow(null)
        if (currentId) playVideo(null)
        return
      }
      let minIdx = Number.POSITIVE_INFINITY
      let maxIdx = Number.NEGATIVE_INFINITY
      let currentStillVisible = false
      for (const v of viewableItems) {
        if (!v?.isViewable || typeof v.index !== 'number') continue
        if (v.index < minIdx) minIdx = v.index
        if (v.index > maxIdx) maxIdx = v.index
        if (v.item?.id && v.item.id === currentId) currentStillVisible = true
      }
      if (minIdx === Number.POSITIVE_INFINITY) {
        setPreloadWindow(null)
        if (currentId) playVideo(null)
        return
      }
      const current = dataRef.current
      const lo = Math.max(0, minIdx - VIDEO_PRELOAD_RADIUS)
      const hi = Math.min(current.length - 1, maxIdx + VIDEO_PRELOAD_RADIUS)
      const ids: string[] = []
      for (let i = lo; i <= hi; i++) {
        const id = current[i]?.id
        if (id) ids.push(id)
      }
      setPreloadWindow(ids)
      // If the user scrolled the currently-playing video out of view, pause.
      if (currentId && !currentStillVisible) playVideo(null)
    },
    [playVideo, setPreloadWindow]
  )

  const handleEndReached = useCallback(() => {
    if (hasNextPage && !isFetchingNextPage) onEndReached?.()
  }, [hasNextPage, isFetchingNextPage, onEndReached])

  const ListEmptyComponent = useMemo(() => {
    if (empty == null) return null
    if (React.isValidElement(empty)) return empty
    const e = empty as { icon?: string; title: string; subtitle?: string }
    return <EmptyState icon={e.icon} title={e.title} subtitle={e.subtitle} />
  }, [empty])

  const ListFooterComponent = useMemo(() => {
    if (isFetchingNextPage) {
      return (
        <View py="$6" justifyContent="center" alignItems="center">
          <ActivityIndicator color={theme.color.val} />
        </View>
      )
    }
    if (endOfFeed && data.length > 0 && !hasNextPage) return endOfFeed
    return null
  }, [isFetchingNextPage, theme.color.val, endOfFeed, data.length, hasNextPage])

  if (isError) {
    return (
      <ErrorState
        message={
          errorMessage ??
          'The service may be temporarily unavailable, please try again later.'
        }
      />
    )
  }

  return (
    <AnimatedFlashList
      ref={listRef}
      data={data}
      keyExtractor={keyExtractor}
      renderItem={renderItem}
      getItemType={getItemType}
      drawDistance={DRAW_DISTANCE}
      ListHeaderComponent={listHeader ?? undefined}
      refreshControl={
        <RefreshControl
          refreshing={isRefetching}
          onRefresh={onRefresh}
          progressViewOffset={scrollInsets?.topInset}
          {...refreshProps}
        />
      }
      showsVerticalScrollIndicator={false}
      ListEmptyComponent={ListEmptyComponent}
      onViewableItemsChanged={onViewableItemsChanged}
      viewabilityConfig={VIEW_CONFIG}
      onEndReached={handleEndReached}
      onEndReachedThreshold={0.5}
      ListFooterComponent={ListFooterComponent}
      onScroll={onScroll}
      onScrollEndDrag={handleScrollSettle}
      onMomentumScrollEnd={handleScrollSettle}
      contentInsetAdjustmentBehavior={scrollInsets?.contentInsetAdjustmentBehavior}
      contentInset={scrollInsets?.contentInset}
      scrollIndicatorInsets={scrollInsets?.scrollIndicatorInsets}
      contentContainerStyle={scrollInsets?.contentContainerStyle}
    />
  )
}
