import { useEffect, useMemo, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import type { HeadingItem } from "@/utils/markdown-manipulation";

interface MemoOutlineProps {
  headings: HeadingItem[];
}

const levelIndent: Record<number, string> = {
  1: "ml-0",
  2: "ml-3",
  3: "ml-6",
  4: "ml-8",
};

const areStringArraysEqual = (a: string[], b: string[]) => {
  if (a.length !== b.length) return false;
  return a.every((item, index) => item === b[index]);
};

/** Outline navigation for memo headings (h1-h4). */
const MemoOutline = ({ headings }: MemoOutlineProps) => {
  const outlineRef = useRef<HTMLElement>(null);
  const itemRefs = useRef(new Map<string, HTMLAnchorElement>());
  const [activeSlugs, setActiveSlugs] = useState<string[]>([]);

  const orderedSlugs = useMemo(() => headings.map((heading) => heading.slug).filter(Boolean), [headings]);
  const activeSlugSet = useMemo(() => new Set(activeSlugs), [activeSlugs]);

  useEffect(() => {
    setActiveSlugs([]);

    const elements = orderedSlugs
      .map((slug) => document.getElementById(slug))
      .filter((element): element is HTMLElement => Boolean(element));

    if (elements.length === 0) {
      return;
    }

    let animationFrame = 0;

    const updateActiveSlugs = () => {
      animationFrame = 0;
      const viewportTop = window.scrollY;
      const viewportBottom = viewportTop + (window.innerHeight || document.documentElement.clientHeight);
      const documentBottom = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
      const headingSections = elements.map((element, index) => {
        const sectionTop = element.getBoundingClientRect().top + window.scrollY;
        const nextElement = elements[index + 1];
        const sectionBottom = nextElement ? nextElement.getBoundingClientRect().top + window.scrollY : documentBottom;
        return { slug: element.id, top: sectionTop, bottom: sectionBottom };
      });
      const nextActiveSlugs = headingSections
        .filter((section) => section.bottom > viewportTop && section.top < viewportBottom)
        .map((section) => section.slug);

      setActiveSlugs((previousSlugs) => (areStringArraysEqual(previousSlugs, nextActiveSlugs) ? previousSlugs : nextActiveSlugs));
    };

    const scheduleUpdateActiveSlugs = () => {
      if (animationFrame) {
        return;
      }
      animationFrame = window.requestAnimationFrame(updateActiveSlugs);
    };

    updateActiveSlugs();
    window.addEventListener("scroll", scheduleUpdateActiveSlugs, { passive: true });
    window.addEventListener("resize", scheduleUpdateActiveSlugs);

    return () => {
      if (animationFrame) {
        window.cancelAnimationFrame(animationFrame);
      }
      window.removeEventListener("scroll", scheduleUpdateActiveSlugs);
      window.removeEventListener("resize", scheduleUpdateActiveSlugs);
    };
  }, [orderedSlugs]);

  useEffect(() => {
    const firstActiveSlug = activeSlugs[0];
    const outline = outlineRef.current;
    const activeItem = firstActiveSlug ? itemRefs.current.get(firstActiveSlug) : undefined;

    if (!outline || !activeItem || outline.scrollHeight <= outline.clientHeight) {
      return;
    }

    const itemTop = activeItem.offsetTop;
    const itemBottom = itemTop + activeItem.offsetHeight;
    const comfortableTop = outline.scrollTop + 24;
    const comfortableBottom = outline.scrollTop + outline.clientHeight - 24;

    if (itemTop >= comfortableTop && itemBottom <= comfortableBottom) {
      return;
    }

    outline.scrollTo({
      top: Math.max(0, itemTop - outline.clientHeight / 2 + activeItem.offsetHeight / 2),
      behavior: "smooth",
    });
  }, [activeSlugs]);

  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, slug: string) => {
    e.preventDefault();
    const el = document.getElementById(slug);
    if (el) {
      el.scrollIntoView({ behavior: "smooth", block: "start" });
      window.history.replaceState(null, "", `#${slug}`);
    }
  };

  return (
    <nav ref={outlineRef} className="relative flex max-h-[45vh] flex-col overflow-y-auto overscroll-contain pr-1">
      {headings.map((heading, index) => {
        const isActive = activeSlugSet.has(heading.slug);

        return (
          <a
            key={`${heading.slug}-${index}`}
            ref={(element) => {
              if (element) {
                itemRefs.current.set(heading.slug, element);
              } else {
                itemRefs.current.delete(heading.slug);
              }
            }}
            href={`#${heading.slug}`}
            aria-current={activeSlugs[0] === heading.slug ? "location" : undefined}
            onClick={(e) => handleClick(e, heading.slug)}
            className={cn(
              "group relative block shrink-0 rounded-l-sm py-[5px] pr-1 pl-2 text-[13px] leading-snug truncate",
              "transition-colors duration-200 ease-out",
              isActive ? "bg-primary/10 font-medium text-primary" : "text-muted-foreground/60 hover:text-foreground/90",
              levelIndent[heading.level],
              heading.level === 1 && !isActive && "font-medium text-muted-foreground/80",
            )}
            title={heading.text}
          >
            <span
              className={cn(
                "absolute top-1.5 bottom-1.5 left-0 w-0.5 rounded-full bg-primary transition-opacity duration-200",
                isActive ? "opacity-100" : "opacity-0",
              )}
            />
            <span className="relative">
              {heading.text}
              <span
                className={cn(
                  "absolute -bottom-px left-0 h-px bg-foreground/30 transition-all duration-200",
                  isActive ? "w-full bg-primary/50" : "w-0 group-hover:w-full",
                )}
              />
            </span>
          </a>
        );
      })}
    </nav>
  );
};

export default MemoOutline;
