import {
  Dialog,
  DialogBackdrop,
  DialogPanel,
  DialogTitle,
} from '@headlessui/react'
import {
  ArrowLeftIcon,
  BellIcon,
  BellSlashIcon,
  ChatBubbleLeftRightIcon,
  DocumentIcon,
  FaceSmileIcon,
  HashtagIcon,
  MagnifyingGlassIcon,
  MapPinIcon,
  ArrowRightIcon,
  ArrowUturnLeftIcon,
  PaperAirplaneIcon,
  PaperClipIcon,
  PlusIcon,
  UserGroupIcon,
  XMarkIcon,
} from '@heroicons/react/24/outline'
import { MapPinIcon as MapPinSolidIcon } from '@heroicons/react/24/solid'
import {
  Fragment,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ChangeEvent,
  type FormEvent,
  type ReactNode,
} from 'react'
import { useNavigate, useParams } from 'react-router'
import Avatar from '../../components/Avatar'
import AddressMapPicker from '../../components/AddressMapPicker'
import LoadingSpinner from '../../components/LoadingSpinner'
import type { User } from '../../components/types'

const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8080'

type ChatUser = {
  id: string
  username: string
  displayName: string
  email: string
  avatar: string
  unit: string
  onlineStatus: boolean
  presenceStatus: 'online' | 'away' | 'offline'
  lastSeenAt?: string | null
  awaySince?: string | null
  lastOnlineAt?: string | null
}

type ChatLocation = {
  lat: number
  lng: number
  label: string
}

type ChatLinkPreview = {
  url: string
  title: string
  description: string
  imageUrl: string
  siteName: string
}

type ChatMessage = {
  id: string
  conversationId: string
  body: string
  createdAt: string
  editedAt?: string | null
  sender: ChatUser
  attachments: ChatAttachment[]
  replyTo?: ChatMessageReference | null
  forwardedFrom?: ChatMessageReference | null
  location?: ChatLocation | null
  linkPreview?: ChatLinkPreview | null
}

type ChatAttachment = {
  id: string
  fileName: string
  contentType: string
  sizeBytes: number
  url: string
}

type ChatMessageReference = {
  id: string
  body: string
  senderDisplayName: string
  createdAt: string
}

type ChatConversation = {
  id: string
  type: 'team' | 'direct' | 'group' | 'channel'
  name: string
  image: string
  teamId: string
  participants: ChatUser[]
  lastMessage?: ChatMessage | null
  lastMessageAt?: string | null
  muted: boolean
  unreadCount: number
  createdAt: string
}

type ChatSearchResult = {
  conversationId: string
  messageId: string
  body: string
  senderDisplayName: string
  createdAt: string
}

type ChatPageProps = {
  currentUser: User
}

type ChatSocketEvent = {
  type:
    | 'connected'
    | 'message.created'
    | 'message.updated'
    | 'typing.updated'
    | 'error'
  clientId?: string
  conversationId?: string
  userId?: string
  userDisplayName?: string
  typing?: boolean
  message?: ChatMessage
  error?: string
}

type UserNotificationEvent = CustomEvent<{
  data?: {
    user?: Partial<ChatUser> & { id: string }
  }
}>

function classNames(...classes: Array<string | false | null | undefined>) {
  return classes.filter(Boolean).join(' ')
}

function displayName(user: ChatUser) {
  return user.displayName || user.username || user.email
}

function conversationTitle(
  conversation: ChatConversation,
  currentUserId: string,
) {
  if (conversation.type !== 'direct') {
    return conversation.name
  }

  const otherUser = conversation.participants.find(
    (participant) => participant.id !== currentUserId,
  )

  return otherUser ? displayName(otherUser) : conversation.name || 'Chat'
}

function formatLastSeen(value?: string | null) {
  if (!value) {
    return 'Offline'
  }

  const date = new Date(value)
  if (Number.isNaN(date.getTime())) {
    return 'Offline'
  }

  const today = new Date()
  const yesterday = new Date()
  yesterday.setDate(today.getDate() - 1)

  const time = new Intl.DateTimeFormat('de-DE', {
    hour: '2-digit',
    minute: '2-digit',
  }).format(date)

  if (date.toDateString() === today.toDateString()) {
    return `Zuletzt online heute um ${time}`
  }
  if (date.toDateString() === yesterday.toDateString()) {
    return `Zuletzt online gestern um ${time}`
  }

  return `Zuletzt online am ${new Intl.DateTimeFormat('de-DE', {
    day: '2-digit',
    month: '2-digit',
    year: 'numeric',
  }).format(date)} um ${time}`
}

function formatAwayDuration(value: string, now: number) {
  const awaySince = new Date(value).getTime()
  if (Number.isNaN(awaySince)) {
    return 'Abwesend'
  }

  const totalMinutes = Math.max(0, Math.floor((now - awaySince) / 60_000))
  if (totalMinutes < 1) {
    return 'Seit gerade eben abwesend'
  }
  if (totalMinutes < 60) {
    return `Seit ${totalMinutes} ${totalMinutes === 1 ? 'Minute' : 'Minuten'} abwesend`
  }

  const totalHours = Math.floor(totalMinutes / 60)
  const remainingMinutes = totalMinutes % 60
  if (totalHours < 24) {
    return remainingMinutes > 0
      ? `Seit ${totalHours} Std. ${remainingMinutes} Min. abwesend`
      : `Seit ${totalHours} ${totalHours === 1 ? 'Stunde' : 'Stunden'} abwesend`
  }

  const days = Math.floor(totalHours / 24)
  return `Seit ${days} ${days === 1 ? 'Tag' : 'Tagen'} abwesend`
}

function userStatusText(user: ChatUser | undefined, now: number) {
  if (!user) {
    return 'Offline'
  }
  if (user.presenceStatus === 'online') {
    return 'Online'
  }
  if (user.presenceStatus === 'away') {
    return user.awaySince
      ? formatAwayDuration(user.awaySince, now)
      : 'Abwesend'
  }
  return formatLastSeen(user.lastOnlineAt)
}

function conversationPartner(
  conversation: ChatConversation,
  currentUserId: string,
) {
  return conversation.participants.find(
    (participant) => participant.id !== currentUserId,
  )
}

function DirectTypingIndicator({ user }: { user: ChatUser | undefined }) {
  const name = user ? displayName(user) : 'Die andere Person'

  return (
    <div
      role="status"
      aria-live="polite"
      className="flex items-end gap-2"
    >
      <Avatar
        src={user?.avatar}
        name={name}
        size={8}
        presenceStatus={user?.presenceStatus}
      />

      <div className="rounded-2xl rounded-bl-sm bg-white px-3.5 py-3 shadow-sm ring-1 ring-gray-200 dark:bg-gray-800 dark:ring-white/10">
        <span className="sr-only">{name} schreibt gerade.</span>
        <span aria-hidden="true" className="flex h-3 items-center gap-1">
          {[-0.3, -0.15, 0].map((delay) => (
            <span
              key={delay}
              className="size-1.5 animate-bounce rounded-full bg-gray-400 motion-reduce:animate-none dark:bg-gray-300"
              style={{ animationDelay: `${delay}s` }}
            />
          ))}
        </span>
      </div>
    </div>
  )
}

function formatTime(value: string) {
  return new Intl.DateTimeFormat('de-DE', {
    hour: '2-digit',
    minute: '2-digit',
  }).format(new Date(value))
}

function isSameMessageDay(first: string, second: string) {
  const firstDate = new Date(first)
  const secondDate = new Date(second)

  if (
    Number.isNaN(firstDate.getTime()) ||
    Number.isNaN(secondDate.getTime())
  ) {
    return first === second
  }

  return (
    firstDate.getFullYear() === secondDate.getFullYear() &&
    firstDate.getMonth() === secondDate.getMonth() &&
    firstDate.getDate() === secondDate.getDate()
  )
}

function formatMessageDate(value: string) {
  const date = new Date(value)
  if (Number.isNaN(date.getTime())) {
    return ''
  }

  const today = new Date()
  const yesterday = new Date(today)
  yesterday.setDate(today.getDate() - 1)

  if (isSameMessageDay(value, today.toISOString())) {
    return 'Heute'
  }
  if (isSameMessageDay(value, yesterday.toISOString())) {
    return 'Gestern'
  }

  return new Intl.DateTimeFormat('de-DE', {
    weekday: 'long',
    day: '2-digit',
    month: 'long',
    year: date.getFullYear() === today.getFullYear() ? undefined : 'numeric',
  }).format(date)
}

function formatConversationTime(value?: string | null) {
  if (!value) {
    return ''
  }

  const date = new Date(value)
  const today = new Date()
  const isToday = date.toDateString() === today.toDateString()

  return new Intl.DateTimeFormat('de-DE', isToday
    ? { hour: '2-digit', minute: '2-digit' }
    : { day: '2-digit', month: '2-digit' }
  ).format(date)
}

function MessageLinkPreview({
  preview,
  ownMessage,
}: {
  preview: ChatLinkPreview
  ownMessage: boolean
}) {
  return (
    <a
      href={preview.url}
      target="_blank"
      rel="noreferrer noopener"
      className={classNames(
        ownMessage
          ? 'border-white/20 bg-white/10 hover:bg-white/15'
          : 'border-gray-200 bg-gray-50 hover:bg-gray-100 dark:border-white/10 dark:bg-white/5 dark:hover:bg-white/10',
        'mb-2 block w-72 max-w-full overflow-hidden rounded-lg border transition',
      )}
    >
      {preview.imageUrl && (
        <img
          src={preview.imageUrl}
          alt=""
          loading="lazy"
          className="h-32 w-full object-cover"
        />
      )}
      <div className="p-2.5">
        {preview.siteName && (
          <p
            className={classNames(
              ownMessage ? 'text-indigo-100' : 'text-gray-400',
              'truncate text-[0.625rem] font-medium uppercase tracking-wide',
            )}
          >
            {preview.siteName}
          </p>
        )}
        <p className="line-clamp-2 text-sm font-semibold">
          {preview.title || preview.url}
        </p>
        {preview.description && (
          <p
            className={classNames(
              ownMessage ? 'text-indigo-100' : 'text-gray-500 dark:text-gray-400',
              'mt-0.5 line-clamp-2 text-xs',
            )}
          >
            {preview.description}
          </p>
        )}
      </div>
    </a>
  )
}

