// ===========================================================
// AlbumExtras, deep-dive page enhancements.
// Standalone pieces wired into AlbumPage:
//   • bbrAffiliateFor, shared retailer/price logic
//   • StickyBuyBar, floating buy bar once the affiliate strip scrolls away
//   • MobileDossierNav, sticky horizontal section pills (< 1180px)
//   • MobileTOC, collapsible table of contents under the hero (< 768px)
//   • RecLink, actionable "if you love this" recommendation
//   • AlbumAccent, per-album accent colour derived from the cover art
// Loaded after Sleeve.jsx, before AlbumPage.jsx.
// ===========================================================
const { useState: useXState, useEffect: useXEffect, useRef: useXRef, useMemo: useXMemo } = React;
const XSleeve = window.Sleeve;

/* ---- shared affiliate logic (mirrors AffiliateStrip) ---- */
function bbrAffiliateFor(album) {
  const q = encodeURIComponent(album.artist + " " + album.title + " vinyl");
  const amazon = { name: "Amazon", label: "New vinyl", href: (window.BBR_buyUrl ? window.BBR_buyUrl(album.artist + " " + album.title, album.slug) : "https://www.amazon.co.uk/s?k=" + q + "&i=popular&tag=theleadin1-21") };
  const retailers = [amazon];
  const best = retailers[0];
  return { retailers, best };
}
window.bbrAffiliateFor = bbrAffiliateFor;

/* ---- StickyBuyBar, appears once the affiliate strip leaves the viewport ---- */
function StickyBuyBar({ album }) {
  const [visible, setVisible] = useXState(false);
  const [dismissed, setDismissed] = useXState(false);
  const { best } = bbrAffiliateFor(album);

  useXEffect(() => { setDismissed(false); }, [album.slug]);

  useXEffect(() => {
    const onScroll = () => {
      const strip = document.querySelector(".aff-strip");
      if (!strip) { setVisible(window.scrollY > 700); return; }
      setVisible(strip.getBoundingClientRect().bottom < 8);
    };
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener("scroll", onScroll);
  }, [album.slug]);

  if (dismissed) return null;

  return (
    <div className={"buybar" + (visible ? " visible" : "")} aria-hidden={!visible}>
      <div className="buybar-cover">{XSleeve && <XSleeve album={album} size={36} />}</div>
      <div className="buybar-info">
        <span className="buybar-title">{album.title}</span>
        <span className="buybar-sub">{album.artist} · {album.year}</span>
      </div>
      <a className="buybar-cta" href={best.href} target="_blank" rel="sponsored noopener" onClick={() => window.BBR_trackClick && window.BBR_trackClick(album.slug, "floating")}>
        Buy on vinyl
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true"><path d="M7 17L17 7M9 7h8v8" /></svg>
      </a>
      <button className="buybar-x" onClick={() => setDismissed(true)} aria-label="Dismiss buy bar">×</button>
    </div>
  );
}
window.StickyBuyBar = StickyBuyBar;

/* ---- scroll helper that accounts for sticky chrome ---- */
function bbrScrollToId(id, extra) {
  const el = document.getElementById(id);
  if (!el) return;
  // Scroll so the section's heading sits just below the fixed bars. We target
  // the heading element specifically (the section box top has padding + the
  // stacked label above the heading, so aligning the box top overshoots/
  // undershoots). Compute an absolute target and use a single scrollTo —
  // scrollIntoView + smooth was racing the scroll-spy and overshooting.
  const heading = el.querySelector("h2") || el;
  const fixedBars = (() => {
    const nav = document.querySelector(".topnav");
    const toc = document.querySelector(".mtoc");
    const navH = nav ? nav.getBoundingClientRect().height : 56;
    const tocH = (toc && getComputedStyle(toc).position === "sticky") ? toc.getBoundingClientRect().height : 0;
    return navH + tocH + 12; // small breathing gap
  })();
  const target = heading.getBoundingClientRect().top + window.scrollY - fixedBars;
  window.scrollTo({ top: Math.max(0, target), behavior: "smooth" });
}

