// Shared playful bits: custom cursor, magnetic buttons, AI buddy, Konami code,
// language hook, JSON content loader.

const { useState, useEffect, useRef } = React;

// ─────────────────────────────── Custom cursor ───────────────────────────────
function CustomCursor() {
  const dotRef = useRef(null);
  const labelRef = useRef(null);

  useEffect(() => {
    if (window.matchMedia("(pointer: coarse)").matches) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    document.body.classList.add("custom-cursor");

    const dot = dotRef.current;
    const lbl = labelRef.current;
    if (!dot) return;

    let mx = window.innerWidth / 2, my = window.innerHeight / 2;
    let x = mx, y = my;
    let raf;

    const move = (e) => {
      mx = e.clientX; my = e.clientY;
      const el = e.target.closest?.("a, button, .skill-chip, .project-card, .timeline-knob, .timeline-track, .quote-card, [data-cursor]");
      if (el) {
        dot.classList.add("hover");
        const tip = el.getAttribute("data-cursor");
        if (tip) {
          lbl.textContent = tip;
          lbl.classList.add("show");
        } else {
          lbl.classList.remove("show");
        }
      } else {
        dot.classList.remove("hover");
        lbl.classList.remove("show");
      }
    };
    const down = () => dot.classList.add("click");
    const up   = () => dot.classList.remove("click");

    window.addEventListener("mousemove", move);
    window.addEventListener("mousedown", down);
    window.addEventListener("mouseup", up);

    const tick = () => {
      x += (mx - x) * 0.22;
      y += (my - y) * 0.22;
      dot.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%)`;
      lbl.style.transform = `translate(${x}px, ${y}px) translate(18px, 18px)`;
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);

    return () => {
      window.removeEventListener("mousemove", move);
      window.removeEventListener("mousedown", down);
      window.removeEventListener("mouseup", up);
      cancelAnimationFrame(raf);
      document.body.classList.remove("custom-cursor");
    };
  }, []);

  return (
    <React.Fragment>
      <div ref={dotRef} className="cur" aria-hidden="true" />
      <div ref={labelRef} className="cur-label" aria-hidden="true" />
    </React.Fragment>
  );
}

// ─────────────────────────── Magnetic wrapper ───────────────────────────
function Magnetic({ children, strength = 0.35 }) {
  const ref = useRef(null);
  useEffect(() => {
    if (window.matchMedia("(pointer: coarse)").matches) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const el = ref.current;
    if (!el) return;
    let raf;
    const move = (e) => {
      const r = el.getBoundingClientRect();
      const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
      const dx = (e.clientX - cx) * strength;
      const dy = (e.clientY - cy) * strength;
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        el.style.transform = `translate(${dx}px, ${dy}px)`;
      });
    };
    const leave = () => { el.style.transform = `translate(0,0)`; };
    el.addEventListener("mousemove", move);
    el.addEventListener("mouseleave", leave);
    return () => {
      el.removeEventListener("mousemove", move);
      el.removeEventListener("mouseleave", leave);
      cancelAnimationFrame(raf);
    };
  }, [strength]);
  return <span ref={ref} style={{ display: "inline-block", transition: "transform .25s cubic-bezier(.2,.7,.2,1)" }}>{children}</span>;
}

// ─────────────────────────── AI buddy ───────────────────────────
function AIBuddy({ lines }) {
  const [open, setOpen] = useState(false);
  const [idx, setIdx] = useState(0);

  useEffect(() => {
    const t = setTimeout(() => setOpen(true), 2200);
    const auto = setTimeout(() => setOpen(false), 12000);
    return () => { clearTimeout(t); clearTimeout(auto); };
  }, []);

  useEffect(() => { if (idx >= lines.length) setIdx(0); }, [lines.length, idx]);

  const next = () => {
    setIdx((i) => (i + 1) % lines.length);
    setOpen(true);
  };

  return (
    <React.Fragment>
      <div className={`buddy-bubble ${open ? "show" : ""}`} role="status" aria-live="polite">
        <button className="close" onClick={() => setOpen(false)} aria-label="close">×</button>
        <div dangerouslySetInnerHTML={{ __html: lines[idx] || lines[0] || "" }} />
      </div>
      <button
        className="buddy"
        onClick={next}
        data-cursor="hi"
        aria-label="AI buddy - next quip"
      >
        <svg viewBox="0 0 32 32" fill="none" aria-hidden="true">
          <circle cx="16" cy="16" r="14" stroke="currentColor" strokeWidth="1.5" />
          <circle cx="11" cy="14" r="2" fill="currentColor" />
          <circle cx="21" cy="14" r="2" fill="currentColor" />
          <path d="M10 21c2 2 4 2.5 6 2.5s4-.5 6-2.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
        </svg>
      </button>
    </React.Fragment>
  );
}

// ─────────────────────────── Konami / boss mode ───────────────────────────
function KonamiTrigger({ message }) {
  const [show, setShow] = useState(false);
  useEffect(() => {
    const seq = ["ArrowUp","ArrowUp","ArrowDown","ArrowDown","ArrowLeft","ArrowRight","ArrowLeft","ArrowRight","b","a"];
    let pos = 0;
    const onKey = (e) => {
      const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
      if (k === seq[pos]) {
        pos++;
        if (pos === seq.length) {
          document.body.classList.toggle("boss-mode");
          setShow(true);
          setTimeout(() => setShow(false), 4000);
          pos = 0;
        }
      } else {
        pos = (k === seq[0]) ? 1 : 0;
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);
  if (!show) return null;
  return <div className="boss-banner" role="status">⌘ {message}</div>;
}

// ─────────────────────────── Content loader ───────────────────────────
// Fetches en.json once at startup. Cached at module scope so the hook returns
// synchronously on re-mount.
let CACHED_CONTENT = null;
let CACHED_PROMISE = null;

function loadContent() {
  if (CACHED_CONTENT) return Promise.resolve(CACHED_CONTENT);
  if (CACHED_PROMISE) return CACHED_PROMISE;
  CACHED_PROMISE = fetch("./en.json")
    .then((r) => { if (!r.ok) throw new Error("en.json " + r.status); return r.json(); })
    .then((data) => { CACHED_CONTENT = data; return CACHED_CONTENT; });
  return CACHED_PROMISE;
}

function useCVContent() {
  const [state, setState] = useState(() => ({
    data: CACHED_CONTENT,
    loading: !CACHED_CONTENT,
    error: null,
  }));
  useEffect(() => {
    if (CACHED_CONTENT) return;
    let cancelled = false;
    loadContent()
      .then((data) => { if (!cancelled) setState({ data, loading: false, error: null }); })
      .catch((err) => { if (!cancelled) setState({ data: null, loading: false, error: err }); });
    return () => { cancelled = true; };
  }, []);
  return state;
}

// Reveal on scroll - also reveals anything already in/above viewport on mount.
function useRevealOnScroll() {
  useEffect(() => {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      document.querySelectorAll(".reveal").forEach((el) => el.classList.add("in"));
      return;
    }
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            e.target.classList.add("in");
            io.unobserve(e.target);
          }
        });
      },
      { threshold: 0.12, rootMargin: "0px 0px -8% 0px" }
    );
    document.querySelectorAll(".reveal").forEach((el) => {
      const r = el.getBoundingClientRect();
      if (r.top < window.innerHeight * 0.92) {
        el.classList.add("in");
      } else {
        io.observe(el);
      }
    });
    return () => io.disconnect();
  });
}

// ─────────────────────────── Visibility flags ───────────────────────────
// Items without a flag default to visible. Set `web: false` to hide from the
// website; `cv: false` to hide from the CV.
function forWeb(item) { return item != null && item.web !== false; }
function forCV(item)  { return item != null && item.cv  !== false; }

Object.assign(window, {
  CustomCursor, Magnetic, AIBuddy, KonamiTrigger,
  useCVContent, useRevealOnScroll,
  forWeb, forCV,
});
