import type {DmMessage} from '@crumbs/core/schemas'
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react'
import {
  FlatList,
  type NativeScrollEvent,
  type NativeSyntheticEvent,
  type ScrollViewProps,
} from 'react-native'
import {KeyboardChatScrollView} from 'react-native-keyboard-controller'
import type {SharedValue} from 'react-native-reanimated'

import {MessageBubble} from '@/components/dm/message-bubble'
import {EmptyState} from '@/components/empty-state'
import {LoadingState} from '@/components/loading-state'
import {atoms as a, tokens} from '@/sprinkles'

// scroll past this much slack and new messages stop pulling the view down
const AT_BOTTOM_SLACK = 100

// chat list mirroring bluesky's MessagesList. deliberately NOT inverted:
// an inverted list rests at native contentOffset 0, which ios sheets read
// as "at top" — a fast downward pan then drags the formSheet instead of
// scrolling. ascending order + maintainVisibleContentPosition (stable
// while older pages prepend) + manual scroll-to-end keeps the resting
// offset at the content end, so the sheet only pans from real top.
// KeyboardChatScrollView lifts content with the keyboard and inputHeight
// (the sticky composer's measured height) extends the scrollable range
export const MessageList = ({
  messages,
  myUserId,
  inputHeight,
  listRef,
  loading,
  loadingMore,
  onStartReached,
}: {
  messages: DmMessage[]
  myUserId?: string
  inputHeight: SharedValue<number>
  listRef: React.RefObject<FlatList<DmMessage> | null>
  loading?: boolean
  loadingMore?: boolean
  onStartReached?: () => void
}) => {
  // the store holds newest first; render oldest → newest, top → bottom
  const ascending = useMemo(() => [...messages].reverse(), [messages])

  const isAtBottom = useRef(true)
  const hasInitiallyScrolled = useRef(false)
  // the list starts anchored at the top; hide it until the initial
  // scroll-to-end lands so there's no flash of oldest-first content
  const [revealed, setRevealed] = useState(false)
  const prevContentHeight = useRef(0)
  const prevItemCount = useRef(0)

  const onScroll = useCallback(
    (e: NativeSyntheticEvent<NativeScrollEvent>) => {
      const {contentOffset, layoutMeasurement, contentSize} =
        e.nativeEvent
      isAtBottom.current =
        contentSize.height - AT_BOTTOM_SLACK <
        contentOffset.y + layoutMeasurement.height
    },
    [],
  )

  // follow appended messages when the reader is at the bottom. keyed on
  // the tail id, not onContentSizeChange: with
  // maintainVisibleContentPosition anchored to item 0, appending below
  // the viewport reports no content-size change on native, so that
  // callback never fires for new messages (bluesky APP-2223). prepended
  // history doesn't move the tail, so it never triggers this
  const lastMessageId = ascending.at(-1)?.id
  useEffect(() => {
    if (!hasInitiallyScrolled.current) return
    if (!isAtBottom.current) return
    listRef.current?.scrollToOffset({
      offset: Number.MAX_SAFE_INTEGER,
      animated: false,
    })
  }, [lastMessageId, listRef])

  // initial render scrolls to the end unanimated; afterwards content
  // growth only follows to the end when the reader is already there —
  // never when older history prepends (maintainVisibleContentPosition
  // holds the view still for that case)
  const onContentSizeChange = useCallback(
    (_width: number, height: number) => {
      if (!hasInitiallyScrolled.current) {
        if (ascending.length === 0) return
        hasInitiallyScrolled.current = true
        listRef.current?.scrollToOffset({
          offset: height,
          animated: false,
        })
        prevContentHeight.current = height
        prevItemCount.current = ascending.length
        requestAnimationFrame(() => setRevealed(true))
        return
      }

      if (isAtBottom.current) {
        listRef.current?.scrollToOffset({
          offset: height,
          animated:
            height > prevContentHeight.current &&
            ascending.length > prevItemCount.current,
        })
      }
      prevContentHeight.current = height
      prevItemCount.current = ascending.length
    },
    [ascending.length, listRef],
  )

  const renderScrollComponent = useCallback(
    (props: ScrollViewProps) => (
      <KeyboardChatScrollView
        {...props}
        extraContentPadding={inputHeight}
        keyboardLiftBehavior="always"
        keyboardDismissMode="interactive"
        keyboardShouldPersistTaps="handled"
      />
    ),
    [inputHeight],
  )

  return (
    <FlatList<DmMessage>
      ref={listRef}
      data={ascending}
      keyExtractor={messageId}
      style={[
        a.flex_1,
        !revealed && ascending.length > 0 && {opacity: 0},
      ]}
      contentContainerStyle={[
        a.px_2xl,
        a.py_lg,
        {gap: tokens.space.xs},
      ]}
      renderScrollComponent={renderScrollComponent}
      maintainVisibleContentPosition={{minIndexForVisible: 0}}
      onScroll={onScroll}
      scrollEventThrottle={100}
      onContentSizeChange={onContentSizeChange}
      renderItem={({item}) => (
        <MessageBubble
          message={item}
          mine={item.senderId === myUserId}
        />
      )}
      ListEmptyComponent={
        loading ? (
          <LoadingState />
        ) : (
          <EmptyState message="No messages yet" />
        )
      }
      ListHeaderComponent={loadingMore ? <LoadingState /> : null}
      onStartReached={onStartReached}
      onStartReachedThreshold={0.6}
    />
  )
}

const messageId = (m: DmMessage) => m.id
