// ===========================================================
// NextRecord (Part 9): one committed pick from The Hundred the
// owner does not yet own, scored against their Collection DNA,
// with a two-sentence editorial "why" from Haiku. Not a
// recommender widget: one call, no reroll.
//
// Unlocks at 10 records (ahead of the DNA fingerprint, which is 20).
// Generation is USER-DRIVEN and presented as a TAP TO REVEAL: past
// the gate we show a face-down record you tap to unwrap. Nothing is
// minted until that tap; the tap runs one Haiku call (loading on the
// card) and then flips to reveal the pick. The pick + why are cached
// on the profile (store.saveNextPick). Revealing is session state:
// the concealed card is the resting state, so a cached pick shows the
// face-down card again on reload and reveals instantly on tap (no new
// call). Once the owner adds the pick, the card offers the next one;
// if the collection has since grown by 10+, the reveal offers a "pick
// again" refresh that re-conceals then reveals the new pick.
//
// SCORING (per the brief; formula reported in the commit):
//   score = 0.55*affinity + 0.45*stretch + 0.05*rankBonus
//   affinity = 0.6*genreShare(c) + 0.4*decadeShare(c)   (feels like them)
//   stretch  = 0.5*genreStretch + 0.5*decadeStretch     (one step further)
//     genreStretch  = present-but-not-dominant genre (0<share<top ? share/top : 0)
//     decadeStretch = 1 if a decade adjacent to their centre, 0.5 two away, else 0
//   rankBonus = (101 - canon rank)/100  (a light nudge toward the more essential)
//
// The why is grounded ONLY in real data: the album's real facts
// (title/artist/year/genre + the app's own editorial blurb) and
// the qualitative shape of the listener's taste. On a Haiku
// failure the last cached pick is kept; a pick is NEVER shown with
// a fabricated or empty why.
//
// Globals in: React, Sleeve, window.BBR_store, window.computeDna,
// window.BLURBS. Exposes window.NextRecord, window.computeNextPick.
// ===========================================================
const { useState: useNrState, useEffect: useNrEffect, useMemo: useNrMemo, useRef: useNrRef } = React;

const NR_MIN = 10;         // unlocks early, ahead of the DNA fingerprint (20)
const NR_REGEN_DELTA = 10; // same regeneration cadence as the DNA read

function nrTokens(s) {
  return String(s || "").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
}

// Pure scorer, exported for headless tests. `items` are resolveV2 outputs;
// `albums` is BBR_ALBUMS. Returns the best not-yet-owned canon album, or null.
function computeNextPick(items, albums) {
  const owned = new Set((items || []).filter((r) => r.slug).map((r) => r.slug));
  const candidates = (albums || []).filter((a) => a && a.slug && !owned.has(a.slug));
  if (!candidates.length) return null;

  const total = items.length || 1;
  // user genre shares (with token sets for lenient canon-vs-Discogs matching)
  const gCounts = {};
  items.forEach((r) => {
    const g = (r.genre || "").trim();
    if (g && g.toLowerCase() !== "other") gCounts[g] = (gCounts[g] || 0) + 1;
  });
  const gEntries = Object.keys(gCounts).map((g) => ({ share: gCounts[g] / total, toks: nrTokens(g) }));
  const topGenreShare = gEntries.reduce((m, e) => Math.max(m, e.share), 0) || 1;

  // user decade shares + centre decade
  const dCounts = {}; let dTotal = 0;
  items.forEach((r) => {
    const y = parseInt(r.year, 10);
    if (Number.isFinite(y) && y >= 1900) { const d = Math.floor(y / 10) * 10; dCounts[d] = (dCounts[d] || 0) + 1; dTotal++; }
  });
  let centreDecade = null, best = -1;
  Object.keys(dCounts).forEach((d) => { if (dCounts[d] > best) { best = dCounts[d]; centreDecade = parseInt(d, 10); } });
  const decadeShare = (d) => (dTotal ? (dCounts[d] || 0) / dTotal : 0);

  function score(c) {
    const cToks = nrTokens(c.genre);
    let gShare = 0;
    gEntries.forEach((e) => { if (e.toks.some((t) => cToks.indexOf(t) !== -1)) gShare += e.share; });
    gShare = Math.min(1, gShare);
    const cd = Math.floor((parseInt(c.year, 10) || 0) / 10) * 10;
    const dShare = decadeShare(cd);
    const affinity = 0.6 * gShare + 0.4 * dShare;
    const genreStretch = (gShare > 0 && gShare < topGenreShare) ? gShare / topGenreShare : 0;
    const dd = centreDecade != null ? Math.abs(cd - centreDecade) : 999;
    const decadeStretch = dd === 10 ? 1 : (dd === 20 ? 0.5 : 0);
    const stretch = 0.5 * genreStretch + 0.5 * decadeStretch;
    const rankBonus = (101 - (c.rank || 100)) / 100;
    return 0.55 * affinity + 0.45 * stretch + 0.05 * rankBonus;
  }

  let pick = null, pickScore = -Infinity;
  candidates.forEach((c) => { const s = score(c); if (s > pickScore) { pickScore = s; pick = c; } });
  return pick;
}

