import { Feather } from '@expo/vector-icons'
import type { BottomSheetModal } from '@gorhom/bottom-sheet'
import { FlashList, type FlashListRef } from '@shopify/flash-list'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { type ErrorBoundaryProps, useLocalSearchParams, useRouter } from 'expo-router'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
  ActivityIndicator,
  KeyboardAvoidingView,
  Platform,
  Pressable,
  TextInput,
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import ChatMessageRow, { type ChatMessage } from 'src/components/chat/ChatMessageRow'
import { ActionSheet, type ActionSheetItem } from 'src/components/common/ActionSheet'
import { ErrorState } from 'src/components/common/ErrorState'
import { useScreenMenu } from 'src/components/menu'
import { useKeyboardVisible } from 'src/components/menu/hooks'
import { BAR_HEIGHT } from 'src/components/menu/MenuBar'
import { listScopeKeys, useCacheMutation } from 'src/hooks/mutations/cacheMutation'
import { useConfirmDialog } from 'src/hooks/useConfirmDialog'
import {
  deleteChatMessage,
  sendChatMessage,
  sendMastodonDirectMessage,
} from 'src/lib/api'
import { haptic } from 'src/lib/haptics'
import { ICON } from 'src/lib/icons'
import { chatQueries } from 'src/lib/queries/chats'
import { emit } from 'src/lib/tapestry/emit'
import { useActiveProfile, useAuth } from 'src/state/AuthProvider'
import { enforceLen } from 'src/utils'
import { Text, useTheme, useToastController, View, XStack } from 'tamagui'

export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
  return <ErrorState message={error.message} onRetry={retry} />
}

type RawMessage = {
  id: string
  created_at: string
  type: string
  text?: string
  media?: string
  isAuthor?: boolean
  reportId?: string
}

type ThreadResponse = {
  id: string
  name?: string | null
  username: string
  // Mastodon-only — the webfinger `acct` (host-qualified for federated
  // actors). The DM send path uses this to build `@user@host` mentions.
  acct?: string
  avatar: string
  messages?: RawMessage[]
}

type AccountStub = {
  id: string
  username: string
  displayName?: string | null
  avatar: string
}

function buildMessage(
  raw: RawMessage,
  other: AccountStub,
  self: AccountStub
): ChatMessage | null {
  const account: AccountStub = raw.isAuthor ? self : other
  const base = {
    id: raw.id,
    createdAt: raw.created_at,
    reportId: raw.reportId,
    isSelf: !!raw.isAuthor,
    account: {
      id: account.id,
      username: account.username,
      displayName: account.displayName ?? null,
      avatar: account.avatar,
    },
  }

  if (raw.type === 'text' || raw.type === 'emoji' || raw.type === 'link') {
    return { ...base, text: raw.text ?? '', image: null }
  }
  if (
    raw.type === 'story:reply' ||
    raw.type === 'story:comment' ||
    raw.type === 'story:react'
  ) {
    return {
      ...base,
      text: `Story Reply: "${enforceLen(raw.text ?? '', 80, true, 'middle')}"`,
      image: null,
    }
  }
  if (raw.type === 'photo' || raw.type === 'photos') {
    return { ...base, text: raw.text ?? null, image: raw.media ?? null }
  }
  return null
}

