/* ===========================================================
   DossierValues — live market values for the pressing table on an album or
   deep-dive page, with a DISCREET fallback.

   WHY
   Until now a dossier showed hand-written editorial figures while the collection
   pages showed live, condition-matched values from lib/valuation.js. The same
   record could therefore carry two different numbers depending on which page a
   reader was on, and the deep dives were worse again: several pressings shipped
   `values: {}` and rendered an empty strip.

   This module puts the dossier on the collection's valuation path. It calls
   /api/discogs-price?action=dossier-values&slug=… which resolves each pressing
   row to its own Discogs release (by catalogue number) and prices all three
   displayed grades through the same valuate() the collection uses.

   THE FALLBACK LADDER — the important part
     1. a live value for this pressing at this grade   -> show it, marked live
     2. the editorial figure for this pressing         -> show it, unmarked
     3. neither                                        -> show nothing at all

   Step 3 is deliberate and is what "discreet" means here. A missing value must
   never render as "N/A", "£0", an error, or a spinner that never resolves. The
   reader simply sees the grades we can stand behind. Nothing on the page moves
   or reflows when the fetch fails, because the editorial figures are rendered
   first and only replaced on success.
   =========================================================== */

/* One in-flight request per slug per page load, plus a short sessionStorage
   cache so moving between pressings (or back to a page) does not refetch. The
   endpoint is CDN-cached for a week; this just avoids duplicate calls in-tab. */
const _dvCache = new Map();      // slug -> resolved payload
const _dvInflight = new Map();   // slug -> Promise
const DV_TTL_MS = 60 * 60 * 1000;

function dvSessionGet(slug) {
  try {
    const raw = sessionStorage.getItem("bbr:dv:" + slug);
    if (!raw) return null;
    const j = JSON.parse(raw);
    if (!j || !j.t || Date.now() - j.t > DV_TTL_MS) return null;
    return j.d;
  } catch { return null; }
}
function dvSessionSet(slug, data) {
  try { sessionStorage.setItem("bbr:dv:" + slug, JSON.stringify({ t: Date.now(), d: data })); }
  catch { /* private mode / quota — the in-memory cache still helps */ }
}

function fetchDossierValues(slug) {
  if (_dvCache.has(slug)) return Promise.resolve(_dvCache.get(slug));
  const cached = dvSessionGet(slug);
  if (cached) { _dvCache.set(slug, cached); return Promise.resolve(cached); }
  if (_dvInflight.has(slug)) return _dvInflight.get(slug);

  const p = fetch("/api/discogs-price?action=dossier-values&slug=" + encodeURIComponent(slug))
    .then((r) => (r.ok ? r.json() : null))
    .then((j) => {
      // Index by pressing id so the renderer does not care about array order.
      const byId = {};
      if (j && Array.isArray(j.pressings)) {
        for (const row of j.pressings) if (row && row.id) byId[row.id] = row;
      }
      const out = { ok: !!(j && j.priced), byId, meta: j || null };
      _dvCache.set(slug, out);
      dvSessionSet(slug, out);
      return out;
    })
    .catch(() => {
      // A failed fetch is indistinguishable from "no data" by design: the
      // editorial figures stay on screen and nothing is said about it.
      const out = { ok: false, byId: {}, meta: null };
      _dvCache.set(slug, out);
      return out;
    })
    .finally(() => _dvInflight.delete(slug));

  _dvInflight.set(slug, p);
  return p;
}

/* Hook: returns { live, ok } where live is { [pressingId]: row }.
   Starts empty, so the first paint always shows the editorial figures. */
function useDossierValues(slug) {
  const [state, setState] = React.useState(() => {
    const seeded = slug && _dvCache.get(slug);
    return seeded || { ok: false, byId: {}, meta: null };
  });

  React.useEffect(() => {
    if (!slug) return;
    let alive = true;
    fetchDossierValues(slug).then((d) => { if (alive) setState(d); });
    return () => { alive = false; };
  }, [slug]);

  return state;
}

/* Editorial figures are a mix of numbers (420) and prose
   ("£280–£420 (sealed mono: £1,800+)"). Render either faithfully: a number gets
   a £ and thousands separators, a string is already formatted copy. */
