import {
  useCallback,
  useEffect,
  useRef,
  useState,
  type MouseEvent as ReactMouseEvent,
  type PointerEvent as ReactPointerEvent,
} from "react";
import { m } from "framer-motion";
import { useNavigate } from "react-router-dom";
import { assetUrl, getJSON, itemsOrEmpty, readJSONCache } from "../api";
import { EASE_OUT } from "../motion";
import { langQuery, useI18n, type Lang } from "../i18n";
import { localizePath, stripLangPrefix } from "../languageRoutes";

const FIGMA_ASSET_BASE = "/assets/ark-library/figma";

export const officialRecommendationCoverFallbacks = [
  `${FIGMA_ASSET_BASE}/official-recommendation-cover.png`,
] as const;

type BannerSlide = {
  id: string;
  mobile: string;
  desktop: string;
  alt: string;
  linkUrl?: string;
};

type BannerApiItem = {
  id: number | string;
  imageUrl: string;
  linkUrl?: string;
  sortOrder?: number;
};

type BannerApiResponse = {
  items?: BannerApiItem[] | null;
};

const AUTOPLAY_MS = 3000;
const RESUME_AFTER_INTERACTION_MS = 8000;
const publicMenuOpenChangeEvent = "ark:public-menu-open-change";

function bannerLangParam(lang: Lang): string {
  return langQuery(lang).toLowerCase();
}

/**
 * If `linkUrl` points inside this app (a relative path, or an absolute URL on
 * the same origin), return the router path so we can navigate client-side —
 * e.g. `/browse?post=<id>` triggers the stream's scroll-to-post without a full
 * reload. External URLs return null and fall back to a plain `<a href>`.
 */
function internalPath(linkUrl: string): string | null {
  try {
    if (linkUrl.startsWith("//")) return null;
    if (linkUrl.startsWith("/")) return linkUrl;
    const url = new URL(linkUrl, window.location.origin);
    if (url.origin !== window.location.origin) return null;
    return `${url.pathname}${url.search}${url.hash}`;
  } catch {
    return null;
  }
}

/**
 * Banner link URLs are stored unprefixed (e.g. `/browse?post=123`). When the
 * viewer is on a non-English locale we must re-prefix them with the active
 * language path (`/malay/browse?post=123`) so navigation doesn't drop into
 * the English version of the post.
 */
function localizeLinkUrl(linkUrl: string, lang: Lang): string {
  const path = internalPath(linkUrl);
  if (!path) return linkUrl;
  const url = new URL(path, window.location.origin);
  const bare = stripLangPrefix(url.pathname);
  return localizePath(bare, lang) + url.search + url.hash;
}

function toSlides(items: BannerApiItem[]): BannerSlide[] {
  return [...items]
    .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
    .filter((item) => item.imageUrl)
    .map((item) => {
      const imageUrl = assetUrl(item.imageUrl);
      return {
        id: String(item.id),
        mobile: imageUrl,
        desktop: imageUrl,
        alt: "",
        linkUrl: item.linkUrl || undefined,
      };
    });
}

