// utils.jsx — shared hooks/components (exported to window)
const { useState, useEffect, useRef, useCallback } = React;

/* ------------------------------------------------------------------
   Arm scroll-reveal ONLY when CSS transitions actually COMPLETE here.
   We optimistically add `reveal-on` (real browsers hide + animate with
   no flash), then run an authoritative probe: fire a 30ms test
   transition and wait for `transitionend`. If it fires, transitions
   work → keep animating. If it never fires within a short grace window
   (screenshot / PDF / share-capture iframes paint-freeze transitions
   even while rAF timestamps advance), switch to `reveal-frozen`, which
   forces every element visible with `!important; transition:none` so
   nothing can be pinned at opacity:0 by an un-completing transition.
------------------------------------------------------------------- */
(function armReveal() {
  const root = document.documentElement;
  root.classList.add("reveal-on");
  let settled = false;
  const freeze = () => { if (settled) return; settled = true; root.classList.add("reveal-frozen"); };
  const live = () => { settled = true; }; // transitions complete — keep reveal-on
  const run = () => {
    try {
      const probe = document.createElement("div");
      probe.style.cssText = "position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;opacity:0.001;transition:opacity 30ms linear;pointer-events:none;";
      document.body.appendChild(probe);
      let done = false;
      const finish = (ok) => {
        if (done) return; done = true;
        probe.removeEventListener("transitionend", onEnd);
        probe.remove();
        ok ? live() : freeze();
      };
      const onEnd = () => finish(true);
      probe.addEventListener("transitionend", onEnd);
      requestAnimationFrame(() => requestAnimationFrame(() => { probe.style.opacity = "1"; }));
      setTimeout(() => finish(false), 600); // no transitionend ⇒ frozen ⇒ force visible
    } catch (_) { setTimeout(freeze, 300); }
  };
  if (document.body) run();
  else document.addEventListener("DOMContentLoaded", run, { once: true });
})();

/* ------------------------------------------------------------------
   Reveal controller. Elements register to be revealed (`.in`) when they
   scroll into view (IntersectionObserver + scroll/resize rect checks),
   driving the entrance animation in a normal browser.

   Robustness guarantees so content is NEVER stuck hidden:
   • Anything already on screen at registration reveals immediately.
   • If IntersectionObserver never fires (dead in some embeds/captures),
     a per-element timer reveals it.
   • An UNCONDITIONAL safety net reveals every remaining element a short
     time after load — by then a real user has either scrolled (IO
     animated it) or hasn't reached it yet (revealing early is harmless).
   Combined with the `reveal-frozen` path above, nothing can stay at
   opacity:0 regardless of timeline/IO/scroll support.
------------------------------------------------------------------- */
const RevealCtl = (() => {
  const pending = new Set(); // { el, margin, cb }
  let started = false;

  const vh = () => window.innerHeight || document.documentElement.clientHeight;
  const fire = (entry) => { if (pending.has(entry)) { pending.delete(entry); entry.cb(); } };

  const checkAll = () => {
    pending.forEach((e) => {
      const r = e.el.getBoundingClientRect();
      if (r.top < vh() * (1 - e.margin) && r.bottom > 0) fire(e);
    });
  };
  const revealAll = () => { [...pending].forEach(fire); };

  let io = null;
  try {
    io = new IntersectionObserver((entries) => {
      entries.forEach((ev) => {
        if (ev.isIntersecting) {
          const entry = [...pending].find((p) => p.el === ev.target);
          if (entry) fire(entry); else io.unobserve(ev.target);
        }
      });
    }, { threshold: 0.01 });
  } catch (_) { /* ignore */ }

  const onScroll = () => checkAll();

  function start() {
    if (started) return;
    started = true;
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll, { passive: true });
    checkAll();
    setTimeout(checkAll, 120);
    setTimeout(checkAll, 480);
  }

  return {
    register(el, margin, cb) {
      const entry = { el, margin, cb };
      pending.add(entry);
      if (io) io.observe(el);
      start();
      // reveal immediately if already in view at registration
      const r = el.getBoundingClientRect();
      if (r.top < vh() * (1 - margin) && r.bottom > 0) { fire(entry); return () => {}; }
      // unconditional safety net — never let this element stay hidden
      const backup = setTimeout(() => fire(entry), 2200);
      return () => { pending.delete(entry); clearTimeout(backup); if (io) io.unobserve(el); };
    },
  };
})();

// Robust "is this element in viewport" hook (fail-open).
function useInViewport(ref, { margin = 0.1 } = {}) {
  const [inView, setInView] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let alive = true;
    const unregister = RevealCtl.register(el, margin, () => { if (alive) setInView(true); });
    return () => { alive = false; unregister && unregister(); };
  }, [ref, margin]);
  return inView;
}

// reveal wrapper
function Reveal({ children, delay = 0, as = "div", className = "", style = {}, ...rest }) {
  const ref = useRef(null);
  const seen = useInViewport(ref, { margin: 0.06 });
  const Tag = as;
  return (
    <Tag
      ref={ref}
      data-reveal=""
      data-delay={delay || undefined}
      className={(seen ? "in " : "") + className}
      style={style}
      {...rest}
    >
      {children}
    </Tag>
  );
}

// count-up animation, started when `start` becomes true
function useCountUp(target, { duration = 1500, start = false } = {}) {
  const [val, setVal] = useState(0);
  useEffect(() => {
    if (!start || target == null) return;
    let raf, t0;
    const reduce = document.documentElement.getAttribute("data-motion") === "off";
    if (reduce) { setVal(target); return; }
    const tick = (t) => {
      if (!t0) t0 = t;
      const p = Math.min(1, (t - t0) / duration);
      const eased = 1 - Math.pow(1 - p, 3);
      setVal(target * eased);
      if (p < 1) raf = requestAnimationFrame(tick);
      else setVal(target);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [target, start, duration]);
  return val;
}

function StatItem({ stat }) {
  const ref = useRef(null);
  const active = useInViewport(ref, { margin: 0.0 });
  const v = useCountUp(stat.value, { start: active, duration: 1600 });
  let text;
  if (stat.value == null) text = stat.display;
  else if (stat.value >= 1000) text = (stat.prefix || "") + Math.round(v / 1000) + "k" + (stat.suffix || "");
  else text = (stat.prefix || "") + Math.round(v) + (stat.suffix || "");
  return (
    <div className="stat" ref={ref}>
      <div className="stat__num">{renderNumWithAccent(text)}</div>
      <div className="stat__label">{stat.label}</div>
    </div>
  );
}

function renderNumWithAccent(text) {
  const m = String(text).match(/^([+]?)([\d]+)(.*)$/);
  if (m) {
    return (
      <>
        {m[1] && <span className="u">{m[1]}</span>}
        {m[2]}
        {m[3] && <span className="u">{m[3]}</span>}
      </>
    );
  }
  return text;
}

Object.assign(window, { Reveal, useCountUp, StatItem, useInViewport });
