import {
  ChevronDownIcon,
  PaperAirplaneIcon,
  XMarkIcon,
} from '@heroicons/react/20/solid'
import {
  ArrowTopRightOnSquareIcon,
  ChatBubbleLeftRightIcon,
  FaceSmileIcon,
  HashtagIcon,
  MapPinIcon,
  PaperClipIcon,
  PlusIcon,
  UserGroupIcon,
} from '@heroicons/react/24/outline'
import {
  useCallback,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
  type ChangeEvent,
  type FormEvent,
} from 'react'
import { useLocation, useNavigate } from 'react-router'
import AddressMapPicker from './AddressMapPicker'
import Avatar, { type PresenceStatus } from './Avatar'
import { useChatSocket } from './ChatSocketProvider'
import LoadingSpinner from './LoadingSpinner'
import Modal, { ModalTitle } from './Modal'
import { scrollbarClassName } from './Scrollbar'

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

export type PinnedChat = {
  id: string
  type: 'direct' | 'group' | 'channel'
  title: string
  image?: string
  isMember?: boolean
  partnerId?: string
  presenceStatus?: PresenceStatus
}

const pinnedChatsEventName = 'teg:pinned-chats-changed'

type DockChatUser = {
  id: string
  username: string
  displayName: string
  email: string
  avatar: string
}

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

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

type DockChatMessage = {
  id: string
  conversationId: string
  type: 'user' | 'system'
  body: string
  createdAt: string
  sender: DockChatUser
  attachments?: DockChatAttachment[]
  location?: DockChatLocation | null
  reactions?: DockChatReaction[]
}

type DockChatReactionUser = {
  id: string
  displayName: string
}

type DockChatReaction = {
  emoji: string
  users: DockChatReactionUser[]
  count: number
}

type DockChatSocketEvent = {
  type:
    | 'connected'
    | 'message.created'
    | 'message.updated'
    | 'messages.reload'
    | 'typing.updated'
    | 'conversation.removed'
    | 'error'
  conversationId?: string
  userId?: string
  userDisplayName?: string
  typing?: boolean
  message?: DockChatMessage
  error?: string
}

type UserNotificationEvent = CustomEvent<{
  data?: {
    user?: {
      id: string
      presenceStatus?: PresenceStatus
    }
  }
}>

function pinnedChatsStorageKey(userId: string) {
  return `teg:pinned-chats:${userId}`
}

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

function resizeDockTextarea(textarea: HTMLTextAreaElement | null) {
  if (!textarea) {
    return
  }

  textarea.style.height = 'auto'
  textarea.style.height = `${textarea.scrollHeight}px`
}

function normalizePinnedChats(value: unknown): PinnedChat[] {
  if (!Array.isArray(value)) {
    return []
  }

  const chats: PinnedChat[] = []

  for (const item of value) {
    if (!item || typeof item !== 'object') {
      continue
    }

    const chat = item as Partial<PinnedChat>
    if (
      typeof chat.id !== 'string' ||
      typeof chat.title !== 'string' ||
      !['direct', 'group', 'channel'].includes(String(chat.type))
    ) {
      continue
    }

    chats.push({
      id: chat.id,
      type: chat.type as PinnedChat['type'],
      title: chat.title,
      image: typeof chat.image === 'string' ? chat.image : undefined,
      isMember: typeof chat.isMember === 'boolean' ? chat.isMember : undefined,
      partnerId: typeof chat.partnerId === 'string' ? chat.partnerId : undefined,
      presenceStatus:
        chat.presenceStatus === 'online' ||
        chat.presenceStatus === 'away' ||
        chat.presenceStatus === 'offline'
          ? chat.presenceStatus
          : undefined,
    })
  }

  return chats
}

export function readPinnedChats(userId: string) {
  if (!userId || typeof window === 'undefined') {
    return []
  }

  try {
    return normalizePinnedChats(
      JSON.parse(
        window.localStorage.getItem(pinnedChatsStorageKey(userId)) ?? '[]',
      ),
    )
  } catch {
    return []
  }
}

export function writePinnedChats(userId: string, chats: PinnedChat[]) {
  if (!userId || typeof window === 'undefined') {
    return
  }

  window.localStorage.setItem(
    pinnedChatsStorageKey(userId),
    JSON.stringify(chats.slice(0, 6)),
  )
  window.dispatchEvent(new CustomEvent(pinnedChatsEventName))
}

export function isPinnedChat(userId: string, chatId: string) {
  return readPinnedChats(userId).some((chat) => chat.id === chatId)
}

export function togglePinnedChat(userId: string, chat: PinnedChat) {
  const currentChats = readPinnedChats(userId)
  const exists = currentChats.some((item) => item.id === chat.id)

  writePinnedChats(
    userId,
    exists
      ? currentChats.filter((item) => item.id !== chat.id)
      : [chat, ...currentChats.filter((item) => item.id !== chat.id)],
  )

  return !exists
}

function chatHref(chat: PinnedChat) {
  return chat.type === 'channel' ? `/chat/channel/${chat.id}` : `/chat/${chat.id}`
}

function isCurrentChatRoute(chat: PinnedChat, pathname: string) {
  return pathname === chatHref(chat)
}

async function readApiError(response: Response, fallback: string) {
  const data = await response.json().catch(() => null)

  return data?.error ?? fallback
}

function displayName(user: DockChatUser) {
  return user.displayName || user.username || user.email || 'Unbekannt'
}

function chatStatusText(chat: PinnedChat) {
  if (chat.type === 'direct') {
    if (chat.presenceStatus === 'online') {
      return 'Online'
    }
    if (chat.presenceStatus === 'away') {
      return 'Abwesend'
    }
    return 'Offline'
  }

  if (chat.type === 'channel') {
    return 'Channel'
  }

  return chat.isMember === false ? 'Öffentlich' : 'Gruppe'
}

