// ===========================================================
// CollectionDNA: a taste-fingerprint panel at the top of the
// collection page. Two parts:
//   (a) deterministic "fingerprint" stats computed live from the
//       owner's own collection (era skew + genre gravity), and
//   (b) a one-paragraph personality read from Claude Haiku, shown
//       as the headline at 50+ records.
//
// Three unlock states, gated on owned-record count:
//   < 20  locked teaser ("add N more to reveal your fingerprint")
//   20-49 raw fingerprint only (no personality read)
//   50+   full DNA: the Haiku read as the hero, fingerprint beneath
//
// Geography and obscurity dimensions were cut for v1: collection_items
// carries no country column (country is only derivable for the ~21% of
// rows that match the Hundred, far below a trustworthy threshold), and
// no honest non-canon obscurity signal is stored. Canon position is a
// SILENT input to the Haiku read only and is never shown as a number.
//
// The personality read is generated ONCE and cached on the owner's
// profile row (store.saveDnaRead), regenerated only when the count
// moves by >= DNA_REGEN_DELTA or when no read exists yet. If the route
// fails, a deterministic templated sentence is shown so the panel never
// renders an error or an empty headline. The Anthropic key stays
// server-side: this only POSTs the computed dimensions to /api/identify.
// ===========================================================
const { useState: useDnaState, useEffect: useDnaEffect, useMemo: useDnaMemo, useRef: useDnaRef } = React;

// Thresholds live here once. No scattered magic numbers.
const DNA_FINGERPRINT_MIN = 20;   // raw fingerprint unlocks here
const DNA_FULL_MIN = 50;          // personality read unlocks here
const DNA_REGEN_DELTA = 10;       // regenerate the read after this many net new records
const DNA_COVERAGE_MIN = 0.6;     // a dimension below this field-coverage is omitted, not faked

// Discogs primary genres arrive raw: stray newlines and casing collisions
// ("Hip-Hop" vs "Hip Hop", "Punk / \n Indie"). Collapse whitespace so the
// same genre groups together; keep the cleaned label for display.
function dnaCleanGenre(g) {
  return String(g || "").replace(/\s+/g, " ").trim();
}

function dnaDecadeLabel(decade) {
  return decade + "s";
}

// Compute the deterministic dimensions from the owner's resolved collection.
// `items` are resolveV2 outputs: each has .year, .genre, .slug and .album
// (the canon row, with .rank, when slug-matched). Returns null dimensions for
// anything below the coverage floor rather than rendering a misleading stat.
function computeDna(items) {
  const count = items.length;
  const thisYear = new Date().getFullYear();

  // ---- Era skew (from year) ----
  let era = null;
  const years = items
    .map((r) => parseInt(r.year, 10))
    .filter((y) => Number.isFinite(y) && y >= 1900 && y <= thisYear + 1);
  if (count > 0 && years.length / count >= DNA_COVERAGE_MIN) {
    const byDecade = {};
    years.forEach((y) => {
      const d = Math.floor(y / 10) * 10;
      byDecade[d] = (byDecade[d] || 0) + 1;
    });
    const decadeEntries = Object.keys(byDecade)
      .map((d) => [parseInt(d, 10), byDecade[d]])
      .sort((a, b) => b[1] - a[1] || b[0] - a[0]);
    const sorted = years.slice().sort((a, b) => a - b);
    const median = sorted[Math.floor(sorted.length / 2)];
    era = {
      centreDecade: decadeEntries[0][0],
      centreYear: median,
      spreadDecades: decadeEntries.length,
      coverage: years.length / count,
      // [["1970s", 22], ...] for both the brief payload and the visible bars.
      topDecades: decadeEntries.map(([d, n]) => [dnaDecadeLabel(d), n]),
    };
  }

  // ---- Genre gravity (from genre) ----
  let genre = null;
  const genreVals = items
    .map((r) => dnaCleanGenre(r.genre))
    .filter((g) => g && g.toLowerCase() !== "other");
  if (count > 0 && genreVals.length / count >= DNA_COVERAGE_MIN) {
    const byGenre = {};
    genreVals.forEach((g) => {
      const key = g.toLowerCase();
      if (!byGenre[key]) byGenre[key] = { label: g, n: 0 };
      byGenre[key].n += 1;
    });
    const entries = Object.keys(byGenre)
      .map((k) => byGenre[k])
      .sort((a, b) => b.n - a.n);
    // Share of the whole collection (honest denominator), rounded for display.
    const top = entries.map((e) => [e.label, e.n / count]);
    const topShare = top.length ? top[0][1] : 0;
    const top3Share = top.slice(0, 3).reduce((s, x) => s + x[1], 0);
    let concentration = "balanced";
    if (topShare >= 0.5) concentration = "focused";
    else if (entries.length >= 6 && top3Share < 0.6) concentration = "wide";
    genre = {
      top: top,
      distinct: entries.length,
      concentration: concentration,
      coverage: genreVals.length / count,
    };
  }

  // ---- Canon posture (SILENT — Haiku context only, never displayed) ----
  const canonItems = items.filter((r) => r.slug);
  const ownedOfHundred = new Set(canonItems.map((r) => r.slug)).size;
  const canonSharePct = count > 0 ? Math.round((canonItems.length / count) * 100) : 0;
  const ranks = canonItems
    .map((r) => r.album && r.album.rank)
    .filter((n) => typeof n === "number" && n > 0);
  const avgRank = ranks.length ? Math.round(ranks.reduce((s, n) => s + n, 0) / ranks.length) : null;
  let lean = "a mix of canon and deeper cuts";
  if (canonSharePct >= 40) lean = "canon forward, toward the safe and celebrated";
  else if (canonSharePct <= 12) lean = "toward deeper cuts, away from the obvious canon";
  const canon = { ownedOfHundred, canonSharePct, avgRank, lean };

  return { count, era, genre, canon };
}

