// Motion system: Lenis smooth scroll + one shared GSAP reveal grammar.
// Every page mounts usePageMotion() on its root; markup opts in via data attributes:
//   data-lines                     masked line reveal on mount (hero headlines)
//   data-reveal / data-reveal-item scroll-triggered fade+rise, staggered
//   data-parallax="0.15"           subtle scrub parallax (± factor)
const MOTION_REDUCED = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const MOTION_OK = !!(window.gsap && window.ScrollTrigger);

if (MOTION_OK) {
  gsap.registerPlugin(ScrollTrigger);
  if (!MOTION_REDUCED && window.Lenis) {
    const lenis = new Lenis({ duration: 1.1, smoothWheel: true });
    lenis.on('scroll', ScrollTrigger.update);
    gsap.ticker.add((time) => lenis.raf(time * 1000));
    gsap.ticker.lagSmoothing(0);
    window.addEventListener('resize', () => ScrollTrigger.refresh());
    window.__lenis = lenis;
  }
}

const usePageMotion = () => {
  const ref = React.useRef(null);

  React.useLayoutEffect(() => {
    if (!MOTION_OK || MOTION_REDUCED || !ref.current) return;

    const ctx = gsap.context(() => {
      ref.current.querySelectorAll('[data-lines]').forEach((el) => {
        gsap.fromTo(el.querySelectorAll('.line-inner'),
          { yPercent: 110 },
          { yPercent: 0, duration: 1.15, stagger: 0.09, ease: 'power4.out', delay: 0.15 });
      });

      ref.current.querySelectorAll('[data-reveal]').forEach((group) => {
        const items = group.querySelectorAll('[data-reveal-item]');
        gsap.fromTo(items.length ? items : [group],
          { opacity: 0, y: 28 },
          {
            opacity: 1, y: 0, duration: 1, ease: 'power3.out', stagger: 0.08,
            scrollTrigger: { trigger: group, start: 'top 82%', once: true },
          });
      });

      ref.current.querySelectorAll('[data-parallax]').forEach((el) => {
        const speed = parseFloat(el.dataset.parallax) || 0.15;
        gsap.fromTo(el,
          { yPercent: speed * -60 },
          {
            yPercent: speed * 60, ease: 'none',
            scrollTrigger: { trigger: el.parentElement, scrub: true },
          });
      });
    }, ref);

    // Tailwind's CDN runtime styles the DOM asynchronously after mount, so
    // trigger positions measured now are wrong; re-measure once layout settles.
    const raf = requestAnimationFrame(() => ScrollTrigger.refresh());
    const late = setTimeout(() => ScrollTrigger.refresh(), 600);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => ScrollTrigger.refresh());

    return () => { cancelAnimationFrame(raf); clearTimeout(late); ctx.revert(); };
  }, []);

  return ref;
};

// Masked-line headline helper: each child of `lines` becomes one revealed line.
const Lines = ({ as: Tag = 'h1', lines, className = '' }) => (
  <Tag data-lines className={className}>
    {lines.map((l, i) => (
      <span key={i} className="line"><span className="line-inner">{l}</span></span>
    ))}
  </Tag>
);

// Magnetic pull for premium CTAs (desktop pointers only).
const useMagnetic = () => {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const el = ref.current;
    if (!el || !MOTION_OK || MOTION_REDUCED || !window.matchMedia('(pointer:fine)').matches) return;
    const move = (e) => {
      const r = el.getBoundingClientRect();
      gsap.to(el, {
        x: (e.clientX - r.left - r.width / 2) * 0.18,
        y: (e.clientY - r.top - r.height / 2) * 0.18,
        duration: 0.45, ease: 'power3.out',
      });
    };
    const leave = () => gsap.to(el, { x: 0, y: 0, duration: 0.7, ease: 'elastic.out(1,0.45)' });
    el.addEventListener('pointermove', move);
    el.addEventListener('pointerleave', leave);
    return () => { el.removeEventListener('pointermove', move); el.removeEventListener('pointerleave', leave); };
  }, []);
  return ref;
};

// Subtle 3D tilt toward the cursor for cards (desktop pointers only).
const useTilt = (max = 6) => {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const el = ref.current;
    if (!el || !MOTION_OK || MOTION_REDUCED || !window.matchMedia('(pointer:fine)').matches) return;
    const move = (e) => {
      const r = el.getBoundingClientRect();
      const px = (e.clientX - r.left) / r.width - 0.5;
      const py = (e.clientY - r.top) / r.height - 0.5;
      gsap.to(el, { rotateY: px * max, rotateX: -py * max, transformPerspective: 900, duration: 0.4, ease: 'power2.out' });
    };
    const leave = () => gsap.to(el, { rotateX: 0, rotateY: 0, duration: 0.6, ease: 'power3.out' });
    el.addEventListener('pointermove', move);
    el.addEventListener('pointerleave', leave);
    return () => { el.removeEventListener('pointermove', move); el.removeEventListener('pointerleave', leave); };
  }, []);
  return ref;
};