// small AI chip shown beside the eyebrow so the feature always reads as
// AI-generated, never a static "editor's pick".
function NrHead() {
  return (
    <div className="nr-head">
      <span className="nr-eyebrow">Your Next Record</span>
      <span className="nr-ai"><span className="nr-spark" aria-hidden="true">✦</span>AI pick</span>
    </div>
  );
}

function NextRecord({ items, albums }) {
  const store = window.BBR_store;
  const count = items.length;
  const albumsBySlug = useNrMemo(() => {
    const m = {}; (albums || []).forEach((a) => { if (a && a.slug) m[a.slug] = a; }); return m;
  }, [albums]);
  const ownedCanon = useNrMemo(() => new Set(items.filter((r) => r.slug).map((r) => r.slug)), [items]);
  const candidateCount = useNrMemo(() => (albums || []).filter((a) => a && a.slug && !ownedCanon.has(a.slug)).length, [albums, ownedCanon]);

  const [loading, setLoading] = useNrState(false);
  const [ackShow, setAckShow] = useNrState(false); // "You added it. Here is your next one."
  const [wished, setWished] = useNrState(false);
  const [error, setError] = useNrState(false);
  const [revealed, setRevealed] = useNrState(false); // tap-to-reveal: false = face-down card
  const inflight = useNrRef(false);

  // Generation is now user-driven: nothing is minted until the owner clicks the
  // CTA. `wasAdded` lets a fresh reveal congratulate them for adding the last pick.
  function generate(wasAdded) {
    if (inflight.current || count < NR_MIN || candidateCount === 0) return;
    const chosen = computeNextPick(items, albums);
    if (!chosen) return;
    inflight.current = true;
    setLoading(true);
    setError(false);
    const dims = window.computeDna ? window.computeDna(items) : null;
    const facts = (window.BLURBS && chosen.rank && window.BLURBS[chosen.rank]) || null;
    fetch("/api/identify", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        kind: "next-pick",
        pick: { title: chosen.title, artist: chosen.artist, year: chosen.year, genre: chosen.genre, rank: chosen.rank, facts: facts },
        dims: dims,
      }),
    })
      .then((r) => (r.ok ? r.json() : Promise.reject(new Error("pick " + r.status))))
      .then((j) => {
        if (j && typeof j.why === "string" && j.why.trim()) {
          setAckShow(!!wasAdded);
          setWished(false);
          store.saveNextPick(chosen.slug, j.why.trim(), count); // caches + notifies
          setRevealed(true); // the tap that generated it also reveals it (one gesture)
        } else {
          throw new Error("empty why");
        }
      })
      .catch(() => { setError(true); })
      .finally(() => { inflight.current = false; setLoading(false); });
  }

  // ---- render ----
  if (count < NR_MIN) {
    const togo = NR_MIN - count;
    return (
      <div className="nr nr-locked">
        <NrHead />
        <div className="nr-lock">Your Next Record unlocks at {NR_MIN} records, {togo} to go.</div>
      </div>
    );
  }

  if (candidateCount === 0) {
    return (
      <div className="nr">
        <NrHead />
        <div className="nr-complete">
          <div className="nr-complete-h">You own all of The Hundred.</div>
          <div className="nr-complete-s">The canon is yours, cover to cover. There is nothing left for us to send you here.</div>
        </div>
      </div>
    );
  }

  const slug = store.nextPickSlug;
  const why = store.nextPickWhy;
  const album = slug ? albumsBySlug[slug] : null;
  const ready = album && why && !ownedCanon.has(slug);
  const pickAdded = slug && ownedCanon.has(slug); // they added the last pick → offer the next
  const grown = ready && (store.nextPickCount == null || Math.abs(count - store.nextPickCount) >= NR_REGEN_DELTA);

  // Tap to reveal. The face-down card is the resting state whenever there is no
  // revealed pick: first ever, after adding the last one, or a fresh reload of a
  // cached pick. Tapping reveals, generating first only when there is nothing
  // cached yet (so a tap never wastes a Haiku call on an already-picked record).
  const showAlbum = ready && revealed;
  function onReveal() {
    if (loading) return;
    if (ready) { setRevealed(true); return; }   // data already cached: flip, no call
    generate(pickAdded);                          // nothing cached: mint, then auto-reveal
  }
  if (!showAlbum) {
    return (
      <div className="nr">
        <NrHead />
        {pickAdded && <div className="nr-ack">Nice, that one's in your collection now.</div>}
        <button type="button" className="nr-reveal" onClick={onReveal} disabled={loading}
                aria-label={ready ? "Reveal your next record" : "Reveal your next record (reads your collection first)"}>
          <div className="nr-reveal-sleeve" aria-hidden="true">
            <span className="nr-reveal-q">{loading ? "" : "?"}</span>
            {loading && <span className="nr-reveal-spin" />}
          </div>
          <div className="nr-reveal-copy">
            <div className="nr-reveal-h">{loading ? "Reading your collection…" : (ready ? "Your next record is ready" : "Your next record is waiting")}</div>
            <p className="nr-reveal-s">
              Our AI reads the shape of your collection, the genres, the eras, the artists you keep coming
              back to, and picks the one record from The Hundred you should own next.
            </p>
            <span className="nr-reveal-cta"><span className="nr-spark" aria-hidden="true">✦</span>{loading ? "One moment" : "Tap to reveal"}</span>
          </div>
        </button>
        {error && <div className="nr-error">That didn't go through. Give it another tap.</div>}
      </div>
    );
  }

  const alreadyWished = wished || (store.wishedSlug && store.wishedSlug(slug));
  function addWish() {
    if (alreadyWished) return;
    store.addToWishlist({ slug: album.slug, condition: "NM" });
    setWished(true);
  }

  return (
    <div className="nr">
      <NrHead />
      {ackShow && <div className="nr-ack">You added it. Here is your next one.</div>}
      <div className="nr-main nr-revealed">
        <div className="nr-cover">{window.Sleeve && <Sleeve album={album} size={92} />}</div>
        <div className="nr-info">
          <div className="nr-title">{album.title}</div>
          <div className="nr-artist">{album.artist}{album.year ? " · " + album.year : ""}</div>
          <p className="nr-why">{why}</p>
          <div className="nr-actions">
            <button className="nr-btn solid" onClick={addWish} disabled={alreadyWished}>
              {alreadyWished ? "On your wishlist" : "Add to wishlist"}
            </button>
            <a className="nr-btn ghost" href={"/album/" + album.slug}>Read the essay</a>
          </div>
          <div className="nr-foot"><span className="nr-spark" aria-hidden="true">✦</span>Chosen by AI from the shape of your collection.</div>
          {grown && (
            <button className="nr-refresh" onClick={() => { setRevealed(false); generate(false); }} disabled={loading}>
              {loading ? "Rereading…" : "Your collection's grown, pick again"}
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { NextRecord, computeNextPick });
