import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
import { createPortal } from "react-dom";
import {
  deleteMessage,
  forwardMessageBulk,
  getChatAttachments,
  listMessageReactions,
  pinMessage,
  toggleMessageReaction
} from "../api/chats";
import type { ChatAttachment, Message, MessageReaction } from "../chat/types";
import { useAuthStore } from "../store/authStore";
import { useChatStore } from "../store/chatStore";
import { useAudioPlayerStore } from "../store/audioPlayerStore";
import { useUiStore } from "../store/uiStore";
import { formatTime } from "../utils/format";
import { formatMessageHtml } from "../utils/formatMessage";
import { MediaViewer } from "./MediaViewer";

type ContextMenuState = {
  x: number;
  y: number;
  messageId: number;
  attachmentUrl?: string | null;
} | null;

type PendingDeleteState = {
  chatId: number;
  messages: Message[];
  expiresAt: number;
  timerId: number;
} | null;

type MediaViewerState = {
  items: Array<{ url: string; type: "image" | "video" }>;
  index: number;
} | null;

const QUICK_REACTIONS = ["👍", "❤️", "🔥", "😂", "😮", "😢"];

export function MessageList() {
  const me = useAuthStore((s) => s.me);
  const activeChatId = useChatStore((s) => s.activeChatId);
  const messagesByChat = useChatStore((s) => s.messagesByChat);
  const typingByChat = useChatStore((s) => s.typingByChat);
  const hasMoreByChat = useChatStore((s) => s.hasMoreByChat);
  const loadingMoreByChat = useChatStore((s) => s.loadingMoreByChat);
  const loadMoreMessages = useChatStore((s) => s.loadMoreMessages);
  const unreadBoundaryByChat = useChatStore((s) => s.unreadBoundaryByChat);
  const focusedMessageIdByChat = useChatStore((s) => s.focusedMessageIdByChat);
  const setFocusedMessage = useChatStore((s) => s.setFocusedMessage);
  const chats = useChatStore((s) => s.chats);
  const setReplyToMessage = useChatStore((s) => s.setReplyToMessage);
  const updateChatPinnedMessage = useChatStore((s) => s.updateChatPinnedMessage);
  const removeMessage = useChatStore((s) => s.removeMessage);
  const restoreMessages = useChatStore((s) => s.restoreMessages);
  const showToast = useUiStore((s) => s.showToast);

  const [ctx, setCtx] = useState<ContextMenuState>(null);
  const [forwardMessageId, setForwardMessageId] = useState<number | null>(null);
  const [forwardQuery, setForwardQuery] = useState("");
  const [forwardError, setForwardError] = useState<string | null>(null);
  const [isForwarding, setIsForwarding] = useState(false);
  const [forwardSelectedChatIds, setForwardSelectedChatIds] = useState<Set<number>>(new Set());
  const [deleteMessageId, setDeleteMessageId] = useState<number | null>(null);
  const [deleteError, setDeleteError] = useState<string | null>(null);
  const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
  const [pendingDelete, setPendingDelete] = useState<PendingDeleteState>(null);
  const [undoTick, setUndoTick] = useState(0);
  const [reactionsByMessage, setReactionsByMessage] = useState<Record<number, MessageReaction[]>>({});
  const [attachmentsByMessage, setAttachmentsByMessage] = useState<Record<number, ChatAttachment[]>>({});
  const [mediaViewer, setMediaViewer] = useState<MediaViewerState>(null);
  const [showScrollToBottom, setShowScrollToBottom] = useState(false);
  const scrollContainerRef = useRef<HTMLDivElement | null>(null);

  const messages = useMemo(() => {
    if (!activeChatId) {
      return [];
    }
    return messagesByChat[activeChatId] ?? [];
  }, [activeChatId, messagesByChat]);

  const messagesMap = useMemo(() => new Map(messages.map((m) => [m.id, m])), [messages]);
  const activeChat = chats.find((chat) => chat.id === activeChatId);
  const forwardTargets = useMemo(() => {
    const q = forwardQuery.trim().toLowerCase();
    if (!q) return chats;
    return chats.filter((chat) => {
      const label = chatLabel(chat).toLowerCase();
      const handle = (chat.handle || "").toLowerCase();
      return label.includes(q) || handle.includes(q);
    });
  }, [chats, forwardQuery]);
  const unreadBoundaryCount = activeChatId ? (unreadBoundaryByChat[activeChatId] ?? 0) : 0;
  const unreadBoundaryIndex = unreadBoundaryCount > 0 ? Math.max(0, messages.length - unreadBoundaryCount) : -1;
  const focusedMessageId = activeChatId ? (focusedMessageIdByChat[activeChatId] ?? null) : null;
  const hasMore = Boolean(activeChatId && hasMoreByChat[activeChatId]);
  const isLoadingMore = Boolean(activeChatId && loadingMoreByChat[activeChatId]);
  const selectedMessages = useMemo(() => messages.filter((m) => selectedIds.has(m.id)), [messages, selectedIds]);
  const channelOnlyDeleteForAll = activeChat?.type === "channel" && !activeChat?.is_saved;
  const canDeleteAllForSelection = useMemo(
    () => selectedMessages.length > 0 && selectedMessages.every((m) => canDeleteForEveryone(m, activeChat, me?.id)),
    [selectedMessages, activeChat, me?.id]
  );

  useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key !== "Escape") {
        return;
      }
      setCtx(null);
      setForwardMessageId(null);
      setForwardSelectedChatIds(new Set());
      setDeleteMessageId(null);
      setSelectedIds(new Set());
      setMediaViewer(null);
    };
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, []);

  useEffect(() => {
    setSelectedIds(new Set());
    setCtx(null);
    setDeleteMessageId(null);
    setForwardMessageId(null);
    setForwardSelectedChatIds(new Set());
    setReactionsByMessage({});
    setAttachmentsByMessage({});
  }, [activeChatId]);

  useEffect(() => {
    if (!activeChatId) {
      setAttachmentsByMessage({});
      return;
    }
    let cancelled = false;
    void (async () => {
      try {
        const rows = await getChatAttachments(activeChatId, 400);
        if (cancelled) {
          return;
        }
        const grouped: Record<number, ChatAttachment[]> = {};
        for (const row of rows) {
          if (!grouped[row.message_id]) {
            grouped[row.message_id] = [];
          }
          grouped[row.message_id].push(row);
        }
        for (const key of Object.keys(grouped)) {
          grouped[Number(key)] = grouped[Number(key)].sort((a, b) => a.id - b.id);
        }
        setAttachmentsByMessage(grouped);
      } catch {
        if (!cancelled) {
          setAttachmentsByMessage({});
        }
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [activeChatId, messages]);

  useEffect(() => {
    if (!pendingDelete) {
      return;
    }
    const interval = window.setInterval(() => setUndoTick((v) => v + 1), 250);
    return () => window.clearInterval(interval);
  }, [pendingDelete]);

  useEffect(() => {
    if (!activeChatId || !focusedMessageId) {
      return;
    }
    const element = document.getElementById(`message-${focusedMessageId}`);
    if (!element) {
      return;
    }
    element.scrollIntoView({ behavior: "smooth", block: "center" });
    const timer = window.setTimeout(() => setFocusedMessage(activeChatId, null), 2500);
    return () => window.clearTimeout(timer);
  }, [activeChatId, focusedMessageId, messages.length, setFocusedMessage]);

  useEffect(() => {
    const container = scrollContainerRef.current;
    if (!container) {
      return;
    }
    const distance = container.scrollHeight - container.scrollTop - container.clientHeight;
    setShowScrollToBottom(distance > 180);
  }, [messages.length, activeChatId]);

  if (!activeChatId) {
    return <div className="flex h-full items-center justify-center text-slate-300/80">Select a chat</div>;
  }
  const chatId = activeChatId;

  function scrollToBottom() {
    const container = scrollContainerRef.current;
    if (!container) {
      return;
    }
    container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
  }

  async function ensureReactionsLoaded(messageId: number) {
    if (reactionsByMessage[messageId]) {
      return;
    }
    try {
      const rows = await listMessageReactions(messageId);
      setReactionsByMessage((state) => ({ ...state, [messageId]: rows }));
    } catch {
      return;
    }
  }

  async function handleToggleReaction(messageId: number, emoji: string) {
    try {
      const rows = await toggleMessageReaction(messageId, emoji);
      setReactionsByMessage((state) => ({ ...state, [messageId]: rows }));
    } catch {
      return;
    }
  }

  async function handleForwardSubmit() {
    if (!forwardMessageId) return;
    const targetChatIds = [...forwardSelectedChatIds];
    if (!targetChatIds.length) {
      setForwardError("Select at least one chat");
      return;
    }
    setIsForwarding(true);
    setForwardError(null);
    try {
      await forwardMessageBulk(forwardMessageId, targetChatIds);
      setForwardMessageId(null);
      setForwardSelectedChatIds(new Set());
      setForwardQuery("");
    } catch {
      setForwardError("Failed to forward message");
    } finally {
      setIsForwarding(false);
    }
    setCtx(null);
  }

  async function handlePin(messageId: number) {
    const nextPinned = activeChat?.pinned_message_id === messageId ? null : messageId;
    const chat = await pinMessage(chatId, nextPinned);
    updateChatPinnedMessage(chatId, chat.pinned_message_id ?? null);
    setCtx(null);
  }

  async function handleDelete(forAll: boolean) {
    if (!deleteMessageId) {
      return;
    }
    try {
      await deleteMessage(deleteMessageId, forAll);
      removeMessage(chatId, deleteMessageId);
      setDeleteMessageId(null);
      setDeleteError(null);
    } catch {
      setDeleteError("Failed to delete message");
    }
  }

  function toggleSelected(messageId: number) {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(messageId)) {
        next.delete(messageId);
      } else {
        next.add(messageId);
      }
      return next;
    });
  }

  async function commitPendingDelete(state: PendingDeleteState) {
    if (!state) {
      return;
    }
    await Promise.allSettled(state.messages.map((message) => deleteMessage(message.id, false)));
    setPendingDelete((current) => {
      if (!current || current.timerId !== state.timerId) {
        return current;
      }
      return null;
    });
  }

  async function deleteSelectedForMe() {
    if (!selectedMessages.length) {
      return;
    }
    if (pendingDelete) {
      window.clearTimeout(pendingDelete.timerId);
      await commitPendingDelete(pendingDelete);
    }
    for (const message of selectedMessages) {
      removeMessage(chatId, message.id);
    }
    const timeoutMs = 6000;
    const timerId = window.setTimeout(() => {
      void commitPendingDelete({
        chatId,
        messages: selectedMessages,
        expiresAt: Date.now() + timeoutMs,
        timerId
      });
    }, timeoutMs);
    setPendingDelete({
      chatId,
      messages: selectedMessages,
      expiresAt: Date.now() + timeoutMs,
      timerId
    });
    setSelectedIds(new Set());
  }

  async function deleteSelectedForEveryone() {
    if (!selectedMessages.length) {
      return;
    }
    const results = await Promise.allSettled(
      selectedMessages.map(async (message) => {
        await deleteMessage(message.id, true);
        return message.id;
      })
    );
    for (const result of results) {
      if (result.status === "fulfilled") {
        removeMessage(chatId, result.value);
      }
    }
    if (results.some((r) => r.status === "rejected")) {
      setDeleteError("Some messages could not be deleted for everyone");
    } else {
      setDeleteError(null);
    }
    setSelectedIds(new Set());
  }

  function undoDeleteForMe() {
    if (!pendingDelete || pendingDelete.chatId !== chatId) {
      setPendingDelete(null);
      return;
    }
    window.clearTimeout(pendingDelete.timerId);
    restoreMessages(chatId, pendingDelete.messages);
    setPendingDelete(null);
  }

  return (
    <div className="flex h-full flex-col" onClick={() => { setCtx(null); }}>
      {activeChat?.pinned_message_id ? (
        <div className="border-b border-slate-700/50 bg-slate-900/60 px-4 py-2 text-xs text-sky-300">
          📌 {messagesMap.get(activeChat.pinned_message_id)?.text || "Pinned message"}
        </div>
      ) : null}
      {selectedIds.size > 0 ? (
        <div className="flex items-center justify-between border-b border-slate-700/50 bg-slate-900/80 px-3 py-2 text-xs">
          <span className="font-semibold text-slate-200">{selectedIds.size} selected</span>
          <div className="flex items-center gap-2">
            {!channelOnlyDeleteForAll ? (
              <button className="rounded bg-red-500 px-2 py-1 font-semibold text-white" onClick={() => void deleteSelectedForMe()}>
                Delete for me
              </button>
            ) : null}
            {canDeleteAllForSelection ? (
              <button className="rounded bg-red-600 px-2 py-1 font-semibold text-white" onClick={() => void deleteSelectedForEveryone()}>
                Delete for everyone
              </button>
            ) : null}
            <button className="rounded bg-slate-700 px-2 py-1 text-slate-200" onClick={() => setSelectedIds(new Set())}>
              Cancel
            </button>
          </div>
        </div>
      ) : null}

      <div
        className="tg-scrollbar flex-1 overflow-auto px-2 py-4 md:px-5"
        ref={scrollContainerRef}
        onScroll={(event) => {
          const target = event.currentTarget;
          const distance = target.scrollHeight - target.scrollTop - target.clientHeight;
          setShowScrollToBottom(distance > 180);
        }}
      >
        {hasMore ? (
          <div className="mb-3 flex justify-center">
            <button
              className="rounded-full border border-slate-700/80 bg-slate-900/70 px-3 py-1 text-xs text-slate-300 hover:bg-slate-800 disabled:opacity-60"
              disabled={isLoadingMore}
              onClick={() => void loadMoreMessages(chatId)}
            >
              {isLoadingMore ? "Loading..." : "Load older messages"}
            </button>
          </div>
        ) : null}

        {messages.map((message, messageIndex) => {
          const own = message.sender_id === me?.id;
          const prev = messageIndex > 0 ? messages[messageIndex - 1] : null;
          const groupedWithPrev = Boolean(
            prev &&
            prev.sender_id === message.sender_id &&
            Math.abs(new Date(message.created_at).getTime() - new Date(prev.created_at).getTime()) < 4 * 60 * 1000
          );
          const replySource = message.reply_to_message_id ? messagesMap.get(message.reply_to_message_id) : null;
          const isSelected = selectedIds.has(message.id);
          const messageReactions = reactionsByMessage[message.id] ?? [];

          return (
            <div key={`${message.id}-${message.client_message_id ?? ""}`}>
              {unreadBoundaryIndex === messageIndex ? (
                <div className="mb-2 mt-1 flex items-center gap-2 px-1">
                  <span className="h-px flex-1 bg-slate-700/60" />
                  <span className="rounded-full border border-slate-700/70 bg-slate-900/80 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-sky-300">
                    New messages
                  </span>
                  <span className="h-px flex-1 bg-slate-700/60" />
                </div>
              ) : null}

              <div className={`${groupedWithPrev ? "mb-1" : "mb-2"} flex ${own ? "justify-end" : "justify-start"}`}>
                <div
                  id={`message-${message.id}`}
                  className={`max-w-[90%] px-3 py-2.5 shadow-sm md:max-w-[70%] ${
                    own
                      ? `${groupedWithPrev ? "rounded-2xl rounded-tr-md" : "rounded-2xl rounded-br-md"} bg-gradient-to-b from-sky-500/95 to-sky-600/90 text-slate-950`
                      : `${groupedWithPrev ? "rounded-2xl rounded-tl-md" : "rounded-2xl rounded-bl-md"} border border-slate-700/60 bg-slate-900/80 text-slate-100`
                  } ${isSelected ? "ring-2 ring-sky-400/80" : ""} ${focusedMessageId === message.id ? "ring-2 ring-amber-300" : ""}`}
                  onClick={() => {
                    if (selectedIds.size > 0) {
                      toggleSelected(message.id);
                    }
                  }}
                  onContextMenu={(event) => {
                    event.preventDefault();
                    void ensureReactionsLoaded(message.id);
                    const pos = getSafeContextPosition(event.clientX, event.clientY, 220, 220);
                    setCtx({
                      x: pos.x,
                      y: pos.y,
                      messageId: message.id,
                      attachmentUrl: getMessageAttachmentUrl(message, attachmentsByMessage[message.id] ?? []),
                    });
                  }}
                >
                  {selectedIds.size > 0 ? (
                    <div
                      className={`mb-1 inline-flex h-4 w-4 items-center justify-center rounded-full border text-[10px] ${
                        isSelected ? "border-sky-300 bg-sky-300 text-slate-900" : "border-slate-400 text-slate-300"
                      }`}
                    >
                      {isSelected ? "✓" : ""}
                    </div>
                  ) : null}

                  {message.forwarded_from_message_id ? (
                    <div
                      className={`mb-1 rounded-md border-l-2 px-2 py-1 text-[11px] ${
                        own
                          ? "border-slate-900/60 bg-slate-900/10 text-slate-900/75"
                          : "border-sky-400 bg-slate-800/60 text-sky-300"
                      }`}
                    >
                      ↪ Forwarded message
                    </div>
                  ) : null}

                  {replySource ? (
                    <div
                      className={`mb-1 rounded-md border-l-2 px-2 py-1 text-[11px] ${
                        own
                          ? "border-slate-900/60 bg-slate-900/10 text-slate-900/75"
                          : "border-sky-400 bg-slate-800/60 text-slate-300"
                      }`}
                    >
                      <p className="truncate font-semibold">{replySource.sender_id === me?.id ? "You" : "Reply"}</p>
                      <p className="truncate">{replySource.text || "[media]"}</p>
                    </div>
                  ) : null}

                  {renderMessageContent(message, {
                    attachments: attachmentsByMessage[message.id] ?? [],
                    onAttachmentContextMenu: (event, url) => {
                      event.preventDefault();
                      void ensureReactionsLoaded(message.id);
                      const pos = getSafeContextPosition(event.clientX, event.clientY, 220, 280);
                      setCtx({ x: pos.x, y: pos.y, messageId: message.id, attachmentUrl: url });
                    },
                    onOpenMedia: (url, type) => {
                      const items = collectMediaItems(messages, attachmentsByMessage);
                      const idx = items.findIndex((i) => i.url === url && i.type === type);
                      if (!items.length) {
                        return;
                      }
                      setMediaViewer({ items, index: idx >= 0 ? idx : 0 });
                    },
                  })}

                  {messageReactions.length > 0 ? (
                    <div className="mt-2 flex flex-wrap gap-1">
                      {messageReactions.map((reaction) => (
                        <span
                          className={`rounded-full border px-2 py-0.5 text-[11px] ${
                            reaction.reacted
                              ? "border-sky-300 bg-sky-500/30"
                              : own
                                ? "border-slate-900/30 bg-slate-900/10"
                                : "border-slate-600/60 bg-slate-800/60"
                          }`}
                          key={`${message.id}-${reaction.emoji}`}
                        >
                          {reaction.emoji} {reaction.count}
                        </span>
                      ))}
                    </div>
                  ) : null}

                  <p className={`mt-1 flex items-center justify-end gap-1 text-[11px] ${own ? "text-slate-900/85" : "text-slate-400"}`}>
                    <span>{formatTime(message.created_at)}</span>
                    {own ? (
                      <span className={message.delivery_status === "read" ? "text-cyan-900" : ""}>{renderStatus(message.delivery_status)}</span>
                    ) : null}
                  </p>
                </div>
              </div>
            </div>
          );
        })}
      </div>

      <div className="px-5 pb-2 text-xs text-slate-300/80">{(typingByChat[activeChatId] ?? []).length > 0 ? "typing..." : ""}</div>
      {showScrollToBottom ? (
        <div className="pointer-events-none absolute bottom-20 right-4 z-40 md:bottom-24">
          <button
            className="pointer-events-auto inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-600/80 bg-slate-900/90 text-lg text-slate-100 shadow-lg hover:bg-slate-800"
            onClick={scrollToBottom}
            type="button"
            title="Scroll to latest"
          >
            ↓
          </button>
        </div>
      ) : null}

      {ctx
        ? createPortal(
            <div className="fixed inset-0 z-[109]" onClick={() => setCtx(null)}>
              <div
                className="fixed z-[110] w-56 rounded-xl border border-slate-700/90 bg-slate-900/95 p-1.5 shadow-2xl"
                style={{ left: ctx.x, top: ctx.y }}
                onClick={(event) => event.stopPropagation()}
              >
              <div className="mb-1 flex flex-wrap gap-1 rounded-lg bg-slate-800/80 p-1">
                {QUICK_REACTIONS.map((emoji) => (
                  <button
                    className="rounded-md px-1.5 py-1 text-sm hover:bg-slate-700"
                    key={emoji}
                    onClick={() => void handleToggleReaction(ctx.messageId, emoji)}
                    type="button"
                  >
                    {emoji}
                  </button>
                ))}
              </div>

              <button
                className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
                onClick={() => {
                  const msg = messagesMap.get(ctx.messageId);
                  if (msg) {
                    setReplyToMessage(chatId, msg);
                  }
                  setCtx(null);
                }}
              >
                Reply
              </button>
              <button
                className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
                onClick={() => {
                  setForwardMessageId(ctx.messageId);
                  setForwardQuery("");
                  setForwardError(null);
                  setForwardSelectedChatIds(new Set());
                  setCtx(null);
                }}
              >
                Forward
              </button>
              <button
                className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
                onClick={() => {
                  setSelectedIds(new Set([ctx.messageId]));
                  setCtx(null);
                }}
              >
                Select
              </button>
              <button
                className="block w-full rounded px-2 py-1.5 text-left text-sm text-red-300 hover:bg-slate-800"
                onClick={() => {
                  setDeleteMessageId(ctx.messageId);
                  setDeleteError(null);
                  setCtx(null);
                }}
              >
                Delete
              </button>
              <button className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800" onClick={() => void handlePin(ctx.messageId)}>
                Pin / Unpin
              </button>
              {ctx.attachmentUrl ? (
                <>
                  <div className="my-1 h-px bg-slate-700/80" />
                  <a className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800" href={ctx.attachmentUrl} rel="noreferrer" target="_blank">
                    Open media
                  </a>
                  <button
                    className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
                    onClick={async () => {
                      const url = ctx.attachmentUrl;
                      if (!url) {
                        return;
                      }
                      try {
                        await downloadFileFromUrl(url);
                        showToast("File downloaded");
                      } catch {
                        showToast("Download failed");
                      } finally {
                        setCtx(null);
                      }
                    }}
                    type="button"
                  >
                    Download
                  </button>
                  <button
                    className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
                    onClick={async () => {
                      const url = ctx.attachmentUrl;
                      if (!url) {
                        return;
                      }
                      try {
                        await navigator.clipboard.writeText(url);
                        showToast("Link copied");
                      } catch {
                        showToast("Copy failed");
                      } finally {
                        setCtx(null);
                      }
                    }}
                    type="button"
                  >
                    Copy link
                  </button>
                </>
              ) : null}
              </div>
            </div>,
            document.body
          )
        : null}

      {forwardMessageId ? (
        <div className="absolute inset-0 z-50 flex items-end justify-center bg-slate-950/55 p-3" onClick={() => setForwardMessageId(null)}>
          <div className="w-full rounded-xl border border-slate-700/80 bg-slate-900 p-3" onClick={(event) => event.stopPropagation()}>
            <p className="mb-2 text-sm font-semibold">Forward message</p>
            <input
              className="mb-2 w-full rounded-lg border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500"
              placeholder="Search chats"
              value={forwardQuery}
              onChange={(event) => setForwardQuery(event.target.value)}
            />
            <div className="tg-scrollbar max-h-56 space-y-1 overflow-auto">
              {forwardTargets.map((chat) => (
                <button
                  className={`block w-full rounded-lg px-3 py-2 text-left text-sm disabled:opacity-60 ${
                    forwardSelectedChatIds.has(chat.id) ? "bg-sky-500/30" : "bg-slate-800 hover:bg-slate-700"
                  }`}
                  disabled={isForwarding}
                  key={chat.id}
                  onClick={() => {
                    setForwardSelectedChatIds((prev) => {
                      const next = new Set(prev);
                      if (next.has(chat.id)) {
                        next.delete(chat.id);
                      } else {
                        next.add(chat.id);
                      }
                      return next;
                    });
                  }}
                >
                  <p className="truncate font-semibold">{chatLabel(chat)}</p>
                  <p className="truncate text-xs text-slate-400">{chat.handle ? `@${chat.handle}` : chat.type}</p>
                </button>
              ))}
              {forwardTargets.length === 0 ? <p className="px-1 py-2 text-xs text-slate-400">No chats found</p> : null}
            </div>
            {forwardError ? <p className="mt-2 text-xs text-red-400">{forwardError}</p> : null}
            <div className="mt-3 flex gap-2">
              <button className="w-full rounded bg-sky-500 px-3 py-2 text-sm font-semibold text-slate-950" onClick={() => void handleForwardSubmit()}>
                Forward ({forwardSelectedChatIds.size})
              </button>
              <button className="w-full rounded bg-slate-700 px-3 py-2 text-sm" onClick={() => setForwardMessageId(null)}>
                Cancel
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {mediaViewer ? (
        <MediaViewer
          index={mediaViewer.index}
          items={mediaViewer.items}
          onClose={() => setMediaViewer(null)}
          onIndexChange={(nextIndex) => setMediaViewer((prev) => (prev ? { ...prev, index: nextIndex } : prev))}
          onToast={showToast}
          open
        />
      ) : null}

      {deleteMessageId ? (
        <div className="absolute inset-0 z-50 flex items-end justify-center bg-slate-950/55 p-3" onClick={() => setDeleteMessageId(null)}>
          <div className="w-full rounded-xl border border-slate-700/80 bg-slate-900 p-3" onClick={(event) => event.stopPropagation()}>
            <p className="mb-2 text-sm font-semibold">Delete message</p>
            <p className="mb-3 text-xs text-slate-400">
              {channelOnlyDeleteForAll
                ? "In channels, messages can only be deleted for everyone."
                : "Choose how to delete this message."}
            </p>
            <div className="space-y-2">
              {!channelOnlyDeleteForAll ? (
                <button className="w-full rounded bg-red-500 px-3 py-2 text-sm font-semibold text-white" onClick={() => void handleDelete(false)}>
                  Delete for me
                </button>
              ) : null}
              {canDeleteForEveryone(messagesMap.get(deleteMessageId), activeChat, me?.id) ? (
                <button className="w-full rounded bg-red-600 px-3 py-2 text-sm font-semibold text-white" onClick={() => void handleDelete(true)}>
                  Delete for everyone
                </button>
              ) : null}
              <button className="w-full rounded bg-slate-700 px-3 py-2 text-sm" onClick={() => setDeleteMessageId(null)}>
                Cancel
              </button>
            </div>
            {deleteError ? <p className="mt-2 text-xs text-red-400">{deleteError}</p> : null}
          </div>
        </div>
      ) : null}

      {pendingDelete && pendingDelete.chatId === chatId ? (
        <div className="pointer-events-none absolute bottom-3 left-0 right-0 z-50 flex justify-center px-3" data-tick={undoTick}>
          <div className="pointer-events-auto flex w-full max-w-sm items-center justify-between rounded-lg border border-slate-700/80 bg-slate-900/95 px-3 py-2 text-xs shadow-xl">
            <span className="text-slate-200">
              Messages deleted
              {` (${Math.max(0, Math.ceil((pendingDelete.expiresAt - Date.now()) / 1000))}s)`}
            </span>
            <button className="rounded bg-sky-500 px-2 py-1 font-semibold text-slate-950" onClick={undoDeleteForMe} type="button">
              Undo
            </button>
          </div>
        </div>
      ) : null}
    </div>
  );
}

function renderMessageContent(
  message: Message,
  opts: {
    attachments: ChatAttachment[];
    onAttachmentContextMenu: (event: MouseEvent<HTMLElement>, url: string) => void;
    onOpenMedia: (url: string, type: "image" | "video") => void;
  }
) {
  const messageType = message.type;
  const text = message.text;
  const legacyAttachment: ChatAttachment[] =
    text && /^https?:\/\//i.test(text)
      ? [
          {
            id: -1,
            message_id: message.id,
            sender_id: message.sender_id,
            message_type: messageType,
            message_created_at: message.created_at,
            file_url: text,
            file_type: guessFileTypeByMessageType(messageType),
            file_size: 0,
            waveform_points: message.attachment_waveform ?? null,
          },
        ]
      : [];
  const attachments = opts.attachments.length > 0 ? opts.attachments : legacyAttachment;
  const captionText =
    text && (!/^https?:\/\//i.test(text) || opts.attachments.length > 0)
      ? text.trim()
      : "";

  if (messageType === "image" || messageType === "video" || messageType === "circle_video") {
    const mediaItems = attachments
      .filter((item) => item.file_type.startsWith("image/") || item.file_type.startsWith("video/"))
      .map((item) => ({
        url: item.file_url,
        type: (item.file_type.startsWith("image/") ? "image" : "video") as "image" | "video",
      }));
    if (!mediaItems.length && text) {
      mediaItems.push({ url: text, type: messageType === "image" ? "image" : "video" });
    }
    if (!mediaItems.length) {
      return <p className="opacity-80">[empty]</p>;
    }
    if (mediaItems.length === 1) {
      const item = mediaItems[0];
      return (
        <div className="space-y-1.5">
          <button
            className="relative block overflow-hidden rounded-xl bg-slate-950/30"
            onClick={() => opts.onOpenMedia(item.url, item.type)}
            onContextMenu={(event) => {
              event.stopPropagation();
              opts.onAttachmentContextMenu(event, item.url);
            }}
            type="button"
          >
            {item.type === "image" ? (
              <img alt="attachment" className="max-h-80 rounded-xl object-cover" draggable={false} src={item.url} />
            ) : (
              <>
                <video className="max-h-80 rounded-xl" muted src={item.url} />
                <span className="absolute inset-0 flex items-center justify-center text-3xl text-white/85">▶</span>
              </>
            )}
          </button>
          {captionText ? (
            <p className="whitespace-pre-wrap break-words text-sm" dangerouslySetInnerHTML={{ __html: formatMessageHtml(captionText) }} />
          ) : null}
        </div>
      );
    }
    const gridClass = getMediaGridClass(mediaItems.length);
    return (
      <div className="space-y-1.5" onContextMenu={(event) => event.stopPropagation()}>
        <div className={`grid gap-1.5 rounded-xl ${gridClass}`}>
          {mediaItems.slice(0, 6).map((item, index) => {
            const tileClass = getMediaTileClass(mediaItems.length, index);
            return (
              <button
                className={`relative overflow-hidden rounded-lg bg-slate-950/30 ${tileClass}`}
                key={`${item.url}-${index}`}
                onClick={() => opts.onOpenMedia(item.url, item.type)}
                onContextMenu={(event) => {
                  event.stopPropagation();
                  opts.onAttachmentContextMenu(event, item.url);
                }}
                type="button"
              >
                {item.type === "image" ? (
                  <img alt="attachment" className="h-full w-full object-cover" draggable={false} src={item.url} />
                ) : (
                  <>
                    <video className="h-full w-full object-cover" muted src={item.url} />
                    <span className="absolute inset-0 flex items-center justify-center text-2xl text-white/85">▶</span>
                  </>
                )}
                {index === 5 && mediaItems.length > 6 ? (
                  <span className="absolute inset-0 flex items-center justify-center bg-slate-950/60 text-lg font-semibold text-white">
                    +{mediaItems.length - 6}
                  </span>
                ) : null}
              </button>
            );
          })}
        </div>
        {captionText ? (
          <p className="whitespace-pre-wrap break-words text-sm" dangerouslySetInnerHTML={{ __html: formatMessageHtml(captionText) }} />
        ) : null}
      </div>
    );
  }

  if (messageType === "voice") {
    const voiceItems = attachments.filter((item) => item.file_type.startsWith("audio/"));
    const items = voiceItems.length ? voiceItems : (text ? legacyAttachment : []);
    if (!items.length) {
      return <p className="opacity-80">[empty]</p>;
    }
    return (
      <div className="space-y-1.5">
        {items.map((item, index) => (
          <div
            className="rounded-xl border border-slate-700/60 bg-slate-950/30 p-2"
            key={`${item.file_url}-${index}`}
            onContextMenu={(event) => {
              event.stopPropagation();
              opts.onAttachmentContextMenu(event, item.file_url);
            }}
          >
            <VoiceInlinePlayer
              src={item.file_url}
              title="Voice message"
              waveform={item.waveform_points ?? message.attachment_waveform ?? null}
            />
          </div>
        ))}
      </div>
    );
  }

  if (messageType === "audio") {
    const audioItems = attachments.filter((item) => item.file_type.startsWith("audio/"));
    const items = audioItems.length ? audioItems : (text ? legacyAttachment : []);
    if (!items.length) {
      return <p className="opacity-80">[empty]</p>;
    }
    return (
      <div className="space-y-1.5">
        {items.map((item, index) => (
          <div
            className="rounded-xl border border-slate-700/60 bg-slate-950/30 p-2"
            key={`${item.file_url}-${index}`}
            onContextMenu={(event) => {
              event.stopPropagation();
              opts.onAttachmentContextMenu(event, item.file_url);
            }}
          >
            <div className="mb-1 flex items-center gap-2 text-xs text-slate-300">
              <span className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-emerald-500/25 text-emerald-200">🎵</span>
              <div className="min-w-0">
                <p className="truncate font-semibold text-slate-100">{extractFileName(item.file_url)}</p>
                <p className="text-[11px] text-slate-400">Audio</p>
              </div>
            </div>
            <AudioInlinePlayer src={item.file_url} title={extractFileName(item.file_url)} />
          </div>
        ))}
      </div>
    );
  }

  if (messageType === "file") {
    const fileItems = attachments.length ? attachments : (text ? legacyAttachment : []);
    if (fileItems.length) {
      return (
        <div className="space-y-1.5">
          {fileItems.map((item, index) => (
            <button
              className="w-full rounded-xl border border-slate-600/60 bg-slate-800/70 px-3 py-2 text-left"
              key={`${item.file_url}-${index}`}
              onContextMenu={(event) => {
                event.stopPropagation();
                opts.onAttachmentContextMenu(event, item.file_url);
              }}
              onClick={() => window.open(item.file_url, "_blank", "noopener,noreferrer")}
              type="button"
            >
              <p className="text-xs uppercase tracking-wide text-slate-400">Document</p>
              <p className="truncate text-sm font-semibold text-slate-100">{extractFileName(item.file_url)}</p>
            </button>
          ))}
          {captionText ? (
            <p className="whitespace-pre-wrap break-words text-sm" dangerouslySetInnerHTML={{ __html: formatMessageHtml(captionText) }} />
          ) : null}
        </div>
      );
    }
  }

  if (!text) {
    return <p className="opacity-80">[empty]</p>;
  }
  return <p className="whitespace-pre-wrap break-words" dangerouslySetInnerHTML={{ __html: formatMessageHtml(text) }} />;
}

function renderStatus(status: string | undefined): string {
  if (status === "sending") return "⌛";
  if (status === "delivered") return "✓✓";
  if (status === "read") return "✓✓";
  return "✓";
}

function getSafeContextPosition(x: number, y: number, menuWidth: number, menuHeight: number): { x: number; y: number } {
  const pad = 8;
  const cursorOffset = 4;
  const wantedX = x + cursorOffset;
  const wantedY = y + cursorOffset;
  const safeX = Math.min(Math.max(pad, wantedX), window.innerWidth - menuWidth - pad);
  const safeY = Math.min(Math.max(pad, wantedY), window.innerHeight - menuHeight - pad);
  return { x: safeX, y: safeY };
}

function chatLabel(chat: { display_title?: string | null; title: string | null; type: "private" | "group" | "channel"; is_saved?: boolean }): string {
  if (chat.display_title?.trim()) return chat.display_title;
  if (chat.title?.trim()) return chat.title;
  if (chat.is_saved) return "Saved Messages";
  if (chat.type === "private") return "Direct chat";
  if (chat.type === "group") return "Group";
  return "Channel";
}

function canDeleteForEveryone(
  message: { sender_id: number } | undefined,
  chat: { type: "private" | "group" | "channel"; is_saved?: boolean; my_role?: "owner" | "admin" | "member" | null } | undefined,
  meId: number | undefined
): boolean {
  if (!message || !chat || !meId) return false;
  if (chat.is_saved) return false;
  if (chat.type === "private") return true;
  if (chat.type === "channel") {
    return chat.my_role === "owner" || chat.my_role === "admin";
  }
  if (chat.type === "group" && (chat.my_role === "owner" || chat.my_role === "admin")) {
    return true;
  }
  return message.sender_id === meId;
}

function guessFileTypeByMessageType(messageType: Message["type"]): string {
  if (messageType === "image") return "image/jpeg";
  if (messageType === "video" || messageType === "circle_video") return "video/mp4";
  if (messageType === "audio" || messageType === "voice") return "audio/mpeg";
  return "application/octet-stream";
}

function collectMediaItems(
  messages: Message[],
  attachmentsByMessage: Record<number, ChatAttachment[]>
): Array<{ url: string; type: "image" | "video" }> {
  const items: Array<{ url: string; type: "image" | "video" }> = [];
  const seen = new Set<string>();
  for (const message of messages) {
    const attachments = attachmentsByMessage[message.id] ?? [];
    for (const attachment of attachments) {
      if (!attachment.file_url) continue;
      if (!attachment.file_type.startsWith("image/") && !attachment.file_type.startsWith("video/")) continue;
      const type = attachment.file_type.startsWith("image/") ? "image" : "video";
      const key = `${type}:${attachment.file_url}`;
      if (seen.has(key)) continue;
      seen.add(key);
      items.push({ url: attachment.file_url, type });
    }
    if (attachments.length === 0 && message.text && /^https?:\/\//i.test(message.text)) {
      if (message.type !== "image" && message.type !== "video" && message.type !== "circle_video") continue;
      const type = message.type === "image" ? "image" : "video";
      const key = `${type}:${message.text}`;
      if (seen.has(key)) continue;
      seen.add(key);
      items.push({ url: message.text, type });
    }
  }
  return items;
}

function getMediaGridClass(count: number): string {
  if (count <= 1) return "grid-cols-1";
  if (count === 2) return "grid-cols-2";
  return "grid-cols-2 auto-rows-[90px] md:auto-rows-[120px]";
}

function getMediaTileClass(count: number, index: number): string {
  if (count <= 2) {
    return "h-32 md:h-40";
  }
  if (count === 3 && index === 0) {
    return "row-span-2 h-full";
  }
  if (count === 5 && index === 0) {
    return "row-span-2 h-full";
  }
  return "h-full";
}

function getMessageAttachmentUrl(message: Message, attachments: ChatAttachment[]): string | null {
  if (attachments.length > 0 && attachments[0].file_url) {
    return attachments[0].file_url;
  }
  const mediaTypes = new Set(["image", "video", "audio", "voice", "file", "circle_video"]);
  if (!mediaTypes.has(message.type)) {
    return null;
  }
  if (!message.text || !/^https?:\/\//i.test(message.text)) {
    return null;
  }
  return message.text;
}

function extractFileName(url: string): string {
  try {
    const parsed = new URL(url);
    const value = parsed.pathname.split("/").pop();
    return decodeURIComponent(value || "file");
  } catch {
    const value = url.split("/").pop();
    return value ? decodeURIComponent(value) : "file";
  }
}

async function downloadFileFromUrl(url: string): Promise<void> {
  const response = await fetch(url, { mode: "cors" });
  if (!response.ok) {
    throw new Error("Download failed");
  }
  const blob = await response.blob();
  const filename = extractFileName(url);
  const blobUrl = window.URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = blobUrl;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  link.remove();
  window.URL.revokeObjectURL(blobUrl);
}

function AudioInlinePlayer({ src, title }: { src: string; title: string }) {
  const track = useAudioPlayerStore((s) => s.track);
  const isPlayingGlobal = useAudioPlayerStore((s) => s.isPlaying);
  const durationGlobal = useAudioPlayerStore((s) => s.duration);
  const positionGlobal = useAudioPlayerStore((s) => s.position);
  const playTrack = useAudioPlayerStore((s) => s.playTrack);
  const togglePlayGlobal = useAudioPlayerStore((s) => s.togglePlay);
  const seekToGlobal = useAudioPlayerStore((s) => s.seekTo);

  const isActiveTrack = track?.src === src;
  const isPlaying = isActiveTrack && isPlayingGlobal;
  const duration = isActiveTrack ? durationGlobal : 0;
  const position = isActiveTrack ? positionGlobal : 0;

  async function togglePlay() {
    if (isActiveTrack) {
      await togglePlayGlobal();
      return;
    }
    await playTrack({ src, title });
  }

  function onSeek(nextValue: number) {
    if (!isActiveTrack) {
      return;
    }
    seekToGlobal(nextValue);
  }

  return (
    <div className="rounded-lg border border-slate-600/70 bg-slate-900/60 px-2 py-2">
      <div className="flex items-center gap-2">
        <button
          className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-emerald-500/80 text-xs text-slate-950 hover:bg-emerald-400"
          onClick={() => void togglePlay()}
          type="button"
        >
          {isPlaying ? "❚❚" : "▶"}
        </button>

        <div className="min-w-0 flex-1">
          <div className="mb-1 flex items-center justify-between gap-2 text-[11px] text-slate-300">
            <span className="truncate">{title}</span>
            <span className="tabular-nums">{formatAudioTime(position)} / {formatAudioTime(duration)}</span>
          </div>
          <input
            className="h-1.5 w-full cursor-pointer accent-emerald-300"
            max={Math.max(duration, 0.01)}
            min={0}
            onChange={(event) => onSeek(Number(event.target.value))}
            step={0.1}
            type="range"
            value={Math.min(position, Math.max(duration, 0.01))}
          />
        </div>

        <span className="inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-slate-800 px-1 text-[10px] text-slate-300">
          ♪
        </span>
      </div>
    </div>
  );
}

function VoiceInlinePlayer({
  src,
  title,
  waveform,
}: {
  src: string;
  title: string;
  waveform: number[] | null;
}) {
  const track = useAudioPlayerStore((s) => s.track);
  const isPlayingGlobal = useAudioPlayerStore((s) => s.isPlaying);
  const durationGlobal = useAudioPlayerStore((s) => s.duration);
  const positionGlobal = useAudioPlayerStore((s) => s.position);
  const playTrack = useAudioPlayerStore((s) => s.playTrack);
  const togglePlayGlobal = useAudioPlayerStore((s) => s.togglePlay);
  const seekToGlobal = useAudioPlayerStore((s) => s.seekTo);

  const isActiveTrack = track?.src === src;
  const isPlaying = isActiveTrack && isPlayingGlobal;
  const duration = isActiveTrack ? durationGlobal : 0;
  const position = isActiveTrack ? positionGlobal : 0;
  const bars = waveform && waveform.length >= 8 ? waveform : buildFallbackWaveform(src);

  async function togglePlay() {
    if (isActiveTrack) {
      await togglePlayGlobal();
      return;
    }
    await playTrack({ src, title });
  }

  function handleWaveClick(index: number) {
    if (!isActiveTrack || duration <= 0) {
      return;
    }
    const ratio = index / Math.max(1, bars.length - 1);
    seekToGlobal(duration * ratio);
  }

  const progressRatio = duration > 0 ? Math.min(1, Math.max(0, position / duration)) : 0;
  const activeBars = Math.floor(progressRatio * bars.length);

  return (
    <div className="flex items-center gap-2 rounded-lg bg-slate-900/40 px-2 py-1.5">
      <button
        className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-sky-500/80 text-xs text-slate-950 hover:bg-sky-400"
        onClick={() => void togglePlay()}
        type="button"
      >
        {isPlaying ? "❚❚" : "▶"}
      </button>
      <div className="flex min-w-0 flex-1 items-end gap-[2px]">
        {bars.map((value, index) => (
          <button
            className={`w-[3px] rounded-full p-0 ${index <= activeBars ? "bg-sky-300" : "bg-slate-500/60"}`}
            key={`${src}-${index}`}
            onClick={() => handleWaveClick(index)}
            style={{ height: `${Math.max(6, value)}px` }}
            type="button"
          />
        ))}
      </div>
      <span className="w-10 text-right text-[11px] tabular-nums text-slate-200">
        {formatAudioTime(duration > 0 ? duration : position)}
      </span>
    </div>
  );
}

function buildFallbackWaveform(seed: string, bars = 48): number[] {
  let hash = 0;
  for (let i = 0; i < seed.length; i += 1) {
    hash = (hash * 31 + seed.charCodeAt(i)) >>> 0;
  }
  const result: number[] = [];
  let value = hash || 1;
  for (let i = 0; i < bars; i += 1) {
    value ^= value << 13;
    value ^= value >> 17;
    value ^= value << 5;
    const normalized = Math.abs(value % 20) + 6;
    result.push(normalized);
  }
  return result;
}

function formatAudioTime(seconds: number): string {
  if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
  const total = Math.floor(seconds);
  const minutes = Math.floor(total / 60);
  const rem = total % 60;
  return `${minutes}:${String(rem).padStart(2, "0")}`;
}