// Deterministic fallback paragraph, assembled from whatever dimensions exist.
// Never empty, never an error, no em dashes, no canon numbers.
function dnaFallbackRead(dims) {
  const bits = [];
  if (dims.genre && dims.genre.top.length) {
    const lead = dims.genre.top[0][0];
    const shape = dims.genre.concentration === "focused"
      ? "a focused collection that knows what it likes"
      : dims.genre.concentration === "wide"
      ? "a wide ranging collection that refuses to sit still"
      : "a balanced collection with a clear centre";
    bits.push("This is " + shape + ", built around " + lead + ".");
  } else {
    bits.push("This is a collection still finding its centre of gravity.");
  }
  if (dims.era) {
    const spread = dims.era.spreadDecades >= 4
      ? "ranges across " + dims.era.spreadDecades + " decades"
      : "stays close to home in time";
    bits.push("It is centred on the " + dnaDecadeLabel(dims.era.centreDecade) + " and " + spread + ".");
  }
  return bits.join(" ");
}

// Small horizontal bar for a distribution row (top decades / top genres).
function DnaBar({ label, value, max, suffix }) {
  const pct = max > 0 ? Math.round((value / max) * 100) : 0;
  return (
    <div className="dna-bar-row">
      <span className="dna-bar-label">{label}</span>
      <span className="dna-bar-track"><span className="dna-bar-fill" style={{ width: pct + "%" }} /></span>
      <span className="dna-bar-n">{value}{suffix || ""}</span>
    </div>
  );
}