// Textkürzel wie :) :D ;) <3 werden direkt in Emojis umgewandelt. Die Wort-
// grenzen (^|\s) … (?=\s|$) verhindern Treffer innerhalb von URLs oder Wörtern.
const emoticonReplacements: Array<[RegExp, string]> = [
  [/(^|\s)<3(?=\s|$)/g, '$1❤️'],
  [/(^|\s):'\((?=\s|$)/g, '$1😢'],
  [/(^|\s):-?\)(?=\s|$)/g, '$1🙂'],
  [/(^|\s):-?D(?=\s|$)/g, '$1😀'],
  [/(^|\s):-?\((?=\s|$)/g, '$1🙁'],
  [/(^|\s);-?\)(?=\s|$)/g, '$1😉'],
  [/(^|\s):-?[pP](?=\s|$)/g, '$1😛'],
  [/(^|\s):-?[oO](?=\s|$)/g, '$1😮'],
  [/(^|\s):-?\|(?=\s|$)/g, '$1😐'],
  [/(^|\s):-?\/(?=\s|$)/g, '$1😕'],
  [/(^|\s)8-?\)(?=\s|$)/g, '$1😎'],
]

function convertEmoticons(text: string) {
  let result = text
  for (const [pattern, replacement] of emoticonReplacements) {
    result = result.replace(pattern, replacement)
  }
  return result
}

const chatUrlPattern = /https?:\/\/[^\s<>"']+/

function extractFirstUrl(text: string) {
  const match = text.match(chatUrlPattern)
  if (!match) {
    return ''
  }
  return match[0].replace(/[.,!?;:)\]}'"]+$/, '')
}

function lastMessagePreviewText(message?: ChatMessage | null) {
  if (!message) {
    return ''
  }
  if (message.body) {
    return message.body
  }
  if (message.location) {
    return '📍 Standort'
  }
  if (message.attachments?.length) {
    return '📎 Datei'
  }
  return ''
}

const chatEmojis = [
  '😀',
  '😂',
  '😊',
  '😍',
  '👍',
  '👏',
  '🙏',
  '🎉',
  '❤️',
  '🔥',
  '✅',
  '🤔',
  '😅',
  '😢',
  '😮',
  '🚀',
]

function formatFileSize(size: number) {
  if (size < 1024) {
    return `${size} B`
  }
  if (size < 1024 * 1024) {
    return `${Math.round(size / 1024)} KB`
  }
  return `${(size / (1024 * 1024)).toFixed(1)} MB`
}

function locationMapsUrl(lat: number, lng: number) {
  return `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=16/${lat}/${lng}`
}

// Leichtgewichtige statische Karten-Vorschau: eine einzelne OSM-Kachel, vertikal
// auf den Punkt zugeschnitten, mit exakt platziertem Pin. Klick öffnet die Karte.
function LocationPreview({
  location,
  ownMessage,
}: {
  location: ChatLocation
  ownMessage: boolean
}) {
  const zoom = 15
  const tileCount = 2 ** zoom
  const latRad = (location.lat * Math.PI) / 180
  const xWorld = ((location.lng + 180) / 360) * tileCount
  const yWorld =
    ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) *
    tileCount

  const xTile = Math.floor(xWorld)
  const yTile = Math.floor(yWorld)
  const pointX = (xWorld - xTile) * 256
  const pointY = (yWorld - yTile) * 256

  const viewportHeight = 160
  const cropTop = Math.min(Math.max(pointY - viewportHeight / 2, 0), 256 - viewportHeight)

  const tileUrl = `https://tile.openstreetmap.org/${zoom}/${xTile}/${yTile}.png`

  return (
    <a
      href={locationMapsUrl(location.lat, location.lng)}
      target="_blank"
      rel="noreferrer noopener"
      className={classNames(
        ownMessage
          ? 'border-white/20 bg-white/10 hover:bg-white/15'
          : 'border-gray-200 bg-gray-50 hover:bg-gray-100 dark:border-white/10 dark:bg-white/5 dark:hover:bg-white/10',
        'mt-1 block w-64 max-w-full overflow-hidden rounded-lg border transition',
      )}
    >
      <div
        className="relative w-full overflow-hidden bg-gray-200 dark:bg-gray-700"
        style={{ height: `${viewportHeight}px` }}
      >
        <img
          src={tileUrl}
          alt="Kartenausschnitt"
          width={256}
          height={256}
          loading="lazy"
          className="absolute left-0 h-64 w-64 max-w-none"
          style={{ top: `-${cropTop}px` }}
        />
        <MapPinSolidIcon
          className="absolute size-7 -translate-x-1/2 -translate-y-full text-red-600 drop-shadow-md"
          style={{ left: `${pointX}px`, top: `${pointY - cropTop}px` }}
        />
      </div>
      <div className="flex items-center gap-1.5 px-2.5 py-2">
        <MapPinIcon className="size-4 shrink-0 opacity-70" />
        <span className="min-w-0 truncate text-xs font-medium">
          {location.label || 'Geteilter Standort'}
        </span>
      </div>
    </a>
  )
}

function renderMessageText(body: string, ownMessage: boolean): ReactNode {
  const urlPattern = /(https?:\/\/[^\s]+)/gi
  const parts = body.split(urlPattern)

  return parts.map((part, index) => {
    if (!/^https?:\/\//i.test(part)) {
      return <span key={`${index}-${part}`}>{part}</span>
    }

    return (
      <a
        key={`${index}-${part}`}
        href={part}
        target="_blank"
        rel="noreferrer noopener"
        className={classNames(
          ownMessage
            ? 'text-white underline decoration-indigo-200'
            : 'text-indigo-600 underline dark:text-indigo-300',
          'break-all underline-offset-2 hover:no-underline',
        )}
      >
        {part}
      </a>
    )
  })
}

async function readApiError(response: Response, fallback: string) {
  try {
    const data = await response.json()
    return data.error || fallback
  } catch {
    return fallback
  }
}

function getChatWebSocketUrl() {
  const url = new URL(`${API_URL.replace(/\/$/, '')}/chat/ws`)
  url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
  return url.toString()
}

