import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useLocation, useSearchParams } from "react-router-dom";
import { LoaderCircle } from "lucide-react";
import { getJSON, postJSON } from "../../api";
import { langQuery, useI18n } from "../../i18n";
import type { Post, PostScope } from "../../types/post";
import { Reveal } from "../../motion";
import { Skeleton } from "../Skeleton";
import { FilterChips } from "./FilterChips";
import { MessageBubble } from "./MessageBubble";
import { useGroupedByDay } from "./hooks/useGroupedByDay";
import { usePostStream } from "./hooks/usePostStream";
import { useFavorites } from "../../favorites/FavoritesProvider";

export type MessageStreamProps = {
  scope: PostScope;
};

export function MessageStream({ scope }: MessageStreamProps) {
  const { t, lang } = useI18n();
  const [sp, setSp] = useSearchParams();
  const { hash } = useLocation();

  const type = sp.get("type") || "all";
  const q = (sp.get("q") || "").trim();
  const sort = sp.get("sort") || "";

  const params = useMemo(
    () => ({ scope, type, q, sort, lang }),
    [scope, type, q, sort, lang],
  );

  const { items, isLoading, error, hasMore, loadMore, reset } =
    usePostStream(params);
  const { ensureFavoriteIds } = useFavorites();
  const retryLabel = t("retry");

  useEffect(() => {
    void ensureFavoriteIds(items.map((item) => item.id)).catch(() => undefined);
  }, [ensureFavoriteIds, items]);

  const sentinelRef = useRef<HTMLDivElement>(null);
  const filterBarRef = useRef<HTMLDivElement>(null);
  const hasMoreRef = useRef(hasMore);
  const isLoadingRef = useRef(isLoading);
  useEffect(() => {
    hasMoreRef.current = hasMore;
  }, [hasMore]);
  useEffect(() => {
    isLoadingRef.current = isLoading;
  }, [isLoading]);

  useEffect(() => {
    if (q) postJSON("/api/search-log", { query: q }).catch(() => {});
  }, [q]);

  useEffect(() => {
    const el = sentinelRef.current;
    if (!el) return;
    const io = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (
            entry.isIntersecting &&
            hasMoreRef.current &&
            !isLoadingRef.current
          ) {
            loadMore();
          }
        }
      },
      // Prefetch the next page well before the sentinel is visible so content
      // is already in place as the user scrolls — no blank gap at the bottom.
      { rootMargin: "1000px" },
    );
    io.observe(el);
    return () => io.disconnect();
  }, [loadMore]);

  // When arriving with a `?post=<id>` query (or legacy `#post-<id>` hash),
  // scroll to that bubble — loading more pages until it shows up — then give
  // it a brief highlight so the user can see where they landed.
  const queryTargetPostId = sp.get("post") || "";
  const hashTargetPostId = hash.startsWith("#post-")
    ? hash.slice("#post-".length)
    : "";
  const targetPostId = queryTargetPostId || hashTargetPostId;
  const [resolvedTargetPost, setResolvedTargetPost] = useState<Post | null>(
    null,
  );
  const [isFetchingTargetPost, setIsFetchingTargetPost] = useState(false);
  const [targetPostFetchFailed, setTargetPostFetchFailed] = useState(false);
  const targetAlreadyInBaseItems = useMemo(
    () =>
      !!queryTargetPostId &&
      items.some((post) => post.id === queryTargetPostId),
    [items, queryTargetPostId],
  );
  const streamItems = useMemo(() => {
    if (
      resolvedTargetPost &&
      !items.some((post) => post.id === resolvedTargetPost.id)
    ) {
      return [resolvedTargetPost, ...items];
    }
    return items;
  }, [items, resolvedTargetPost]);
  const groups = useGroupedByDay(streamItems, lang);
  // Lock only engages while we are actively running the smooth-scroll animation
  // — not during the wait/pagination phase — so the page never feels frozen
  // before the bubble exists.
  const [isAligningQueryTarget, setIsAligningQueryTarget] = useState(false);
  const handledTargetRef = useRef<string>("");
  const targetScrollTimersRef = useRef<number[]>([]);
  const targetScrollFrameRef = useRef<number | null>(null);
  // Timestamp (perf clock) when the first batch of real items became visible
  // for this target. Used to delay the deep-link smooth scroll until the
  // initial Reveal in-view animations have had a moment to play, so the user
  // sees content before the page starts moving.
  const firstContentAtRef = useRef<number | null>(null);

  const clearTargetScrollTimers = () => {
    for (const timer of targetScrollTimersRef.current) {
      window.clearTimeout(timer);
    }
    targetScrollTimersRef.current = [];
    if (targetScrollFrameRef.current !== null) {
      window.cancelAnimationFrame(targetScrollFrameRef.current);
      targetScrollFrameRef.current = null;
    }
  };

  useEffect(() => {
    handledTargetRef.current = "";
    firstContentAtRef.current = null;
    clearTargetScrollTimers();
    setIsAligningQueryTarget(false);
  }, [queryTargetPostId, targetPostId]);

  // Mark when first real content becomes visible (skeletons gone, items in).
  // Captured per-target via the reset above so a later navigation re-measures.
  useEffect(() => {
    if (
      streamItems.length > 0 &&
      !isLoading &&
      firstContentAtRef.current === null
    ) {
      firstContentAtRef.current = performance.now();
    }
  }, [streamItems.length, isLoading]);

  // Banner / deep-link arrivals (`?post=<id>`) should always begin the
  // smooth-scroll positioning from the top of the stream, so the user sees a
  // clear, satisfying journey to the target post instead of a tiny nudge when
  // they happen to revisit the page mid-scroll. Run before paint so the user
  // never briefly sees the previous scrollY before the jump.
  useLayoutEffect(() => {
    if (!queryTargetPostId) return;
    window.scrollTo({ top: 0, left: 0, behavior: "auto" });
  }, [queryTargetPostId]);

  // Search result clicks can target very old posts that are nowhere near the
  // first paginated /browse page. Do not make the user wait while the stream
  // loads page after page; fetch the target post directly and inject it at the
  // top so it can render and be highlighted immediately. The normal stream
  // still loads underneath for context / scrolling.
  useEffect(() => {
    if (!queryTargetPostId) {
      setResolvedTargetPost(null);
      setIsFetchingTargetPost(false);
      setTargetPostFetchFailed(false);
      return;
    }
    if (targetAlreadyInBaseItems) {
      setResolvedTargetPost(null);
      setIsFetchingTargetPost(false);
      setTargetPostFetchFailed(false);
      return;
    }

    let cancelled = false;
    setIsFetchingTargetPost(true);
    setTargetPostFetchFailed(false);
    getJSON<Post>(
      `/api/posts/${encodeURIComponent(queryTargetPostId)}?lang=${encodeURIComponent(
        langQuery(lang),
      )}`,
    )
      .then((post) => {
        if (cancelled) return;
        setResolvedTargetPost(post);
      })
      .catch(() => {
        if (cancelled) return;
        setResolvedTargetPost(null);
        setTargetPostFetchFailed(true);
      })
      .finally(() => {
        if (!cancelled) setIsFetchingTargetPost(false);
      });

    return () => {
      cancelled = true;
    };
  }, [lang, queryTargetPostId, targetAlreadyInBaseItems]);

  useEffect(() => clearTargetScrollTimers, []);

  useEffect(() => {
    if (!isAligningQueryTarget) return;
    const preventScroll = (event: Event) => event.preventDefault();
    const preventScrollKeys = (event: KeyboardEvent) => {
      if (
        [
          "ArrowDown",
          "ArrowUp",
          "PageDown",
          "PageUp",
          "Home",
          "End",
          " ",
        ].includes(event.key)
      ) {
        event.preventDefault();
      }
    };
    const html = document.documentElement;
    const previousOverscroll = html.style.overscrollBehavior;
    html.style.overscrollBehavior = "none";
    window.addEventListener("wheel", preventScroll, {
      capture: true,
      passive: false,
    });
    window.addEventListener("touchmove", preventScroll, {
      capture: true,
      passive: false,
    });
    window.addEventListener("keydown", preventScrollKeys, { capture: true });
    return () => {
      html.style.overscrollBehavior = previousOverscroll;
      window.removeEventListener("wheel", preventScroll, { capture: true });
      window.removeEventListener("touchmove", preventScroll, { capture: true });
      window.removeEventListener("keydown", preventScrollKeys, {
        capture: true,
      });
    };
  }, [isAligningQueryTarget]);

  useEffect(() => {
    if (!targetPostId || handledTargetRef.current === targetPostId) return;

    const el = document.getElementById(`post-${targetPostId}`);
    if (el) {
      handledTargetRef.current = targetPostId;
      clearTargetScrollTimers();

      const targetScrollTop = () => {
        const target = document.getElementById(`post-${targetPostId}`);
        if (!target) return null;
        const filterBottom =
          filterBarRef.current?.getBoundingClientRect().bottom ?? 0;
        const targetTop = target.getBoundingClientRect().top + window.scrollY;
        return Math.max(0, targetTop - filterBottom - 12);
      };

      const scrollToTarget = (behavior: ScrollBehavior = "auto") => {
        const top = targetScrollTop();
        if (top === null) return;
        window.scrollTo({ top, left: 0, behavior });
      };

      const prefersReducedMotion = window.matchMedia(
        "(prefers-reduced-motion: reduce)",
      ).matches;

      if (queryTargetPostId) {
        // Query deep-links (`?post=<id>`) come from banner / Home card clicks.
        // Use the browser-native smooth scroll (runs on the compositor, much
        // smoother than a hand-rolled rAF) and lock user scroll only for the
        // duration of the animation so the page never feels frozen. Quiet
        // re-alignments run inside the lock window to absorb late image-shift
        // above the target; nothing nudges the user after the lock releases.
        //
        // Hold the animation until the page has had ~300ms after the first
        // content paint, so the in-view Reveal staggers on the top bubbles
        // have time to fade in. Otherwise the smooth scroll starts before the
        // user can see anything, and the journey reads as "blank flash".
        const REVEAL_SETTLE_MS = 300;
        const elapsed =
          firstContentAtRef.current !== null
            ? performance.now() - firstContentAtRef.current
            : 0;
        const settle = Math.max(0, REVEAL_SETTLE_MS - elapsed);

        setIsAligningQueryTarget(true);
        targetScrollTimersRef.current = [
          window.setTimeout(() => {
            window.requestAnimationFrame(() =>
              scrollToTarget(prefersReducedMotion ? "auto" : "smooth"),
            );
          }, settle),
          window.setTimeout(() => scrollToTarget("auto"), settle + 450),
          window.setTimeout(() => scrollToTarget("auto"), settle + 750),
          window.setTimeout(() => {
            scrollToTarget("auto");
            setIsAligningQueryTarget(false);
          }, settle + 950),
        ];
      } else {
        window.requestAnimationFrame(() =>
          scrollToTarget(prefersReducedMotion ? "auto" : "smooth"),
        );

        // Media above the target can finish loading after the first scroll and
        // shift the target downward. Re-align after the smooth animation while
        // stream image/video heights settle, so the final resting point is exact.
        targetScrollTimersRef.current = [900, 1400, 2000].map((ms) =>
          window.setTimeout(() => scrollToTarget("auto"), ms),
        );
      }

      el.classList.add("ark-bubble-highlight");
      window.setTimeout(
        () => el.classList.remove("ark-bubble-highlight"),
        2000,
      );
      return;
    }

    // Not loaded yet — keep paging until it appears or the stream is
    // exhausted. If the previous loadMore errored, stop the loop so the user
    // sees the inline retry button instead of an endless retry cycle, and
    // release the scroll lock so they can interact with the page.
    if (error) {
      setIsAligningQueryTarget(false);
      return;
    }
    if (hasMore && !isLoading) loadMore();
    else if (!hasMore && !isLoading) setIsAligningQueryTarget(false);
  }, [targetPostId, streamItems, hasMore, isLoading, error, loadMore]);

  const updateParam = (key: string, value: string) => {
    const n = new URLSearchParams(sp);
    if (!value || value === "all") n.delete(key);
    else n.set(key, value);
    setSp(n, { replace: true });
  };

  const isInitialLoad = isLoading && streamItems.length === 0;

  // When the user arrives via /browse?post=<id> (typically from search or a
  // banner) and the target post lives deep in the stream, pagination has to
  // keep loading older pages until it surfaces. Show an explicit "finding
  // your post" indicator so the user knows we're actively searching for
  // their specific post, not just lazily loading the feed.
  const targetInLoadedItems =
    !!queryTargetPostId && streamItems.some((p) => p.id === queryTargetPostId);
  const isSearchingDeepTarget =
    !!queryTargetPostId &&
    !targetInLoadedItems &&
    !error &&
    (isFetchingTargetPost || hasMore || isLoading);
  const targetNotFoundInStream =
    !!queryTargetPostId &&
    !targetInLoadedItems &&
    !error &&
    targetPostFetchFailed &&
    !hasMore &&
    !isLoading &&
    streamItems.length > 0;

  return (
    <div className="mx-auto max-w-full md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
      {/* Filters stay pinned below the global header (which shows the page
          name) so users can switch filters while scrolling. */}
      <div
        ref={filterBarRef}
        className="sticky top-[64px] z-30 bg-ark-bg md:top-[70px]"
      >
        <FilterChips type={type} onTypeChange={(v) => updateParam("type", v)} />
      </div>

      <div className="flex flex-col gap-3 px-4 pt-4 md:px-0 md:pt-2">
        {isSearchingDeepTarget ? (
          <div
            role="status"
            aria-live="polite"
            className="mx-auto flex w-fit max-w-full items-center gap-2 rounded-full border border-ark-gold/40 bg-ark-gold/10 px-4 py-2 text-sm text-ark-gold shadow-sm"
          >
            <LoaderCircle className="h-4 w-4 animate-spin" aria-hidden />
            <span>{t("searchingForPost")}</span>
          </div>
        ) : null}
        {targetNotFoundInStream ? (
          <div
            role="status"
            className="mx-auto w-fit max-w-full rounded-full border border-yellow-700/40 bg-yellow-950/30 px-4 py-2 text-center text-sm text-yellow-200"
          >
            {t("postNotFound")}
          </div>
        ) : null}
        {isInitialLoad ? (
          <>
            {Array.from({ length: 10 }).map((_, i) => (
              <div
                key={i}
                className="mx-auto w-full max-w-[358px] md:max-w-[680px] lg:max-w-[900px] xl:max-w-[1120px]"
              >
                <Skeleton
                  className={`rounded-2xl ${
                    i % 3 === 0 ? "h-[220px]" : "h-[80px]"
                  }`}
                />
              </div>
            ))}
          </>
        ) : (
          <>
            {groups.map((group) => (
              <div key={group.dayKey} className="flex flex-col gap-3">
                {group.items.map((post, index) => (
                  <Reveal key={post.id} delay={Math.min(index, 8) * 0.05}>
                    <MessageBubble post={post} />
                  </Reveal>
                ))}
              </div>
            ))}

            {!isLoading && !error && streamItems.length === 0 ? (
              <p className="py-10 text-center text-sm text-neutral-400">
                {t("noResults")}
              </p>
            ) : null}

            {error ? (
              <div
                role="alert"
                className="my-4 flex flex-col gap-3 rounded-xl border border-red-900 bg-red-950/40 px-4 py-3 text-sm text-red-200 sm:flex-row sm:items-center sm:justify-between"
              >
                <span className="break-words">{t("loadMoreFailed")}</span>
                <button
                  type="button"
                  onClick={() =>
                    streamItems.length === 0 ? reset() : loadMore()
                  }
                  className="shrink-0 self-start rounded-full border border-red-700 px-3 py-1 text-xs text-red-100 hover:border-red-500 sm:self-auto"
                >
                  {retryLabel}
                </button>
              </div>
            ) : null}

            {isLoading && !error ? (
              <div
                aria-live="polite"
                aria-label={t("loading")}
                className="mx-auto w-full max-w-[358px] md:max-w-[680px] lg:max-w-[900px] xl:max-w-[1120px]"
              >
                <Skeleton className="h-[80px] rounded-2xl" />
              </div>
            ) : null}
          </>
        )}

        <div ref={sentinelRef} aria-hidden className="h-1" />
      </div>
    </div>
  );
}