function formatEditorial(v) {
  if (v == null || v === "") return null;
  if (typeof v === "number") return isFinite(v) ? "£" + v.toLocaleString("en-GB") : null;
  const s = String(v).trim();
  return s || null;
}
function formatLive(n) {
  if (typeof n !== "number" || !isFinite(n)) return null;
  // Market values are shown to the pound; pennies imply a precision we do not have.
  return "£" + Math.round(n).toLocaleString("en-GB");
}

const GRADE_ORDER = ["mint", "vgplus", "vg"];
const GRADE_LABEL = { mint: "Mint", vgplus: "VG+", vg: "VG", sealed: "Sealed" };

/* PressingValues — the three-grade strip for ONE pressing row.
   `pressing` is the editorial row; `live` is the endpoint row for it (or null). */
function PressingValues({ pressing, live }) {
  const editorial = (pressing && pressing.values) || {};
  const liveVals = (live && live.values) || {};

  const cells = [];
  for (const g of GRADE_ORDER) {
    const l = liveVals[g] && liveVals[g].value != null ? formatLive(liveVals[g].value) : null;
    const e = formatEditorial(editorial[g]);
    // Ladder: live, else editorial, else omit the cell entirely.
    if (!l && !e) continue;
    cells.push({ grade: g, text: l || e, isLive: !!l });
  }
  // Sealed is editorial-only (Discogs has no sealed grade) and optional.
  const sealed = formatEditorial(editorial.sealed);
  if (sealed) cells.push({ grade: "sealed", text: sealed, isLive: false });

  if (!cells.length) return null;   // discreet: no values, no strip, no message

  const anyLive = cells.some((c) => c.isLive);

  return (
    <div className="ld-values">
      {cells.map((c) => (
        <div className={"ldv" + (c.isLive ? " ldv-live" : "")} key={c.grade}>
          <span className="ldv-k">{GRADE_LABEL[c.grade] || c.grade.toUpperCase()}</span>
          <span className="ldv-v">{c.text}</span>
        </div>
      ))}
      {anyLive && <ValueProvenance live={live} />}
    </div>
  );
}

/* One quiet line saying where a live number came from. Only rendered when at
   least one cell IS live — we never advertise the fallback. */
function ValueProvenance({ live }) {
  if (!live) return null;
  const conf = live.confidence || null;
  const exact = live.granularity === "pressing";
  const label = exact
    ? "Live market value for this pressing"
    : "Live market value for this album";
  return (
    <p className="ldv-provenance">
      <span className={"ldv-dot conf-" + (conf || "low")} aria-hidden="true" />
      {label}
      {conf ? " · " + conf + " confidence" : ""}
    </p>
  );
}

/* ValuesStrip — the headline Mint / VG+ / VG strip in the Collector's corner.
   Uses the best available live figure across pressings (the highest-confidence
   pressing-level hit) and otherwise the editorial copy, which on the deep dives
   is prose rather than a number and must render as written. */
function ValuesStrip({ collector, live }) {
  const editorial = (collector && collector.values) || {};
  const pressings = (collector && collector.pressings) || [];

  const cells = GRADE_ORDER.map((g) => {
    // Prefer the live value of the pressing the editorial calls the grail —
    // the first row — falling back to any priced row.
    let liveText = null, conf = null;
    for (const p of pressings) {
      const row = live && live.byId ? live.byId[p.id] : null;
      const cell = row && row.values && row.values[g];
      if (cell && cell.value != null) { liveText = formatLive(cell.value); conf = cell.confidence; break; }
    }
    const e = formatEditorial(editorial[g]);
    if (!liveText && !e) return null;
    return { grade: g, text: liveText || e, isLive: !!liveText, conf };
  }).filter(Boolean);

  if (!cells.length) return null;

  return (
    <div className="values-strip">
      {cells.map((c) => (
        <div key={c.grade} className={c.isLive ? "vs-live" : undefined}>
          <b>{GRADE_LABEL[c.grade]}</b>
          <span>{c.text}</span>
        </div>
      ))}
    </div>
  );
}

Object.assign(window, {
  useDossierValues,
  fetchDossierValues,
  PressingValues,
  ValuesStrip,
  formatEditorialValue: formatEditorial,
});