function CollectionDNA({ items }) {
  const store = window.BBR_store;
  const count = items.length;
  const dims = useDnaMemo(() => computeDna(items), [items]);

  // Freshly generated / fallback read for this session. The cached read is read
  // straight off the store each render (so a late profile load is picked up).
  const [generated, setGenerated] = useDnaState(null);
  const [loading, setLoading] = useDnaState(false);
  const inflight = useDnaRef(false);

  useDnaEffect(() => {
    if (count < DNA_FULL_MIN) return;             // read only exists at 50+
    const cached = store.dnaRead;
    const cachedAt = store.dnaReadCount;
    const needsRegen = !cached || cachedAt == null || Math.abs(count - cachedAt) >= DNA_REGEN_DELTA;
    if (!needsRegen) return;                       // stable: render the cached read, no call
    if (inflight.current) return;                  // one request in flight at a time
    inflight.current = true;
    setLoading(true);
    let cancelled = false;
    fetch("/api/identify", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ kind: "dna", dims: dims }),
    })
      .then((r) => (r.ok ? r.json() : Promise.reject(new Error("dna " + r.status))))
      .then((j) => {
        if (cancelled) return;
        if (j && typeof j.read === "string" && j.read.trim()) {
          setGenerated(j.read.trim());
          store.saveDnaRead(j.read.trim(), count);  // cache once
        } else {
          setGenerated(dnaFallbackRead(dims));      // junk reply -> template
        }
      })
      .catch(() => { if (!cancelled) setGenerated(dnaFallbackRead(dims)); })
      .finally(() => { inflight.current = false; if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [count, dims]);

  // ---- State 1: locked teaser (< 20) ----
  if (count < DNA_FINGERPRINT_MIN) {
    const togo = DNA_FINGERPRINT_MIN - count;
    return (
      <div className="dna dna-locked">
        <div className="dna-lock-mark" aria-hidden="true">
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
            <rect x="5" y="11" width="14" height="9" rx="2" /><path d="M8 11V8a4 4 0 0 1 8 0v3" />
          </svg>
        </div>
        <div className="dna-eyebrow">Collection DNA</div>
        <div className="dna-lock-head">Your taste fingerprint is still developing</div>
        <p className="dna-lock-sub">
          Add {togo} more {togo === 1 ? "record" : "records"} to reveal your fingerprint. We read the eras
          and genres you actually own, then call it like we see it.
        </p>
      </div>
    );
  }

  const hasFingerprint = dims.era || dims.genre;
  const cachedRead = store.dnaRead;
  const read = generated || cachedRead;
  const showRead = count >= DNA_FULL_MIN;

  return (
    <div className="dna">
      <div className="dna-eyebrow">Collection DNA</div>

      {/* Hero personality read (50+) */}
      {showRead && (
        <div className="dna-read" aria-busy={loading && !read ? "true" : "false"}>
          {read
            ? <p className="dna-read-body">{read}</p>
            : <p className="dna-read-body dna-read-loading">Reading your collection…</p>}
        </div>
      )}

      {/* Unlock nudge for 20-49 */}
      {count < DNA_FULL_MIN && (
        <div className="dna-nudge">
          Your full Collection DNA unlocks at {DNA_FULL_MIN} records, {DNA_FULL_MIN - count} to go.
        </div>
      )}

      {/* Deterministic fingerprint dimensions */}
      {hasFingerprint && (
        <div className="dna-dims">
          {dims.era && (
            <div className="dna-dim">
              <div className="dna-dim-k">Era</div>
              <div className="dna-dim-v">{dnaDecadeLabel(dims.era.centreDecade)} heavy</div>
              <div className="dna-dim-note">
                {dims.era.spreadDecades >= 4
                  ? "Spread across " + dims.era.spreadDecades + " decades"
                  : "Centred in " + dims.era.spreadDecades + (dims.era.spreadDecades === 1 ? " decade" : " decades")}
              </div>
              <div className="dna-bars">
                {dims.era.topDecades.slice(0, 4).map(([label, n]) => (
                  <DnaBar key={label} label={label} value={n} max={dims.era.topDecades[0][1]} />
                ))}
              </div>
            </div>
          )}
          {dims.genre && (
            <div className="dna-dim">
              <div className="dna-dim-k">Genre</div>
              <div className="dna-dim-v">{dims.genre.top[0][0]}</div>
              <div className="dna-dim-note">
                {dims.genre.concentration === "focused"
                  ? "Focused"
                  : dims.genre.concentration === "wide"
                  ? "Wide ranging, " + dims.genre.distinct + " genres"
                  : "Balanced, " + dims.genre.distinct + " genres"}
              </div>
              <div className="dna-bars">
                {dims.genre.top.slice(0, 4).map(([label, share]) => (
                  <DnaBar key={label} label={label} value={Math.round(share * 100)} max={Math.round(dims.genre.top[0][1] * 100)} suffix="%" />
                ))}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// Expose the component plus the pure helpers (handy for headless QA).
Object.assign(window, { CollectionDNA, computeDna, dnaFallbackRead });
