/* global React */
/* =====================================================================
   DealAgent, motion foundation
   Reusable, restrained animation primitives. Everything degrades
   gracefully under prefers-reduced-motion.
   ===================================================================== */

const { useState, useEffect, useRef, useCallback } = React;

/* Single source of truth, set synchronously in <head> before paint:
   animations run only when the document is visible AND motion is allowed.
   When off, every component renders its final, visible resting state so
   nothing is ever stuck at opacity:0 or a frozen count of 0. */
const ANIM =
  typeof document !== "undefined" &&
  document.documentElement.classList.contains("anim-on");

const REDUCED = !ANIM;

const EASE_OUT_EXPO = (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t));

/* ---------- useInView : fire once when element enters viewport ---------- */
function useInView(options) {
  const { threshold = 0.18, rootMargin = "0px 0px -8% 0px", once = true } =
    options || {};
  const ref = useRef(null);
  const [inView, setInView] = useState(false);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (REDUCED) {
      setInView(true);
      return;
    }
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            setInView(true);
            if (once) io.unobserve(e.target);
          } else if (!once) {
            setInView(false);
          }
        });
      },
      { threshold, rootMargin }
    );
    io.observe(el);
    return () => io.disconnect();
  }, [threshold, rootMargin, once]);

  return [ref, inView];
}

/* ---------- Reveal : gentle fade + rise, optional stagger delay ---------- */
function Reveal({
  children,
  delay = 0,
  y = 16,
  as = "div",
  className = "",
  style = {},
  threshold,
  ...rest
}) {
  const [ref, inView] = useInView({ threshold });
  const Tag = as;
  const base = {
    transition: REDUCED
      ? "none"
      : "opacity 0.7s var(--ease-out), transform 0.7s var(--ease-out)",
    transitionDelay: `${delay}ms`,
    opacity: inView ? 1 : 0,
    transform: inView ? "translateY(0)" : `translateY(${y}px)`,
    willChange: "opacity, transform",
    ...style,
  };
  return (
    <Tag ref={ref} className={className} style={base} {...rest}>
      {children}
    </Tag>
  );
}

/* ---------- Stagger : reveal a list of children with incremental delay ---------- */
function Stagger({ children, step = 80, base = 0, className = "", style = {} }) {
  const arr = React.Children.toArray(children);
  return (
    <div className={className} style={style}>
      {arr.map((child, i) => (
        <Reveal key={i} delay={base + i * step}>
          {child}
        </Reveal>
      ))}
    </div>
  );
}

/* ---------- CountUp : animate a number when it scrolls into view ---------- */
function CountUp({
  value,
  decimals = 0,
  prefix = "",
  suffix = "",
  separator = ",",
  duration = 1500,
  className = "",
  style = {},
}) {
  const [ref, inView] = useInView({ threshold: 0.5 });
  const [display, setDisplay] = useState(REDUCED ? value : 0);
  const started = useRef(false);

  useEffect(() => {
    if (!inView || started.current) return;
    started.current = true;
    if (REDUCED) {
      setDisplay(value);
      return;
    }
    let raf;
    const start = performance.now();
    const tick = (now) => {
      const p = Math.min((now - start) / duration, 1);
      setDisplay(value * EASE_OUT_EXPO(p));
      if (p < 1) raf = requestAnimationFrame(tick);
      else setDisplay(value);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [inView, value, duration]);

  const fmt = (n) => {
    const fixed = n.toFixed(decimals);
    const [intPart, decPart] = fixed.split(".");
    const withSep = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
    return decPart ? `${withSep}.${decPart}` : withSep;
  };

  return (
    <span ref={ref} className={className} style={style}>
      {prefix}
      {fmt(display)}
      {suffix}
    </span>
  );
}

/* ---------- Magnetic : button/element that leans toward the cursor ---------- */
function Magnetic({ children, strength = 0.32, className = "", style = {}, ...rest }) {
  const ref = useRef(null);

  const onMove = useCallback(
    (e) => {
      if (REDUCED) return;
      const el = ref.current;
      if (!el) return;
      const r = el.getBoundingClientRect();
      const mx = e.clientX - (r.left + r.width / 2);
      const my = e.clientY - (r.top + r.height / 2);
      el.style.transform = `translate(${mx * strength}px, ${my * strength}px)`;
    },
    [strength]
  );

  const onLeave = useCallback(() => {
    const el = ref.current;
    if (el) el.style.transform = "translate(0,0)";
  }, []);

  return (
    <span
      ref={ref}
      className={`magnetic ${className}`}
      style={{ display: "inline-flex", ...style }}
      onMouseMove={onMove}
      onMouseLeave={onLeave}
      {...rest}
    >
      {children}
    </span>
  );
}

/* ---------- useParallax : translate element a fraction of scroll delta ---------- */
function useParallax(speed = 0.12, max = 60) {
  const ref = useRef(null);
  useEffect(() => {
    if (REDUCED) return;
    const el = ref.current;
    if (!el) return;
    let raf = null;
    const update = () => {
      raf = null;
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight;
      // progress: -1 (below) .. 1 (above) relative to viewport center
      const center = r.top + r.height / 2;
      const prog = (center - vh / 2) / vh;
      const shift = Math.max(-max, Math.min(max, -prog * speed * vh));
      el.style.setProperty("--parallax", `${shift}px`);
    };
    const onScroll = () => {
      if (raf == null) raf = requestAnimationFrame(update);
    };
    update();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
    };
  }, [speed, max]);
  return ref;
}

/* ---------- useScrollProgress : 0..1 page scroll for the nav indicator ---------- */
function useScrollProgress() {
  const [p, setP] = useState(0);
  useEffect(() => {
    let raf = null;
    const update = () => {
      raf = null;
      const h = document.documentElement;
      const max = h.scrollHeight - h.clientHeight;
      setP(max > 0 ? h.scrollTop / max : 0);
    };
    const onScroll = () => {
      if (raf == null) raf = requestAnimationFrame(update);
    };
    update();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  return p;
}

/* ---------- usePassedThreshold : true once page is scrolled past N px ---------- */
function usePassedThreshold(px = 40) {
  const [passed, setPassed] = useState(false);
  useEffect(() => {
    const onScroll = () => setPassed(window.scrollY > px);
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, [px]);
  return passed;
}

Object.assign(window, {
  REDUCED,
  EASE_OUT_EXPO,
  useInView,
  Reveal,
  Stagger,
  CountUp,
  Magnetic,
  useParallax,
  useScrollProgress,
  usePassedThreshold,
});