// Count-up number that fires once when scrolled into view.
const CountUp = ({ to, decimals = 0, duration = 1.6, className = '', suffix = '', prefix = '' }) => {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const fmt = (v) => prefix + (decimals ? v.toFixed(decimals) : Math.round(v).toString()) + suffix;
    if (!MOTION_OK || MOTION_REDUCED) { el.textContent = fmt(to); return; }
    const obj = { v: 0 };
    const tween = gsap.to(obj, {
      v: to, duration, ease: 'power2.out',
      onUpdate: () => { el.textContent = fmt(obj.v); },
      scrollTrigger: { trigger: el, start: 'top 88%', once: true },
    });
    return () => tween.scrollTrigger && tween.scrollTrigger.kill();
  }, [to]);
  return <span ref={ref} className={className}>{prefix}{decimals ? to.toFixed(decimals) : to}{suffix}</span>;
};

// Pinned scrollytelling: scrubs a [data-scrub-video] frame-by-frame with scroll
// (falling back to crossfading [data-shot] layers) and toggles [data-step] active
// state across N beats while the sticky panel is pinned.
const usePinnedStory = (beats) => {
  const ref = React.useRef(null);
  React.useLayoutEffect(() => {
    if (!MOTION_OK || MOTION_REDUCED || !ref.current) return;
    const root = ref.current;
    const shots = root.querySelectorAll('[data-shot]');
    const steps = root.querySelectorAll('[data-step]');
    const sticky = root.querySelector('[data-sticky]');
    const video = root.querySelector('[data-scrub-video]');
    const numEl = root.querySelector('[data-shot-num]');
    if (!shots.length && !video) return;

    // Video scrubbing needs metadata (for duration) and a decoded first frame.
    let vidDur = 0;
    if (video) {
      const readMeta = () => { vidDur = video.duration || 0; };
      if (video.readyState >= 1) readMeta(); else video.addEventListener('loadedmetadata', readMeta);
      // iOS/Safari won't paint a seeked frame until the video has played once.
      const warm = () => { const pr = video.play(); if (pr && pr.then) pr.then(() => video.pause()).catch(() => {}); };
      if (video.readyState >= 2) warm(); else video.addEventListener('canplay', warm, { once: true });
    }
    const seek = (t) => {
      if (!video || !vidDur) return;
      video.currentTime = Math.max(0, Math.min(vidDur - 0.04, t));
    };

    const ctx = gsap.context(() => {
      if (shots.length) { gsap.set(shots, { opacity: 0 }); gsap.set(shots[0], { opacity: 1 }); }
      steps.forEach((s, i) => s.classList.toggle('is-active', i === 0));

      ScrollTrigger.create({
        trigger: root,
        start: 'top top',
        end: () => '+=' + (root.offsetHeight - window.innerHeight),
        pin: sticky,
        scrub: 0.6,
        onUpdate: (self) => {
          const p = self.progress;
          const idx = Math.min(beats - 1, Math.floor(p * beats));
          steps.forEach((st, i) => st.classList.toggle('is-active', i === idx));
          if (numEl) numEl.textContent = String(idx + 1).padStart(2, '0');
          if (video && vidDur) seek(p * vidDur);
          else shots.forEach((sh, i) => gsap.to(sh, { opacity: i === idx ? 1 : 0, duration: 0.4, overwrite: true }));
        },
      });
    }, ref);

    const raf = requestAnimationFrame(() => ScrollTrigger.refresh());
    return () => { cancelAnimationFrame(raf); ctx.revert(); };
  }, []);
  return ref;
};

// Global chrome: scroll-progress bar + custom cursor (desktop, motion-on only).
const ScrollProgress = () => {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const bar = ref.current;
    if (!bar) return;
    const onScroll = () => {
      const h = document.documentElement.scrollHeight - window.innerHeight;
      bar.style.transform = `scaleX(${h > 0 ? Math.min(1, window.scrollY / h) : 0})`;
    };
    const src = window.__lenis ? (cb) => window.__lenis.on('scroll', cb) : null;
    if (src) src(onScroll); else window.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => { if (!src) window.removeEventListener('scroll', onScroll); };
  }, []);
  return <div className="scroll-progress"><span ref={ref}/></div>;
};

const CustomCursor = () => {
  const dot = React.useRef(null);
  const ring = React.useRef(null);
  React.useEffect(() => {
    if (MOTION_REDUCED || !window.matchMedia('(pointer:fine)').matches) return;
    document.body.classList.add('has-cursor');
    const rx = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
    const move = (e) => {
      gsap.set(dot.current, { x: e.clientX, y: e.clientY });
      gsap.to(ring.current, { x: e.clientX, y: e.clientY, duration: 0.35, ease: 'power3.out' });
      const t = e.target.closest('a,button,[role=button],input,select,summary,[data-cursor]');
      ring.current.classList.toggle('is-hover', !!t);
    };
    window.addEventListener('pointermove', move);
    return () => { window.removeEventListener('pointermove', move); document.body.classList.remove('has-cursor'); };
  }, []);
  if (MOTION_REDUCED) return null;
  return (<>
    <div ref={ring} className="cursor-ring" aria-hidden="true"/>
    <div ref={dot} className="cursor-dot" aria-hidden="true"/>
  </>);
};

Object.assign(window, {
  usePageMotion, useMagnetic, useTilt, usePinnedStory,
  Lines, CountUp, ScrollProgress, CustomCursor, MOTION_REDUCED,
});