function formatTime(value: string) {
  const date = new Date(value)

  if (Number.isNaN(date.getTime())) {
    return ''
  }

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

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

function DockLocationPreview({
  isOwn,
  location,
}: {
  isOwn: boolean
  location: DockChatLocation
}) {
  return (
    <a
      href={dockLocationMapsUrl(location.lat, location.lng)}
      target="_blank"
      rel="noreferrer noopener"
      className={classNames(
        isOwn
          ? 'bg-white/10 hover:bg-white/15'
          : 'bg-gray-50 hover:bg-gray-100 dark:bg-white/5 dark:hover:bg-white/10',
        'mt-1 flex max-w-full items-center gap-1.5 rounded-lg px-2.5 py-2 text-xs transition',
      )}
    >
      <MapPinIcon className="size-4 shrink-0 opacity-70" />
      <span className="min-w-0 truncate font-medium">
        {location.label || 'Geteilter Standort'}
      </span>
    </a>
  )
}

function typingStatusText(names: string[]) {
  if (names.length === 1) {
    return `${names[0]} schreibt...`
  }

  if (names.length > 1) {
    return `${names.length} Personen schreiben...`
  }

  return ''
}

function DockStatusLine({
  chat,
  typingNames,
}: {
  chat: PinnedChat
  typingNames: string[]
}) {
  const typingText = typingStatusText(typingNames)

  if (typingText) {
    return (
      <span className="mt-0.5 flex min-w-0 text-xs text-gray-500 dark:text-gray-400">
        <span className="truncate">{typingText}</span>
      </span>
    )
  }

  return (
    <span className="mt-0.5 block truncate text-xs text-gray-500 dark:text-gray-400">
      {chatStatusText(chat)}
    </span>
  )
}

function mergeMessage(
  messages: DockChatMessage[] | undefined,
  message: DockChatMessage,
) {
  const currentMessages = messages ?? []
  const nextMessages = currentMessages.some((item) => item.id === message.id)
    ? currentMessages.map((item) => (item.id === message.id ? message : item))
    : [...currentMessages, message]

  return nextMessages.sort((left, right) => {
    const leftTime = new Date(left.createdAt).getTime()
    const rightTime = new Date(right.createdAt).getTime()

    if (leftTime !== rightTime) {
      return leftTime - rightTime
    }

    return left.id.localeCompare(right.id)
  })
}

const dockChatEmojis = [
  '\u{1F600}',
  '\u{1F602}',
  '\u{1F60A}',
  '\u{1F60D}',
  '\u{1F44D}',
  '\u{1F44F}',
  '\u{1F64F}',
  '\u{1F389}',
  '\u2764\uFE0F',
  '\u{1F525}',
  '\u2705',
  '\u{1F914}',
  '\u{1F605}',
  '\u{1F622}',
  '\u{1F62E}',
  '\u{1F680}',
]

const dockReactionEmojis = dockChatEmojis.slice(0, 8)

function dockEmojiShortcut(emoji: string) {
  switch (emoji) {
    case '\u{1F600}':
      return ':D'
    case '\u{1F60A}':
      return ':)'
    case '\u{1F622}':
      return ":'("
    case '\u{1F62E}':
      return ':O'
    case '\u2764\uFE0F':
      return '<3'
    case '\u{1F602}':
      return 'Freudentränen'
    case '\u{1F60D}':
      return 'Verliebt'
    case '\u{1F44D}':
      return 'Daumen hoch'
    case '\u{1F44F}':
      return 'Applaus'
    case '\u{1F64F}':
      return 'Danke'
    case '\u{1F389}':
      return 'Feiern'
    case '\u{1F525}':
      return 'Feuer'
    case '\u2705':
      return 'Erledigt'
    case '\u{1F914}':
      return 'Nachdenklich'
    case '\u{1F605}':
      return 'Schwitzend lachen'
    case '\u{1F680}':
      return 'Rakete'
    default:
      return emoji
  }
}

const dockEmoticonReplacements: Array<[RegExp, string]> = [
  [/(^|\s)<3(?=\s|$)/g, '$1\u2764\uFE0F'],
  [/(^|\s):'\((?=\s|$)/g, '$1\u{1F622}'],
  [/(^|\s):-?\)(?=\s|$)/g, '$1\u{1F642}'],
  [/(^|\s):-?D(?=\s|$)/g, '$1\u{1F600}'],
  [/(^|\s):-?\((?=\s|$)/g, '$1\u{1F641}'],
  [/(^|\s);-?\)(?=\s|$)/g, '$1\u{1F609}'],
  [/(^|\s):-?[pP](?=\s|$)/g, '$1\u{1F61B}'],
  [/(^|\s):-?[oO](?=\s|$)/g, '$1\u{1F62E}'],
  [/(^|\s):-?\|(?=\s|$)/g, '$1\u{1F610}'],
  [/(^|\s):-?\/(?=\s|$)/g, '$1\u{1F615}'],
  [/(^|\s)8-?\)(?=\s|$)/g, '$1\u{1F60E}'],
]

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