export default function Page() {
  const { id } = useLocalSearchParams<{ id: string }>()
  const router = useRouter()
  const queryClient = useQueryClient()
  const { profile: selfUser, summary } = useActiveProfile()
  const { user: account } = useAuth()
  // Phase C §4.5 of the Mastodon follow-up plan: the screen used to assume
  // Pixelfed-shaped DMs. The thread query layer normalizes both protocols
  // into the same `ThreadResponse`/`RawMessage` shape, so the only branch
  // here is the send path (Pixelfed: `sendChatMessage` to a profile id;
  // Mastodon: `sendMastodonDirectMessage` with `@user@host` mentions).
  const isMastodon = account?.type === 'mastodon'
  const theme = useTheme()
  const toast = useToastController()
  const [composer, setComposer] = useState('')
  const [deletedIds, setDeletedIds] = useState<Set<string>>(new Set())
  const [isReloading, setReloading] = useState(false)
  const keyboardVisible = useKeyboardVisible()
  const { confirm, dialog: confirmDialog } = useConfirmDialog()
  const optionsSheetRef = useRef<BottomSheetModal>(null)
  const [optionsItems, setOptionsItems] = useState<ActionSheetItem[]>([])

  // Prefer the registry's userId so a corrupted cached blob can't mis-route
  // outbound DMs.
  const selfId = summary?.userId ?? selfUser?.id ?? ''
  const self = useMemo(
    () => ({
      id: String(selfId),
      username: selfUser?.username ?? '',
      displayName: selfUser?.display_name ?? '',
      avatar: selfUser?.avatar ?? '',
    }),
    [selfId, selfUser?.username, selfUser?.display_name, selfUser?.avatar]
  )

  const {
    data: thread,
    isLoading,
    isError,
  } = useQuery({
    ...chatQueries.thread(id),
    select: (data) => data as ThreadResponse,
  })

  const other = useMemo(
    () =>
      thread
        ? {
            id: thread.id,
            username: thread.username,
            displayName: thread.name,
            avatar: thread.avatar,
          }
        : null,
    [thread]
  )

  const messages = useMemo<ChatMessage[]>(() => {
    if (!thread || !other) return []
    const rawMessages = Array.isArray(thread.messages) ? thread.messages : []
    const fromCache = rawMessages
      .map((raw) => buildMessage(raw, other, self))
      .filter((m): m is ChatMessage => m !== null)
      .filter((m) => !deletedIds.has(m.id))
    // Oldest → newest so the bottom row is the most recent message.
    return fromCache.sort(
      (a, b) =>
        new Date(a.createdAt as any).getTime() - new Date(b.createdAt as any).getTime()
    )
  }, [thread, other, self, deletedIds])

  const listRef = useRef<FlashListRef<ChatMessage>>(null)

  useEffect(() => {
    if (!messages.length) return
    // Defer so layout settles before the scroll call.
    const handle = setTimeout(() => {
      listRef.current?.scrollToEnd({ animated: false })
    }, 50)
    return () => clearTimeout(handle)
  }, [messages.length])

  const threadKey = chatQueries.thread(id).queryKey

  // Optimistic DM send: the message appears instantly under a temp id, is
  // reconciled to the server's real id on success, and the factory rolls the
  // pending entry back out of the thread cache on failure.
  //
  // Pixelfed: `sendChatMessage(profileId, text)` against the Pixelfed-only
  // direct-thread endpoint. Mastodon: `POST /api/v1/statuses` with
  // visibility=direct + `@user@host` mentions for every other participant +
  // `in_reply_to_id` set to the anchor status. Both paths reconcile to the
  // same normalized `RawMessage` shape.
  const sendMutation = useCacheMutation<{ id: string }, { text: string; tempId: string }>(
    {
      label: 'Sending…',
      mutationFn: async ({ text }) => {
        if (isMastodon) {
          // Two routes lead here:
          //   1. An existing conversation — `id` is `Conversation.id`,
          //      and the inbox infinite-query cache holds the row with
          //      participants + anchor `last_status.id`.
          //   2. A new compose target — `id` is an Account id, the
          //      thread query fell back to fetching that account, and
          //      the conversation row doesn't exist yet (server creates
          //      it on first send). The thread cache holds the recipient
          //      Account in `username`/`avatar`/`id`; we read its acct
          //      from there and post without `in_reply_to_id`.
          const convData = queryClient.getQueryData<{
            pages: Array<{
              data: Array<{
                id: string
                participants: { acct: string }[]
                origin: { lastStatusId: string | null }
              }>
            }>
          }>(chatQueries.conversations().queryKey)
          const row = convData?.pages.flatMap((p) => p.data).find((r) => r.id === id)
          let mentions: string[]
          let inReplyToId: string | null
          if (row) {
            mentions = row.participants.map((p) => `@${p.acct}`)
            inReplyToId = row.origin.lastStatusId
          } else if (thread?.acct || thread?.username) {
            // Fallback to the recipient resolved by the thread query.
            // Prefer `acct` (host-qualified for federated actors) so the
            // mention reaches a remote recipient; `username` is the
            // local-only form and would silently drop the host.
            const handle = thread.acct || thread.username
            mentions = [`@${handle}`]
            inReplyToId = null
          } else {
            mentions = []
            inReplyToId = null
          }
          const res = await sendMastodonDirectMessage({
            text,
            mentions,
            inReplyToId,
          })
          // Refresh the inbox so the just-created conversation appears
          // and (best-effort) the route eventually maps to it. The plan
          // accepts a one-frame stutter on the new-thread path.
          queryClient.invalidateQueries({
            queryKey: chatQueries.conversations().queryKey,
          })
          return { id: String(res.id) }
        }
        const res = (await sendChatMessage(id, text)) as any
        if (res?.error) throw new Error(res.error)
        return res as { id: string }
      },
      snapshotKeys: () => listScopeKeys(threadKey),
      applyOptimistic: (qc, { text, tempId }) => {
        qc.setQueryData<ThreadResponse>(threadKey, (prev) => {
          if (!prev) return prev
          const message: RawMessage = {
            id: tempId,
            created_at: new Date().toISOString(),
            type: 'text',
            text,
            isAuthor: true,
          }
          return {
            ...prev,
            messages: [message, ...(prev.messages ?? [])],
          }
        })
      },
      reconcile: (qc, data, { tempId }) => {
        qc.setQueryData<ThreadResponse>(threadKey, (prev) => {
          if (!prev) return prev
          return {
            ...prev,
            messages: (prev.messages ?? []).map((m) =>
              m.id === tempId ? { ...m, id: String(data.id) } : m
            ),
          }
        })
      },
      onSuccess: () => {
        emit('D')
      },
      onError: (err) => {
        const message = err instanceof Error ? err.message : undefined
        toast.show('Failed to send message', { message, preset: 'error' })
      },
    }
  )

  const deleteMutation = useMutation({
    mutationFn: async (message: ChatMessage) => {
      setDeletedIds((prev) => {
        const next = new Set(prev)
        next.add(message.id)
        return next
      })
      if (message.reportId) {
        await deleteChatMessage(message.reportId)
      }
    },
  })

  const onSend = useCallback(() => {
    const text = composer.trim()
    if (!text) return
    haptic.light()
    setComposer('')
    sendMutation.mutate({ text, tempId: `pending-${Date.now()}` })
  }, [composer, sendMutation])

  const confirmDelete = useCallback(
    async (message: ChatMessage) => {
      const ok = await confirm({
        title: 'Confirm',
        message: 'Are you sure you want to delete this direct message?',
        confirmLabel: 'Delete',
        destructive: true,
      })
      if (ok) deleteMutation.mutate(message)
    },
    [confirm, deleteMutation]
  )

  const onLongPress = useCallback(
    (message: ChatMessage) => {
      const items: ActionSheetItem[] = []
      if (message.isSelf) {
        items.push({
          id: 'delete',
          label: 'Delete',
          destructive: true,
          onPress: () => {
            optionsSheetRef.current?.close()
            confirmDelete(message)
          },
        })
      } else {
        items.push({
          id: 'view-profile',
          label: 'View Profile',
          onPress: () => {
            optionsSheetRef.current?.close()
            router.push(`/profile/${message.account.id}`)
          },
        })
      }
      setOptionsItems(items)
      optionsSheetRef.current?.present()
    },
    [confirmDelete, router]
  )

  const onAvatarPress = useCallback(
    (accountId: string) => {
      router.push(`/profile/${accountId}`)
    },
    [router]
  )

  const onMentionPress = useCallback(
    (username: string) => {
      router.push(`/profile/0?byUsername=${username}`)
    },
    [router]
  )

  const onHashtagPress = useCallback(
    (tag: string) => {
      router.push(`/hashtag/${tag.replace(/^#/, '')}`)
    },
    [router]
  )

  const reloadChat = useCallback(async () => {
    setReloading(true)
    await queryClient.invalidateQueries({ queryKey: threadKey })
    setDeletedIds(new Set())
    setReloading(false)
  }, [queryClient, threadKey])

  useScreenMenu({
    intents: [
      {
        kind: 'menu',
        accessibilityLabel: 'Conversation options',
        items: [
          {
            id: 'refresh',
            label: 'Refresh',
            icon: 'refresh-cw',
            onPress: reloadChat,
            disabled: isReloading,
          },
          {
            id: 'view-profile',
            label: 'View profile',
            icon: 'user',
            onPress: () => router.push(`/profile/${id}`),
          },
          {
            id: 'report-user',
            label: 'Report user',
            icon: 'alert-triangle',
            destructive: true,
            onPress: () => router.push(`/profile/report/${id}`),
          },
        ],
      },
    ],
  })

  const renderItem = useCallback(
    ({ item }: { item: ChatMessage }) => (
      <ChatMessageRow
        message={item}
        onLongPress={onLongPress}
        onAvatarPress={onAvatarPress}
        onMentionPress={onMentionPress}
        onHashtagPress={onHashtagPress}
      />
    ),
    [onLongPress, onAvatarPress, onMentionPress, onHashtagPress]
  )

  if (isLoading) {
    return (
      <SafeAreaView
        style={{ flex: 1, backgroundColor: theme.background.val }}
        edges={['top', 'bottom']}
      >
        <View flex={1} justifyContent="center" alignItems="center">
          <ActivityIndicator color={theme.color.val} />
        </View>
      </SafeAreaView>
    )
  }

  if (isError) {
    return (
      <SafeAreaView
        style={{ flex: 1, backgroundColor: theme.background.val }}
        edges={['top', 'bottom']}
      >
        <View flex={1} justifyContent="center" alignItems="center" p="$4">
          <Text color={theme.color.val}>Couldn't load this conversation.</Text>
        </View>
      </SafeAreaView>
    )
  }

  return (
    <SafeAreaView
      style={{ flex: 1, backgroundColor: theme.background.val }}
      edges={['top', 'bottom']}
    >
      <KeyboardAvoidingView
        behavior={Platform.OS === 'ios' ? 'padding' : undefined}
        keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
        style={{ flex: 1 }}
      >
        <View flex={1}>
          <FlashList
            ref={listRef}
            data={messages}
            renderItem={renderItem}
            keyExtractor={(item) => item.id}
            keyboardShouldPersistTaps="handled"
            contentContainerStyle={{ paddingVertical: 8 }}
          />
        </View>

        <XStack
          padding="$3"
          marginBottom={keyboardVisible ? 0 : BAR_HEIGHT + 16}
          gap="$2"
          alignItems="flex-end"
          borderTopWidth={1}
          borderColor="$borderColor"
          bg="$background"
        >
          <View
            flexGrow={1}
            bg="$backgroundTertiary"
            borderRadius={12}
            paddingHorizontal="$3"
            paddingVertical="$2"
            minHeight={44}
            maxHeight={150}
            justifyContent="center"
          >
            <TextInput
              value={composer}
              onChangeText={setComposer}
              placeholder="Message"
              placeholderTextColor={theme.colorTertiary.val as any}
              multiline
              maxLength={500}
              style={{
                color: theme.color.val as any,
                fontSize: 16,
                padding: 0,
                margin: 0,
                maxHeight: 130,
              }}
            />
          </View>
          <Pressable
            onPress={onSend}
            disabled={!composer.trim() || sendMutation.isPending}
            hitSlop={8}
            style={{ paddingHorizontal: 6, paddingVertical: 10 }}
          >
            <Feather
              name="send"
              size={ICON.lg}
              color={
                composer.trim() && !sendMutation.isPending
                  ? (theme.interactiveActive.val as any)
                  : (theme.colorTertiary.val as any)
              }
            />
          </Pressable>
        </XStack>
      </KeyboardAvoidingView>
      <ActionSheet
        ref={optionsSheetRef}
        title="Chat Options"
        items={optionsItems}
        onCancel={() => optionsSheetRef.current?.close()}
      />
      {confirmDialog}
    </SafeAreaView>
  )
}
