import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import { FiMoreVertical } from "react-icons/fi";
import type { Message as MessageModel } from "@/api/generated/private/models/message";
import { getApiSocialChatsListQueryKey, useApiSocialChatsRetrieve } from "@/api/generated/private/chat/chat";
import { useAuth } from "@/hooks/useAuth";
import { useInfiniteMessages } from "@/hooks/useInfiniteMessages";
import { useChatSocket, type ChatSocketEvent } from "@/hooks/useChatSocket";
import { useIntersectionLoader } from "@/hooks/useIntersectionLoader";
import { messagesQueryKey, type CursorPaginated } from "@/api/social/feed";
import { sendChatMessage } from "@/api/social/chatSend";
import Message from "@/components/social/chat/Message";
import MessageComposer from "@/components/social/chat/MessageComposer";
import ChatSettingsModal from "@/components/social/chat/ChatSettingsModal";
import Spinner from "@/components/ui/Spinner";
import EmptyState from "@/components/ui/EmptyState";
import Avatar from "@/components/ui/Avatar";

export default function ChatRoomPage() {
  const { t } = useTranslation("social");
  const { chatId: chatIdParam } = useParams<{ chatId: string }>();
  const chatId = Number(chatIdParam);
  const queryClient = useQueryClient();

  const { user } = useAuth();
  const { data: chat } = useApiSocialChatsRetrieve(String(chatId));
  const {
    messages,
    isLoading,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteMessages({ chatId });

  const [replyTo, setReplyTo] = useState<MessageModel | null>(null);
  const [typingUsers, setTypingUsers] = useState<string[]>([]);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const bottomRef = useRef<HTMLDivElement | null>(null);

  // --- reply-jump logic ---
  const [highlightedId, setHighlightedId] = useState<number | null>(null);
  /** Message id we're waiting to appear in the DOM while fetching older pages. */
  const [pendingScrollId, setPendingScrollId] = useState<number | null>(null);
  /** IDs removed by a delete WS event — so we don't hunt for them in older pages. */
  const deletedMessageIds = useRef(new Set<number>());

  function flashMessage(id: number) {
    setHighlightedId(id);
    setTimeout(() => setHighlightedId(null), 1500);
  }

  function scrollToMessage(id: number) {
    // Don't chase a message that was deleted during this session.
    if (deletedMessageIds.current.has(id)) return;

    const el = document.getElementById(`msg-${id}`);
    if (el) {
      el.scrollIntoView({ behavior: "smooth", block: "center" });
      flashMessage(id);
    } else if (hasNextPage) {
      // Message not rendered yet — start fetching older pages until it appears.
      setPendingScrollId(id);
      void fetchNextPage();
    }
    // else: exhausted history, message not found — silently ignore.
  }

  // After every page load, try to resolve a pending scroll-to.
  useEffect(() => {
    if (!pendingScrollId) return;
    const el = document.getElementById(`msg-${pendingScrollId}`);
    if (el) {
      el.scrollIntoView({ behavior: "smooth", block: "center" });
      flashMessage(pendingScrollId);
      setPendingScrollId(null);
    } else if (!isFetchingNextPage) {
      if (hasNextPage) {
        void fetchNextPage();          // keep paging back until found
      } else {
        setPendingScrollId(null);      // exhausted history, give up
      }
    }
  // Re-run whenever a new page arrives or fetching state changes.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [messages.length, isFetchingNextPage]);

  // Top sentinel triggers loading older messages (scroll-back).
  const topSentinelRef = useIntersectionLoader<HTMLDivElement>(
    () => {
      if (hasNextPage && !isFetchingNextPage) void fetchNextPage();
    },
    { enabled: hasNextPage && !isLoading },
  );

  // Append a freshly received message into the cursor cache.
  const appendMessage = useCallback(
    (msg: MessageModel) => {
      queryClient.setQueryData<{
        pages: CursorPaginated<MessageModel>[];
        pageParams: unknown[];
      }>(messagesQueryKey(chatId), (old) => {
        if (!old) return old as never;
        const [first, ...rest] = old.pages;
        if (!first) return old;
        if (first.results.some((m) => m.id === msg.id)) return old;
        return {
          ...old,
          pages: [{ ...first, results: [msg, ...first.results] }, ...rest],
        };
      });
    },
    [queryClient, chatId],
  );

  const removeMessage = useCallback(
    (messageId: number) => {
      deletedMessageIds.current.add(messageId);
      // Also cancel any pending scroll to this message.
      setPendingScrollId((prev) => (prev === messageId ? null : prev));
      queryClient.setQueryData<{
        pages: CursorPaginated<MessageModel>[];
        pageParams: unknown[];
      }>(messagesQueryKey(chatId), (old) => {
        if (!old) return old as never;
        return {
          ...old,
          pages: old.pages.map((p) => ({
            ...p,
            results: p.results
              // Remove the deleted message itself.
              .filter((m) => m.id !== messageId)
              // Tombstone reply_to.content for any message quoting the deleted one.
              .map((m) =>
                m.reply_to?.id === messageId
                  ? { ...m, reply_to: { ...m.reply_to, content: undefined as unknown as string } }
                  : m,
              ),
          })),
        };
      });
    },
    [queryClient, chatId],
  );

  const handleSocketEvent = useCallback(
    (event: ChatSocketEvent) => {
      if (event.type === "new_chat_message" || event.type === "new_reply_chat_message") {
        let replyTo: MessageModel["reply_to"] | null = null;
        if (event.reply_to_id != null) {
          const cached = queryClient.getQueryData<{ pages: CursorPaginated<MessageModel>[] }>(
            messagesQueryKey(chatId),
          );
          const found = cached?.pages.flatMap((p) => p.results).find((m) => m.id === event.reply_to_id);
          replyTo = found
            ? { id: found.id, content: found.content ?? "", sender: found.sender, created_at: found.created_at, media_files: found.media_files }
            : { id: event.reply_to_id, content: "", sender: { id: 0, username: "…", avatar: "" }, created_at: new Date(), media_files: [] };
        }
        appendMessage({
          id: event.message_id,
          chat: chatId,
          sender: { id: event.sender_id, username: event.sender, avatar: event.sender_avatar } as never,
          reply_to: replyTo as never,
          content: event.message,
          is_edited: false,
          edited_at: null,
          created_at: new Date(),
          updated_at: new Date(),
          media_files: (event.media_files ?? []) as never,
          reactions: [],
        });
      } else if (event.type === "delete_chat_message") {
        removeMessage(event.message_id);
      } else if (event.type === "read_status") {
        if (event.user === user?.username) {
          queryClient.invalidateQueries({ queryKey: getApiSocialChatsListQueryKey() });
        }
      } else if (event.type === "typing") {
        setTypingUsers((prev) =>
          event.is_typing
            ? prev.includes(event.user) ? prev : [...prev, event.user]
            : prev.filter((u) => u !== event.user),
        );
      } else if (event.type === "stop_typing") {
        setTypingUsers((prev) => prev.filter((u) => u !== event.user));
      } else if (event.type === "reaction") {
        queryClient.setQueryData<{
          pages: CursorPaginated<MessageModel>[];
          pageParams: unknown[];
        }>(messagesQueryKey(chatId), (old) => {
          if (!old) return old as never;
          return {
            ...old,
            pages: old.pages.map((p) => ({
              ...p,
              results: p.results.map((m) => {
                if (m.id !== event.message_id) return m;
                let reactions = [...(m.reactions ?? [])] as typeof m.reactions;
                if (event.action === "added") {
                  const alreadyExists = reactions.some(
                    (r) => (r.user as unknown as number) === event.user_id && r.emoji === event.emoji,
                  );
                  if (!alreadyExists) {
                    reactions = [...reactions, { id: -Date.now(), user: event.user_id, emoji: event.emoji, created_at: new Date().toISOString() } as never];
                  }
                } else if (event.action === "removed") {
                  reactions = reactions.filter((r) => !((r.user as unknown as number) === event.user_id && r.emoji === event.emoji));
                } else if (event.action === "switched") {
                  reactions = reactions.map((r) => (r.user as unknown as number) === event.user_id ? { ...r, emoji: event.emoji } : r);
                }
                return { ...m, reactions };
              }),
            })),
          };
        });
      }
    },
    [appendMessage, removeMessage, chatId, user, queryClient],
  );

  const { status, sendMessage, sendReply, sendReaction, sendTyping, sendMarkRead } = useChatSocket({
    chatId: Number.isFinite(chatId) ? chatId : null,
    onEvent: handleSocketEvent,
  });

  // Mark the chat as read as soon as the socket opens.
  useEffect(() => {
    if (status === "open") sendMarkRead();
  }, [status, sendMarkRead]);

  // Auto-scroll to bottom when new messages arrive.
  // Suppressed while chasing a reply-jump so both effects don't fight each other.
  useEffect(() => {
    if (!pendingScrollId) {
      bottomRef.current?.scrollIntoView({ behavior: "auto" });
    }
    if (status === "open") sendMarkRead();
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [messages.length]);

  async function handleSend(text: string, replyToId?: number, files?: File[]): Promise<boolean> {
    // Files must go via HTTP multipart; WS handles text-only messages.
    if (files?.length) {
      try {
        const msg = await sendChatMessage({ chatId, content: text || undefined, replyToId, files });
        appendMessage(msg); // WS broadcast will deduplicate via the id-check in appendMessage
        return true;
      } catch {
        return false;
      }
    }
    return replyToId ? sendReply(text, replyToId) : sendMessage(text);
  }

  if (!Number.isFinite(chatId)) {
    return <EmptyState message={t("chat.room.selectChat")} />;
  }

  return (
    <div className="flex h-full flex-col">
      <header className="flex items-center gap-3 border-b border-brand-lines/15 py-3 pl-12 pr-4">
        <Avatar
          name={chat?.name ?? `chat ${chatId}`}
          src={chat?.icon ?? undefined}
          size={36}
        />
        <div className="min-w-0 flex-1">
          <div className="truncate text-sm font-semibold text-brand-text">
            {chat?.name || `Chat #${chatId}`}
          </div>
          {status !== "open" && (
            <div className="text-xs text-brand-text/60">
              {t("chat.room.disconnected")}
            </div>
          )}
        </div>
        <button
          type="button"
          onClick={() => setSettingsOpen(true)}
          className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-brand-text/60 hover:bg-brand-lines/15 hover:text-brand-text transition-colors"
          aria-label={t("chat.settings.title")}
        >
          <FiMoreVertical size={17} />
        </button>
      </header>

      {chat && (
        <ChatSettingsModal
          chat={chat as Parameters<typeof ChatSettingsModal>[0]["chat"]}
          open={settingsOpen}
          onClose={() => setSettingsOpen(false)}
        />
      )}

      <div className="flex-1 overflow-y-auto py-2">
        {hasNextPage && (
          <div ref={topSentinelRef} className="flex justify-center py-2">
            {isFetchingNextPage ? (
              <Spinner size={18} />
            ) : (
              <span className="text-xs text-brand-text/50">
                {t("chat.room.loadingHistory")}
              </span>
            )}
          </div>
        )}

        {isLoading && (
          <div className="flex justify-center py-6">
            <Spinner size={24} />
          </div>
        )}

        {!isLoading && messages.length === 0 && (
          <EmptyState message={t("chat.room.noMessages")} />
        )}

        {/* Beginning-of-chat banner — shown only once all pages are confirmed loaded */}
        {!hasNextPage && !isLoading && chat && (
          <div className="flex flex-col items-center gap-3 px-6 py-10 text-center">
            <Avatar
              name={chat.name ?? `Chat ${chatId}`}
              src={chat.icon ?? undefined}
              size={72}
            />
            <div>
              <p className="text-base font-bold text-brand-text">
                {chat.name || `Chat #${chatId}`}
              </p>
              <p className="mt-1 text-xs text-brand-text/50">
                {t("chat.room.chatBeginning")}
              </p>
              <p className="mt-0.5 text-xs text-brand-text/40">
                {t("chat.room.chatCreated", {
                  date: new Date(chat.created_at).toLocaleDateString("cs-CZ", {
                    day: "numeric",
                    month: "long",
                    year: "numeric",
                  }),
                })}
              </p>
            </div>
            <div className="h-px w-24 bg-brand-lines/20" />
          </div>
        )}

        {messages.map((m) => (
          <Message
            key={m.id}
            message={m}
            chat={chat ?? null}
            onReply={setReplyTo}
            onReact={(msg, emoji) => sendReaction(msg.id, emoji)}
            highlighted={m.id === highlightedId}
            onScrollToMessage={scrollToMessage}
          />
        ))}

        <div ref={bottomRef} />
      </div>

      {typingUsers.length > 0 && (
        <div className="px-4 py-1 text-xs text-brand-text/60">
          {typingUsers.length === 1
            ? t("chat.room.typing", { user: typingUsers[0] })
            : t("chat.room.typingMany")}
        </div>
      )}

      <MessageComposer
        disabled={status !== "open"}
        replyTo={replyTo}
        onCancelReply={() => setReplyTo(null)}
        onSend={handleSend}
        onTyping={sendTyping}
      />
    </div>
  );
}