function DockTypingIndicator({
  chat,
  names,
}: {
  chat: PinnedChat
  names: string[]
}) {
  const name =
    names.length === 1
      ? names[0]
      : names.length > 1
        ? `${names.length} Personen`
        : 'Jemand'

  return (
    <div
      role="status"
      aria-live="polite"
      className="flex items-end gap-2"
    >
      <Avatar
        src={chat.type === 'direct' ? chat.image : undefined}
        name={name}
        size={8}
        presenceStatus={chat.type === 'direct' ? chat.presenceStatus : undefined}
      />

      <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 DockMessageReactions({
  userId,
  isOwn,
  reactions,
  disabled,
  onToggle,
}: {
  userId: string
  isOwn: boolean
  reactions?: DockChatReaction[]
  disabled?: boolean
  onToggle: (emoji: string) => void
}) {
  const visibleReactions = (reactions ?? []).filter(
    (reaction) => reaction.count > 0,
  )

  if (visibleReactions.length === 0) {
    return null
  }

  return (
    <div
      className={classNames(
        isOwn ? 'justify-end' : 'justify-start',
        'flex flex-wrap gap-1',
      )}
    >
      {visibleReactions.map((reaction) => {
        const reactedByCurrentUser = reaction.users.some(
          (reactionUser) => reactionUser.id === userId,
        )
        const title = reaction.users
          .map((reactionUser) =>
            reactionUser.id === userId
              ? 'Du'
              : reactionUser.displayName || 'Unbekannt',
          )
          .join(', ')

        return (
          <button
            key={reaction.emoji}
            type="button"
            disabled={disabled}
            onClick={() => onToggle(reaction.emoji)}
            title={title || 'Reaktion'}
            className={classNames(
              reactedByCurrentUser
                ? 'bg-indigo-50 text-indigo-700 ring-indigo-200 dark:bg-indigo-500/15 dark:text-indigo-200 dark:ring-indigo-400/30'
                : 'bg-white text-gray-600 ring-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/10',
              'inline-flex h-6 items-center gap-1 rounded-full px-2 text-xs font-medium shadow-sm ring-1 ring-inset transition disabled:cursor-not-allowed disabled:opacity-60',
            )}
          >
            <span className="text-sm leading-none">{reaction.emoji}</span>
            {reaction.count > 1 && (
              <span className="tabular-nums">{reaction.count}</span>
            )}
          </button>
        )
      })}
    </div>
  )
}

function ChatIcon({ chat }: { chat: PinnedChat }) {
  if (chat.image) {
    return (
      <Avatar
        src={chat.image}
        name={chat.title}
        size={8}
        shape="circle"
        presenceStatus={chat.type === 'direct' ? chat.presenceStatus : undefined}
      />
    )
  }

  if (chat.type === 'direct') {
    return (
      <Avatar
        name={chat.title}
        size={8}
        presenceStatus={chat.presenceStatus}
      />
    )
  }

  const Icon =
    chat.type === 'channel'
      ? HashtagIcon
      : chat.type === 'group'
        ? UserGroupIcon
        : ChatBubbleLeftRightIcon

  return (
    <span
      className={classNames(
        'inline-flex size-8 shrink-0 items-center justify-center',
        chat.type === 'channel'
          ? 'rounded-full bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300'
          : chat.type === 'group'
            ? 'rounded-full bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300'
            : 'rounded-lg bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300',
      )}
    >
      <Icon aria-hidden="true" className="size-4" />
    </span>
  )
}

export default function PinnedChatDock({ userId }: { userId: string }) {
  const navigate = useNavigate()
  const location = useLocation()
  const { send, subscribe } = useChatSocket()
  const [pinnedChats, setPinnedChats] = useState(() => readPinnedChats(userId))
  const [openChatId, setOpenChatId] = useState<string | null>(null)
  const [messagesByChatId, setMessagesByChatId] = useState<
    Record<string, DockChatMessage[]>
  >({})
  const [draftsByChatId, setDraftsByChatId] = useState<Record<string, string>>({})
  const [pendingAttachmentsByChatId, setPendingAttachmentsByChatId] = useState<
    Record<string, DockChatAttachment[]>
  >({})
  const [loadingChatIds, setLoadingChatIds] = useState(() => new Set<string>())
  const [sendingChatIds, setSendingChatIds] = useState(() => new Set<string>())
  const [uploadingChatIds, setUploadingChatIds] = useState(() => new Set<string>())
  const [errorsByChatId, setErrorsByChatId] = useState<Record<string, string>>({})
  const [emojiOpenChatId, setEmojiOpenChatId] = useState<string | null>(null)
  const [reactionPickerMessageId, setReactionPickerMessageId] = useState('')
  const [reactingMessageIds, setReactingMessageIds] = useState(
    () => new Set<string>(),
  )
  const [attachMenuOpenChatId, setAttachMenuOpenChatId] = useState<string | null>(null)
  const [locationPickerChatId, setLocationPickerChatId] = useState<string | null>(null)
  const [locationDraft, setLocationDraft] = useState<{
    lat: number
    lon: number
  } | null>(null)
  const [locationLabel, setLocationLabel] = useState('')
  const [typingNow, setTypingNow] = useState(() => Date.now())
  const [typingUsers, setTypingUsers] = useState<
    Record<string, Record<string, { name: string; expiresAt: number }>>
  >({})
  const attachmentInputRef = useRef<HTMLInputElement | null>(null)
  const textareaRefsByChatId = useRef(new Map<string, HTMLTextAreaElement>())
  const messagesEndRef = useRef<HTMLDivElement | null>(null)
  const typingChatIdRef = useRef<string | undefined>(undefined)
  const typingStopTimerRef = useRef<number | undefined>(undefined)
  const isChatRoute =
    location.pathname === '/chat' || location.pathname.startsWith('/chat/')

  const reloadPinnedChats = useCallback(() => {
    const chats = readPinnedChats(userId)
    setPinnedChats(chats)
    setOpenChatId((current) =>
      current && chats.some((chat) => chat.id === current) ? current : null,
    )
  }, [userId])

  const openChatMessages = openChatId
    ? messagesByChatId[openChatId] ?? []
    : []
  const openChatDraft = openChatId ? draftsByChatId[openChatId] ?? '' : ''
  const openTypingCount = openChatId
    ? Object.values(typingUsers[openChatId] ?? {}).filter(
        (typingUser) => typingUser.expiresAt > typingNow,
      ).length
    : 0
  const visiblePinnedChats = useMemo(
    () =>
      pinnedChats.filter(
        (chat) => !isCurrentChatRoute(chat, location.pathname),
      ),
    [location.pathname, pinnedChats],
  )
  const dockChats = useMemo(
    () =>
      isChatRoute
        ? visiblePinnedChats.filter((chat) => chat.id === openChatId)
        : visiblePinnedChats,
    [isChatRoute, openChatId, visiblePinnedChats],
  )
  const locationChat = locationPickerChatId
    ? pinnedChats.find((chat) => chat.id === locationPickerChatId)
    : undefined

  const setChatLoading = useCallback((chatId: string, loading: boolean) => {
    setLoadingChatIds((current) => {
      const next = new Set(current)
      if (loading) {
        next.add(chatId)
      } else {
        next.delete(chatId)
      }
      return next
    })
  }, [])

  const setChatSending = useCallback((chatId: string, sending: boolean) => {
    setSendingChatIds((current) => {
      const next = new Set(current)
      if (sending) {
        next.add(chatId)
      } else {
        next.delete(chatId)
      }
      return next
    })
  }, [])

  const setChatUploading = useCallback((chatId: string, uploading: boolean) => {
    setUploadingChatIds((current) => {
      const next = new Set(current)
      if (uploading) {
        next.add(chatId)
      } else {
        next.delete(chatId)
      }
      return next
    })
  }, [])

  const sendTypingState = useCallback(
    (chatId: string, typing: boolean) => {
      send({
        type: typing ? 'typing.start' : 'typing.stop',
        conversationId: chatId,
      })
    },
    [send],
  )

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

    const chatId = typingChatIdRef.current
    if (chatId) {
      sendTypingState(chatId, false)
      typingChatIdRef.current = undefined
    }
  }, [sendTypingState])

  const loadMessages = useCallback(
    async (chatId: string) => {
      setChatLoading(chatId, true)
      setErrorsByChatId((current) => ({ ...current, [chatId]: '' }))

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

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

        const data = await response.json()
        setMessagesByChatId((current) => ({
          ...current,
          [chatId]: data.messages ?? [],
        }))
        window.dispatchEvent(new Event('chat-unread-changed'))
      } catch (error) {
        setErrorsByChatId((current) => ({
          ...current,
          [chatId]:
            error instanceof Error
              ? error.message
              : 'Nachrichten konnten nicht geladen werden',
        }))
      } finally {
        setChatLoading(chatId, false)
      }
    },
    [setChatLoading],
  )

  function updateDraft(chat: PinnedChat, value: string) {
    const nextValue = convertDockEmoticons(value)

    setDraftsByChatId((current) => ({
      ...current,
      [chat.id]: nextValue,
    }))

    if (!nextValue.trim() || chat.type === 'channel' || chat.isMember === false) {
      if (typingChatIdRef.current === chat.id) {
        stopTyping()
      }
      return
    }

    if (typingChatIdRef.current !== chat.id) {
      stopTyping()
      sendTypingState(chat.id, true)
      typingChatIdRef.current = chat.id
    }

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

  async function uploadAttachments(
    chat: PinnedChat,
    event: ChangeEvent<HTMLInputElement>,
  ) {
    const files = Array.from(event.target.files ?? [])
    event.target.value = ''

    if (files.length === 0) {
      return
    }

    const oversizedFile = files.find((file) => file.size > 10 * 1024 * 1024)
    if (oversizedFile) {
      setErrorsByChatId((current) => ({
        ...current,
        [chat.id]: 'Dateien dürfen höchstens 10 MB groß sein.',
      }))
      return
    }

    setChatUploading(chat.id, true)
    setErrorsByChatId((current) => ({ ...current, [chat.id]: '' }))

    try {
      const uploadedAttachments: DockChatAttachment[] = []

      for (const file of files) {
        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()
        if (data.attachment) {
          uploadedAttachments.push(data.attachment as DockChatAttachment)
        }
      }

      setPendingAttachmentsByChatId((current) => ({
        ...current,
        [chat.id]: [...(current[chat.id] ?? []), ...uploadedAttachments],
      }))
    } catch (error) {
      setErrorsByChatId((current) => ({
        ...current,
        [chat.id]:
          error instanceof Error
            ? error.message
            : 'Datei konnte nicht hochgeladen werden',
      }))
    } finally {
      setChatUploading(chat.id, false)
    }
  }

  async function sendMessage(chat: PinnedChat, event: FormEvent<HTMLFormElement>) {
    event.preventDefault()

    const body = draftsByChatId[chat.id]?.trim() ?? ''
    const pendingAttachments = pendingAttachmentsByChatId[chat.id] ?? []
    if (
      (!body && pendingAttachments.length === 0) ||
      chat.type === 'channel' ||
      chat.isMember === false
    ) {
      return
    }

    setChatSending(chat.id, true)
    setErrorsByChatId((current) => ({ ...current, [chat.id]: '' }))

    try {
      const response = await fetch(
        `${API_URL}/chat/conversations/${chat.id}/messages`,
        {
          method: 'POST',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            body,
            attachmentIds: pendingAttachments.map((attachment) => attachment.id),
          }),
        },
      )

      if (!response.ok) {
        throw new Error(
          await readApiError(response, 'Nachricht konnte nicht gesendet werden'),
        )
      }

      const data = await response.json()
      if (data.message) {
        setMessagesByChatId((current) => ({
          ...current,
          [chat.id]: mergeMessage(current[chat.id], data.message),
        }))
      }
      setDraftsByChatId((current) => ({ ...current, [chat.id]: '' }))
      setPendingAttachmentsByChatId((current) => ({
        ...current,
        [chat.id]: [],
      }))
      setEmojiOpenChatId((current) => (current === chat.id ? null : current))
      stopTyping()
    } catch (error) {
      setErrorsByChatId((current) => ({
        ...current,
        [chat.id]:
          error instanceof Error
            ? error.message
            : 'Nachricht konnte nicht gesendet werden',
      }))
    } finally {
      setChatSending(chat.id, false)
    }
  }

  function openLocationPicker(chat: PinnedChat) {
    setAttachMenuOpenChatId(null)
    setEmojiOpenChatId(null)
    setLocationDraft(null)
    setLocationLabel('')
    setLocationPickerChatId(chat.id)
  }

  function closeLocationPicker() {
    setLocationPickerChatId(null)
    setLocationDraft(null)
    setLocationLabel('')
  }

  async function sendLocation() {
    const chat = locationPickerChatId
      ? pinnedChats.find((item) => item.id === locationPickerChatId)
      : undefined

    if (
      !chat ||
      !locationDraft ||
      chat.type === 'channel' ||
      chat.isMember === false
    ) {
      return
    }

    setChatSending(chat.id, true)
    setErrorsByChatId((current) => ({ ...current, [chat.id]: '' }))

    try {
      const response = await fetch(
        `${API_URL}/chat/conversations/${chat.id}/messages`,
        {
          method: 'POST',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            body: '',
            location: {
              lat: locationDraft.lat,
              lng: locationDraft.lon,
              label: locationLabel.trim(),
            },
          }),
        },
      )

      if (!response.ok) {
        throw new Error(
          await readApiError(response, 'Standort konnte nicht gesendet werden'),
        )
      }

      const data = await response.json()
      if (data.message) {
        setMessagesByChatId((current) => ({
          ...current,
          [chat.id]: mergeMessage(current[chat.id], data.message),
        }))
      }
      closeLocationPicker()
    } catch (error) {
      setErrorsByChatId((current) => ({
        ...current,
        [chat.id]:
          error instanceof Error
            ? error.message
            : 'Standort konnte nicht gesendet werden',
      }))
    } finally {
      setChatSending(chat.id, false)
    }
  }

  function removePendingAttachment(chatId: string, attachmentId: string) {
    setPendingAttachmentsByChatId((current) => ({
      ...current,
      [chatId]: (current[chatId] ?? []).filter(
        (attachment) => attachment.id !== attachmentId,
      ),
    }))
  }

  function addEmoji(chat: PinnedChat, emoji: string) {
    updateDraft(chat, `${draftsByChatId[chat.id] ?? ''}${emoji}`)
    setAttachMenuOpenChatId(null)
    setEmojiOpenChatId(null)
  }

  function setDockMessageReacting(messageId: string, reacting: boolean) {
    setReactingMessageIds((current) => {
      const next = new Set(current)
      if (reacting) {
        next.add(messageId)
      } else {
        next.delete(messageId)
      }
      return next
    })
  }

  async function toggleMessageReaction(
    message: DockChatMessage,
    emoji: string,
  ) {
    if (message.type === 'system' || reactingMessageIds.has(message.id)) {
      return
    }

    setDockMessageReacting(message.id, true)
    setErrorsByChatId((current) => ({
      ...current,
      [message.conversationId]: '',
    }))

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

      if (!response.ok) {
        throw new Error(
          await readApiError(response, 'Reaktion konnte nicht gespeichert werden'),
        )
      }

      const data = await response.json()
      if (data.message) {
        const updatedMessage = data.message as DockChatMessage
        setMessagesByChatId((current) => ({
          ...current,
          [updatedMessage.conversationId]: mergeMessage(
            current[updatedMessage.conversationId],
            updatedMessage,
          ),
        }))
      }
    } catch (error) {
      setErrorsByChatId((current) => ({
        ...current,
        [message.conversationId]:
          error instanceof Error
            ? error.message
            : 'Reaktion konnte nicht gespeichert werden',
      }))
    } finally {
      setDockMessageReacting(message.id, false)
      setReactionPickerMessageId((current) =>
        current === message.id ? '' : current,
      )
    }
  }

  useEffect(() => {
    reloadPinnedChats()

    window.addEventListener(pinnedChatsEventName, reloadPinnedChats)
    window.addEventListener('storage', reloadPinnedChats)

    return () => {
      window.removeEventListener(pinnedChatsEventName, reloadPinnedChats)
      window.removeEventListener('storage', reloadPinnedChats)
    }
  }, [reloadPinnedChats])

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

      let changed = false
      const nextPinnedChats = readPinnedChats(userId).map((chat) => {
        if (
          chat.partnerId !== user.id ||
          chat.presenceStatus === user.presenceStatus
        ) {
          return chat
        }

        changed = true
        return { ...chat, presenceStatus: user.presenceStatus }
      })

      if (!changed) {
        return
      }

      writePinnedChats(userId, nextPinnedChats)
    }

    window.addEventListener('user-notification', handleUserNotification)

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

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

    void loadMessages(openChatId)
  }, [loadMessages, openChatId])

  useEffect(() => {
    const unsubscribe = subscribe((rawPayload) => {
      const payload = rawPayload as DockChatSocketEvent

      if (payload.type === 'conversation.removed' && payload.conversationId) {
        writePinnedChats(
          userId,
          readPinnedChats(userId).filter(
            (chat) => chat.id !== payload.conversationId,
          ),
        )
        return
      }

      if (payload.type === 'messages.reload' && payload.conversationId) {
        if (payload.conversationId === openChatId) {
          void loadMessages(payload.conversationId)
        }
        return
      }

      if (
        payload.type === 'typing.updated' &&
        payload.conversationId &&
        payload.userId &&
        payload.userId !== 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.created' || payload.type === 'message.updated') {
        setMessagesByChatId((current) => ({
          ...current,
          [message.conversationId]: mergeMessage(
            current[message.conversationId],
            message,
          ),
        }))

        if (message.conversationId === openChatId && message.sender.id !== userId) {
          fetch(`${API_URL}/chat/conversations/${message.conversationId}/read`, {
            method: 'POST',
            credentials: 'include',
          }).catch(() => undefined)
          window.dispatchEvent(new Event('chat-unread-changed'))
        }
      }
    })

    return unsubscribe
  }, [loadMessages, openChatId, subscribe, userId])

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

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

  useEffect(() => {
    return () => {
      stopTyping()
    }
  }, [stopTyping])

  useEffect(() => {
    if (typingChatIdRef.current && typingChatIdRef.current !== openChatId) {
      stopTyping()
    }
    setReactionPickerMessageId('')
  }, [openChatId, stopTyping])

  useLayoutEffect(() => {
    if (!openChatId) {
      return
    }

    resizeDockTextarea(textareaRefsByChatId.current.get(openChatId) ?? null)
  }, [openChatDraft, openChatId])

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ block: 'end' })
  }, [openChatId, openChatMessages.length, openTypingCount])

  useEffect(() => {
    if (
      openChatId &&
      visiblePinnedChats.every((chat) => chat.id !== openChatId)
    ) {
      setOpenChatId(null)
    }
  }, [openChatId, visiblePinnedChats])

  if (dockChats.length === 0) {
    return null
  }

  return (
    <>
      <div className="pointer-events-none fixed right-4 bottom-0 left-4 z-[8500] flex flex-wrap items-end justify-end gap-2 lg:left-76">
      {dockChats.map((chat) => {
        const isOpen = chat.id === openChatId
        const messages = messagesByChatId[chat.id] ?? []
        const draft = draftsByChatId[chat.id] ?? ''
        const pendingAttachments = pendingAttachmentsByChatId[chat.id] ?? []
        const isLoading = loadingChatIds.has(chat.id)
        const isSending = sendingChatIds.has(chat.id)
        const isUploading = uploadingChatIds.has(chat.id)
        const error = errorsByChatId[chat.id]
        const canWrite = chat.type !== 'channel' && chat.isMember !== false
        const typingNames = Object.values(typingUsers[chat.id] ?? {})
          .filter((typingUser) => typingUser.expiresAt > typingNow)
          .map((typingUser) => typingUser.name)

        if (isOpen) {
          return (
            <section
              key={chat.id}
              className="pointer-events-auto flex h-[28rem] w-80 max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-t-xl border border-gray-200 bg-white shadow-xl shadow-gray-900/15 dark:border-white/10 dark:bg-gray-900 dark:shadow-black/25"
            >
              <div className="flex items-center gap-2 border-b border-gray-200 px-3 py-2 dark:border-white/10">
                <button
                  type="button"
                  onClick={() => setOpenChatId(null)}
                  className="min-w-0 flex flex-1 items-center gap-2 text-left"
                >
                  <ChatIcon chat={chat} />
                  <span className="min-w-0">
                    <span className="block truncate text-sm font-semibold text-gray-900 dark:text-white">
                      {chat.title}
                    </span>
                    <DockStatusLine chat={chat} typingNames={typingNames} />
                  </span>
                </button>

                <button
                  type="button"
                  onClick={() => navigate(chatHref(chat))}
                  className="inline-flex size-8 shrink-0 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-indigo-600 dark:hover:bg-white/10 dark:hover:text-indigo-300"
                >
                  <span className="sr-only">Chat vollständig öffnen</span>
                  <ArrowTopRightOnSquareIcon aria-hidden="true" className="size-4" />
                </button>

                <button
                  type="button"
                  onClick={() => setOpenChatId(null)}
                  className="inline-flex size-8 shrink-0 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-white/10 dark:hover:text-white"
                >
                  <span className="sr-only">Chat minimieren</span>
                  <ChevronDownIcon aria-hidden="true" className="size-4" />
                </button>

                <button
                  type="button"
                  onClick={() =>
                    writePinnedChats(
                      userId,
                      pinnedChats.filter((item) => item.id !== chat.id),
                    )
                  }
                  className="inline-flex size-8 shrink-0 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-white/10 dark:hover:text-white"
                >
                  <span className="sr-only">Fixierung entfernen</span>
                  <XMarkIcon aria-hidden="true" className="size-4" />
                </button>
              </div>

              <div className="flex-1 overflow-y-auto bg-gray-50 px-3 py-3 dark:bg-gray-950/40">
                {isLoading && messages.length === 0 ? (
                  <div className="flex h-full items-center justify-center text-sm text-gray-500 dark:text-gray-400">
                    Nachrichten werden geladen...
                  </div>
                ) : messages.length === 0 && typingNames.length === 0 ? (
                  <div className="flex h-full items-center justify-center text-center text-sm text-gray-500 dark:text-gray-400">
                    Noch keine Nachrichten.
                  </div>
                ) : (
                  <div className="space-y-2">
                    {messages.map((message) => {
                      const isOwn = message.sender.id === userId
                      const hasReactions = (message.reactions ?? []).some(
                        (reaction) => reaction.count > 0,
                      )

                      if (message.type === 'system') {
                        return (
                          <div
                            key={message.id}
                            className="mx-auto max-w-[85%] rounded-full bg-white px-3 py-1 text-center text-xs text-gray-500 ring-1 ring-gray-200 dark:bg-white/5 dark:text-gray-400 dark:ring-white/10"
                          >
                            {message.body}
                          </div>
                        )
                      }

                      return (
                        <div
                          key={message.id}
                          className={classNames(
                            'group flex flex-col',
                            isOwn ? 'items-end' : 'items-start',
                          )}
                        >
                          <div
                            className={classNames(
                              'max-w-[85%] rounded-2xl px-3 py-2 text-sm shadow-sm',
                              isOwn
                                ? 'rounded-br-md bg-indigo-600 text-white'
                                : 'rounded-bl-md bg-white text-gray-900 ring-1 ring-gray-200 dark:bg-gray-800 dark:text-white dark:ring-white/10',
                            )}
                          >
                            {!isOwn && (
                              <p className="mb-0.5 truncate text-xs font-semibold text-indigo-600">
                                {displayName(message.sender)}
                              </p>
                            )}
                            {message.body && (
                              <p className="whitespace-pre-wrap break-words">
                                {message.body}
                              </p>
                            )}
                            {message.location && (
                              <DockLocationPreview
                                isOwn={isOwn}
                                location={message.location}
                              />
                            )}
                            {message.attachments &&
                              message.attachments.length > 0 && (
                              <p
                                className={classNames(
                                  'mt-1 text-xs',
                                  isOwn ? 'text-indigo-100' : 'text-gray-500',
                                )}
                              >
                                {message.attachments.length} Anhang
                                {message.attachments.length === 1 ? '' : 'e'}
                              </p>
                            )}
                          </div>
                          <span className="mt-0.5 px-1 text-[11px] text-gray-400">
                            {formatTime(message.createdAt)}
                          </span>
                          {canWrite && reactionPickerMessageId === message.id && (
                            <div
                              className={classNames(
                                isOwn ? 'self-end' : 'self-start',
                                'mt-1 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',
                              )}
                            >
                              {dockReactionEmojis.map((emoji) => (
                                <button
                                  key={emoji}
                                  type="button"
                                  disabled={reactingMessageIds.has(message.id)}
                                  title={dockEmojiShortcut(emoji)}
                                  aria-label={`Mit ${dockEmojiShortcut(emoji)} reagieren`}
                                  onClick={() =>
                                    void toggleMessageReaction(message, emoji)
                                  }
                                  className="inline-flex size-7 items-center justify-center rounded text-base leading-none hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-white/10"
                                >
                                  {emoji}
                                </button>
                              ))}
                            </div>
                          )}
                          {(hasReactions || canWrite) && (
                            <div
                              className={classNames(
                                isOwn ? 'justify-end' : 'justify-start',
                                hasReactions
                                  ? 'flex'
                                  : 'hidden group-hover:flex',
                                'mt-1 max-w-[85%] items-center gap-1',
                              )}
                            >
                              <DockMessageReactions
                                userId={userId}
                                isOwn={isOwn}
                                reactions={message.reactions}
                                disabled={
                                  !canWrite || reactingMessageIds.has(message.id)
                                }
                                onToggle={(emoji) =>
                                  void toggleMessageReaction(message, emoji)
                                }
                              />
                              {canWrite && (
                                <button
                                  type="button"
                                  disabled={reactingMessageIds.has(message.id)}
                                  onClick={() =>
                                    setReactionPickerMessageId((current) =>
                                      current === message.id ? '' : message.id,
                                    )
                                  }
                                  title="Reagieren"
                                  className="inline-flex size-6 shrink-0 items-center justify-center rounded-full bg-white text-gray-500 shadow-sm ring-1 ring-gray-200 transition hover:bg-gray-50 hover:text-indigo-600 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/10"
                                >
                                  <FaceSmileIcon
                                    aria-hidden="true"
                                    className="size-3.5 stroke-[1.8]"
                                  />
                                </button>
                              )}
                            </div>
                          )}
                        </div>
                      )
                    })}
                    {typingNames.length > 0 && (
                      <DockTypingIndicator chat={chat} names={typingNames} />
                    )}
                    <div ref={messagesEndRef} />
                  </div>
                )}
              </div>

              {error && (
                <div className="border-t border-red-100 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-500/20 dark:bg-red-950/30 dark:text-red-200">
                  {error}
                </div>
              )}

              {canWrite ? (
                <form
                  onSubmit={(event) => void sendMessage(chat, event)}
                  className="relative border-t border-gray-200 p-2 dark:border-white/10"
                >
                  {emojiOpenChatId === chat.id && (
                    <div className="absolute bottom-full left-2 z-10 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">
                      {dockChatEmojis.map((emoji) => (
                        <button
                          key={emoji}
                          type="button"
                          title={dockEmojiShortcut(emoji)}
                          aria-label={`Emoji ${dockEmojiShortcut(emoji)}`}
                          onClick={() => addEmoji(chat, emoji)}
                          className="inline-flex size-8 items-center justify-center rounded text-lg leading-none hover:bg-gray-100 dark:hover:bg-white/10"
                        >
                          {emoji}
                        </button>
                      ))}
                    </div>
                  )}

                  {pendingAttachments.length > 0 && (
                    <div className="mb-2 flex flex-wrap gap-1.5">
                      {pendingAttachments.map((attachment) => (
                        <span
                          key={attachment.id}
                          className="inline-flex max-w-full items-center gap-1 rounded-full bg-indigo-50 px-2 py-1 text-xs font-medium text-indigo-700 ring-1 ring-indigo-100 dark:bg-indigo-500/10 dark:text-indigo-200 dark:ring-indigo-400/20"
                        >
                          <PaperClipIcon className="size-3.5 shrink-0" />
                          <span className="truncate">{attachment.fileName}</span>
                          <button
                            type="button"
                            onClick={() =>
                              removePendingAttachment(chat.id, attachment.id)
                            }
                            className="ml-0.5 rounded-full text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-100"
                          >
                            <span className="sr-only">Anhang entfernen</span>
                            <XMarkIcon aria-hidden="true" className="size-3.5" />
                          </button>
                        </span>
                      ))}
                    </div>
                  )}

                  <div className="flex items-end gap-1.5">
                    <input
                      ref={attachmentInputRef}
                      type="file"
                      multiple
                      className="sr-only"
                      onChange={(event) => void uploadAttachments(chat, event)}
                    />
                    <div className="relative">
                      <button
                        type="button"
                        disabled={isUploading}
                        onClick={() => {
                          setEmojiOpenChatId(null)
                          setAttachMenuOpenChatId((current) =>
                            current === chat.id ? null : chat.id,
                          )
                        }}
                        title="Anhängen"
                        className="inline-flex size-9 shrink-0 items-center justify-center rounded-full text-gray-600 hover:bg-gray-100 hover:text-gray-900 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-300 dark:hover:bg-white/10 dark:hover:text-white"
                      >
                        <span className="sr-only">Anhängen</span>
                        {isUploading ? (
                          <span className="size-4 animate-spin rounded-full border-2 border-gray-300 border-t-indigo-600 dark:border-white/20 dark:border-t-indigo-300" />
                        ) : (
                          <PlusIcon
                            aria-hidden="true"
                            className="size-5 stroke-[1.8]"
                          />
                        )}
                      </button>

                      {attachMenuOpenChatId === chat.id && (
                        <>
                          <button
                            type="button"
                            aria-hidden="true"
                            tabIndex={-1}
                            onClick={() => setAttachMenuOpenChatId(null)}
                            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={() => {
                                setAttachMenuOpenChatId(null)
                                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 stroke-[1.8] text-gray-500 dark:text-gray-300" />
                              Datei
                            </button>
                            <button
                              type="button"
                              onClick={() => openLocationPicker(chat)}
                              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 stroke-[1.8] text-gray-500 dark:text-gray-300" />
                              Standort
                            </button>
                          </div>
                        </>
                      )}
                    </div>
                    <button
                      type="button"
                      onClick={() => {
                        setAttachMenuOpenChatId(null)
                        setEmojiOpenChatId((current) =>
                          current === chat.id ? null : chat.id,
                        )
                      }}
                      title="Emoji auswählen"
                      className="inline-flex size-9 shrink-0 items-center justify-center rounded-full text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-white/10 dark:hover:text-white"
                    >
                      <span className="sr-only">Emoji auswählen</span>
                      <FaceSmileIcon
                        aria-hidden="true"
                        className="size-5 stroke-[1.8]"
                      />
                    </button>
                    <textarea
                      ref={(element) => {
                        if (element) {
                          textareaRefsByChatId.current.set(chat.id, element)
                          resizeDockTextarea(element)
                        } else {
                          textareaRefsByChatId.current.delete(chat.id)
                        }
                      }}
                      value={draft}
                      onChange={(event) => updateDraft(chat, event.target.value)}
                      onKeyDown={(event) => {
                        if (event.key === 'Enter' && !event.shiftKey) {
                          event.preventDefault()
                          event.currentTarget.form?.requestSubmit()
                        }
                      }}
                      rows={1}
                      maxLength={4000}
                      placeholder="Nachricht schreiben"
                      className={[
                        'max-h-32 min-h-9 min-w-0 flex-1 resize-none overflow-y-auto rounded-lg border-0 bg-gray-100 px-3 py-2 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',
                        scrollbarClassName,
                      ].join(' ')}
                    />
                    <button
                      type="submit"
                      disabled={
                        (!draft.trim() && pendingAttachments.length === 0) ||
                        isSending
                      }
                      className="inline-flex size-9 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>
                      {isSending ? (
                        <LoadingSpinner size="sm" className="text-white" />
                      ) : (
                        <PaperAirplaneIcon className="size-4" aria-hidden="true" />
                      )}
                    </button>
                  </div>
                </form>
              ) : (
                <div className="border-t border-gray-200 px-3 py-2 text-xs text-gray-500 dark:border-white/10 dark:text-gray-400">
                  Dieser Chat ist im Mini-Fenster nur lesbar.
                </div>
              )}
            </section>
          )
        }

        return (
          <div
            key={chat.id}
            className="pointer-events-auto flex w-64 max-w-[calc(100vw-2rem)] items-center gap-2 rounded-t-xl border border-gray-200 bg-white px-3 py-2 shadow-lg shadow-gray-900/10 dark:border-white/10 dark:bg-gray-900 dark:shadow-black/20"
          >
            <button
              type="button"
              onClick={() => setOpenChatId(chat.id)}
              className="min-w-0 flex flex-1 items-center gap-2 text-left"
            >
              <ChatIcon chat={chat} />
              <span className="min-w-0">
                <span className="block truncate text-sm font-semibold text-gray-900 dark:text-white">
                  {chat.title}
                </span>
                <DockStatusLine chat={chat} typingNames={typingNames} />
              </span>
            </button>

            <button
              type="button"
              onClick={() =>
                writePinnedChats(
                  userId,
                  pinnedChats.filter((item) => item.id !== chat.id),
                )
              }
              className="inline-flex size-7 shrink-0 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-white/10 dark:hover:text-white"
            >
              <span className="sr-only">Fixierung entfernen</span>
              <XMarkIcon aria-hidden="true" className="size-4" />
            </button>
          </div>
        )
      })}
      </div>

      <Modal
        open={Boolean(locationPickerChatId)}
        setOpen={(open) => {
          if (!open) {
            closeLocationPicker()
          }
        }}
        zIndexClassName="z-[10000]"
        backdropClassName="bg-gray-950/60 backdrop-blur-sm dark:bg-gray-950/60"
        panelClassName="flex max-h-[90vh] max-w-lg flex-col rounded-2xl bg-white dark:bg-gray-900"
      >
        <div className="flex items-center justify-between border-b border-gray-200 px-5 py-4 dark:border-white/10">
          <ModalTitle>Standort senden</ModalTitle>
          <button
            type="button"
            onClick={closeLocationPicker}
            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={Boolean(locationPickerChatId)}
            focusKey={locationPickerChatId ?? '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={closeLocationPicker}
            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 ||
              !locationChat ||
              sendingChatIds.has(locationChat.id)
            }
            onClick={() => void 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>
      </Modal>
    </>
  )
}