export function FigmaBanner() {
  const { lang } = useI18n();
  const navigate = useNavigate();
  const [slides, setSlides] = useState<BannerSlide[]>([]);
  const scrollerRef = useRef<HTMLDivElement>(null);
  // Set when a mouse drag just moved the carousel, so the synthetic `click`
  // that follows pointerup doesn't navigate the slide's link.
  const suppressClickRef = useRef(false);
  const [activeIndex, setActiveIndex] = useState(0);
  const [autoplayPaused, setAutoplayPaused] = useState(false);
  const [publicMenuOpen, setPublicMenuOpen] = useState(false);
  const resumeTimerRef = useRef<number | null>(null);
  const dragStateRef = useRef<{
    pointerId: number;
    startX: number;
    startScrollLeft: number;
    moved: boolean;
  } | null>(null);
  const hasMultiple = slides.length > 1;

  useEffect(() => {
    let cancelled = false;
    const bannersUrl = `/api/banners?lang=${bannerLangParam(lang)}`;
    setActiveIndex(0);

    const cachedBanners = readJSONCache<BannerApiResponse>(bannersUrl);
    if (cachedBanners) setSlides(toSlides(itemsOrEmpty(cachedBanners.items)));

    getJSON<BannerApiResponse>(bannersUrl)
      .then((res) => {
        if (cancelled) return;
        setSlides(toSlides(itemsOrEmpty(res.items)));
      })
      .catch(() => {
        if (!cancelled && !cachedBanners) setSlides([]);
      });
    return () => {
      cancelled = true;
    };
  }, [lang]);

  const goTo = useCallback((index: number, behavior: ScrollBehavior) => {
    const scroller = scrollerRef.current;
    if (!scroller) return;
    const target = scroller.children[index] as HTMLElement | undefined;
    if (!target) return;
    // Center the slide in the viewport. On mobile (full-width slides) the
    // offset is 0 so this matches the previous snap-start behavior.
    const centerOffset = (scroller.clientWidth - target.offsetWidth) / 2;
    const left = Math.max(0, target.offsetLeft - centerOffset);
    scroller.scrollTo({ left, behavior });
  }, []);

  const pauseAutoplay = useCallback(() => {
    setAutoplayPaused(true);
    if (resumeTimerRef.current !== null) {
      window.clearTimeout(resumeTimerRef.current);
    }
    resumeTimerRef.current = window.setTimeout(() => {
      setAutoplayPaused(false);
      resumeTimerRef.current = null;
    }, RESUME_AFTER_INTERACTION_MS);
  }, []);

  useEffect(
    () => () => {
      if (resumeTimerRef.current !== null) {
        window.clearTimeout(resumeTimerRef.current);
      }
    },
    [],
  );

  useEffect(() => {
    const handlePublicMenuOpenChange = (event: Event) => {
      setPublicMenuOpen(Boolean((event as CustomEvent<boolean>).detail));
    };

    window.addEventListener(
      publicMenuOpenChangeEvent,
      handlePublicMenuOpenChange,
    );
    return () => {
      window.removeEventListener(
        publicMenuOpenChangeEvent,
        handlePublicMenuOpenChange,
      );
    };
  }, []);

  useEffect(() => {
    const scroller = scrollerRef.current;
    if (!scroller) return;

    const handleScroll = () => {
      const firstChild = scroller.children[0] as HTMLElement | undefined;
      const slideWidth = firstChild?.offsetWidth ?? scroller.clientWidth;
      if (slideWidth === 0) return;
      const next = Math.round(scroller.scrollLeft / slideWidth);
      setActiveIndex((prev) => (prev === next ? prev : next));
    };

    handleScroll();
    scroller.addEventListener("scroll", handleScroll, { passive: true });
    return () => scroller.removeEventListener("scroll", handleScroll);
  }, [slides.length]);

  useEffect(() => {
    if (!hasMultiple || autoplayPaused || publicMenuOpen) return;
    const timer = window.setInterval(() => {
      setActiveIndex((prev) => {
        const next = (prev + 1) % slides.length;
        goTo(next, "smooth");
        return next;
      });
    }, AUTOPLAY_MS);
    return () => window.clearInterval(timer);
  }, [hasMultiple, autoplayPaused, publicMenuOpen, slides.length, goTo]);

  const handlePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
    if (event.pointerType !== "mouse") return;
    const scroller = scrollerRef.current;
    if (!scroller) return;
    dragStateRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startScrollLeft: scroller.scrollLeft,
      moved: false,
    };
    // Don't capture the pointer yet: capturing on press makes the browser
    // dispatch the following `click` to the scroller instead of the slide's
    // <a>, swallowing the link. Capture only once a real drag begins (below).
    pauseAutoplay();
  };

  const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
    const drag = dragStateRef.current;
    if (!drag || drag.pointerId !== event.pointerId) return;
    const scroller = scrollerRef.current;
    if (!scroller) return;
    const dx = event.clientX - drag.startX;
    if (!drag.moved && Math.abs(dx) > 4) {
      drag.moved = true;
      scroller.setPointerCapture(event.pointerId);
      scroller.style.scrollSnapType = "none";
    }
    if (drag.moved) {
      scroller.scrollLeft = drag.startScrollLeft - dx;
    }
  };

  const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
    const drag = dragStateRef.current;
    if (!drag || drag.pointerId !== event.pointerId) return;
    const scroller = scrollerRef.current;
    dragStateRef.current = null;
    if (!scroller) return;
    if (scroller.hasPointerCapture(event.pointerId)) {
      scroller.releasePointerCapture(event.pointerId);
    }
    if (drag.moved) {
      // Swallow the click that the browser fires right after a drag-release.
      suppressClickRef.current = true;
      window.setTimeout(() => {
        suppressClickRef.current = false;
      }, 0);
      const firstChild = scroller.children[0] as HTMLElement | undefined;
      const slideWidth = firstChild?.offsetWidth || scroller.clientWidth || 1;
      const nearest = Math.round(scroller.scrollLeft / slideWidth);
      const clamped = Math.max(0, Math.min(slides.length - 1, nearest));
      scroller.style.scrollSnapType = "";
      goTo(clamped, "smooth");
    }
  };

  const handleSlideClick = (
    event: ReactMouseEvent<HTMLAnchorElement>,
    linkUrl: string,
  ) => {
    if (suppressClickRef.current) {
      suppressClickRef.current = false;
      event.preventDefault();
      return;
    }
    // Let modified clicks (new tab / window) and external links use default
    // browser behavior; route same-app links through the SPA so the stream's
    // `?post=<id>` scroll-to-post runs without a full page reload.
    if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
      return;
    }
    const path = internalPath(linkUrl);
    if (!path) return;
    event.preventDefault();
    navigate(localizeLinkUrl(path, lang));
  };

  if (slides.length === 0) return null;

  // Show every slide's dot. The row stays within the screen width and wraps to
  // a second row (and beyond if needed) instead of overflowing horizontally.
  const pagination = hasMultiple ? (
    <div className="px-4">
      <div
        className="mx-auto flex max-w-full flex-wrap items-center justify-center gap-1.5 md:gap-2"
        role="tablist"
        aria-label="Banner pagination"
      >
        {slides.map((slide, index) => {
          const active = index === activeIndex;
          return (
            <button
              key={slide.id}
              type="button"
              role="tab"
              aria-selected={active}
              aria-label={`Go to slide ${index + 1}`}
              onClick={() => {
                pauseAutoplay();
                setActiveIndex(index);
                goTo(index, "smooth");
              }}
              className={`h-1.5 shrink-0 rounded-full transition-all ${
                active
                  ? "w-6 bg-ark-gold"
                  : "w-1.5 bg-[#7C7C7C] hover:bg-white/50"
              }`}
            />
          );
        })}
      </div>
    </div>
  ) : null;

  return (
    <div className="relative md:mx-auto md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
      <div
        ref={scrollerRef}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={endDrag}
        onPointerCancel={endDrag}
        onTouchStart={pauseAutoplay}
        className="flex cursor-grab snap-x snap-mandatory overflow-x-auto overflow-y-hidden scroll-smooth select-none touch-pan-x [-ms-overflow-style:none] [scrollbar-width:none] active:cursor-grabbing [&::-webkit-scrollbar]:hidden"
        role="region"
        aria-roledescription="carousel"
        aria-label="ARK Library banner"
      >
        {slides.map((slide, index) => {
          const isActive = index === activeIndex;
          const image = (
            <picture className="block aspect-video w-full overflow-hidden bg-black md:rounded-xl">
              <source media="(max-width: 767px)" srcSet={slide.mobile} />
              <img
                src={slide.desktop}
                alt={slide.alt}
                className="pointer-events-none h-full w-full object-cover"
                width={1280}
                height={720}
                loading={index === 0 ? "eager" : "lazy"}
                decoding="async"
                draggable={false}
              />
            </picture>
          );
          const animatedImage = (
            <m.div
              className="h-full w-full origin-center will-change-transform"
              animate={{
                filter: isActive ? "blur(0px)" : "blur(6px)",
                opacity: isActive ? 1 : 0.55,
                scale: isActive ? 1 : 0.93,
              }}
              transition={{
                duration: 0.45,
                ease: EASE_OUT as unknown as number[],
              }}
            >
              {image}
            </m.div>
          );

          return (
            <div
              key={slide.id}
              className="relative w-full shrink-0 snap-center md:w-[78%] md:first:ml-[11%] md:last:mr-[11%] lg:w-[72%] lg:first:ml-[14%] lg:last:mr-[14%] xl:w-[60%] xl:first:ml-[20%] xl:last:mr-[20%]"
              role="group"
              aria-roledescription="slide"
              aria-label={`${index + 1} / ${slides.length}`}
            >
              {slide.linkUrl ? (
                <a
                  href={localizeLinkUrl(slide.linkUrl, lang)}
                  className="block"
                  rel="noreferrer"
                  onClick={(event) => handleSlideClick(event, slide.linkUrl!)}
                >
                  {animatedImage}
                </a>
              ) : (
                animatedImage
              )}
            </div>
          );
        })}
      </div>

      {hasMultiple ? (
        <>
          <div className="pointer-events-none absolute inset-x-0 bottom-0 flex h-[30px] items-center justify-center bg-gradient-to-b from-[#14131900] to-[#141319] md:hidden">
            <div className="pointer-events-auto">{pagination}</div>
          </div>
          <div className="mt-3 hidden md:block">{pagination}</div>
        </>
      ) : null}
    </div>
  );
}
