"use client";

import { MobileMenuProvider } from "@/components/header/mobile-menu-context";
import { type NavItem, NavItemsProvider } from "@/components/header/nav-items-context";
import { cn } from "@/lib/utils";
import { useSelectedLayoutSegment } from "next/navigation";
import { type ReactNode, useEffect, useRef, useState } from "react";

type Props = {
  label: string;
  items: ReadonlyArray<NavItem>;
  cta: NavItem;
  children: ReactNode;
  panel: ReactNode;
};

/** Fallback used when `--breakpoint-nav` cannot be read (SSR-less edge cases). */
const NAV_BREAKPOINT_FALLBACK = "1440px";

export default function HeaderShell({ label, items, cta, children, panel }: Props) {
  const [scrolled, setScrolled] = useState(false);
  const [open, setOpen] = useState(false);
  const [hidden, setHidden] = useState(false);
  const headerRef = useRef<HTMLElement>(null);
  const lastYRef = useRef(0);

  // The header owns the *only* segment subscription: the desktop bar, the mobile
  // panel and the CTA all read `currentPath` off the context instead of calling
  // `useSelectedLayoutSegment()` each.
  const segment = useSelectedLayoutSegment();
  const currentPath = segment ? `/${segment}` : "/";

  // An open mobile menu must never leave the header slid off-screen.
  const effectiveHidden = hidden && !open;

  useEffect(() => {
    lastYRef.current = window.scrollY;
    const onScroll = () => {
      const y = window.scrollY;
      setScrolled(y > 40);
      // Auto-hide is opt-in per route (blog pages mount HeaderAutoHide, which sets
      // this flag). Read it live at scroll time — no re-render for route detection.
      if (document.documentElement.dataset.headerAutohide === "1") {
        const delta = y - lastYRef.current;
        if (y > 140 && delta > 4) setHidden(true);
        else if (delta < -4 || y < 80) setHidden(false);
      } else {
        setHidden(false);
      }
      lastYRef.current = y;
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  // Publish the live header height (it shrinks on scroll) so sticky elements
  // below — e.g. the blog category sub-nav — can pin themselves flush under it.
  // When the header is hidden the height collapses to 0 so those elements rise up.
  //
  // The observer is created once: it used to be torn down and re-attached on
  // every `effectiveHidden` flip, purely to write `0px`. The flag now travels
  // through a ref, and a second effect republishes when it changes (a translated
  // header keeps its size, so the observer never fires on hide/show).
  const hiddenRef = useRef(effectiveHidden);
  const publishRef = useRef<(() => void) | null>(null);

  useEffect(() => {
    const el = headerRef.current;
    if (!el) return;
    const publish = () => {
      document.documentElement.style.setProperty(
        "--header-h",
        hiddenRef.current ? "0px" : `${Math.round(el.getBoundingClientRect().height)}px`,
      );
    };
    publishRef.current = publish;
    publish();
    const observer = new ResizeObserver(publish);
    observer.observe(el);
    return () => {
      observer.disconnect();
      publishRef.current = null;
    };
  }, []);

  useEffect(() => {
    hiddenRef.current = effectiveHidden;
    publishRef.current?.();
  }, [effectiveHidden]);

  // Close the mobile panel on Escape or once the desktop nav takes over.
  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") setOpen(false);
    };
    // Read the breakpoint off the token rather than hard-coding 1440px, so the
    // media query cannot drift from `--breakpoint-nav` (src/app/globals.css).
    const breakpoint =
      getComputedStyle(document.documentElement).getPropertyValue("--breakpoint-nav").trim() ||
      NAV_BREAKPOINT_FALLBACK;
    const mq = window.matchMedia(`(min-width: ${breakpoint})`);
    const onChange = () => {
      if (mq.matches) setOpen(false);
    };
    window.addEventListener("keydown", onKey);
    mq.addEventListener("change", onChange);
    return () => {
      window.removeEventListener("keydown", onKey);
      mq.removeEventListener("change", onChange);
    };
  }, [open]);

  // `reactCompiler: true` (next.config.ts) memoises these object literals.
  const menu = {
    open,
    toggle: () => setOpen((o) => !o),
    close: () => setOpen(false),
  };
  const nav = { items, cta, currentPath };

  return (
    <NavItemsProvider value={nav}>
      <MobileMenuProvider value={menu}>
        <header
          ref={headerRef}
          className={cn(
            "fixed inset-x-0 top-0 z-50 w-full border-b border-hairline bg-background/95 backdrop-blur-header nav:bg-background/80 nav:backdrop-blur-header-compact motion-safe:transition-[box-shadow,transform] motion-safe:duration-300",
            scrolled && "shadow-header",
            effectiveHidden && "-translate-y-full",
          )}
        >
          <nav
            aria-label={label}
            className={cn(
              // Pleine largeur (demande client 2026-08-25) : gouttieres 24/64px sans
              // `max-w-editorial`, la barre s'etire avec le viewport.
              // Le menu horizontal complet tient dans les 1312px utiles des le
              // breakpoint `nav`, au prix de gaps resserres a 12px (l'anglais est la
              // locale la plus longue). Au-dessous : menu burger.
              "mx-auto flex w-full items-center justify-between gap-x-6 px-6 lg:px-16 motion-safe:transition-[padding] motion-safe:duration-300 motion-safe:ease-out nav:gap-x-3",
              scrolled ? "py-header-y lg:py-3.5" : "py-header-y lg:py-header-y-lg",
            )}
          >
            {children}
          </nav>
          {panel}
        </header>
      </MobileMenuProvider>
    </NavItemsProvider>
  );
}
