import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { getEcho } from '../../bootstrap'
import MessageBubble from './MessageBubble'

export default function ConversationThread({
  conversationId,
  conversation,
  realtimeEnabled,
  realtimeStatus,
  currentUserId,
  currentUsername,
  onlineUserIds,
  apiFetch,
  onBack,
  onMarkRead,
  onConversationPatched,
  onUnreadTotalPatched,
}) {
  const [messages, setMessages] = useState([])
  const [loading, setLoading] = useState(true)
  const [loadingMore, setLoadingMore] = useState(false)
  const [nextCursor, setNextCursor] = useState(null)
  const [body, setBody] = useState('')
  const [files, setFiles] = useState([])
  const [sending, setSending] = useState(false)
  const [error, setError] = useState(null)
  const [typingUsers, setTypingUsers] = useState([])
  const [participantState, setParticipantState] = useState(conversation?.all_participants ?? [])
  const [presenceUsers, setPresenceUsers] = useState([])
  const [threadSearch, setThreadSearch] = useState('')
  const [busyAction, setBusyAction] = useState(null)
  const [lightbox, setLightbox] = useState(null)
  const [draftTitle, setDraftTitle] = useState(conversation?.title ?? '')
  const listRef = useRef(null)
  const fileInputRef = useRef(null)
  const typingRef = useRef(null)
  const stopTypingRef = useRef(null)
  const readReceiptRef = useRef(null)
  const typingExpiryTimersRef = useRef(new Map())
  const messagesRef = useRef([])
  const lastStartRef = useRef(0)
  const initialLoadRef = useRef(true)
  const shouldStickToBottomRef = useRef(true)
  const previousScrollHeightRef = useRef(0)
  const pendingPrependRef = useRef(false)
  const pendingComposerScrollRef = useRef(false)
  const knownMessageIdsRef = useRef(new Set())
  const animatedMessageIdsRef = useRef(new Set())
  const [animatedMessageIds, setAnimatedMessageIds] = useState({})
  const prefersReducedMotion = usePrefersReducedMotion()
  const draftStorageKey = useMemo(() => (conversationId ? `nova_draft_${conversationId}` : null), [conversationId])

  const readDraftFromStorage = useCallback(() => {
    if (!draftStorageKey || typeof window === 'undefined') {
      return ''
    }

    try {
      return window.localStorage.getItem(draftStorageKey) ?? ''
    } catch {
      return ''
    }
  }, [draftStorageKey])

  const persistDraftToStorage = useCallback((value) => {
    if (!draftStorageKey || typeof window === 'undefined') {
      return
    }

    try {
      if (value.trim() === '') {
        window.localStorage.removeItem(draftStorageKey)
      } else {
        window.localStorage.setItem(draftStorageKey, value)
      }
    } catch {
      // no-op
    }
  }, [draftStorageKey])

  const myParticipant = useMemo(() => (
    conversation?.my_participant
    ?? participantState.find((participant) => participant.user_id === currentUserId)
    ?? null
  ), [conversation, currentUserId, participantState])

  const participants = useMemo(() => participantState, [participantState])
  const participantNames = useMemo(() => (
    participants
      .map((participant) => participant.user?.username)
      .filter(Boolean)
  ), [participants])
  const remoteParticipantNames = useMemo(() => (
    participants
      .filter((participant) => participant.user_id !== currentUserId)
      .map((participant) => participant.user?.username)
      .filter(Boolean)
  ), [currentUserId, participants])
  const directParticipant = useMemo(() => (
    participants.find((participant) => participant.user_id !== currentUserId) ?? null
  ), [currentUserId, participants])
  const remoteIsOnline = directParticipant ? onlineUserIds.includes(Number(directParticipant.user_id)) : false
  const remoteIsViewingConversation = directParticipant
    ? presenceUsers.some((user) => Number(user?.id) === Number(directParticipant.user_id))
    : false

  const filteredMessages = useMemo(() => {
    const query = threadSearch.trim().toLowerCase()
    if (!query) return messages
    return messages.filter((message) => {
      const sender = message.sender?.username?.toLowerCase() ?? ''
      const text = message.body?.toLowerCase() ?? ''
      const attachmentNames = (message.attachments ?? []).map((attachment) => attachment.original_name?.toLowerCase() ?? '').join(' ')
      return sender.includes(query) || text.includes(query) || attachmentNames.includes(query)
    })
  }, [messages, threadSearch])

  const conversationLabel = useMemo(() => {
    if (!conversation) return 'Conversation'
    if (conversation.type === 'group') return conversation.title ?? 'Group conversation'
    return participants.find((participant) => participant.user_id !== currentUserId)?.user?.username ?? 'Direct message'
  }, [conversation, currentUserId, participants])

  const patchConversation = useCallback((patch) => {
    if (!patch) {
      return
    }

    onConversationPatched?.({
      id: conversationId,
      ...patch,
    })
  }, [conversationId, onConversationPatched])

  const patchLastMessage = useCallback((message, extra = {}) => {
    if (!message) {
      return
    }

    patchConversation({
      latest_message: message,
      last_message_at: message.created_at ?? new Date().toISOString(),
      ...extra,
    })
  }, [patchConversation])

  const patchMyParticipantState = useCallback((changes) => {
    if (!changes) {
      return
    }

    setParticipantState((prev) => prev.map((participant) => (
      participant.user_id === currentUserId
        ? { ...participant, ...changes }
        : participant
    )))

    patchConversation({
      my_participant: {
        ...(myParticipant ?? {}),
        ...changes,
      },
    })
  }, [currentUserId, myParticipant, patchConversation])

  const scrollToBottom = useCallback(() => {
    if (!listRef.current) {
      return
    }

    listRef.current.scrollTop = listRef.current.scrollHeight
    shouldStickToBottomRef.current = true
  }, [])

  const loadMessages = useCallback(async ({ cursor = null, append = false, silent = false } = {}) => {
    if (append) setLoadingMore(true)
    else if (!silent) setLoading(true)

    if (append && listRef.current) {
      previousScrollHeightRef.current = listRef.current.scrollHeight
      pendingPrependRef.current = true
    }

    try {
      const url = cursor
        ? `/api/messages/${conversationId}?cursor=${encodeURIComponent(cursor)}`
        : `/api/messages/${conversationId}`
      const data = await apiFetch(url)
      const incoming = normalizeMessages(data.data ?? [], currentUserId)
      setNextCursor(data.next_cursor ?? null)
      setMessages((prev) => append ? mergeMessageLists(incoming, prev) : incoming)
      setError(null)
    } catch (e) {
      setError(e.message)
    } finally {
      if (!silent) setLoading(false)
      setLoadingMore(false)
    }
  }, [apiFetch, conversationId, currentUserId])

  const loadTyping = useCallback(async () => {
    try {
      const data = await apiFetch(`/api/messages/${conversationId}/typing`)
      setTypingUsers(data.typing ?? [])
    } catch {
      setTypingUsers([])
    }
  }, [apiFetch, conversationId])

  const markConversationRead = useCallback(async (messageId = null) => {
    try {
      const response = await apiFetch(`/api/messages/${conversationId}/read`, {
        method: 'POST',
        body: JSON.stringify(messageId ? { message_id: messageId } : {}),
      })

      setParticipantState((prev) => prev.map((participant) => (
        participant.user_id === currentUserId
          ? {
              ...participant,
              last_read_at: response.last_read_at ?? new Date().toISOString(),
              last_read_message_id: response.last_read_message_id ?? messageId ?? participant.last_read_message_id,
            }
          : participant
      )))
      onMarkRead?.(conversationId, response?.unread_total ?? null)
    } catch {
      // no-op
    }
  }, [apiFetch, conversationId, currentUserId, onMarkRead])

  const queueReadReceipt = useCallback((messageId = null) => {
    if (readReceiptRef.current) {
      window.clearTimeout(readReceiptRef.current)
    }

    readReceiptRef.current = window.setTimeout(() => {
      markConversationRead(messageId)
    }, 220)
  }, [markConversationRead])

  useEffect(() => {
    initialLoadRef.current = true
    shouldStickToBottomRef.current = true
    previousScrollHeightRef.current = 0
    pendingPrependRef.current = false
    pendingComposerScrollRef.current = false
    knownMessageIdsRef.current = new Set()
    animatedMessageIdsRef.current = new Set()
    setMessages([])
    setPresenceUsers([])
    setTypingUsers([])
    setNextCursor(null)
    setBody(readDraftFromStorage())
    setFiles([])
    loadMessages()
    if (!realtimeEnabled) {
      loadTyping()
    }
  }, [conversationId, loadMessages, loadTyping, readDraftFromStorage, realtimeEnabled])

  useEffect(() => {
    setParticipantState(conversation?.all_participants ?? [])
    setDraftTitle(conversation?.title ?? '')
  }, [conversation?.all_participants, conversation?.title])

  useEffect(() => {
    markConversationRead()
  }, [markConversationRead])

  useEffect(() => {
    if (realtimeEnabled) {
      return undefined
    }

    const timer = window.setInterval(() => {
      loadTyping()
      loadMessages({ silent: true })
    }, 8000)

    return () => window.clearInterval(timer)
  }, [loadMessages, loadTyping, realtimeEnabled])

  useEffect(() => () => {
    if (typingRef.current) window.clearTimeout(typingRef.current)
    if (stopTypingRef.current) window.clearTimeout(stopTypingRef.current)
    if (readReceiptRef.current) window.clearTimeout(readReceiptRef.current)
    typingExpiryTimersRef.current.forEach((timer) => window.clearTimeout(timer))
  }, [])

  useEffect(() => {
    messagesRef.current = messages
  }, [messages])

  useLayoutEffect(() => {
    const container = listRef.current
    if (!container) {
      return
    }

    if (initialLoadRef.current) {
      scrollToBottom()
      initialLoadRef.current = false
      previousScrollHeightRef.current = container.scrollHeight
      return
    }

    if (pendingPrependRef.current) {
      const heightDelta = container.scrollHeight - previousScrollHeightRef.current
      container.scrollTop += heightDelta
      pendingPrependRef.current = false
      previousScrollHeightRef.current = container.scrollHeight
      return
    }

    const latestMessage = messages[messages.length - 1] ?? null
    const shouldScroll = pendingComposerScrollRef.current
      || shouldStickToBottomRef.current
      || latestMessage?._optimistic
      || latestMessage?.sender_id === currentUserId

    if (shouldScroll) {
      scrollToBottom()
    }

    pendingComposerScrollRef.current = false
    previousScrollHeightRef.current = container.scrollHeight
  }, [currentUserId, messages, scrollToBottom])

  useEffect(() => {
    if (!realtimeEnabled) {
      return undefined
    }

    const echo = getEcho()
    if (!echo) {
      return undefined
    }

    const syncMissedMessages = async () => {
      const lastServerMessage = [...messagesRef.current]
        .reverse()
        .find((message) => Number.isFinite(Number(message.id)) && !message._optimistic)

      if (!lastServerMessage?.id) {
        return
      }

      try {
        const data = await apiFetch(`/api/messages/${conversationId}/delta?after_message_id=${encodeURIComponent(lastServerMessage.id)}`)
        const incoming = normalizeMessages(data.data ?? [], currentUserId)

        if (data?.conversation?.id) {
          patchConversation(data.conversation)
        }

        if (Number.isFinite(Number(data?.summary?.unread_total))) {
          onUnreadTotalPatched?.(data.summary.unread_total)
        }

        if (incoming.length > 0) {
          setMessages((prev) => mergeMessageLists(prev, incoming))

          const latestIncoming = incoming[incoming.length - 1] ?? null
          const latestRemoteIncoming = [...incoming].reverse().find((message) => message.sender_id !== currentUserId) ?? null

          if (latestIncoming) {
            patchLastMessage(latestIncoming)
          }

          if (latestRemoteIncoming && document.visibilityState === 'visible') {
            queueReadReceipt(latestRemoteIncoming.id)
          }
        }
      } catch {
        // no-op
      }
    }

    const handleMessageCreated = (payload) => {
      if (!payload?.message) return
      const incoming = normalizeMessage(payload.message, currentUserId)
      setMessages((prev) => mergeMessageLists(prev, [incoming]))
      patchLastMessage(incoming, { unread_count: 0 })

      if (incoming.sender_id !== currentUserId && document.visibilityState === 'visible') {
        queueReadReceipt(incoming.id)
      }
    }

    const handleMessageUpdated = (payload) => {
      if (!payload?.message) return
      const updated = normalizeMessage(payload.message, currentUserId)
      setMessages((prev) => mergeMessageLists(prev, [updated]))
      patchLastMessage(updated)
    }

    const handleMessageDeleted = (payload) => {
      const deletedAt = payload?.deleted_at ?? new Date().toISOString()
      setMessages((prev) => prev.map((message) => (
        messagesMatch(message, payload)
          ? { ...message, body: '', deleted_at: deletedAt, attachments: [] }
          : message
      )))
      patchConversation({ last_message_at: deletedAt })
    }

    const handleMessageRead = (payload) => {
      if (!payload?.user?.id) return
      setParticipantState((prev) => prev.map((participant) => (
        participant.user_id === payload.user.id
          ? {
              ...participant,
              last_read_at: payload.last_read_at ?? participant.last_read_at,
              last_read_message_id: payload.last_read_message_id ?? participant.last_read_message_id,
            }
          : participant
      )))
    }

    const removeTypingUser = (userId) => {
      const existingTimer = typingExpiryTimersRef.current.get(userId)
      if (existingTimer) {
        window.clearTimeout(existingTimer)
        typingExpiryTimersRef.current.delete(userId)
      }

      setTypingUsers((prev) => prev.filter((user) => user.user_id !== userId && user.id !== userId))
    }

    const handleTypingStarted = (payload) => {
      const user = payload?.user
      if (!user?.id || user.id === currentUserId) return

      setTypingUsers((prev) => mergeTypingUsers(prev, {
        user_id: user.id,
        username: user.username,
      }))

      removeTypingUser(user.id)
      const timeout = window.setTimeout(() => removeTypingUser(user.id), Number(payload?.expires_in_ms ?? 3500))
      typingExpiryTimersRef.current.set(user.id, timeout)
    }

    const handleTypingStopped = (payload) => {
      const userId = payload?.user?.id
      if (!userId) return
      removeTypingUser(userId)
    }

    const privateChannel = echo.private(`conversation.${conversationId}`)
    privateChannel.listen('.message.created', handleMessageCreated)
    privateChannel.listen('.message.updated', handleMessageUpdated)
    privateChannel.listen('.message.deleted', handleMessageDeleted)
    privateChannel.listen('.message.read', handleMessageRead)

    const presenceChannel = echo.join(`conversation.${conversationId}`)
    presenceChannel
      .here((users) => setPresenceUsers(normalizePresenceUsers(users, currentUserId)))
      .joining((user) => setPresenceUsers((prev) => mergePresenceUsers(prev, user, currentUserId)))
      .leaving((user) => setPresenceUsers((prev) => prev.filter((member) => member.id !== user?.id)))
      .listen('.typing.started', handleTypingStarted)
      .listen('.typing.stopped', handleTypingStopped)

    const connection = echo.connector?.pusher?.connection
    connection?.bind?.('connected', syncMissedMessages)

    syncMissedMessages()

    return () => {
      connection?.unbind?.('connected', syncMissedMessages)
      typingExpiryTimersRef.current.forEach((timer) => window.clearTimeout(timer))
      typingExpiryTimersRef.current.clear()
      echo.leave(`conversation.${conversationId}`)
    }
  }, [apiFetch, conversationId, currentUserId, onUnreadTotalPatched, patchConversation, patchLastMessage, queueReadReceipt, realtimeEnabled])

  useEffect(() => {
    const known = knownMessageIdsRef.current
    const nextAnimatedIds = []

    for (const message of messages) {
      if (known.has(message.id)) continue

      known.add(message.id)
      if (!initialLoadRef.current && !message._optimistic && !animatedMessageIdsRef.current.has(message.id)) {
        animatedMessageIdsRef.current.add(message.id)
        nextAnimatedIds.push(message.id)
      }
    }

    if (prefersReducedMotion || nextAnimatedIds.length === 0) return undefined

    setAnimatedMessageIds((prev) => {
      const next = { ...prev }
      nextAnimatedIds.forEach((id) => {
        next[id] = true
      })
      return next
    })

    const timer = window.setTimeout(() => {
      setAnimatedMessageIds((prev) => {
        const next = { ...prev }
        nextAnimatedIds.forEach((id) => {
          delete next[id]
        })
        return next
      })
    }, 2200)

    return () => window.clearTimeout(timer)
  }, [messages, prefersReducedMotion])

  const handleBodyChange = useCallback((value) => {
    setBody(value)
    persistDraftToStorage(value)

    if (value.trim() === '') {
      if (stopTypingRef.current) window.clearTimeout(stopTypingRef.current)
      apiFetch(`/api/messages/${conversationId}/typing/stop`, { method: 'POST' }).catch(() => {})
      return
    }

    const now = Date.now()
    if (now - lastStartRef.current > 2500) {
      lastStartRef.current = now
      apiFetch(`/api/messages/${conversationId}/typing`, { method: 'POST' }).catch(() => {})
    }

    if (stopTypingRef.current) window.clearTimeout(stopTypingRef.current)
    stopTypingRef.current = window.setTimeout(() => {
      apiFetch(`/api/messages/${conversationId}/typing/stop`, { method: 'POST' }).catch(() => {})
    }, 1200)
  }, [apiFetch, conversationId, persistDraftToStorage])

  const handleFiles = useCallback((selectedFiles) => {
    const nextFiles = Array.from(selectedFiles || []).slice(0, 5)
    setFiles(nextFiles)
  }, [])

  const handleSend = useCallback(async (e) => {
    e.preventDefault()

    if (sending) return
    const trimmed = body.trim()
    if (!trimmed && files.length === 0) return

    const optimisticId = `optimistic-${Date.now()}`
    const clientTempId = `tmp_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
    const optimisticMessage = normalizeMessage({
      id: optimisticId,
      client_temp_id: clientTempId,
      body: trimmed,
      sender: { id: currentUserId, username: currentUsername },
      sender_id: currentUserId,
      created_at: new Date().toISOString(),
      attachments: files.map((file, index) => ({
        id: `${optimisticId}-${index}`,
        type: file.type?.startsWith('image/') ? 'image' : 'file',
        original_name: file.name,
      })),
      reactions: [],
      _optimistic: true,
    }, currentUserId)

    pendingComposerScrollRef.current = true
    shouldStickToBottomRef.current = true
    setMessages((prev) => mergeMessageLists(prev, [optimisticMessage]))
    setBody('')
    persistDraftToStorage('')
    setFiles([])
    if (fileInputRef.current) fileInputRef.current.value = ''
    setSending(true)
    setError(null)

    const formData = new FormData()
    if (trimmed) formData.append('body', trimmed)
    formData.append('client_temp_id', clientTempId)
    files.forEach((file) => formData.append('attachments[]', file))

    try {
      const created = await apiFetch(`/api/messages/${conversationId}`, {
        method: 'POST',
        body: formData,
      })

      if (draftStorageKey && typeof window !== 'undefined') {
        try {
          window.localStorage.removeItem(draftStorageKey)
        } catch {
          // no-op
        }
      }

      const normalized = normalizeMessage(created, currentUserId)
      setMessages((prev) => mergeMessageLists(prev, [normalized]))
      patchLastMessage(normalized, { unread_count: 0 })
    } catch (err) {
      setMessages((prev) => prev.filter((message) => !messagesMatch(message, { id: optimisticId, client_temp_id: clientTempId })))
      setBody(trimmed)
      persistDraftToStorage(trimmed)
      setFiles(files)
      setError(err.message)
    } finally {
      setSending(false)
      apiFetch(`/api/messages/${conversationId}/typing/stop`, { method: 'POST' }).catch(() => {})
    }
  }, [apiFetch, body, conversationId, currentUserId, currentUsername, files, patchLastMessage, persistDraftToStorage, sending])

  const updateReactions = useCallback((messageId, summary) => {
    setMessages((prev) => prev.map((message) => {
      if (message.id !== messageId) return message
      return {
        ...message,
        reactions: summaryToReactionArray(summary, currentUserId),
      }
    }))
  }, [currentUserId])

  const handleReact = useCallback(async (messageId, reaction) => {
    const summary = await apiFetch(`/api/messages/${messageId}/reactions`, {
      method: 'POST',
      body: JSON.stringify({ reaction }),
    })
    updateReactions(messageId, summary)
  }, [apiFetch, updateReactions])

  const handleUnreact = useCallback(async (messageId, reaction) => {
    const summary = await apiFetch(`/api/messages/${messageId}/reactions`, {
      method: 'DELETE',
      body: JSON.stringify({ reaction }),
    })
    updateReactions(messageId, summary)
  }, [apiFetch, updateReactions])

  const handleEdit = useCallback(async (messageId, nextBody) => {
    const updated = await apiFetch(`/api/messages/message/${messageId}`, {
      method: 'PATCH',
      body: JSON.stringify({ body: nextBody }),
    })

    setMessages((prev) => prev.map((message) => (
      message.id === messageId ? normalizeMessage({ ...message, ...updated }, currentUserId) : message
    )))
    patchLastMessage(normalizeMessage(updated, currentUserId))
  }, [apiFetch, currentUserId, patchLastMessage])

  const handleDelete = useCallback(async (messageId) => {
    await apiFetch(`/api/messages/message/${messageId}`, { method: 'DELETE' })
    setMessages((prev) => prev.map((message) => (
      message.id === messageId
        ? { ...message, body: '', deleted_at: new Date().toISOString(), attachments: [] }
        : message
    )))
    patchConversation({ last_message_at: new Date().toISOString() })
  }, [apiFetch, patchConversation])

  const runConversationAction = useCallback(async (action, url, apply) => {
    setBusyAction(action)
    setError(null)
    try {
      const response = await apiFetch(url, { method: action === 'leave' ? 'DELETE' : 'POST' })
      apply?.(response)
      if (action === 'leave') onBack?.()
    } catch (e) {
      setError(e.message)
    } finally {
      setBusyAction(null)
    }
  }, [apiFetch, onBack])

  const handleRename = useCallback(async () => {
    const title = draftTitle.trim()
    if (!title || title === conversation?.title) return

    setBusyAction('rename')
    setError(null)

    try {
      await apiFetch(`/api/messages/${conversationId}/rename`, {
        method: 'POST',
        body: JSON.stringify({ title }),
      })
      patchConversation({ title })
    } catch (e) {
      setError(e.message)
    } finally {
      setBusyAction(null)
    }
  }, [apiFetch, conversation?.title, conversationId, draftTitle, patchConversation])

  const visibleMessages = filteredMessages
  const messagesWithDecorators = useMemo(() => decorateMessages(visibleMessages, currentUserId, myParticipant), [visibleMessages, currentUserId, myParticipant])
  const typingLabel = buildTypingLabel(typingUsers)
  const presenceLabel = conversation?.type === 'group'
    ? (presenceUsers.length > 0 ? `${presenceUsers.length} active now` : null)
    : (remoteIsViewingConversation ? 'Viewing this conversation' : (remoteIsOnline ? 'Online now' : null))
  const typingSummary = typingUsers.length > 0
    ? `${typingLabel} ${conversation?.type === 'group' ? '' : 'Reply will appear here instantly.'}`.trim()
    : null

  return (
    <div className="flex min-h-0 flex-1 flex-col">
      <header className="border-b border-white/[0.06] px-3 py-4 sm:px-6">
        <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
          <div className="min-w-0">
            <div className="flex items-center gap-2 lg:hidden">
              <button onClick={onBack} className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.04] text-white/60 transition hover:bg-white/[0.08] hover:text-white">
                <i className="fa-solid fa-arrow-left text-sm" />
              </button>
              <span className="text-[11px] font-semibold uppercase tracking-[0.18em] text-white/35">Inbox</span>
            </div>

            <div className="mt-1 flex flex-wrap items-center gap-2">
              <h2 className="truncate text-2xl font-semibold text-white">{conversationLabel}</h2>
              {conversation?.type === 'group' ? <span className="rounded-full border border-fuchsia-400/20 bg-fuchsia-500/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-fuchsia-200">Group</span> : <span className="rounded-full border border-sky-400/20 bg-sky-500/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-sky-200">Direct</span>}
              {realtimeEnabled ? <RealtimeStatusBadge status={realtimeStatus} /> : null}
              {myParticipant?.is_pinned ? <span className="rounded-full border border-amber-400/20 bg-amber-500/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-200">Pinned</span> : null}
              {myParticipant?.is_muted ? <span className="rounded-full border border-white/[0.08] bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-white/45">Muted</span> : null}
              {myParticipant?.is_archived ? <span className="rounded-full border border-white/[0.08] bg-white/[0.04] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-white/45">Archived</span> : null}
            </div>

            <p className="mt-2 max-w-3xl text-sm leading-6 text-white/50">
              {conversation?.type === 'group'
                ? `Participants: ${participantNames.join(', ')}`
                : `Private thread with ${participantNames.find((name) => name !== currentUsername) ?? conversationLabel}.`}
            </p>
            {typingSummary ? (
              <div className="mt-2 inline-flex items-center gap-2 rounded-full border border-emerald-400/18 bg-emerald-500/10 px-3 py-1.5 text-xs font-medium text-emerald-100/90">
                <TypingPulse />
                <span>{typingSummary}</span>
              </div>
            ) : presenceLabel ? <p className="mt-1 text-xs text-emerald-200/70">{presenceLabel}</p> : conversation?.type === 'direct' && remoteParticipantNames.length > 0 ? <p className="mt-1 text-xs text-white/38">Chatting with @{remoteParticipantNames[0]} in realtime.</p> : null}
          </div>

          <div className="-mx-1 flex gap-2 overflow-x-auto px-1 pb-1 lg:mx-0 lg:flex-wrap lg:overflow-visible lg:px-0 lg:pb-0">
            <button
              onClick={() => runConversationAction('archive', `/api/messages/${conversationId}/archive`, (response) => {
                patchMyParticipantState({ is_archived: !!response.is_archived })
              })}
              className={actionButtonClass(busyAction === 'archive')}
            >
              <i className="fa-solid fa-box-archive text-[11px]" />
              {myParticipant?.is_archived ? 'Unarchive' : 'Archive'}
            </button>
            <button
              onClick={() => runConversationAction('mute', `/api/messages/${conversationId}/mute`, (response) => {
                patchMyParticipantState({ is_muted: !!response.is_muted })
              })}
              className={actionButtonClass(busyAction === 'mute')}
            >
              <i className="fa-solid fa-bell-slash text-[11px]" />
              {myParticipant?.is_muted ? 'Unmute' : 'Mute'}
            </button>
            <button
              onClick={() => runConversationAction(myParticipant?.is_pinned ? 'unpin' : 'pin', `/api/messages/${conversationId}/${myParticipant?.is_pinned ? 'unpin' : 'pin'}`, (response) => {
                patchMyParticipantState({
                  is_pinned: !!response.is_pinned,
                  pinned_at: response.is_pinned ? new Date().toISOString() : null,
                })
              })}
              className={actionButtonClass(busyAction === 'pin' || busyAction === 'unpin')}
            >
              <i className="fa-solid fa-thumbtack text-[11px]" />
              {myParticipant?.is_pinned ? 'Unpin' : 'Pin'}
            </button>
            {conversation?.type === 'group' ? (
              <button
                onClick={() => runConversationAction('leave', `/api/messages/${conversationId}/leave`)}
                className="inline-flex items-center gap-2 rounded-full border border-rose-400/18 bg-rose-500/10 px-4 py-2 text-sm font-medium text-rose-200 transition hover:bg-rose-500/16"
              >
                <i className="fa-solid fa-right-from-bracket text-[11px]" />
                Leave
              </button>
            ) : null}
          </div>
        </div>

        <div className="mt-4 grid gap-3 lg:grid-cols-[minmax(0,1fr)_260px]">
          <div className="rounded-2xl border border-white/[0.08] bg-black/15 px-3 py-2.5 transition focus-within:border-sky-400/30 focus-within:bg-black/25">
            <div className="flex items-center gap-3">
              <i className="fa-solid fa-magnifying-glass text-xs text-white/30" />
              <input
                type="search"
                value={threadSearch}
                onChange={(e) => setThreadSearch(e.target.value)}
                placeholder="Search inside this thread…"
                className="w-full bg-transparent text-sm text-white outline-none placeholder:text-white/25"
              />
            </div>
          </div>

          {conversation?.type === 'group' ? (
            <div className="flex flex-col gap-2 sm:flex-row">
              <input
                type="text"
                value={draftTitle}
                onChange={(e) => setDraftTitle(e.target.value)}
                placeholder="Rename group"
                className="min-w-0 flex-1 rounded-2xl border border-white/[0.08] bg-black/15 px-3 py-2.5 text-sm text-white outline-none placeholder:text-white/25 focus:border-sky-400/30 focus:bg-black/25"
              />
              <button onClick={handleRename} disabled={busyAction === 'rename'} className={actionButtonClass(busyAction === 'rename')}>
                Save
              </button>
            </div>
          ) : (
            <div className="rounded-2xl border border-white/[0.08] bg-white/[0.03] px-4 py-2.5 text-sm text-white/45">
              {participantNames.length} participant{participantNames.length === 1 ? '' : 's'}
            </div>
          )}
        </div>
      </header>

      {error ? (
        <div className="border-b border-rose-500/14 bg-rose-500/8 px-4 py-3 text-sm text-rose-200 sm:px-6">
          {error}
        </div>
      ) : null}

      <div
        ref={listRef}
        onScroll={() => {
          if (!listRef.current) {
            return
          }

          shouldStickToBottomRef.current = isNearBottom(listRef.current)
        }}
        className="nova-scrollbar-message flex-1 overflow-y-auto px-3 py-4 pr-2 sm:px-6 sm:py-5"
      >
        {nextCursor ? (
          <div className="mb-4 flex justify-center">
            <button
              onClick={() => loadMessages({ cursor: nextCursor, append: true })}
              disabled={loadingMore}
              className="rounded-full border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-xs font-medium text-white/60 transition hover:bg-white/[0.08] hover:text-white disabled:opacity-50"
            >
              {loadingMore ? 'Loading older messages…' : 'Load older messages'}
            </button>
          </div>
        ) : null}

        {loading ? (
          <div className="flex h-full items-center justify-center">
            <div className="rounded-2xl border border-white/[0.06] bg-white/[0.03] px-5 py-4 text-sm text-white/45">Loading conversation…</div>
          </div>
        ) : visibleMessages.length === 0 ? (
          <div className="flex h-full items-center justify-center">
            <div className="max-w-md rounded-[28px] border border-white/[0.06] bg-white/[0.03] px-6 py-8 text-center">
              <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-[22px] border border-white/[0.08] bg-white/[0.04] text-white/35">
                <i className="fa-solid fa-message text-2xl" />
              </div>
              <h3 className="mt-4 text-lg font-semibold text-white">{threadSearch.trim() ? 'No matching messages' : 'No messages yet'}</h3>
              <p className="mt-2 text-sm leading-6 text-white/45">
                {threadSearch.trim()
                  ? 'Try a different search term or clear the filter to see the full thread.'
                  : 'Start the conversation with a message, file, or quick note.'}
              </p>
            </div>
          </div>
        ) : (
          <div className="space-y-1.5 sm:space-y-1">
            {messagesWithDecorators.map(({ message, showUnreadMarker, dayLabel }, index) => {
              const previous = messagesWithDecorators[index - 1]?.message
              const next = messagesWithDecorators[index + 1]?.message
              const showAvatar = !previous || previous.sender_id !== message.sender_id
              const endsSequence = !next || next.sender_id !== message.sender_id
              const seenText = isLastMineMessage(visibleMessages, index, currentUserId)
                ? buildSeenText(participants, currentUserId, message)
                : null

              return (
                <div key={message.id} className="relative">
                  {dayLabel ? (
                    <div className="sticky top-2 z-[1] my-4 flex justify-center first:mt-0">
                      <span className="rounded-full border border-white/[0.08] bg-[#0b1220]/90 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-white/45 shadow-[0_10px_30px_rgba(0,0,0,0.22)] backdrop-blur">
                        {dayLabel}
                      </span>
                    </div>
                  ) : null}
                  {showUnreadMarker ? <UnreadMarker prefersReducedMotion={prefersReducedMotion} /> : null}
                  <MessageBubble
                    message={message}
                    isMine={message.sender_id === currentUserId}
                    showAvatar={showAvatar}
                    endsSequence={endsSequence}
                    isNewlyArrived={!!animatedMessageIds[message.id]}
                    prefersReducedMotion={prefersReducedMotion}
                    onReact={handleReact}
                    onUnreact={handleUnreact}
                    onEdit={handleEdit}
                    onDelete={handleDelete}
                    onOpenImage={setLightbox}
                    seenText={seenText}
                  />
                </div>
              )
            })}
          </div>
        )}
      </div>

      <div className="border-t border-white/[0.06] bg-white/[0.02] px-3 py-4 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:px-6">
        <div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
          <div className="text-sm text-white/45">
            {typingLabel ? <span>{typingLabel}</span> : <span>Markdown supported. Images and files can be sent inline.</span>}
          </div>
          {files.length > 0 ? <span className="text-xs text-white/30">{files.length}/5 attachments</span> : null}
        </div>

        {files.length > 0 ? (
          <div className="mb-3 flex flex-wrap gap-2">
            {files.map((file, index) => (
              <span key={`${file.name}-${index}`} className="inline-flex items-center gap-2 rounded-full border border-white/[0.08] bg-white/[0.04] px-3 py-1.5 text-xs text-white/65">
                <i className={`fa-solid ${file.type?.startsWith('image/') ? 'fa-image' : 'fa-paperclip'} text-[10px]`} />
                {file.name}
              </span>
            ))}
          </div>
        ) : null}

        <form onSubmit={handleSend} className="rounded-[28px] border border-white/[0.08] bg-black/15 p-3 shadow-[0_18px_45px_rgba(0,0,0,0.2)] sm:p-3.5">
          <textarea
            value={body}
            onChange={(e) => handleBodyChange(e.target.value)}
            placeholder="Write a message…"
            rows={3}
            maxLength={5000}
            className="w-full resize-none bg-transparent px-2 py-2 text-sm leading-6 text-white outline-none placeholder:text-white/25"
          />

          <div className="mt-3 flex flex-col gap-3 border-t border-white/[0.06] pt-3 sm:flex-row sm:items-center sm:justify-between">
            <div className="flex flex-wrap items-center gap-2">
              <input
                ref={fileInputRef}
                type="file"
                multiple
                onChange={(e) => handleFiles(e.target.files)}
                className="hidden"
              />
              <button
                type="button"
                onClick={() => fileInputRef.current?.click()}
                className="inline-flex min-h-11 items-center gap-2 rounded-full border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/65 transition hover:bg-white/[0.08] hover:text-white"
              >
                <i className="fa-solid fa-paperclip text-[11px]" />
                Attach files
              </button>
              {files.length > 0 ? (
                <button
                  type="button"
                  onClick={() => {
                    setFiles([])
                    if (fileInputRef.current) fileInputRef.current.value = ''
                  }}
                  className="inline-flex min-h-11 items-center gap-2 rounded-full border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/45 transition hover:bg-white/[0.08] hover:text-white"
                >
                  Clear
                </button>
              ) : null}
            </div>

            <button
              type="submit"
              disabled={sending || (!body.trim() && files.length === 0)}
              className="inline-flex min-h-11 items-center justify-center gap-2 rounded-full bg-sky-500 px-5 py-2.5 text-sm font-semibold text-slate-950 transition hover:bg-sky-400 disabled:opacity-50"
            >
              <i className={`fa-solid ${sending ? 'fa-spinner fa-spin' : 'fa-paper-plane'} text-[11px]`} />
              {sending ? 'Sending…' : 'Send message'}
            </button>
          </div>
        </form>
      </div>

      {lightbox ? (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-[#020611e6] p-4 backdrop-blur-md" onClick={() => setLightbox(null)}>
          <div className="relative max-h-[90vh] max-w-5xl" onClick={(e) => e.stopPropagation()}>
            <button
              type="button"
              onClick={() => setLightbox(null)}
              className="absolute right-3 top-3 inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/15 bg-black/30 text-white/80 transition hover:bg-black/45"
            >
              <i className="fa-solid fa-xmark" />
            </button>
            <img src={lightbox.url} alt={lightbox.original_name ?? 'Attachment'} className="max-h-[90vh] rounded-[28px] border border-white/10" />
          </div>
        </div>
      ) : null}
    </div>
  )
}

function actionButtonClass(isBusy) {
  return `inline-flex min-h-11 shrink-0 items-center gap-2 rounded-full border border-white/[0.08] bg-white/[0.04] px-4 py-2 text-sm font-medium text-white/65 transition hover:bg-white/[0.08] hover:text-white ${isBusy ? 'opacity-60' : ''}`
}

function normalizeMessages(messages, currentUserId) {
  return messages.map((message) => normalizeMessage(message, currentUserId))
}

function normalizeMessage(message, currentUserId) {
  const reactionSummary = message.reaction_summary ?? null
  return {
    ...message,
    sender_id: message.sender_id ?? message.sender?.id ?? null,
    reactions: reactionSummary ? summaryToReactionArray(reactionSummary, currentUserId) : normalizeReactionArray(message.reactions ?? [], currentUserId),
  }
}

function normalizeReactionArray(reactions, currentUserId) {
  return reactions.map((reaction) => ({
    ...reaction,
    _iMine: reaction._iMine ?? reaction.user_id === currentUserId,
  }))
}

function summaryToReactionArray(summary, currentUserId) {
  if (!summary || typeof summary !== 'object') return []
  const mine = Array.isArray(summary.me) ? summary.me : []
  return Object.entries(summary)
    .filter(([emoji]) => emoji !== 'me')
    .flatMap(([emoji, count]) => Array.from({ length: Number(count) || 0 }, (_, index) => ({
      id: `${emoji}-${index}`,
      reaction: emoji,
      user_id: mine.includes(emoji) && index === 0 ? currentUserId : null,
      _iMine: mine.includes(emoji) && index === 0,
    })))
}

function mergeMessageLists(existing, incoming) {
  const next = [...existing]

  for (const incomingMessage of incoming) {
    const existingIndex = next.findIndex((message) => messagesMatch(message, incomingMessage))
    if (existingIndex >= 0) {
      next[existingIndex] = {
        ...next[existingIndex],
        ...incomingMessage,
        _optimistic: false,
      }
      continue
    }

    next.push(incomingMessage)
  }

  return next.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
}

function messagesMatch(left, right) {
  if (!left || !right) return false
  if (left.id && right.id && String(left.id) === String(right.id)) return true
  if (left.uuid && right.uuid && left.uuid === right.uuid) return true
  if (left.client_temp_id && right.client_temp_id && left.client_temp_id === right.client_temp_id) return true
  return false
}

function mergeTypingUsers(existing, incoming) {
  const matchIndex = existing.findIndex((user) => String(user.user_id ?? user.id) === String(incoming.user_id ?? incoming.id))
  if (matchIndex === -1) {
    return [...existing, incoming]
  }

  const next = [...existing]
  next[matchIndex] = { ...next[matchIndex], ...incoming }
  return next
}

function normalizePresenceUsers(users, currentUserId) {
  return (users ?? []).filter((user) => user?.id !== currentUserId)
}

function mergePresenceUsers(existing, incoming, currentUserId) {
  if (!incoming?.id || incoming.id === currentUserId) {
    return existing
  }

  const next = [...existing]
  const index = next.findIndex((user) => user.id === incoming.id)
  if (index >= 0) {
    next[index] = { ...next[index], ...incoming }
    return next
  }

  next.push(incoming)
  return next
}

function isNearBottom(container, threshold = 120) {
  return container.scrollHeight - container.scrollTop - container.clientHeight <= threshold
}

function buildTypingLabel(users) {
  if (!users || users.length === 0) return ''
  const names = users.map((user) => `@${user.username}`)
  if (names.length === 1) return `${names[0]} is typing…`
  if (names.length === 2) return `${names[0]} and ${names[1]} are typing…`
  return `${names.slice(0, 2).join(', ')} and ${names.length - 2} others are typing…`
}

function isLastMineMessage(messages, index, currentUserId) {
  const current = messages[index]
  if (!current || current.sender_id !== currentUserId) return false
  for (let i = index + 1; i < messages.length; i += 1) {
    if (messages[i].sender_id === currentUserId) return false
  }
  return true
}

function buildSeenText(participants, currentUserId, message) {
  const seenBy = participants.filter((participant) => participant.user_id !== currentUserId && participantHasReadMessage(participant, message))

  if (seenBy.length === 0) return 'Sent'

  if (seenBy.length === 1) {
    const readAt = seenBy[0]?.last_read_at
    return readAt ? `Seen ${formatSeenTime(readAt)}` : 'Seen'
  }

  return `Seen by ${seenBy.length} people`
}

function decorateMessages(messages, currentUserId, participant) {
  let unreadMarked = false
  const lastReadMessageId = Number(participant?.last_read_message_id ?? 0)
  const lastReadAt = participant?.last_read_at ?? null

  return messages.map((message, index) => {
    const previous = messages[index - 1]
    const currentDay = dayKey(message.created_at)
    const previousDay = previous ? dayKey(previous.created_at) : null
    const shouldMarkUnread = !unreadMarked
      && message.sender_id !== currentUserId
      && !message.deleted_at
      && (
        lastReadMessageId > 0
          ? Number(message.id) > lastReadMessageId
          : lastReadAt
            ? new Date(message.created_at).getTime() > new Date(lastReadAt).getTime()
            : true
      )

    if (shouldMarkUnread) unreadMarked = true

    return {
      message,
      showUnreadMarker: shouldMarkUnread,
      dayLabel: currentDay !== previousDay ? formatDayLabel(message.created_at) : null,
    }
  })
}

function participantHasReadMessage(participant, message) {
  const lastReadMessageId = Number(participant?.last_read_message_id ?? 0)
  if (lastReadMessageId > 0) {
    return Number(message?.id ?? 0) > 0 && lastReadMessageId >= Number(message.id)
  }

  if (participant?.last_read_at && message?.created_at) {
    return new Date(participant.last_read_at).getTime() >= new Date(message.created_at).getTime()
  }

  return false
}

function formatSeenTime(iso) {
  const then = new Date(iso).getTime()
  if (!Number.isFinite(then)) {
    return 'moments ago'
  }

  const diffSeconds = Math.max(0, Math.floor((Date.now() - then) / 1000))
  if (diffSeconds < 60) return 'moments ago'
  if (diffSeconds < 3600) return `${Math.floor(diffSeconds / 60)}m ago`
  if (diffSeconds < 86400) return `${Math.floor(diffSeconds / 3600)}h ago`
  if (diffSeconds < 604800) return `${Math.floor(diffSeconds / 86400)}d ago`

  return new Date(iso).toLocaleDateString(undefined, {
    month: 'short',
    day: 'numeric',
  })
}

function dayKey(iso) {
  const date = new Date(iso)
  return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
}

function formatDayLabel(iso) {
  const date = new Date(iso)
  const today = new Date()
  const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate())
  const startOfDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
  const diffDays = Math.round((startOfToday.getTime() - startOfDate.getTime()) / 86400000)

  if (diffDays === 0) return 'Today'
  if (diffDays === 1) return 'Yesterday'

  return date.toLocaleDateString(undefined, {
    weekday: 'short',
    month: 'short',
    day: 'numeric',
  })
}

function UnreadMarker({ prefersReducedMotion }) {
  const [isVisible, setIsVisible] = useState(prefersReducedMotion)

  useEffect(() => {
    if (prefersReducedMotion) return undefined

    setIsVisible(false)
    const frame = window.requestAnimationFrame(() => setIsVisible(true))
    return () => window.cancelAnimationFrame(frame)
  }, [prefersReducedMotion])

  return (
    <div
      className="my-4 flex items-center gap-3"
      style={prefersReducedMotion ? undefined : {
        opacity: isVisible ? 1 : 0.45,
        transform: isVisible ? 'scale(1)' : 'scale(0.985)',
        transition: 'opacity 280ms ease, transform 360ms ease',
      }}
    >
      <span className="h-px flex-1 bg-sky-400/25" />
      <span className="rounded-full border border-sky-400/20 bg-sky-500/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-sky-200">
        Unread messages
      </span>
      <span className="h-px flex-1 bg-sky-400/25" />
    </div>
  )
}

function TypingPulse() {
  return (
    <span className="inline-flex items-center gap-1">
      <span className="h-2 w-2 rounded-full bg-emerald-300 animate-[pulse_1s_ease-in-out_infinite]" />
      <span className="h-2 w-2 rounded-full bg-emerald-300/80 animate-[pulse_1s_ease-in-out_160ms_infinite]" />
      <span className="h-2 w-2 rounded-full bg-emerald-300/60 animate-[pulse_1s_ease-in-out_320ms_infinite]" />
    </span>
  )
}

function RealtimeStatusBadge({ status }) {
  const label = status === 'connected'
    ? 'Socket live'
    : status === 'connecting'
      ? 'Socket connecting'
      : 'Socket offline'

  const tone = status === 'connected'
    ? 'border-emerald-400/20 bg-emerald-500/10 text-emerald-200'
    : status === 'connecting'
      ? 'border-amber-400/20 bg-amber-500/10 text-amber-200'
      : 'border-rose-400/18 bg-rose-500/10 text-rose-200'

  const dot = status === 'connected'
    ? 'bg-emerald-300'
    : status === 'connecting'
      ? 'bg-amber-300'
      : 'bg-rose-300'

  return (
    <span className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] ${tone}`}>
      <span className={`h-1.5 w-1.5 rounded-full ${dot}`} />
      {label}
    </span>
  )
}

function usePrefersReducedMotion() {
  const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)

  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
    const handleChange = () => setPrefersReducedMotion(mediaQuery.matches)

    handleChange()
    mediaQuery.addEventListener('change', handleChange)

    return () => mediaQuery.removeEventListener('change', handleChange)
  }, [])

  return prefersReducedMotion
}
