import {
  Stack,
  useFocusEffect,
  useLocalSearchParams,
} from 'expo-router'
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react'
import {type FlatList, type LayoutChangeEvent} from 'react-native'
import {
  KeyboardGestureArea,
  KeyboardStickyView,
} from 'react-native-keyboard-controller'
import {useSharedValue} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'

import {DmClosedBar} from '@/components/dm/dm-closed-bar'
import {DmInputBar} from '@/components/dm/dm-input-bar'
import {MessageList} from '@/components/dm/message-list'
import {useFocusedPoll} from '@/hooks/use-focused-poll'
import {atoms as a, useTheme} from '@/sprinkles'
import {useAuth} from '@/stores/auth'
import {
  useConversation,
  useLoadMessages,
  useLoadMoreMessages,
  useMarkConversationRead,
  useMessages,
  useMessagesLoading,
  useMessagesLoadingMore,
  usePollNewMessages,
  useRefreshConversations,
  useSendMessage,
  useSetActiveConversation,
  type DmMessage,
} from '@/stores/dms'

const CONVERSATION_POLL_INTERVAL_MS = 4_000
const SEND_SCROLL_BURST_MS = 250

export default function ConversationScreen() {
  const {conversationId} = useLocalSearchParams<{
    conversationId: string
  }>()
  const t = useTheme()
  const insets = useSafeAreaInsets()
  const user = useAuth((s) => s.user)
  const conversation = useConversation(conversationId)
  const messages = useMessages(conversationId)
  const loading = useMessagesLoading(conversationId)
  const loadingMore = useMessagesLoadingMore(conversationId)
  const load = useLoadMessages()
  const loadMore = useLoadMoreMessages()
  const send = useSendMessage()
  const pollNew = usePollNewMessages()
  const markRead = useMarkConversationRead()
  const setActive = useSetActiveConversation()
  const refreshConversations = useRefreshConversations()

  const listRef = useRef<FlatList<DmMessage>>(null)

  // follow a just-sent message to the bottom even from a scrolled-up
  // position (mirrors bluesky's send-scroll burst). a single scroll can't
  // do it: the saturating offset clamps to the bottom at call time, and a
  // send settles over several layout passes (the new bubble measuring,
  // the composer collapsing back to one line), so the true bottom keeps
  // moving. re-asserting the scroll each frame for a short window means
  // the last one lands settled. saturating offset rather than
  // scrollToEnd: the keyboard lift is contentInset that scrollToEnd's
  // target math doesn't know about, so it lands short
  const sendScrollRaf = useRef(0)
  const scrollToBottomBurst = useCallback(() => {
    cancelAnimationFrame(sendScrollRaf.current)
    const deadline = Date.now() + SEND_SCROLL_BURST_MS
    const tick = () => {
      listRef.current?.scrollToOffset({
        offset: Number.MAX_SAFE_INTEGER,
        animated: false,
      })
      sendScrollRaf.current =
        Date.now() < deadline ? requestAnimationFrame(tick) : 0
    }
    tick()
  }, [])
  useEffect(
    () => () => cancelAnimationFrame(sendScrollRaf.current),
    [],
  )

  const onSend = useCallback(
    async (message: string) => {
      await send(conversationId, message)
      scrollToBottomBurst()
    },
    [conversationId, send, scrollToBottomBurst],
  )

  // mirrors bluesky's MessagesList: the composer sits in a sticky view
  // that translates with the keyboard; its measured height extends the
  // list's scrollable range (shared value) and sizes the dismiss gesture
  // area (js state)
  const inputHeight = useSharedValue(0)
  const [inputHeightJS, setInputHeightJS] = useState(0)
  const onInputLayout = useCallback(
    (event: LayoutChangeEvent) => {
      const height = event.nativeEvent.layout.height
      inputHeight.set(height)
      setInputHeightJS(Math.round(height))
    },
    [inputHeight],
  )

  // slide the bar's resting safe-area padding behind the open keyboard
  const stickyOffset = useMemo(
    () => ({closed: 0, opened: insets.bottom}),
    [insets.bottom],
  )

  useEffect(() => {
    void load(conversationId)
  }, [conversationId, load])

  // deep links land here before the inbox has the peer's profile
  useEffect(() => {
    if (!conversation) void refreshConversations()
  }, [conversation, refreshConversations])

  // while on screen: suppress this conversation's push banners and keep
  // its read state current
  useFocusEffect(
    useCallback(() => {
      setActive(conversationId)
      // expected to fail while offline; the next poll re-asserts it
      void markRead(conversationId).catch(() => {})
      return () => setActive(null)
    }, [conversationId, setActive, markRead]),
  )

  useFocusedPoll(
    useCallback(
      // polling is expected to fail while offline — swallow rather than
      // reject unhandled every interval
      () => void pollNew(conversationId).catch(() => {}),
      [conversationId, pollNew],
    ),
    CONVERSATION_POLL_INTERVAL_MS,
  )

  return (
    <>
      <Stack.Screen
        options={{
          title: conversation
            ? `@${conversation.peer.handle}`
            : 'Messages',
        }}
      />
      <KeyboardGestureArea
        interpolator="ios"
        offset={inputHeightJS}
        style={[a.flex_1, t.atoms.bg]}
      >
        <MessageList
          messages={messages}
          myUserId={user?.id}
          inputHeight={inputHeight}
          listRef={listRef}
          loading={loading}
          loadingMore={loadingMore}
          onStartReached={() => void loadMore(conversationId)}
        />
        <KeyboardStickyView
          style={[a.absolute, {bottom: 0, left: 0, right: 0}]}
          offset={stickyOffset}
          onLayout={onInputLayout}
        >
          {conversation?.peerIsFriend === false ? (
            <DmClosedBar handle={conversation.peer.handle} />
          ) : (
            <DmInputBar onSend={onSend} />
          )}
        </KeyboardStickyView>
      </KeyboardGestureArea>
    </>
  )
}
