// Shared low-level components used across all variations
const { useEffect, useRef, useState } = React;

// Typewriter hook
function useTypewriter(text, { speed = 28, startDelay = 200, enabled = true } = {}) {
  const [out, setOut] = useState("");
  const [done, setDone] = useState(false);
  useEffect(() => {
    if (!enabled) { setOut(text); setDone(true); return; }
    setOut(""); setDone(false);
    let i = 0;
    const t = setTimeout(function tick() {
      i++;
      setOut(text.slice(0, i));
      if (i < text.length) setTimeout(tick, speed);
      else setDone(true);
    }, startDelay);
    return () => clearTimeout(t);
  }, [text, enabled]);
  return [out, done];
}

// Blinking caret
function Caret({ char = "▊", className = "" }) {
  return <span className={`coc-caret ${className}`}>{char}</span>;
}

// Redactable text — hover to reveal
function Redact({ children, reveal, className = "" }) {
  return (
    <span className={`coc-redact ${className}`} data-reveal={reveal || children}>
      <span className="coc-redact-bar">{children}</span>
      <span className="coc-redact-real">{reveal || children}</span>
    </span>
  );
}

// Scanline overlay
function Scanlines({ opacity = 0.04 }) {
  return <div className="coc-scanlines" style={{ opacity }} aria-hidden="true" />;
}

// Console easter egg
function useConsoleEgg() {
  useEffect(() => {
    if (window.__cocEgg) return;
    window.__cocEgg = true;
    const s1 = "color:#a78bfa;font-family:monospace;font-size:13px;font-weight:600";
    const s2 = "color:#9a9aa0;font-family:monospace;font-size:11px;line-height:1.6";
    console.log("%c// crime of curiosity", s1);
    console.log("%cYou may stop this individual,\nbut you can't stop us all...\n\nwe're hiring. careers@crimeofcuriosity.sh", s2);
  }, []);
}

// Scroll-reveal — fades + rises elements as they enter the viewport.
// Auto-targets curated selectors; respects prefers-reduced-motion.
function useScrollReveal() {
  useEffect(() => {
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduce || !("IntersectionObserver" in window)) return;

    // [selector, stagger?] — stagger applies an incremental delay to siblings
    const groups = [
      [".noir-eyebrow", false],
      [".noir-title-wrap", false],
      [".noir-lede", false],
      [".noir-meta", false],
      [".noir-cta", false],
      [".noir-footline", false],
      [".sec-head", false],
      [".svc-card", true],
      [".roster-section", true],
      [".logo-tile", true],
      [".write-row", true],
      [".contact-form", false],
      [".contact-aside", false],
    ];

    const seen = new Set();
    const targets = [];
    groups.forEach(([sel, stagger]) => {
      const els = Array.from(document.querySelectorAll(sel));
      els.forEach((el, i) => {
        if (seen.has(el)) return;
        seen.add(el);
        el.classList.add("coc-reveal");
        if (stagger) {
          // cap the stagger so long lists don't drag
          el.style.setProperty("--reveal-delay", Math.min(i, 8) * 55 + "ms");
        }
        targets.push(el);
      });
    });

    const io = new IntersectionObserver((entries, obs) => {
      entries.forEach((e) => {
        if (e.isIntersecting) {
          e.target.classList.add("coc-in");
          obs.unobserve(e.target);
        }
      });
    }, { rootMargin: "0px 0px -8% 0px", threshold: 0.08 });

    // Register the hidden base state with a forced reflow + a frame of paint
    // BEFORE observing. Elements already in the viewport (the hero) otherwise
    // receive `.coc-reveal` and `.coc-in` in the same synchronous tick and the
    // transition never settles to the end-state — leaving them stuck invisible.
    void document.body.offsetHeight; // force reflow → base state is committed
    const raf = requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        targets.forEach((t) => io.observe(t));
      });
    });

    // Safety net: if anything never resolves, drop the reveal class entirely so
    // the element returns to its natural (visible) state — guaranteed rescue.
    const failsafe = setTimeout(() => {
      targets.forEach((t) => {
        if (!t.classList.contains("coc-in")) t.classList.remove("coc-reveal");
      });
    }, 2500);

    return () => { io.disconnect(); cancelAnimationFrame(raf); clearTimeout(failsafe); };
  }, []);
}

// Scrollspy — highlights the nav link whose section is currently in view.
function useScrollSpy() {
  useEffect(() => {
    const links = Array.from(document.querySelectorAll(".coc-nav-links a"));
    if (!links.length) return;

    // Map section-id → nav link, only for sections that exist on this page
    const pairs = [];
    links.forEach((a) => {
      const href = a.getAttribute("href") || "";
      if (!href.startsWith("#")) return;
      const id = href.slice(1);
      const sec = document.getElementById(id);
      if (sec) pairs.push({ id, sec, link: a });
    });

    // The "blog" nav item points to a separate page; map it to the on-page
    // writing/blog teaser section so it still lights up while scrolling past it.
    const blogLink = links.find((a) => /blog\.html/i.test(a.getAttribute("href") || ""));
    const writingSec = document.getElementById("writing");
    if (blogLink && writingSec && !pairs.some((p) => p.link === blogLink)) {
      pairs.push({ id: "writing", sec: writingSec, link: blogLink });
    }

    if (!pairs.length) return;

    // Order by document position so "last passed" logic is correct
    pairs.sort((a, b) => a.sec.offsetTop - b.sec.offsetTop);

    let active = null;
    const apply = (id) => {
      if (id === active) return;
      active = id;
      pairs.forEach((p) => p.link.classList.toggle("is-active", p.id === id));
    };

    let ticking = false;
    const onScroll = () => {
      if (ticking) return;
      ticking = true;
      requestAnimationFrame(() => {
        ticking = false;
        const probe = window.scrollY + window.innerHeight * 0.32;
        let current = null;
        pairs.forEach((p) => {
          if (p.sec.offsetTop <= probe) current = p.id;
        });
        // Snap to the last link once we hit the very bottom of the page
        if (window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 4) {
          current = pairs[pairs.length - 1].id;
        }
        apply(current); // null while still in the hero → nothing highlighted
      });
    };

    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
    };
  }, []);
}

Object.assign(window, { useTypewriter, Caret, Redact, Scanlines, useConsoleEgg, useScrollReveal, useScrollSpy });
