import {
  useCallback,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";

import InputComponent from "./components/input";
import Message from "./components/message";
import { useChat } from "./context";
import {
  PAGE_SIZE,
  FALLBACK_MESSAGE_HEIGHT,
  MESSAGES_PER_VIRTUAL_ROW,
  type LiveMessage,
  type RawMessage,
} from "./values";
import Wrapper from "@tensamin/user/wrapper";
import { Skeleton } from "@tensamin/ui";

type MessageChunk = {
  key: string;
  messages: Array<RawMessage | LiveMessage>;
  startIndex: number;
};

function shouldFetchPreviousPage({
  entry,
  hasNextPage,
  isFetchingNextPage,
  userScrolledUp,
}: {
  entry: IntersectionObserverEntry | undefined;
  hasNextPage: boolean;
  isFetchingNextPage: boolean;
  userScrolledUp: boolean;
}) {
  return [
    entry?.isIntersecting === true,
    userScrolledUp,
    hasNextPage,
    !isFetchingNextPage,
  ].every(Boolean);
}

function getMessageRenderKey(message: RawMessage | LiveMessage) {
  return "localId" in message ? message.localId : String(message.SendTime);
}

function buildMessageChunks(
  messages: Array<RawMessage | LiveMessage>,
  keyPrefix: string,
  startOffset = 0,
) {
  const chunks: MessageChunk[] = [];

  for (let end = messages.length; end > 0; end -= MESSAGES_PER_VIRTUAL_ROW) {
    const start = Math.max(0, end - MESSAGES_PER_VIRTUAL_ROW);
    const chunkMessages = messages.slice(start, end);
    const firstMessage = chunkMessages[0];

    if (firstMessage) {
      chunks.push({
        key: `${keyPrefix}-${getMessageRenderKey(firstMessage)}`,
        messages: chunkMessages,
        startIndex: startOffset + start,
      });
    }
  }

  return chunks;
}

/**
 * Renders the chat screen with virtualized history and live message updates.
 * @returns Chat screen JSX.
 */
export default function Screen() {
  const {
    getMessages,
    liveMessages,
    clearLiveMessages,
    userId,
    chatSecret,
    error,
    errorDescription,
  } = useChat();
  const scrollRef = useRef<HTMLDivElement | null>(null);
  const topSentinelRef = useRef<HTMLDivElement | null>(null);
  const didInitialScrollRef = useRef(false);
  const userScrolledUpRef = useRef(false);
  const isAtBottomRef = useRef(true);
  const smoothScrollFrameRef = useRef<number | null>(null);
  const smoothScrollTargetRef = useRef(0);
  const hasNextPageRef = useRef(false);
  const isFetchingNextPageRef = useRef(false);
  const fetchNextPageRef = useRef<(() => void) | null>(null);

  const [lastLiveMessageCount, setLastLiveMessageCount] = useState(0);
  const [didInitialScroll, setDidInitialScroll] = useState(false);
  const [viewportHeight, setViewportHeight] = useState(0);
  const [value, setValue] = useState("");

  const hasValidChatUser = Number.isSafeInteger(userId) && userId > 0;
  const hasChatSecret = chatSecret !== null;

  const messagesQuery = useInfiniteQuery({
    queryKey: ["chat-messages", String(userId), hasChatSecret],
    initialPageParam: 0,
    queryFn: ({ pageParam }) => getMessages(PAGE_SIZE, Number(pageParam)),
    enabled: hasValidChatUser && hasChatSecret,
    getNextPageParam: (lastPage, allPages) => {
      if (lastPage.length < PAGE_SIZE) {
        return undefined;
      }

      return allPages.length * PAGE_SIZE;
    },
  });
  const {
    fetchNextPage: fetchMessagesNextPage,
    hasNextPage: hasMessagesNextPage,
    isFetchingNextPage: isFetchingMessagesNextPage,
  } = messagesQuery;

  useEffect(() => {
    hasNextPageRef.current = hasMessagesNextPage;
    isFetchingNextPageRef.current = isFetchingMessagesNextPage;
    fetchNextPageRef.current = () => {
      void fetchMessagesNextPage();
    };
  }, [fetchMessagesNextPage, hasMessagesNextPage, isFetchingMessagesNextPage]);

  useEffect(() => {
    clearLiveMessages();
    didInitialScrollRef.current = false;
    userScrolledUpRef.current = false;
    isAtBottomRef.current = true;
    setDidInitialScroll(false);
    setLastLiveMessageCount(0);
  }, [clearLiveMessages, userId]);

  const historicalMessages = useMemo(() => {
    const pages = messagesQuery.data?.pages ?? [];
    const seenSendTimes = new Set<number>();
    const dedupedMessages: RawMessage[] = [];

    for (const message of [...pages].reverse().flat()) {
      if (seenSendTimes.has(message.SendTime)) {
        continue;
      }

      seenSendTimes.add(message.SendTime);
      dedupedMessages.push(message);
    }

    return dedupedMessages;
  }, [messagesQuery.data]);

  const liveMessagesSnapshot = liveMessages();

  const liveWithoutDuplicates = useMemo(() => {
    const historicalSendTimes = new Set(
      historicalMessages.map((message) => message.SendTime),
    );

    return liveMessagesSnapshot.filter(
      (message) => !historicalSendTimes.has(message.SendTime),
    );
  }, [historicalMessages, liveMessagesSnapshot]);

  const messages = useMemo(() => {
    return [...historicalMessages, ...liveWithoutDuplicates];
  }, [historicalMessages, liveWithoutDuplicates]);

  const historicalMessageChunks = useMemo(() => {
    return buildMessageChunks(historicalMessages, "history");
  }, [historicalMessages]);

  const liveMessageChunks = useMemo(() => {
    return buildMessageChunks(
      liveWithoutDuplicates,
      "live",
      historicalMessages.length,
    );
  }, [historicalMessages.length, liveWithoutDuplicates]);

  const messageChunks = useMemo(() => {
    return [...liveMessageChunks, ...historicalMessageChunks];
  }, [historicalMessageChunks, liveMessageChunks]);

  const virtualRowCount = messageChunks.length;

  const getItemKey = useCallback(
    (index: number) => messageChunks[index]?.key ?? index,
    [messageChunks],
  );

  const estimateSize = useCallback(() => FALLBACK_MESSAGE_HEIGHT, []);

  // eslint-disable-next-line react-hooks/incompatible-library
  const virtualizer = useVirtualizer({
    count: virtualRowCount,
    getScrollElement: () => scrollRef.current,
    getItemKey,
    estimateSize,
    overscan: 2,
  });
  const totalSize = virtualizer.getTotalSize();
  const contentHeight = Math.max(totalSize, viewportHeight);
  const verticalOffset = Math.max(0, viewportHeight - totalSize);

  useLayoutEffect(() => {
    const element = scrollRef.current;
    if (!element || typeof ResizeObserver === "undefined") {
      return;
    }

    const updateViewportHeight = () => {
      setViewportHeight(element.clientHeight);
    };

    updateViewportHeight();

    const observer = new ResizeObserver(updateViewportHeight);
    observer.observe(element);

    return () => {
      observer.disconnect();
    };
  }, []);

  useLayoutEffect(() => {
    if (didInitialScrollRef.current || virtualRowCount === 0) {
      return;
    }

    requestAnimationFrame(() => {
      if (scrollRef.current) {
        scrollRef.current.scrollTop = 0;
      }

      requestAnimationFrame(() => {
        didInitialScrollRef.current = true;
        setDidInitialScroll(true);
      });
    });
  }, [virtualizer, virtualRowCount]);

  useLayoutEffect(() => {
    const count = liveMessagesSnapshot.length;

    if (count > lastLiveMessageCount && isAtBottomRef.current) {
      requestAnimationFrame(() => {
        if (scrollRef.current) {
          scrollRef.current.scrollTop = 0;
        }
      });
    }

    setLastLiveMessageCount(count);
  }, [
    lastLiveMessageCount,
    liveMessagesSnapshot.length,
    virtualRowCount,
    virtualizer,
  ]);

  useEffect(() => {
    if (
      viewportHeight === 0 ||
      totalSize > viewportHeight ||
      messagesQuery.isFetchingNextPage ||
      !messagesQuery.hasNextPage
    ) {
      return;
    }

    void messagesQuery.fetchNextPage();
  }, [didInitialScroll, messagesQuery, totalSize, viewportHeight]);

  useLayoutEffect(() => {
    if (
      !didInitialScroll ||
      userScrolledUpRef.current ||
      virtualRowCount === 0
    ) {
      return;
    }

    requestAnimationFrame(() => {
      if (scrollRef.current) {
        scrollRef.current.scrollTop = 0;
      }
    });
  }, [didInitialScroll, virtualRowCount]);

  useEffect(() => {
    const root = scrollRef.current;
    const sentinel = topSentinelRef.current;

    if (!root || !sentinel || !didInitialScroll) {
      return;
    }

    const observer = new IntersectionObserver(
      (entries) => {
        const entry = entries[0];
        if (
          shouldFetchPreviousPage({
            entry,
            hasNextPage: messagesQuery.hasNextPage,
            isFetchingNextPage: messagesQuery.isFetchingNextPage,
            userScrolledUp: userScrolledUpRef.current,
          })
        ) {
          void messagesQuery.fetchNextPage();
        }
      },
      { root, rootMargin: "240px 0px 0px 0px" },
    );

    observer.observe(sentinel);

    return () => {
      observer.disconnect();
    };
  }, [didInitialScroll, messagesQuery, virtualRowCount]);

  const handleContainerScroll = useCallback(() => {
    if (!scrollRef.current) {
      return;
    }

    isAtBottomRef.current = scrollRef.current.scrollTop <= 140;

    if (didInitialScrollRef.current && scrollRef.current.scrollTop > 140) {
      userScrolledUpRef.current = true;
    }
  }, []);

  useEffect(() => {
    const element = scrollRef.current;
    if (!element) {
      return;
    }

    smoothScrollTargetRef.current = element.scrollTop;

    const animateScroll = () => {
      const distance = smoothScrollTargetRef.current - element.scrollTop;
      if (Math.abs(distance) < 0.5) {
        element.scrollTop = smoothScrollTargetRef.current;
        smoothScrollFrameRef.current = null;
        handleContainerScroll();
        return;
      }

      element.scrollTop += distance * 0.35;
      handleContainerScroll();
      smoothScrollFrameRef.current = requestAnimationFrame(animateScroll);
    };

    const fetchNextPageNearTop = (scrollTop: number, maxScrollTop: number) => {
      if (didInitialScrollRef.current && scrollTop > 140) {
        userScrolledUpRef.current = true;
      }

      if (
        scrollTop >= maxScrollTop - 240 &&
        userScrolledUpRef.current &&
        hasNextPageRef.current &&
        !isFetchingNextPageRef.current
      ) {
        fetchNextPageRef.current?.();
      }
    };

    const handleWheel = (event: WheelEvent) => {
      event.preventDefault();
      event.stopPropagation();

      if (smoothScrollFrameRef.current === null) {
        smoothScrollTargetRef.current = element.scrollTop;
      }

      const maxScrollTop = Math.max(
        0,
        element.scrollHeight - element.clientHeight,
      );
      smoothScrollTargetRef.current = Math.min(
        Math.max(0, smoothScrollTargetRef.current - event.deltaY),
        maxScrollTop,
      );
      fetchNextPageNearTop(smoothScrollTargetRef.current, maxScrollTop);

      if (smoothScrollFrameRef.current === null) {
        smoothScrollFrameRef.current = requestAnimationFrame(animateScroll);
      }
    };

    element.addEventListener("wheel", handleWheel, { passive: false });

    return () => {
      element.removeEventListener("wheel", handleWheel);
      if (smoothScrollFrameRef.current !== null) {
        cancelAnimationFrame(smoothScrollFrameRef.current);
        smoothScrollFrameRef.current = null;
      }
    };
  }, [handleContainerScroll]);

  if (!hasValidChatUser) {
    return (
      <div className="w-full h-full flex flex-col gap-2 items-center justify-center">
        <p className="font-semibold text-xl">Invalid User</p>
      </div>
    );
  }

  return (
    <div className="relative flex h-full min-h-0 w-full flex-col overflow-hidden">
      {error !== "" && errorDescription !== "" ? (
        <div className="w-full h-full flex flex-col gap-2 items-center justify-center">
          <p className="font-semibold text-xl">{error}</p>
          <p className="text-muted-foreground text-lg">{errorDescription}</p>
        </div>
      ) : (
        <>
          <div
            ref={scrollRef}
            id="chat_container"
            className="min-h-0 flex-1 overflow-y-auto"
            style={{
              overflowAnchor: "none",
              paddingTop: "22px",
              transform: "scaleY(-1)",
            }}
            onScroll={handleContainerScroll}
          >
            <div
              className="relative w-full"
              style={{ height: `${contentHeight}px` }}
            >
              <div
                ref={topSentinelRef}
                className="absolute bottom-0 left-0 h-px w-full"
              />
              {virtualizer.getVirtualItems().map((virtualRow) => {
                const chunkIndex = virtualRow.index;
                const chunk = messageChunks[chunkIndex];
                if (!chunk) {
                  return null;
                }

                return (
                  <div
                    key={chunk.key}
                    data-index={virtualRow.index}
                    className="flex flex-col"
                    ref={virtualizer.measureElement}
                    style={{
                      position: "absolute",
                      top: 0,
                      left: 0,
                      width: "100%",
                      transform: `translateY(${verticalOffset + virtualRow.start}px)`,
                    }}
                  >
                    <div className="flex flex-col scale-y-[-1]">
                      {chunk.messages.map((message, chunkMessageIndex) => {
                        const messageIndex =
                          chunk.startIndex + chunkMessageIndex;
                        const lastMessage = messages[messageIndex - 1];
                        const isGrouped =
                          lastMessage &&
                          lastMessage.SenderId === message.SenderId &&
                          Math.round(lastMessage.SendTime / 10000) ===
                            Math.round(message.SendTime / 10000);

                        return (
                          <Wrapper
                            key={getMessageRenderKey(message)}
                            userId={message.SenderId}
                            loading={<Skeleton className="w-30 h-5" />}
                            component={(user) => (
                              <Message
                                grouped={isGrouped}
                                message={message}
                                user={user}
                              />
                            )}
                          />
                        );
                      })}
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
          <div className="z-10 shrink-0">
            <InputComponent setValue={setValue} value={value} />
          </div>
        </>
      )}
    </div>
  );
}