export default function ChatPage({ currentUser }: ChatPageProps) {
  const navigate = useNavigate()
  const { channelId, conversationId, teamId } = useParams()
  const messagesEndRef = useRef<HTMLDivElement | null>(null)
  const attachmentInputRef = useRef<HTMLInputElement | null>(null)
  const lastPreviewUrlRef = useRef('')
  const chatSocketRef = useRef<WebSocket | null>(null)
  const selectedConversationIdRef = useRef<string | undefined>(undefined)
  const typingConversationIdRef = useRef<string | undefined>(undefined)
  const typingStopTimerRef = useRef<number | undefined>(undefined)
  const pendingMessageRef = useRef<{
    clientId: string
    body: string
    attachments: ChatAttachment[]
    replyTo?: ChatMessage | null
    forwardedFrom?: ChatMessage | null
  } | null>(null)

  const [conversations, setConversations] = useState<ChatConversation[]>([])
  const [users, setUsers] = useState<ChatUser[]>([])
  const [messages, setMessages] = useState<ChatMessage[]>([])
  const [activeTab, setActiveTab] = useState<'chats' | 'channels'>(() =>
    channelId ? 'channels' : 'chats',
  )
  const [search, setSearch] = useState('')
  const [searchResults, setSearchResults] = useState<ChatSearchResult[]>([])
  const [searchingMessages, setSearchingMessages] = useState(false)
  const [userSearch, setUserSearch] = useState('')
  const [messageBody, setMessageBody] = useState('')
  const [pendingAttachments, setPendingAttachments] = useState<ChatAttachment[]>([])
  const [replyTo, setReplyTo] = useState<ChatMessage | null>(null)
  const [forwardMessage, setForwardMessage] = useState<ChatMessage | null>(null)
  const [forwardDraft, setForwardDraft] = useState<ChatMessage | null>(null)
  const [emojiOpen, setEmojiOpen] = useState(false)
  const [uploadingAttachment, setUploadingAttachment] = useState(false)
  const [loadingConversations, setLoadingConversations] = useState(true)
  const [loadingMessages, setLoadingMessages] = useState(true)
  const [sending, setSending] = useState(false)
  const [startingDirectChat, setStartingDirectChat] = useState(false)
  const [newChatOpen, setNewChatOpen] = useState(false)
  const [membersOpen, setMembersOpen] = useState(false)
  const [locationPickerOpen, setLocationPickerOpen] = useState(false)
  const [locationDraft, setLocationDraft] = useState<{
    lat: number
    lon: number
  } | null>(null)
  const [locationLabel, setLocationLabel] = useState('')
  const [attachMenuOpen, setAttachMenuOpen] = useState(false)
  const [draftPreview, setDraftPreview] = useState<ChatLinkPreview | null>(null)
  const [dismissedPreviewUrl, setDismissedPreviewUrl] = useState('')
  const [socketConnected, setSocketConnected] = useState(false)
  const [statusNow, setStatusNow] = useState(() => Date.now())
  const [typingUsers, setTypingUsers] = useState<
    Record<string, Record<string, { name: string; expiresAt: number }>>
  >({})
  const [error, setError] = useState('')

  const selectedConversation = useMemo(() => {
    if (channelId) {
      return conversations.find(
        (conversation) =>
          conversation.id === channelId && conversation.type === 'channel',
      )
    }

    if (teamId) {
      return conversations.find(
        (conversation) => conversation.teamId === teamId,
      )
    }

    if (conversationId) {
      return conversations.find(
        (conversation) => conversation.id === conversationId,
      )
    }

    return undefined
  }, [channelId, conversationId, conversations, teamId])

  const visibleTab = selectedConversation
    ? selectedConversation.type === 'channel'
      ? 'channels'
      : 'chats'
    : activeTab

  const tabConversations = useMemo(
    () =>
      conversations.filter((conversation) =>
        visibleTab === 'channels'
          ? conversation.type === 'channel'
          : conversation.type !== 'channel',
      ),
    [conversations, visibleTab],
  )

  const unreadByTab = useMemo(
    () =>
      conversations.reduce(
        (counts, conversation) => {
          const tab =
            conversation.type === 'channel' ? 'channels' : 'chats'
          counts[tab] += conversation.unreadCount
          return counts
        },
        { chats: 0, channels: 0 },
      ),
    [conversations],
  )

  const filteredConversations = useMemo(() => {
    const normalizedSearch = search.trim().toLowerCase()
    if (!normalizedSearch) {
      return tabConversations
    }

    const matchingConversationIds = new Set(
      searchResults.map((result) => result.conversationId),
    )

    return tabConversations.filter(
      (conversation) =>
        conversationTitle(conversation, currentUser.id)
          .toLowerCase()
          .includes(normalizedSearch) ||
        matchingConversationIds.has(conversation.id),
    )
  }, [currentUser.id, search, searchResults, tabConversations])

  const searchResultByConversation = useMemo(
    () =>
      new Map(
        searchResults.map((result) => [result.conversationId, result] as const),
      ),
    [searchResults],
  )

  const filteredUsers = useMemo(() => {
    const normalizedSearch = userSearch.trim().toLowerCase()
    if (!normalizedSearch) {
      return users
    }

    return users.filter((user) =>
      [displayName(user), user.email, user.unit]
        .join(' ')
        .toLowerCase()
        .includes(normalizedSearch),
    )
  }, [userSearch, users])

  async function loadOverview(showLoading: boolean) {
    try {
      const [conversationsResponse, usersResponse] = await Promise.all([
        fetch(`${API_URL}/chat/conversations`, { credentials: 'include' }),
        fetch(`${API_URL}/chat/users`, { credentials: 'include' }),
      ])

      if (!conversationsResponse.ok || !usersResponse.ok) {
        throw new Error('Chats konnten nicht geladen werden')
      }

      const [conversationsData, usersData] = await Promise.all([
        conversationsResponse.json(),
        usersResponse.json(),
      ])

      setConversations(conversationsData.conversations ?? [])
      setUsers(usersData.users ?? [])
      setError('')
    } catch (loadError) {
      if (showLoading) {
        setError(
          loadError instanceof Error
            ? loadError.message
            : 'Chats konnten nicht geladen werden',
        )
      }
    } finally {
      if (showLoading) {
        setLoadingConversations(false)
      }
    }
  }

  async function loadMessages(selectedConversationId: string) {
    await Promise.resolve()
    setLoadingMessages(true)
    setError('')

    try {
      const response = await fetch(
        `${API_URL}/chat/conversations/${selectedConversationId}/messages`,
        { credentials: 'include' },
      )

      if (!response.ok) {
        throw new Error(
          await readApiError(response, 'Nachrichten konnten nicht geladen werden'),
        )
      }

      const data = await response.json()
      setMessages(data.messages ?? [])
      setConversations((current) =>
        current.map((conversation) =>
          conversation.id === selectedConversationId
            ? { ...conversation, unreadCount: 0 }
            : conversation,
        ),
      )
      window.dispatchEvent(new Event('chat-unread-changed'))
    } catch (loadError) {
      setError(
        loadError instanceof Error
          ? loadError.message
          : 'Nachrichten konnten nicht geladen werden',
      )
    } finally {
      setLoadingMessages(false)
    }
  }

  async function startDirectChat(userId: string) {
    setStartingDirectChat(true)
    setError('')

    try {
      const response = await fetch(`${API_URL}/chat/direct`, {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId }),
      })

      if (!response.ok) {
        throw new Error(
          await readApiError(response, 'Direktchat konnte nicht geöffnet werden'),
        )
      }

      const data = await response.json()
      const conversation = data.conversation as ChatConversation

      setConversations((current) => [
        conversation,
        ...current.filter((item) => item.id !== conversation.id),
      ])
      setNewChatOpen(false)
      setUserSearch('')
      navigate(`/chat/${conversation.id}`)
    } catch (startError) {
      setError(
        startError instanceof Error
          ? startError.message
          : 'Direktchat konnte nicht geöffnet werden',
      )
    } finally {
      setStartingDirectChat(false)
    }
  }

  async function toggleConversationMute(conversation: ChatConversation) {
    const muted = !conversation.muted
    setError('')

    try {
      const response = await fetch(
        `${API_URL}/chat/conversations/${conversation.id}/mute`,
        {
          method: 'PUT',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ muted }),
        },
      )
      if (!response.ok) {
        throw new Error(
          await readApiError(
            response,
            'Stummschaltung konnte nicht gespeichert werden',
          ),
        )
      }

      setConversations((current) =>
        current.map((item) =>
          item.id === conversation.id ? { ...item, muted } : item,
        ),
      )
    } catch (muteError) {
      setError(
        muteError instanceof Error
          ? muteError.message
          : 'Stummschaltung konnte nicht gespeichert werden',
      )
    }
  }

  const sendTypingState = useCallback((conversationId: string, typing: boolean) => {
    const socket = chatSocketRef.current
    if (!socket || socket.readyState !== WebSocket.OPEN) {
      return
    }

    socket.send(
      JSON.stringify({
        type: typing ? 'typing.start' : 'typing.stop',
        conversationId,
      }),
    )
  }, [])

  const stopTyping = useCallback(() => {
    if (typingStopTimerRef.current !== undefined) {
      window.clearTimeout(typingStopTimerRef.current)
      typingStopTimerRef.current = undefined
    }

    const conversationId = typingConversationIdRef.current
    if (conversationId) {
      sendTypingState(conversationId, false)
      typingConversationIdRef.current = undefined
    }
  }, [sendTypingState])

  function handleMessageBodyChange(value: string) {
    const nextValue = convertEmoticons(value)
    setMessageBody(nextValue)

    const nextUrl = extractFirstUrl(nextValue)
    if (!nextUrl || nextUrl === dismissedPreviewUrl) {
      setDraftPreview(null)
      lastPreviewUrlRef.current = ''
    }

    if (!selectedConversationId || !nextValue.trim()) {
      stopTyping()
      return
    }

    if (typingConversationIdRef.current !== selectedConversationId) {
      stopTyping()
      sendTypingState(selectedConversationId, true)
      typingConversationIdRef.current = selectedConversationId
    }

    if (typingStopTimerRef.current !== undefined) {
      window.clearTimeout(typingStopTimerRef.current)
    }
    typingStopTimerRef.current = window.setTimeout(stopTyping, 2_000)
  }

  async function markConversationRead(conversationId: string) {
    setConversations((current) =>
      current.map((conversation) =>
        conversation.id === conversationId
          ? { ...conversation, unreadCount: 0 }
          : conversation,
      ),
    )
    window.dispatchEvent(new Event('chat-unread-changed'))

    await fetch(`${API_URL}/chat/conversations/${conversationId}/read`, {
      method: 'POST',
      credentials: 'include',
    }).catch(() => undefined)
    window.dispatchEvent(new Event('chat-unread-changed'))
  }

  function sendSocketMessage(
    conversation: ChatConversation,
    body: string,
    options?: {
      attachments?: ChatAttachment[]
      replyToMessageId?: string
      forwardedMessage?: ChatMessage
      location?: ChatLocation
    },
  ) {
    const socket = chatSocketRef.current
    if (!socket || socket.readyState !== WebSocket.OPEN) {
      setError('Die Chat-Verbindung wird gerade neu aufgebaut.')
      return false
    }

    const clientId = crypto.randomUUID()
    pendingMessageRef.current = {
      clientId,
      body,
      attachments: options?.attachments ?? [],
      replyTo,
      forwardedFrom: options?.forwardedMessage,
    }
    setSending(true)
    setError('')

    try {
      socket.send(
        JSON.stringify({
          type: 'message.send',
          clientId,
          conversationId: conversation.id,
          body,
          attachmentIds: (options?.attachments ?? []).map(
            (attachment) => attachment.id,
          ),
          replyToMessageId: options?.replyToMessageId ?? '',
          forwardedFromMessageId: options?.forwardedMessage?.id ?? '',
          location: options?.location ?? null,
        }),
      )
      return true
    } catch {
      pendingMessageRef.current = null
      setSending(false)
      setError('Nachricht konnte nicht gesendet werden')
      return false
    }
  }

  function sendMessage(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()

    const body = messageBody.trim()
    if (
      (!body && pendingAttachments.length === 0 && !forwardDraft) ||
      !selectedConversation ||
      sending
    ) {
      return
    }

    const sent = sendSocketMessage(selectedConversation, body, {
      attachments: pendingAttachments,
      replyToMessageId: replyTo?.id,
      forwardedMessage: forwardDraft ?? undefined,
    })

    if (sent) {
      stopTyping()
      setMessageBody('')
      setPendingAttachments([])
      setReplyTo(null)
      setForwardDraft(null)
      setEmojiOpen(false)
      setDraftPreview(null)
      setDismissedPreviewUrl('')
      lastPreviewUrlRef.current = ''
    }
  }

  function sendLocation() {
    if (!selectedConversation || !locationDraft || sending) {
      return
    }

    const sent = sendSocketMessage(selectedConversation, '', {
      location: {
        lat: locationDraft.lat,
        lng: locationDraft.lon,
        label: locationLabel.trim(),
      },
    })

    if (sent) {
      setLocationPickerOpen(false)
      setLocationDraft(null)
      setLocationLabel('')
    }
  }

  async function uploadAttachment(event: ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0]
    event.target.value = ''

    if (!file) {
      return
    }
    if (file.size > 10 * 1024 * 1024) {
      setError('Dateien dürfen höchstens 10 MB groß sein.')
      return
    }

    setUploadingAttachment(true)
    setError('')

    try {
      const formData = new FormData()
      formData.append('file', file)

      const response = await fetch(`${API_URL}/chat/attachments`, {
        method: 'POST',
        credentials: 'include',
        body: formData,
      })
      if (!response.ok) {
        throw new Error(
          await readApiError(response, 'Datei konnte nicht hochgeladen werden'),
        )
      }

      const data = await response.json()
      setPendingAttachments((current) => [
        ...current,
        data.attachment as ChatAttachment,
      ])
    } catch (uploadError) {
      setError(
        uploadError instanceof Error
          ? uploadError.message
          : 'Datei konnte nicht hochgeladen werden',
      )
    } finally {
      setUploadingAttachment(false)
    }
  }

  function forwardToConversation(conversation: ChatConversation) {
    if (
      !forwardMessage ||
      sending ||
      conversation.type === 'channel' ||
      conversation.id === forwardMessage.conversationId
    ) {
      return
    }

    setMessageBody('')
    setPendingAttachments([])
    setReplyTo(null)
    setDraftPreview(null)
    setDismissedPreviewUrl('')
    setForwardDraft(forwardMessage)
    setForwardMessage(null)
    openConversation(conversation, true)
  }

  function openConversation(
    conversation: ChatConversation,
    preserveForwardDraft = false,
  ) {
    setLoadingMessages(true)
    setError('')
    if (!preserveForwardDraft) {
      setForwardDraft(null)
    }

    if (conversation.type === 'channel') {
      setActiveTab('channels')
      navigate(`/chat/channel/${conversation.id}`)
      return
    }

    setActiveTab('chats')
    if (conversation.type === 'team' && conversation.teamId) {
      navigate(`/chat/team/${conversation.teamId}`)
      return
    }

    navigate(`/chat/${conversation.id}`)
  }

  const selectedConversationId = selectedConversation?.id

  useEffect(() => {
    selectedConversationIdRef.current = selectedConversationId
  }, [selectedConversationId])

  useEffect(() => {
    let socket: WebSocket | null = null
    let reconnectTimer: number | undefined
    let reconnectAttempt = 0
    let stopped = false

    function restorePendingMessage(message: string) {
      const pendingMessage = pendingMessageRef.current
      pendingMessageRef.current = null

      if (pendingMessage) {
        setMessageBody((current) => current || pendingMessage.body)
        setPendingAttachments((current) =>
          current.length > 0 ? current : pendingMessage.attachments,
        )
        if (pendingMessage.replyTo) {
          setReplyTo(pendingMessage.replyTo)
        }
        if (pendingMessage.forwardedFrom) {
          setForwardDraft(pendingMessage.forwardedFrom)
        }
      }

      setSending(false)
      setError(message)
    }

    function connect() {
      socket = new WebSocket(getChatWebSocketUrl())
      chatSocketRef.current = socket

      socket.onopen = () => {
        reconnectAttempt = 0
        setSocketConnected(true)
      }

      socket.onmessage = (event) => {
        let payload: ChatSocketEvent

        try {
          payload = JSON.parse(event.data) as ChatSocketEvent
        } catch {
          return
        }

        if (payload.type === 'connected') {
          setSocketConnected(true)
          return
        }

        if (payload.type === 'error') {
          if (
            payload.clientId &&
            payload.clientId === pendingMessageRef.current?.clientId
          ) {
            restorePendingMessage(
              payload.error || 'Nachricht konnte nicht gesendet werden',
            )
          }
          return
        }

        if (
          payload.type === 'typing.updated' &&
          payload.conversationId &&
          payload.userId
        ) {
          setTypingUsers((current) => {
            const conversationUsers = {
              ...(current[payload.conversationId as string] ?? {}),
            }

            if (payload.typing) {
              conversationUsers[payload.userId as string] = {
                name: payload.userDisplayName || 'Jemand',
                expiresAt: Date.now() + 4_000,
              }
            } else {
              delete conversationUsers[payload.userId as string]
            }

            return {
              ...current,
              [payload.conversationId as string]: conversationUsers,
            }
          })
          return
        }

        const message = payload.message
        if (!message) {
          return
        }

        if (payload.type === 'message.updated') {
          setMessages((current) =>
            current.map((item) => (item.id === message.id ? message : item)),
          )
          setConversations((current) =>
            current.map((conversation) =>
              conversation.lastMessage?.id === message.id
                ? { ...conversation, lastMessage: message }
                : conversation,
            ),
          )
          return
        }

        if (payload.type !== 'message.created') {
          return
        }

        setConversations((current) => {
          const next = current.map((conversation) =>
            conversation.id === message.conversationId
              ? {
                  ...conversation,
                  lastMessage: message,
                  lastMessageAt: message.createdAt,
                  unreadCount:
                    message.sender.id === currentUser.id ||
                    message.conversationId === selectedConversationIdRef.current
                      ? 0
                      : (conversation.unreadCount ?? 0) + 1,
                }
              : conversation,
          )

          return [...next].sort(
            (a, b) =>
              new Date(b.lastMessageAt || b.createdAt).getTime() -
              new Date(a.lastMessageAt || a.createdAt).getTime(),
          )
        })

        if (
          message.conversationId === selectedConversationIdRef.current
        ) {
          setMessages((current) =>
            current.some((item) => item.id === message.id)
              ? current
              : [...current, message],
          )
          if (message.sender.id !== currentUser.id) {
            void markConversationRead(message.conversationId)
          }
        } else if (message.sender.id !== currentUser.id) {
          window.dispatchEvent(new Event('chat-unread-changed'))
        }

        if (
          payload.clientId &&
          payload.clientId === pendingMessageRef.current?.clientId
        ) {
          pendingMessageRef.current = null
          setSending(false)
        }
      }

      socket.onclose = () => {
        if (chatSocketRef.current === socket) {
          chatSocketRef.current = null
        }

        setSocketConnected(false)
        typingConversationIdRef.current = undefined

        if (pendingMessageRef.current) {
          restorePendingMessage(
            'Die Verbindung wurde unterbrochen. Die Nachricht wurde nicht gesendet.',
          )
        }

        if (stopped) {
          return
        }

        const delay = Math.min(1000 * 2 ** reconnectAttempt, 10_000)
        reconnectAttempt += 1
        reconnectTimer = window.setTimeout(connect, delay)
      }
    }

    connect()

    return () => {
      stopped = true
      setSocketConnected(false)

      if (reconnectTimer !== undefined) {
        window.clearTimeout(reconnectTimer)
      }

      chatSocketRef.current = null
      stopTyping()
      socket?.close(1000, 'Chat geschlossen')
    }
  }, [currentUser.id, stopTyping])

  useEffect(() => {
    const initialLoadId = window.setTimeout(() => {
      void loadOverview(true)
    }, 0)

    const intervalId = window.setInterval(() => {
      void loadOverview(false)
    }, 30_000)

    return () => {
      window.clearTimeout(initialLoadId)
      window.clearInterval(intervalId)
    }
  }, [])

  useEffect(() => {
    const query = search.trim()
    if (query.length < 2) {
      return
    }

    const controller = new AbortController()
    const timeoutId = window.setTimeout(() => {
      setSearchingMessages(true)
      void fetch(`${API_URL}/chat/search?q=${encodeURIComponent(query)}`, {
        credentials: 'include',
        signal: controller.signal,
      })
        .then((response) => (response.ok ? response.json() : null))
        .then((data) => {
          if (data) {
            setSearchResults(data.results ?? [])
          }
        })
        .catch(() => undefined)
        .finally(() => {
          if (!controller.signal.aborted) {
            setSearchingMessages(false)
          }
        })
    }, 250)

    return () => {
      window.clearTimeout(timeoutId)
      controller.abort()
    }
  }, [search])

  useEffect(() => {
    const intervalId = window.setInterval(() => {
      setStatusNow(Date.now())
    }, 30_000)

    return () => window.clearInterval(intervalId)
  }, [])

  useEffect(() => {
    const intervalId = window.setInterval(() => {
      const now = Date.now()
      setTypingUsers((current) => {
        let changed = false
        const next: typeof current = {}

        for (const [conversationId, usersById] of Object.entries(current)) {
          const activeUsers = Object.fromEntries(
            Object.entries(usersById).filter(([, typingUser]) => {
              const active = typingUser.expiresAt > now
              changed ||= !active
              return active
            }),
          )
          next[conversationId] = activeUsers
        }

        return changed ? next : current
      })
    }, 1_000)

    return () => window.clearInterval(intervalId)
  }, [])

  useEffect(() => {
    function handleUserNotification(event: Event) {
      const updatedUser = (event as UserNotificationEvent).detail?.data?.user
      if (!updatedUser?.id) {
        return
      }

      const updateChatUser = (chatUser: ChatUser) =>
        chatUser.id === updatedUser.id
          ? { ...chatUser, ...updatedUser }
          : chatUser

      setUsers((current) => current.map(updateChatUser))
      setConversations((current) =>
        current.map((conversation) => ({
          ...conversation,
          participants: conversation.participants.map(updateChatUser),
          lastMessage: conversation.lastMessage
            ? {
                ...conversation.lastMessage,
                sender: updateChatUser(conversation.lastMessage.sender),
              }
            : conversation.lastMessage,
        })),
      )
      setMessages((current) =>
        current.map((message) => ({
          ...message,
          sender: updateChatUser(message.sender),
        })),
      )
      void loadOverview(false)
    }

    window.addEventListener('user-notification', handleUserNotification)
    return () => {
      window.removeEventListener('user-notification', handleUserNotification)
    }
  }, [])

  useEffect(() => {
    if (!selectedConversationId) {
      stopTyping()
      return
    }

    if (
      typingConversationIdRef.current &&
      typingConversationIdRef.current !== selectedConversationId
    ) {
      stopTyping()
    }

    const loadId = window.setTimeout(() => {
      void loadMessages(selectedConversationId)
    }, 0)

    return () => window.clearTimeout(loadId)
  }, [selectedConversationId, stopTyping])

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  useEffect(() => {
    const url = extractFirstUrl(messageBody)

    if (!url || url === dismissedPreviewUrl) {
      lastPreviewUrlRef.current = ''
      return
    }

    if (url === lastPreviewUrlRef.current) {
      return
    }

    let cancelled = false
    const timeoutId = window.setTimeout(() => {
      lastPreviewUrlRef.current = url
      void fetch(`${API_URL}/chat/link-preview`, {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url }),
      })
        .then((response) => (response.ok ? response.json() : null))
        .then((data) => {
          if (!cancelled) {
            setDraftPreview(data?.preview ?? null)
          }
        })
        .catch(() => undefined)
    }, 600)

    return () => {
      cancelled = true
      window.clearTimeout(timeoutId)
    }
  }, [messageBody, dismissedPreviewUrl])

  const selectedPartner = selectedConversation
    ? conversationPartner(selectedConversation, currentUser.id)
    : undefined
  const selectedTypingUsers = selectedConversationId
    ? Object.values(typingUsers[selectedConversationId] ?? {})
    : []
  const selectedTypingText =
    selectedTypingUsers.length === 1
      ? `${selectedTypingUsers[0].name} schreibt...`
      : selectedTypingUsers.length > 1
        ? `${selectedTypingUsers.length} Personen schreiben...`
        : ''
  const showDirectTypingIndicator =
    selectedConversation?.type === 'direct' &&
    selectedTypingUsers.length > 0

  useEffect(() => {
    if (!showDirectTypingIndicator) {
      return
    }

    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [showDirectTypingIndicator])

  function handleSearchChange(value: string) {
    setSearch(value)
    if (value.trim().length < 2) {
      setSearchResults([])
      setSearchingMessages(false)
    }
  }

  function selectTab(tab: 'chats' | 'channels') {
    setActiveTab(tab)
    setSearch('')
    setSearchResults([])

    if (
      selectedConversation &&
      (selectedConversation.type === 'channel') !== (tab === 'channels')
    ) {
      navigate('/chat')
    }
  }

  return (
    <div className="flex h-full min-h-0 bg-gray-50 dark:bg-gray-950">
      <aside
        className={classNames(
          selectedConversation ? 'hidden md:flex' : 'flex',
          'w-full shrink-0 flex-col border-r border-gray-200 bg-white md:w-80 dark:border-white/10 dark:bg-gray-900',
        )}
      >
        <div className="border-b border-gray-200 p-4 dark:border-white/10">
          <div className="flex items-center justify-between gap-3">
            <div>
              <h1 className="text-lg font-semibold text-gray-950 dark:text-white">
                Nachrichten
              </h1>
              <p className="text-xs text-gray-500 dark:text-gray-400">
                Chats und Integrations-Channels
              </p>
            </div>
            {visibleTab === 'chats' && (
              <button
                type="button"
                onClick={() => setNewChatOpen(true)}
                className="inline-flex size-9 items-center justify-center rounded-full bg-indigo-600 text-white shadow-sm transition hover:bg-indigo-500"
              >
                <span className="sr-only">Neue Direktnachricht</span>
                <PlusIcon className="size-5" aria-hidden="true" />
              </button>
            )}
          </div>

          <div className="mt-4 grid grid-cols-2 rounded-xl bg-gray-100 p-1 dark:bg-white/5">
            {(['chats', 'channels'] as const).map((tab) => (
              <button
                key={tab}
                type="button"
                onClick={() => selectTab(tab)}
                className={classNames(
                  visibleTab === tab
                    ? 'bg-white text-indigo-700 shadow-sm dark:bg-gray-800 dark:text-indigo-300'
                    : 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white',
                  'flex items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition',
                )}
              >
                {tab === 'chats' ? 'Chats' : 'Channels'}
                {unreadByTab[tab] > 0 && (
                  <span className="inline-flex min-w-5 items-center justify-center rounded-full bg-indigo-600 px-1.5 py-0.5 text-[0.6875rem] font-semibold leading-none text-white">
                    {unreadByTab[tab] > 99 ? '99+' : unreadByTab[tab]}
                  </span>
                )}
              </button>
            ))}
          </div>

          <div className="relative mt-4">
            <MagnifyingGlassIcon
              className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-gray-400"
              aria-hidden="true"
            />
            <input
              value={search}
              onChange={(event) => handleSearchChange(event.target.value)}
              placeholder={
                visibleTab === 'channels'
                  ? 'Channels und Meldungen durchsuchen'
                  : 'Chats und Nachrichten durchsuchen'
              }
              className="block w-full rounded-xl border-0 bg-gray-100 py-2 pr-3 pl-9 text-sm text-gray-900 outline-none ring-1 ring-transparent placeholder:text-gray-500 focus:ring-indigo-500 dark:bg-white/5 dark:text-white dark:placeholder:text-gray-500"
            />
          </div>
        </div>

        <div className="min-h-0 flex-1 overflow-y-auto p-2">
          {loadingConversations ? (
            <div className="flex h-full items-center justify-center">
              <LoadingSpinner label="Chats werden geladen..." />
            </div>
          ) : filteredConversations.length === 0 ? (
            <div className="px-6 py-16 text-center">
              <ChatBubbleLeftRightIcon className="mx-auto size-10 text-gray-300 dark:text-gray-600" />
              <p className="mt-3 text-sm font-medium text-gray-900 dark:text-white">
                {search.trim()
                  ? 'Keine Treffer'
                  : visibleTab === 'channels'
                    ? 'Noch keine Channels'
                    : 'Noch keine Chats'}
              </p>
              <p className="mt-1 text-xs text-gray-500">
                {search.trim()
                  ? searchingMessages
                    ? 'Nachrichten werden durchsucht...'
                    : visibleTab === 'channels'
                      ? 'Kein Channel und keine Meldung passt zur Suche.'
                      : 'Kein Chat und keine Nachricht passt zur Suche.'
                  : visibleTab === 'channels'
                    ? 'Channels werden in der Administration eingerichtet.'
                    : 'Öffne ein Team oder starte eine Direktnachricht.'}
              </p>
            </div>
          ) : (
            <ul className="space-y-1">
              {filteredConversations.map((conversation) => {
                const partner = conversationPartner(
                  conversation,
                  currentUser.id,
                )
                const active = conversation.id === selectedConversation?.id
                const searchResult = searchResultByConversation.get(
                  conversation.id,
                )
                const conversationTypingUsers = Object.values(
                  typingUsers[conversation.id] ?? {},
                )

                return (
                  <li key={conversation.id}>
                    <button
                      type="button"
                      onClick={() => openConversation(conversation)}
                      className={classNames(
                        active
                          ? 'bg-indigo-50 ring-1 ring-indigo-100 dark:bg-indigo-500/10 dark:ring-indigo-400/20'
                          : 'hover:bg-gray-50 dark:hover:bg-white/5',
                        'flex w-full items-center gap-3 rounded-xl p-3 text-left transition',
                      )}
                    >
                      {conversation.type === 'channel' ? (
                        conversation.image ? (
                          <Avatar
                            src={conversation.image}
                            name={conversation.name}
                            size={10}
                            shape="rounded"
                          />
                        ) : (
                          <span className="inline-flex size-10 shrink-0 items-center justify-center rounded-xl bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300">
                            <HashtagIcon className="size-5" aria-hidden="true" />
                          </span>
                        )
                      ) : conversation.type !== 'direct' ? (
                        <span className="inline-flex size-10 shrink-0 items-center justify-center rounded-xl bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300">
                          <UserGroupIcon className="size-5" aria-hidden="true" />
                        </span>
                      ) : (
                        <Avatar
                          src={partner?.avatar}
                          name={partner ? displayName(partner) : 'Chat'}
                          size={10}
                          presenceStatus={partner?.presenceStatus}
                        />
                      )}

                      <span className="min-w-0 flex-1">
                        <span className="flex items-baseline justify-between gap-2">
                          <span className="truncate text-sm font-semibold text-gray-900 dark:text-white">
                            {conversationTitle(conversation, currentUser.id)}
                          </span>
                          <span className="shrink-0 text-[0.6875rem] text-gray-400">
                            <span className="flex items-center gap-1">
                              {conversation.muted && (
                                <BellSlashIcon className="size-3.5" />
                              )}
                              {formatConversationTime(
                                conversation.lastMessageAt,
                              )}
                              {conversation.unreadCount > 0 && (
                                <span className="inline-flex min-w-5 items-center justify-center rounded-full bg-indigo-600 px-1.5 py-0.5 text-[0.6875rem] font-semibold leading-none text-white">
                                  {conversation.unreadCount > 99
                                    ? '99+'
                                    : conversation.unreadCount}
                                </span>
                              )}
                            </span>
                          </span>
                        </span>
                        <span className="mt-0.5 block truncate text-xs text-gray-500 dark:text-gray-400">
                          {conversationTypingUsers.length > 0
                            ? conversationTypingUsers.length === 1
                              ? `${conversationTypingUsers[0].name} schreibt...`
                              : `${conversationTypingUsers.length} Personen schreiben...`
                            : search.trim() && searchResult
                              ? `${searchResult.senderDisplayName}: ${searchResult.body}`
                              : lastMessagePreviewText(conversation.lastMessage) ||
                                (conversation.type === 'channel'
                                  ? 'Schreibgeschützter Channel'
                                  : conversation.type !== 'direct'
                                  ? `${conversation.participants.length} Mitglieder`
                                  : userStatusText(partner, statusNow))}
                        </span>
                      </span>
                    </button>
                  </li>
                )
              })}
            </ul>
          )}
        </div>
      </aside>

      <section
        className={classNames(
          selectedConversation ? 'flex' : 'hidden md:flex',
          'min-w-0 flex-1 flex-col bg-white dark:bg-gray-900',
        )}
      >
        {selectedConversation ? (
          <>
            <header className="flex h-17 shrink-0 items-center gap-3 border-b border-gray-200 px-4 dark:border-white/10">
              <button
                type="button"
                onClick={() => navigate('/chat')}
                className="inline-flex size-9 items-center justify-center rounded-full text-gray-500 hover:bg-gray-100 md:hidden dark:hover:bg-white/10"
              >
                <span className="sr-only">Zurück zur Chatliste</span>
                <ArrowLeftIcon className="size-5" aria-hidden="true" />
              </button>

              {selectedConversation.type === 'channel' ? (
                selectedConversation.image ? (
                  <Avatar
                    src={selectedConversation.image}
                    name={selectedConversation.name}
                    size={10}
                    shape="rounded"
                  />
                ) : (
                  <span className="inline-flex size-10 items-center justify-center rounded-xl bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300">
                    <HashtagIcon className="size-5" aria-hidden="true" />
                  </span>
                )
              ) : selectedConversation.type !== 'direct' ? (
                <span className="inline-flex size-10 items-center justify-center rounded-xl bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300">
                  <UserGroupIcon className="size-5" aria-hidden="true" />
                </span>
              ) : (
                <Avatar
                  src={selectedPartner?.avatar}
                  name={selectedPartner ? displayName(selectedPartner) : 'Chat'}
                  size={10}
                  presenceStatus={selectedPartner?.presenceStatus}
                />
              )}

              {selectedConversation.type === 'channel' ? (
                <div className="min-w-0 flex-1">
                  <h2 className="truncate text-sm font-semibold text-gray-950 dark:text-white">
                    {conversationTitle(selectedConversation, currentUser.id)}
                  </h2>
                  <p className="truncate text-xs text-gray-500 dark:text-gray-400">
                    Schreibgeschützter Channel
                  </p>
                </div>
              ) : selectedConversation.type !== 'direct' ? (
                <button
                  type="button"
                  onClick={() => setMembersOpen(true)}
                  title="Mitglieder anzeigen"
                  className="min-w-0 flex-1 text-left"
                >
                  <h2 className="truncate text-sm font-semibold text-gray-950 dark:text-white">
                    {conversationTitle(selectedConversation, currentUser.id)}
                  </h2>
                  <p className="truncate text-xs text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400">
                    {selectedTypingText ||
                      `${selectedConversation.participants.length} Mitglieder anzeigen`}
                  </p>
                </button>
              ) : (
                <div className="min-w-0 flex-1">
                  <h2 className="truncate text-sm font-semibold text-gray-950 dark:text-white">
                    {conversationTitle(selectedConversation, currentUser.id)}
                  </h2>
                  <p className="truncate text-xs text-gray-500 dark:text-gray-400">
                    {selectedTypingText ||
                      userStatusText(selectedPartner, statusNow)}
                  </p>
                </div>
              )}

              {selectedConversation.type !== 'direct' && (
                <button
                  type="button"
                  onClick={() => void toggleConversationMute(selectedConversation)}
                  title={
                    selectedConversation.muted
                      ? 'Benachrichtigungen aktivieren'
                      : selectedConversation.type === 'channel'
                        ? 'Channel stummschalten'
                        : 'Chat stummschalten'
                  }
                  className="inline-flex size-9 shrink-0 items-center justify-center rounded-full text-gray-500 hover:bg-gray-100 hover:text-indigo-600 dark:hover:bg-white/10"
                >
                  {selectedConversation.muted ? (
                    <BellSlashIcon className="size-5" />
                  ) : (
                    <BellIcon className="size-5" />
                  )}
                </button>
              )}
            </header>

            <div className="min-h-0 flex-1 overflow-y-auto bg-gray-50 px-4 py-6 dark:bg-gray-950/60">
              {loadingMessages ? (
                <div className="flex h-full items-center justify-center">
                  <LoadingSpinner label="Nachrichten werden geladen..." />
                </div>
              ) : messages.length === 0 ? (
                <div className="mx-auto flex h-full max-w-3xl flex-col">
                  <div className="flex min-h-0 flex-1 items-center justify-center text-center">
                    <div>
                      <ChatBubbleLeftRightIcon className="mx-auto size-12 text-gray-300 dark:text-gray-600" />
                      <p className="mt-3 text-sm font-semibold text-gray-900 dark:text-white">
                        {selectedConversation.type === 'channel'
                          ? 'Noch keine Meldungen'
                          : 'Hier beginnt der Chat'}
                      </p>
                      <p className="mt-1 text-sm text-gray-500">
                        {selectedConversation.type === 'channel'
                          ? 'Neue Meldungen erscheinen hier automatisch.'
                          : 'Schreibe die erste Nachricht.'}
                      </p>
                    </div>
                  </div>

                  {showDirectTypingIndicator && (
                    <DirectTypingIndicator user={selectedPartner} />
                  )}
                  <div ref={messagesEndRef} />
                </div>
              ) : (
                <div className="mx-auto max-w-3xl space-y-4">
                  {messages.map((message, index) => {
                    const ownMessage = message.sender.id === currentUser.id
                    const previousMessage = messages[index - 1]
                    const showDateSeparator =
                      !previousMessage ||
                      !isSameMessageDay(
                        previousMessage.createdAt,
                        message.createdAt,
                      )
                    const dateLabel = showDateSeparator
                      ? formatMessageDate(message.createdAt)
                      : ''

                    return (
                      <Fragment key={message.id}>
                        {showDateSeparator && (
                          <div
                            role="separator"
                            aria-label={dateLabel}
                            className="flex items-center gap-3 py-1"
                          >
                            <span className="h-px flex-1 bg-gray-200 dark:bg-white/10" />
                            <span className="rounded-full bg-white px-3 py-1 text-[0.6875rem] font-semibold text-gray-500 shadow-sm ring-1 ring-gray-200 dark:bg-gray-900 dark:text-gray-400 dark:ring-white/10">
                              {dateLabel}
                            </span>
                            <span className="h-px flex-1 bg-gray-200 dark:bg-white/10" />
                          </div>
                        )}

                        <div
                          className={classNames(
                            ownMessage ? 'justify-end' : 'justify-start',
                            'flex items-end gap-2',
                          )}
                        >
                        {!ownMessage && (
                          <Avatar
                            src={message.sender.avatar}
                            name={displayName(message.sender)}
                            size={8}
                            presenceStatus={
                              selectedConversation.type === 'channel'
                                ? undefined
                                : message.sender.presenceStatus
                            }
                          />
                        )}
                        <div className="group relative max-w-[min(34rem,82%)]">
                          <div
                            className={classNames(
                              ownMessage
                                ? 'rounded-br-sm bg-indigo-600 text-white'
                                : 'rounded-bl-sm bg-white text-gray-900 ring-1 ring-gray-200 dark:bg-gray-800 dark:text-white dark:ring-white/10',
                              'rounded-2xl px-3.5 py-2.5 shadow-sm',
                            )}
                          >
                          {!ownMessage && selectedConversation.type !== 'direct' && (
                            <p className="mb-1 text-xs font-semibold text-indigo-600 dark:text-indigo-300">
                              {displayName(message.sender)}
                            </p>
                          )}

                          {message.forwardedFrom && (
                            <div
                              className={classNames(
                                ownMessage
                                  ? 'border-indigo-200 bg-white/10'
                                  : 'border-indigo-400 bg-gray-50 dark:bg-white/5',
                                'mb-2 rounded-lg border-l-2 px-2.5 py-2 text-xs',
                              )}
                            >
                              <p className="flex items-center gap-1 font-semibold">
                                <ArrowRightIcon className="size-3.5" />
                                Weitergeleitet von{' '}
                                {message.forwardedFrom.senderDisplayName}
                              </p>
                              <p className="mt-1 line-clamp-4 whitespace-pre-wrap opacity-80">
                                {message.forwardedFrom.body
                                  ? renderMessageText(
                                      message.forwardedFrom.body,
                                      ownMessage,
                                    )
                                  : 'Dateianhang'}
                              </p>
                            </div>
                          )}

                          {message.replyTo && (
                            <div
                              className={classNames(
                                ownMessage
                                  ? 'border-indigo-200 bg-white/10'
                                  : 'border-indigo-400 bg-gray-50 dark:bg-white/5',
                                'mb-2 rounded-lg border-l-2 px-2.5 py-1.5 text-xs',
                              )}
                            >
                              <p className="font-semibold">
                                {message.replyTo.senderDisplayName}
                              </p>
                              <p className="mt-0.5 line-clamp-2 opacity-80">
                                {message.replyTo.body || 'Dateianhang'}
                              </p>
                            </div>
                          )}

                          {message.linkPreview && (
                            <MessageLinkPreview
                              preview={message.linkPreview}
                              ownMessage={ownMessage}
                            />
                          )}

                          {message.body && (
                            <p className="whitespace-pre-wrap break-words text-sm">
                              {renderMessageText(message.body, ownMessage)}
                            </p>
                          )}

                          {message.location && (
                            <LocationPreview
                              location={message.location}
                              ownMessage={ownMessage}
                            />
                          )}

                          {message.attachments?.length > 0 && (
                            <div className={classNames(message.body && 'mt-2', 'space-y-1.5')}>
                              {message.attachments.map((attachment) =>
                                attachment.contentType.startsWith('image/') ? (
                                  <a
                                    key={attachment.id}
                                    href={`${API_URL}${attachment.url}`}
                                    target="_blank"
                                    rel="noreferrer"
                                    className="block overflow-hidden rounded-lg"
                                  >
                                    <img
                                      src={`${API_URL}${attachment.url}`}
                                      alt={attachment.fileName}
                                      loading="lazy"
                                      className="max-h-64 w-full max-w-xs rounded-lg object-cover"
                                    />
                                  </a>
                                ) : (
                                  <a
                                    key={attachment.id}
                                    href={`${API_URL}${attachment.url}`}
                                    target="_blank"
                                    rel="noreferrer"
                                    className={classNames(
                                      ownMessage
                                        ? 'bg-white/10 hover:bg-white/15'
                                        : 'bg-gray-50 hover:bg-gray-100 dark:bg-white/5 dark:hover:bg-white/10',
                                      'flex items-center gap-2 rounded-lg px-2.5 py-2 text-left transition',
                                    )}
                                  >
                                    <DocumentIcon className="size-5 shrink-0" />
                                    <span className="min-w-0">
                                      <span className="block truncate text-xs font-medium">
                                        {attachment.fileName}
                                      </span>
                                      <span className="block text-[0.625rem] opacity-70">
                                        {formatFileSize(attachment.sizeBytes)}
                                      </span>
                                    </span>
                                  </a>
                                ),
                              )}
                            </div>
                          )}

                          <p
                            className={classNames(
                              ownMessage
                                ? 'text-indigo-100'
                                : 'text-gray-400',
                              'mt-1 text-right text-[0.625rem]',
                            )}
                          >
                            {formatTime(message.createdAt)}
                          </p>
                          </div>

                          <div
                            className={classNames(
                              ownMessage ? 'right-0' : 'left-0',
                              'absolute -top-8 hidden items-center gap-1 rounded-lg bg-white p-1 shadow-md ring-1 ring-black/5 group-hover:flex dark:bg-gray-800 dark:ring-white/10',
                            )}
                          >
                            {selectedConversation.type !== 'channel' && (
                              <button
                                type="button"
                                onClick={() => setReplyTo(message)}
                                title="Nachricht zitieren"
                                className="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-indigo-600 dark:hover:bg-white/10"
                              >
                                <ArrowUturnLeftIcon className="size-4" />
                              </button>
                            )}
                            <button
                              type="button"
                              onClick={() => setForwardMessage(message)}
                              title="Nachricht weiterleiten"
                              className="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-indigo-600 dark:hover:bg-white/10"
                            >
                              <ArrowRightIcon className="size-4" />
                            </button>
                          </div>
                        </div>
                        </div>
                      </Fragment>
                    )
                  })}
                  {showDirectTypingIndicator && (
                    <DirectTypingIndicator user={selectedPartner} />
                  )}
                  <div ref={messagesEndRef} />
                </div>
              )}
            </div>

            {error && (
              <div className="border-t border-red-200 bg-red-50 px-4 py-2 text-sm text-red-700 dark:border-red-500/20 dark:bg-red-500/10 dark:text-red-300">
                {error}
              </div>
            )}

            {selectedConversation.type === 'channel' ? (
              <div className="flex shrink-0 items-center gap-3 border-t border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 dark:border-white/10 dark:bg-gray-900 dark:text-gray-400">
                <span className="inline-flex size-9 shrink-0 items-center justify-center rounded-lg bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300">
                  <HashtagIcon className="size-5" aria-hidden="true" />
                </span>
                Dieser Channel ist schreibgeschützt. Meldungen werden von der
                angebundenen Integration veröffentlicht.
              </div>
            ) : (
              <form
                onSubmit={sendMessage}
                className="relative shrink-0 border-t border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-gray-900"
              >
                {forwardDraft && (
                  <div className="mb-3 flex items-start justify-between gap-3 rounded-lg border-l-2 border-indigo-500 bg-indigo-50 px-3 py-2 text-xs dark:bg-indigo-500/10">
                    <div className="min-w-0">
                      <p className="flex items-center gap-1 font-semibold text-indigo-700 dark:text-indigo-300">
                        <ArrowRightIcon className="size-3.5" />
                        Weiterleiten von {displayName(forwardDraft.sender)}
                      </p>
                      <p className="mt-0.5 line-clamp-3 whitespace-pre-wrap text-gray-600 dark:text-gray-300">
                        {forwardDraft.body || 'Dateianhang'}
                      </p>
                    </div>
                    <button
                      type="button"
                      onClick={() => setForwardDraft(null)}
                      title="Weiterleitung entfernen"
                      className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
                    >
                      <XMarkIcon className="size-4" />
                    </button>
                  </div>
                )}

              {replyTo && (
                <div className="mb-3 flex items-start justify-between gap-3 rounded-lg border-l-2 border-indigo-500 bg-indigo-50 px-3 py-2 text-xs dark:bg-indigo-500/10">
                  <div className="min-w-0">
                    <p className="font-semibold text-indigo-700 dark:text-indigo-300">
                      Antwort auf {displayName(replyTo.sender)}
                    </p>
                    <p className="mt-0.5 truncate text-gray-600 dark:text-gray-300">
                      {replyTo.body || 'Dateianhang'}
                    </p>
                  </div>
                  <button
                    type="button"
                    onClick={() => setReplyTo(null)}
                    className="text-gray-400 hover:text-gray-700"
                  >
                    <XMarkIcon className="size-4" />
                  </button>
                </div>
              )}

              {pendingAttachments.length > 0 && (
                <div className="mb-3 flex flex-wrap gap-2">
                  {pendingAttachments.map((attachment) => (
                    <span
                      key={attachment.id}
                      className="inline-flex max-w-xs items-center gap-2 rounded-lg bg-gray-100 px-2.5 py-1.5 text-xs text-gray-700 dark:bg-white/10 dark:text-gray-200"
                    >
                      <DocumentIcon className="size-4 shrink-0" />
                      <span className="truncate">{attachment.fileName}</span>
                      <button
                        type="button"
                        onClick={() =>
                          setPendingAttachments((current) =>
                            current.filter((item) => item.id !== attachment.id),
                          )
                        }
                        className="text-gray-400 hover:text-red-500"
                      >
                        <XMarkIcon className="size-3.5" />
                      </button>
                    </span>
                  ))}
                </div>
              )}

              {draftPreview && (
                <div className="mb-3 flex items-start gap-3 rounded-lg border border-gray-200 bg-gray-50 p-2.5 dark:border-white/10 dark:bg-white/5">
                  {draftPreview.imageUrl && (
                    <img
                      src={draftPreview.imageUrl}
                      alt=""
                      loading="lazy"
                      className="size-14 shrink-0 rounded-md object-cover"
                    />
                  )}
                  <div className="min-w-0 flex-1">
                    {draftPreview.siteName && (
                      <p className="truncate text-[0.625rem] font-medium uppercase tracking-wide text-gray-400">
                        {draftPreview.siteName}
                      </p>
                    )}
                    <p className="truncate text-sm font-semibold text-gray-900 dark:text-white">
                      {draftPreview.title || draftPreview.url}
                    </p>
                    {draftPreview.description && (
                      <p className="line-clamp-1 text-xs text-gray-500 dark:text-gray-400">
                        {draftPreview.description}
                      </p>
                    )}
                  </div>
                  <button
                    type="button"
                    onClick={() => {
                      setDismissedPreviewUrl(draftPreview.url)
                      setDraftPreview(null)
                    }}
                    title="Vorschau ausblenden"
                    className="shrink-0 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
                  >
                    <XMarkIcon className="size-4" />
                  </button>
                </div>
              )}

              {emojiOpen && (
                <div className="absolute bottom-full left-4 mb-2 grid grid-cols-8 gap-1 rounded-xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10">
                  {chatEmojis.map((emoji) => (
                    <button
                      key={emoji}
                      type="button"
                      onClick={() => {
                        setMessageBody((current) => current + emoji)
                        setEmojiOpen(false)
                      }}
                      className="rounded p-1.5 text-xl hover:bg-gray-100 dark:hover:bg-white/10"
                    >
                      {emoji}
                    </button>
                  ))}
                </div>
              )}

              <div className="flex items-end gap-2">
                <input
                  ref={attachmentInputRef}
                  type="file"
                  className="sr-only"
                  onChange={(event) => void uploadAttachment(event)}
                />
                <div className="relative">
                  <button
                    type="button"
                    disabled={uploadingAttachment}
                    onClick={() => {
                      setEmojiOpen(false)
                      setAttachMenuOpen((current) => !current)
                    }}
                    title="Anhängen"
                    className="inline-flex size-10 shrink-0 items-center justify-center rounded-full text-gray-500 hover:bg-gray-100 hover:text-indigo-600 disabled:opacity-50 dark:hover:bg-white/10"
                  >
                    {uploadingAttachment ? (
                      <LoadingSpinner size="sm" />
                    ) : (
                      <PlusIcon className="size-5" />
                    )}
                  </button>

                  {attachMenuOpen && (
                    <>
                      <button
                        type="button"
                        aria-hidden="true"
                        tabIndex={-1}
                        onClick={() => setAttachMenuOpen(false)}
                        className="fixed inset-0 z-10 cursor-default"
                      />
                      <div className="absolute bottom-full left-0 z-20 mb-2 w-44 overflow-hidden rounded-xl bg-white py-1 shadow-xl ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10">
                        <button
                          type="button"
                          onClick={() => {
                            setAttachMenuOpen(false)
                            attachmentInputRef.current?.click()
                          }}
                          className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/10"
                        >
                          <PaperClipIcon className="size-5 text-gray-400" />
                          Datei
                        </button>
                        <button
                          type="button"
                          onClick={() => {
                            setAttachMenuOpen(false)
                            setLocationDraft(null)
                            setLocationLabel('')
                            setLocationPickerOpen(true)
                          }}
                          className="flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/10"
                        >
                          <MapPinIcon className="size-5 text-gray-400" />
                          Standort
                        </button>
                      </div>
                    </>
                  )}
                </div>
                <button
                  type="button"
                  onClick={() => {
                    setAttachMenuOpen(false)
                    setEmojiOpen((current) => !current)
                  }}
                  title="Emoji auswählen"
                  className="inline-flex size-10 shrink-0 items-center justify-center rounded-full text-gray-500 hover:bg-gray-100 hover:text-indigo-600 dark:hover:bg-white/10"
                >
                  <FaceSmileIcon className="size-5" />
                </button>
                <textarea
                  value={messageBody}
                  onChange={(event) =>
                    handleMessageBodyChange(event.target.value)
                  }
                  onKeyDown={(event) => {
                    if (event.key === 'Enter' && !event.shiftKey) {
                      event.preventDefault()
                      event.currentTarget.form?.requestSubmit()
                    }
                  }}
                  rows={1}
                  maxLength={4000}
                  placeholder={
                    socketConnected
                      ? forwardDraft
                        ? 'Kommentar hinzufügen...'
                        : 'Nachricht schreiben...'
                      : 'Chat-Verbindung wird hergestellt...'
                  }
                  className="max-h-32 min-h-11 flex-1 resize-none rounded-2xl border-0 bg-gray-100 px-4 py-3 text-sm text-gray-900 outline-none ring-1 ring-transparent placeholder:text-gray-500 focus:ring-indigo-500 dark:bg-white/5 dark:text-white"
                />
                <button
                  type="submit"
                  disabled={
                    (!messageBody.trim() &&
                      pendingAttachments.length === 0 &&
                      !forwardDraft) ||
                    sending ||
                    !socketConnected
                  }
                  className="inline-flex size-11 shrink-0 items-center justify-center rounded-full bg-indigo-600 text-white shadow-sm transition hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-50"
                >
                  <span className="sr-only">Nachricht senden</span>
                  {sending ? (
                    <LoadingSpinner size="sm" className="text-white" />
                  ) : (
                    <PaperAirplaneIcon className="size-5" aria-hidden="true" />
                  )}
                </button>
              </div>
              </form>
            )}
          </>
        ) : (
          <div className="flex h-full items-center justify-center bg-gray-50 px-6 text-center dark:bg-gray-950/60">
            <div>
              <ChatBubbleLeftRightIcon className="mx-auto size-14 text-gray-300 dark:text-gray-600" />
              <h2 className="mt-4 text-base font-semibold text-gray-900 dark:text-white">
                {visibleTab === 'channels'
                  ? 'Wähle einen Channel aus'
                  : 'Wähle einen Chat aus'}
              </h2>
              <p className="mt-1 max-w-sm text-sm text-gray-500">
                {visibleTab === 'channels'
                  ? 'Öffne links einen Channel, um eingehende Meldungen zu lesen.'
                  : 'Öffne links einen Teamchat oder starte eine Unterhaltung mit einer Person.'}
              </p>
            </div>
          </div>
        )}
      </section>

      <Dialog
        open={newChatOpen}
        onClose={setNewChatOpen}
        className="relative z-[10000]"
      >
        <DialogBackdrop className="fixed inset-0 bg-gray-950/60 backdrop-blur-sm" />
        <div className="fixed inset-0 flex items-center justify-center p-4">
          <DialogPanel className="flex max-h-[min(42rem,90vh)] w-full max-w-md flex-col overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10">
            <div className="flex items-center justify-between border-b border-gray-200 px-5 py-4 dark:border-white/10">
              <DialogTitle className="text-base font-semibold text-gray-950 dark:text-white">
                Neue Direktnachricht
              </DialogTitle>
              <button
                type="button"
                onClick={() => setNewChatOpen(false)}
                className="inline-flex size-8 items-center justify-center rounded-full text-gray-500 hover:bg-gray-100 dark:hover:bg-white/10"
              >
                <span className="sr-only">Schließen</span>
                <XMarkIcon className="size-5" aria-hidden="true" />
              </button>
            </div>

            <div className="border-b border-gray-200 p-4 dark:border-white/10">
              <div className="relative">
                <MagnifyingGlassIcon
                  className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-gray-400"
                  aria-hidden="true"
                />
                <input
                  autoFocus
                  value={userSearch}
                  onChange={(event) => setUserSearch(event.target.value)}
                  placeholder="Person suchen"
                  className="block w-full rounded-xl border-0 bg-gray-100 py-2.5 pr-3 pl-9 text-sm text-gray-900 outline-none ring-1 ring-transparent focus:ring-indigo-500 dark:bg-white/5 dark:text-white"
                />
              </div>
            </div>

            <div className="min-h-0 flex-1 overflow-y-auto p-2">
              {filteredUsers.length === 0 ? (
                <p className="px-4 py-10 text-center text-sm text-gray-500">
                  Keine Personen gefunden.
                </p>
              ) : (
                <ul className="space-y-1">
                  {filteredUsers.map((user) => (
                    <li key={user.id}>
                      <button
                        type="button"
                        disabled={startingDirectChat}
                        onClick={() => void startDirectChat(user.id)}
                        className="flex w-full items-center gap-3 rounded-xl p-3 text-left transition hover:bg-gray-50 disabled:opacity-50 dark:hover:bg-white/5"
                      >
                        <Avatar
                          src={user.avatar}
                          name={displayName(user)}
                          size={10}
                          presenceStatus={user.presenceStatus}
                        />
                        <span className="min-w-0 flex-1">
                          <span className="block truncate text-sm font-semibold text-gray-900 dark:text-white">
                            {displayName(user)}
                          </span>
                          <span className="block truncate text-xs text-gray-500">
                            {user.presenceStatus === 'offline'
                              ? user.unit || user.email
                              : user.presenceStatus === 'away'
                                ? 'Abwesend'
                                : userStatusText(user, statusNow)}
                          </span>
                        </span>
                      </button>
                    </li>
                  ))}
                </ul>
              )}
            </div>
          </DialogPanel>
        </div>
      </Dialog>

      <Dialog
        open={Boolean(forwardMessage)}
        onClose={() => setForwardMessage(null)}
        className="relative z-[10000]"
      >
        <DialogBackdrop className="fixed inset-0 bg-gray-950/60 backdrop-blur-sm" />
        <div className="fixed inset-0 flex items-center justify-center p-4">
          <DialogPanel className="flex max-h-[80vh] w-full max-w-md flex-col overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10">
            <div className="flex items-center justify-between border-b border-gray-200 px-5 py-4 dark:border-white/10">
              <DialogTitle className="text-base font-semibold text-gray-950 dark:text-white">
                Nachricht weiterleiten
              </DialogTitle>
              <button
                type="button"
                onClick={() => setForwardMessage(null)}
                className="rounded-full p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-white/10"
              >
                <XMarkIcon className="size-5" />
              </button>
            </div>

            {forwardMessage && (
              <div className="border-b border-gray-200 bg-gray-50 px-5 py-3 text-sm text-gray-600 dark:border-white/10 dark:bg-white/5 dark:text-gray-300">
                <p className="line-clamp-3">
                  {forwardMessage.body || 'Dateianhang'}
                </p>
              </div>
            )}

            <div className="min-h-0 flex-1 overflow-y-auto p-2">
              {conversations
                .filter(
                  (conversation) =>
                    conversation.type !== 'channel' &&
                    conversation.id !== forwardMessage?.conversationId,
                )
                .map((conversation) => (
                <button
                  key={conversation.id}
                  type="button"
                  disabled={sending}
                  onClick={() => forwardToConversation(conversation)}
                  className="flex w-full items-center gap-3 rounded-xl p-3 text-left hover:bg-gray-50 disabled:opacity-50 dark:hover:bg-white/5"
                >
                  <span className="inline-flex size-9 items-center justify-center rounded-lg bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300">
                    {conversation.type !== 'direct' ? (
                      <UserGroupIcon className="size-5" />
                    ) : (
                      <ArrowRightIcon className="size-5" />
                    )}
                  </span>
                  <span className="truncate text-sm font-medium text-gray-900 dark:text-white">
                    {conversationTitle(conversation, currentUser.id)}
                  </span>
                </button>
              ))}
              {conversations.every(
                (conversation) =>
                  conversation.type === 'channel' ||
                  conversation.id === forwardMessage?.conversationId,
              ) && (
                <p className="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
                  Kein anderer Chat zum Weiterleiten verfügbar.
                </p>
              )}
            </div>
          </DialogPanel>
        </div>
      </Dialog>

      <Dialog
        open={membersOpen}
        onClose={() => setMembersOpen(false)}
        className="relative z-[10000]"
      >
        <DialogBackdrop className="fixed inset-0 bg-gray-950/60 backdrop-blur-sm" />
        <div className="fixed inset-0 flex items-center justify-center p-4">
          <DialogPanel className="flex max-h-[min(40rem,85vh)] w-full max-w-md flex-col overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10">
            <div className="flex items-center justify-between border-b border-gray-200 px-5 py-4 dark:border-white/10">
              <DialogTitle className="text-base font-semibold text-gray-950 dark:text-white">
                Mitglieder
                {selectedConversation
                  ? ` · ${selectedConversation.participants.length}`
                  : ''}
              </DialogTitle>
              <button
                type="button"
                onClick={() => setMembersOpen(false)}
                className="inline-flex size-8 items-center justify-center rounded-full text-gray-500 hover:bg-gray-100 dark:hover:bg-white/10"
              >
                <span className="sr-only">Schließen</span>
                <XMarkIcon className="size-5" aria-hidden="true" />
              </button>
            </div>

            <div className="min-h-0 flex-1 overflow-y-auto p-2">
              <ul className="space-y-1">
                {(selectedConversation?.participants ?? []).map((member) => (
                  <li
                    key={member.id}
                    className="flex items-center gap-3 rounded-xl p-3"
                  >
                    <Avatar
                      src={member.avatar}
                      name={displayName(member)}
                      size={10}
                      presenceStatus={member.presenceStatus}
                    />
                    <span className="min-w-0 flex-1">
                      <span className="block truncate text-sm font-semibold text-gray-900 dark:text-white">
                        {displayName(member)}
                        {member.id === currentUser.id ? ' (Du)' : ''}
                      </span>
                      <span className="block truncate text-xs text-gray-500">
                        {member.unit || member.email}
                      </span>
                    </span>
                  </li>
                ))}
              </ul>
            </div>
          </DialogPanel>
        </div>
      </Dialog>

      <Dialog
        open={locationPickerOpen}
        onClose={() => setLocationPickerOpen(false)}
        className="relative z-[10000]"
      >
        <DialogBackdrop className="fixed inset-0 bg-gray-950/60 backdrop-blur-sm" />
        <div className="fixed inset-0 flex items-center justify-center p-4">
          <DialogPanel className="flex max-h-[90vh] w-full max-w-lg flex-col overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10">
            <div className="flex items-center justify-between border-b border-gray-200 px-5 py-4 dark:border-white/10">
              <DialogTitle className="text-base font-semibold text-gray-950 dark:text-white">
                Standort senden
              </DialogTitle>
              <button
                type="button"
                onClick={() => setLocationPickerOpen(false)}
                className="inline-flex size-8 items-center justify-center rounded-full text-gray-500 hover:bg-gray-100 dark:hover:bg-white/10"
              >
                <span className="sr-only">Schließen</span>
                <XMarkIcon className="size-5" aria-hidden="true" />
              </button>
            </div>

            <div className="min-h-0 flex-1 overflow-y-auto p-4">
              <p className="text-sm text-gray-500 dark:text-gray-400">
                Tippe in die Karte oder ziehe den Pin auf die gewünschte Position.
              </p>

              <AddressMapPicker
                value={locationDraft}
                visible={locationPickerOpen}
                focusKey={locationPickerOpen ? 'open' : 'closed'}
                onChange={setLocationDraft}
              />

              <input
                value={locationLabel}
                onChange={(event) => setLocationLabel(event.target.value)}
                placeholder="Beschreibung (optional), z. B. Treffpunkt"
                maxLength={200}
                className="mt-3 block w-full rounded-xl border-0 bg-gray-100 px-3 py-2.5 text-sm text-gray-900 outline-none ring-1 ring-transparent focus:ring-indigo-500 dark:bg-white/5 dark:text-white"
              />

              {locationDraft && (
                <p className="mt-2 text-xs text-gray-400">
                  {locationDraft.lat.toFixed(5)}, {locationDraft.lon.toFixed(5)}
                </p>
              )}
            </div>

            <div className="flex items-center justify-end gap-2 border-t border-gray-200 px-5 py-3 dark:border-white/10">
              <button
                type="button"
                onClick={() => setLocationPickerOpen(false)}
                className="rounded-lg px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/10"
              >
                Abbrechen
              </button>
              <button
                type="button"
                disabled={!locationDraft || sending || !socketConnected}
                onClick={sendLocation}
                className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-50"
              >
                <PaperAirplaneIcon className="size-4" />
                Standort senden
              </button>
            </div>
          </DialogPanel>
        </div>
      </Dialog>
    </div>
  )
}