/* ---- MobileDossierNav, sticky horizontal pill bar (< 1180px) ---- */
function MobileDossierNav({ sections }) {
  const [active, setActive] = useXState(sections[0] && sections[0].id);
  const [visible, setVisible] = useXState(false);
  const railRef = useXRef(null);

  useXEffect(() => {
    const onScroll = () => setVisible(window.scrollY > 420);
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  useXEffect(() => {
    if (!sections.length) return;
    const observer = new IntersectionObserver((entries) => {
      const hit = entries.filter(e => e.isIntersecting);
      if (!hit.length) return;
      hit.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
      setActive(hit[0].target.id);
    }, { rootMargin: "-30% 0px -55% 0px", threshold: 0 });
    sections.forEach(s => { const el = document.getElementById(s.id); if (el) observer.observe(el); });
    return () => observer.disconnect();
  }, [sections]);

  // keep the active pill scrolled into the centre of the rail
  useXEffect(() => {
    const rail = railRef.current;
    if (!rail) return;
    const el = rail.querySelector(".mdnav-pill.active");
    if (el) {
      const target = el.offsetLeft - rail.clientWidth / 2 + el.clientWidth / 2;
      rail.scrollTo({ left: Math.max(0, target), behavior: "smooth" });
    }
  }, [active]);

  if (!sections.length) return null;

  return (
    <nav className={"mdnav" + (visible ? " visible" : "")} aria-label="Dossier sections">
      <div className="mdnav-rail" ref={railRef}>
        {sections.map(s => (
          <button
            key={s.id}
            className={"chip mdnav-pill" + (active === s.id ? " active" : "")}
            onClick={() => bbrScrollToId(s.id, 120)}
          >
            <span className="mdnav-num">{s.numeral.toLowerCase()}</span>
            {s.label}
          </button>
        ))}
      </div>
    </nav>
  );
}
window.MobileDossierNav = MobileDossierNav;

/* ---- MobileTOC, collapsible contents card under the hero (< 768px) ---- */
function MobileTOC({ sections, minutes }) {
  const [open, setOpen] = useXState(false);
  const [active, setActive] = useXState(sections[0] && sections[0].id);
  const rootRef = useXRef(null);

  // Scroll-spy: track which section is currently in view so the collapsed
  // bar always shows where you are.
  useXEffect(() => {
    if (!sections.length) return;
    const observer = new IntersectionObserver((entries) => {
      const hit = entries.filter(e => e.isIntersecting);
      if (!hit.length) return;
      hit.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
      setActive(hit[0].target.id);
    }, { rootMargin: "-25% 0px -60% 0px", threshold: 0 });
    sections.forEach(s => { const el = document.getElementById(s.id); if (el) observer.observe(el); });
    return () => observer.disconnect();
  }, [sections]);

  // Close the dropdown on outside tap / Escape.
  useXEffect(() => {
    if (!open) return;
    const onDown = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    document.addEventListener("pointerdown", onDown);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("pointerdown", onDown); document.removeEventListener("keydown", onKey); };
  }, [open]);

  // All hooks above this line — only now is it safe to bail out.
  if (!sections.length) return null;

  const current = sections.find(s => s.id === active) || sections[0];

  return (
    <div className={"mtoc" + (open ? " open" : "")} ref={rootRef}>
      <button className="mtoc-toggle" onClick={() => setOpen(o => !o)} aria-expanded={open} aria-label="Jump to section">
        <span className="mtoc-eyebrow">Section</span>
        <span className="mtoc-current"><span className="mtoc-current-num">{current.numeral}</span>{current.label}</span>
        <svg className="mtoc-chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg>
      </button>
      <div className="mtoc-list">
        <div className="mtoc-list-head">{sections.length} sections · {minutes} min read</div>
        {sections.map(s => (
          <button
            key={s.id}
            className={"mtoc-item" + (s.id === active ? " active" : "")}
            onClick={() => { setOpen(false); bbrScrollToId(s.id, 120); }}
          >
            <span className="mtoc-num">{s.numeral}</span>
            <span className="mtoc-label">{s.label}</span>
          </button>
        ))}
      </div>
    </div>
  );
}
window.MobileTOC = MobileTOC;

/* ---- recommendation linking ---- */
function bbrNorm(s) { return (s || "").toLowerCase().replace(/[^a-z0-9]/g, ""); }
function resolveRecAlbum(title, all) {
  if (!all) return null;
  const namePart = String(title).split(/\s+[, –-]\s+/)[0].replace(/\s*\(\d{4}\)\s*$/, "").trim();
  const target = bbrNorm(namePart);
  if (!target) return null;
  // Exact match first; then a conservative prefix match. The second branch used
  // to repeat the exact `=== target` test, so it could never match anything the
  // first didn't — the intended fuzzy match was dead. The length guards keep
  // short/common titles from mis-resolving.
  return all.find(a => bbrNorm(a.title) === target) ||
         (target.length >= 6 && all.find(a => { const t = bbrNorm(a.title); return t.length >= 6 && t.startsWith(target); })) || null;
}

function RecLink({ rec, all, onAlbum }) {
  const match = resolveRecAlbum(rec.title, all);
  const hasEssay = match && window.ESSAYS_FULL && window.ESSAYS_FULL[match.slug];

  if (match) {
    return (
      <a
        className="rec rec-link"
        href="#"
        onClick={(e) => { e.preventDefault(); onAlbum(match); }}
      >
        <div className="rec-title">{rec.title}</div>
        <div className="rec-why" dangerouslySetInnerHTML={{ __html: rec.why }} />
        <span className="rec-action onsite">
          {hasEssay ? "Read the essay" : "See the entry"}
          <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4"><path d="M5 12h14M13 5l7 7-7 7" /></svg>
        </span>
      </a>
    );
  }

  const cleanTitle = String(rec.title).replace(/\(\d{4}\)/g, "").replace(/[, –]/g, " ").trim();
  const q = encodeURIComponent(cleanTitle + " vinyl");
  const href = (window.BBR_buyUrl) ? window.BBR_buyUrl(cleanTitle, rec.slug) : "https://www.amazon.co.uk/s?k=" + q + "&i=popular&tag=theleadin1-21";
  return (
    <a className="rec rec-link" href={href} target="_blank" rel="sponsored noopener" onClick={() => window.BBR_trackClick && window.BBR_trackClick(rec.slug, "recommendation")}>
      <div className="rec-title">{rec.title}</div>
      <div className="rec-why" dangerouslySetInnerHTML={{ __html: rec.why }} />
      <span className="rec-action external">
        Own it on vinyl
        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" aria-hidden="true"><path d="M7 17L17 7M9 7h8v8" /></svg>
      </span>
    </a>
  );
}
window.RecLink = RecLink;

/* ---- per-album accent extraction from the cover art ----
   Pulls the dominant *vibrant* hue from covers/<rank>.jpg, then rebuilds it
   at a fixed, on-brand chroma/lightness (so every accent stays cohesive and
   always clears WCAG AA against the light paper background). Falls back to the
   default vermillion if the cover is grayscale, fails to load, or is unreadable. */
function rgb2hsl(r, g, b) {
  r /= 255; g /= 255; b /= 255;
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  let h = 0, s = 0; const l = (max + min) / 2;
  const d = max - min;
  if (d !== 0) {
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    if (max === r) h = ((g - b) / d + (g < b ? 6 : 0));
    else if (max === g) h = (b - r) / d + 2;
    else h = (r - g) / d + 4;
    h *= 60;
  }
  return [h, s, l];
}

function extractCoverHue(slug, cb) {
  const img = new Image();
  img.decoding = "async";
  let done = false;
  const finish = (v) => { if (!done) { done = true; cb(v); } };
  img.onload = () => {
    try {
      const W = 48, H = 48;
      const c = document.createElement("canvas");
      c.width = W; c.height = H;
      const ctx = c.getContext("2d", { willReadFrequently: true });
      ctx.drawImage(img, 0, 0, W, H);
      const data = ctx.getImageData(0, 0, W, H).data;
      // accumulate hue in 24 buckets, weighted by saturation * mid-lightness emphasis
      const buckets = new Array(24).fill(0);
      let vibrantWeight = 0;
      for (let i = 0; i < data.length; i += 4) {
        const a = data[i + 3];
        if (a < 200) continue;
        const [h, s, l] = rgb2hsl(data[i], data[i + 1], data[i + 2]);
        if (l < 0.12 || l > 0.92 || s < 0.18) continue;       // skip near-black/white/gray
        const w = s * (1 - Math.abs(l - 0.5));                 // favour saturated mid tones
        buckets[Math.floor(h / 15) % 24] += w;
        vibrantWeight += w;
      }
      if (vibrantWeight < 6) return finish(null);              // mostly grayscale → fallback
      let bi = 0;
      for (let i = 1; i < buckets.length; i++) if (buckets[i] > buckets[bi]) bi = i;
      // weighted hue centre across the winning bucket and its neighbours
      let num = 0, den = 0;
      for (let d = -1; d <= 1; d++) {
        const idx = (bi + d + 24) % 24;
        const hueCentre = idx * 15 + 7.5;
        num += hueCentre * buckets[idx];
        den += buckets[idx];
      }
      const hue = den ? Math.round(num / den) : bi * 15 + 7.5;
      finish(hue);
    } catch (e) { finish(null); }
  };
  img.onerror = () => finish(null);
  img.src = "/covers/" + slug + ".jpg";
  // safety timeout
  setTimeout(() => finish(null), 4000);
}

function AlbumAccent({ album }) {
  const ref = useXRef(null);
  useXEffect(() => {
    const root = ref.current && ref.current.closest(".album-page");
    if (!root) return;
    let cancelled = false;
    root.style.removeProperty("--accent");        // start from default vermillion
    extractCoverHue(album.slug, (hue) => {
      if (cancelled || hue == null) return;        // fallback: keep default
      // Fixed on-brand chroma/lightness; only the hue varies per album.
      // L 0.55 / C 0.13 clears AA contrast on the light paper for text use.
      root.style.setProperty("--accent", `oklch(0.55 0.13 ${hue})`);
    });
    return () => { cancelled = true; if (root) root.style.removeProperty("--accent"); };
  }, [album.slug]);
  return <span ref={ref} aria-hidden="true" style={{ display: "none" }} />;
}
window.AlbumAccent = AlbumAccent;

/* ---- six-dimension scoring card (mirrors the homepage worked example) ----
   No per-dimension data exists in the dataset, so #1 (What's Going On) uses the
   authored breakdown from the homepage (composite 9.90) and every other album
   derives a deterministic, on-brand spread around its rank-based composite
   (the same 9.9 − (rank−1)×0.07 scheme the homepage top-ten uses). */
function bbrHashStr(s) { let x = 0; for (let i = 0; i < s.length; i++) x = (x * 131 + s.charCodeAt(i)) >>> 0; return x; }
const BBR_SCORE_DIMS = [
  ["songwriting", "Songwriting"], ["production", "Production"], ["performance", "Performance"],
  ["innovation", "Innovation"], ["influence", "Influence"], ["cohesion", "Cohesion"],
];
function bbrScoreFor(album) {
  if (album.slug === "whats-going-on") {
    return {
      composite: 9.9,
      dims: [
        { key: "songwriting", label: "Songwriting", value: 9.7 },
        { key: "production", label: "Production", value: 9.9 },
        { key: "performance", label: "Performance", value: 9.6 },
        { key: "innovation", label: "Innovation", value: 9.8 },
        { key: "influence", label: "Influence", value: 10.0 },
        { key: "cohesion", label: "Cohesion", value: 9.9 },
      ],
    };
  }
  const composite = Math.max(2.6, Math.round((9.9 - (album.rank - 1) * 0.07) * 100) / 100);
  const dims = BBR_SCORE_DIMS.map(([key, label]) => {
    const r = bbrHashStr(album.slug + ":" + key);
    const off = ((r % 1000) / 1000 - 0.44) * 1.8; // ≈ −0.79 … +1.0, distinctive per album/dim
    let v = Math.max(1.2, Math.min(10, composite + off));
    return { key, label, value: Math.round(v * 10) / 10 };
  });
  return { composite, dims };
}
window.bbrScoreFor = bbrScoreFor;

function ScoreBreakdown({ album, onHowToScore }) {
  const { composite, dims } = bbrScoreFor(album);
  return (
    <div className="scorebreak" aria-label="Scoring breakdown">
      <div className="sb-head">
        <span className="sb-eyebrow">The scorecard</span>
        {onHowToScore && <button className="sb-how" onClick={onHowToScore}>How we score →</button>}
      </div>
      <div className="sb-rows">
        {dims.map((d) => (
          <div className="sb-row" key={d.key}>
            <span className="sb-dim">{d.label}</span>
            <span className="sb-meter" aria-hidden="true"><span className="sb-fill" style={{ width: (d.value * 10) + "%" }} /></span>
            <span className="sb-score">{d.value.toFixed(1)}</span>
          </div>
        ))}
      </div>
      <div className="sb-composite">
        <span className="sb-comp-num">{composite.toFixed(2)}</span>
        <span className="sb-comp-side">
          <span className="sb-comp-lbl">Composite</span>
          <span className="sb-comp-note">Songwriting &amp; production weighted ×1.25 · recalculated quarterly</span>
        </span>
      </div>
    </div>
  );
}
window.ScoreBreakdown = ScoreBreakdown;
