// ===========================================================
// AppraisalExport: the signed-in collector's "Collection Appraisal"
// — a dated, print-ready valuation document for an insurer, a
// probate/estate file, or a claim. This is the one artefact the app
// showed everywhere (value-over-time, weekly movers) but never let
// you take OUT of the app as a formal document.
//
// Architecture (deliberately "packaging, not engineering"):
//   • No serverless function, no dependency, no new valuation. The
//     numbers are the SAME est_value / granularity / confidence the
//     app already shows — we just format and date them.
//   • The overlay chrome reuses the av2-backdrop/av2-modal layer
//     (like InviteFriend), so on mobile it is the full-cover,
//     body-scroll-locked sheet and on desktop a centred card.
//   • On-screen PREVIEW is a self-contained HTML page in an
//     <iframe srcDoc> (isolated from the SPA). To SAVE A PDF we open the
//     SAME document as a full page in a NEW TAB (via a blob: URL on a
//     real <a target="_blank">, so iOS never blocks it) where the user
//     prints/saves. This is the only reliable iOS path: printing an
//     iframe, OR a hidden-then-revealed print node in the SPA, both
//     produced a BLANK PDF on iPhone. Same markup + CSS feed both
//     (apxDocMarkup/apxDocCss), so preview == the saved document.
//
// Globals in: React, window.BBR_supabase. Exposes window.AppraisalExport.
// ===========================================================
const { useState: useApxState, useEffect: useApxEffect, useRef: useApxRef } = React;

// DB grade enum -> display code (VG_PLUS -> VG+; null -> em dash).
function apxGrade(code) { return code ? String(code).replace("_PLUS", "+") : "—"; }

// GBP, whole pounds (appraisal figures don't need pence).
function apxGbp(n) { return "£" + Math.round(Number(n) || 0).toLocaleString("en-GB"); }

// HTML-escape everything user/data-derived before it goes into the
// document string (artist/title can carry &, <, ", etc.).
function apxEsc(s) {
  return String(s == null ? "" : s)
    .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}

// Human valuation-basis label from the stored data-quality signals.
// Mirrors the language used by ValueHistoryChart's confidence chip so
// the document and the in-app chart never contradict each other.
function apxBasis(it) {
  if (it.source_count && it.source_count >= 2) return "Corroborated · two marketplaces";
  if (it.confidence === "high" || it.granularity === "pressing") return "Price guide · this pressing";
  if (it.confidence === "medium" || it.granularity === "master") return "Price guide · release";
  if (it.confidence === "low") return "Market-low estimate";
  return "Estimate";
}

// Condition string for the table. Prefer the media/sleeve pair (that's
// what the detail view shows); fall back to a single grade; else em dash.
function apxCondition(it) {
  const m = it.media_condition, s = it.sleeve_condition;
  if (m || s) return apxGrade(m) + " / " + apxGrade(s);
  if (it.condition) return apxGrade(it.condition);
  return "—";
}

function apxArtist(it) { return it.artist || (it.album && it.album.artist) || "Unknown artist"; }
function apxTitle(it) { return it.title || (it.album && it.album.title) || "Untitled"; }

// @page rule (shared by the iframe preview and the print node).
function apxPageCss() { return '@page { size: A4; margin: 16mm 14mm 18mm; }'; }

// All document styling, scoped under `.apx-doc` so it is SAFE to inject into
// the live app's document (for printing) without leaking into the SPA. No bare
// element selectors, no :root, no html/body — everything hangs off .apx-doc.
function apxDocCss() {
  return (
    '.apx-doc{--ink:#17130f;--ink2:#3a322a;--mute:#7d7365;--rule:#ded6c6;--accent:#c8442a;--paper:#fdfbf6;' +
      "color:var(--ink);font-family:'Helvetica Neue',Arial,system-ui,sans-serif;font-size:12px;line-height:1.5;" +
      'background:var(--paper);-webkit-print-color-adjust:exact;print-color-adjust:exact;}' +
    '.apx-doc *{box-sizing:border-box;}' +
    '.apx-doc .doc{max-width:720px;margin:0 auto;padding:28px 30px 40px;}' +
    '.apx-doc .lh{display:flex;justify-content:space-between;align-items:flex-end;border-bottom:2px solid var(--ink);padding-bottom:12px;}' +
    ".apx-doc .wm{font-family:Georgia,'Times New Roman',serif;font-size:22px;letter-spacing:.02em;color:var(--accent);font-weight:700;}" +
    '.apx-doc .wm b{color:var(--ink);}' +
    '.apx-doc .lh-r{text-align:right;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--mute);line-height:1.7;}' +
    ".apx-doc .h1{font-family:Georgia,serif;font-size:26px;margin:22px 0 2px;font-weight:700;letter-spacing:-.01em;}" +
    '.apx-doc .sub{color:var(--mute);font-size:12px;margin:0 0 20px;}' +
    '.apx-doc .meta{display:grid;grid-template-columns:1fr 1fr;gap:0;border:1px solid var(--rule);border-radius:6px;overflow:hidden;margin-bottom:8px;}' +
    '.apx-doc .meta div{padding:9px 12px;border-bottom:1px solid var(--rule);}' +
    '.apx-doc .meta div:nth-child(odd){border-right:1px solid var(--rule);}' +
    '.apx-doc .meta div:nth-last-child(-n+2){border-bottom:0;}' +
    '.apx-doc .meta .k{font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--mute);display:block;margin-bottom:2px;}' +
    '.apx-doc .meta .v{font-size:13px;color:var(--ink);}' +
    '.apx-doc .head-fig{margin:20px 0 6px;padding:16px 18px;background:var(--ink);color:var(--paper);border-radius:8px;display:flex;justify-content:space-between;align-items:center;}' +
    '.apx-doc .head-fig .lab{font-size:10px;letter-spacing:.16em;text-transform:uppercase;opacity:.8;}' +
    ".apx-doc .head-fig .amt{font-family:Georgia,serif;font-size:30px;font-weight:700;}" +
    '.apx-doc table{width:100%;border-collapse:collapse;margin-top:16px;}' +
    '.apx-doc thead th{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--mute);text-align:left;' +
      'padding:0 8px 7px;border-bottom:1.5px solid var(--ink);font-weight:600;}' +
    '.apx-doc th.num,.apx-doc td.num{text-align:right;}' +
    '.apx-doc tbody td{padding:9px 8px;border-bottom:1px solid var(--rule);vertical-align:top;}' +
    '.apx-doc tr{page-break-inside:avoid;}' +
    '.apx-doc .c-num{color:var(--mute);width:22px;font-variant-numeric:tabular-nums;}' +
    '.apx-doc .c-item{min-width:180px;}' +
    '.apx-doc .it-artist{display:block;font-weight:600;color:var(--ink);}' +
    '.apx-doc .it-title{display:block;color:var(--ink2);font-style:italic;}' +
    '.apx-doc .it-sub{display:block;font-size:10px;color:var(--mute);margin-top:1px;}' +
    '.apx-doc .c-year,.apx-doc .c-fmt{color:var(--ink2);white-space:nowrap;}' +
    '.apx-doc .c-cond{white-space:nowrap;font-variant-numeric:tabular-nums;}' +
    '.apx-doc .c-basis{color:var(--mute);font-size:10.5px;max-width:150px;}' +
    '.apx-doc .c-val{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap;}' +
    '.apx-doc .c-paid{font-variant-numeric:tabular-nums;color:var(--ink2);white-space:nowrap;}' +
    '.apx-doc .pend{color:var(--mute);font-style:italic;font-weight:400;}' +
    '.apx-doc .total-row td{border-top:1.5px solid var(--ink);border-bottom:0;padding-top:11px;font-weight:600;}' +
    '.apx-doc .total-row .c-item{font-weight:700;}' +
    '.apx-doc .total-row .c-basis{font-weight:400;}' +
    '.apx-doc .total-row .strong{font-size:15px;}' +
    '.apx-doc .notes{margin-top:26px;border-top:1px solid var(--rule);padding-top:16px;}' +
    '.apx-doc .notes h3{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink);margin:0 0 8px;}' +
    '.apx-doc .notes p{margin:0 0 8px;color:var(--ink2);font-size:11px;line-height:1.6;}' +
    '.apx-doc .legend{display:flex;flex-wrap:wrap;gap:6px 14px;margin:4px 0 10px;font-size:10.5px;color:var(--ink2);}' +
    '.apx-doc .legend b{color:var(--ink);}' +
    '.apx-doc .disc{font-size:10px;color:var(--mute);line-height:1.55;}' +
    '.apx-doc .foot{margin-top:22px;border-top:1px solid var(--rule);padding-top:10px;display:flex;justify-content:space-between;' +
      'font-size:9.5px;letter-spacing:.06em;color:var(--mute);text-transform:uppercase;}'
  );
}

// Build the document's inner markup (the `.apx-doc` node), shared verbatim by
// the on-screen iframe preview and the top-document print node.
// meta = { name, dateLong, ref, count, valuedCount }.
function apxDocMarkup(items, meta) {
  // Highest value first — the way an appraiser or an insurer reads it.
  const rows = items.slice().sort((a, b) => (b.value || 0) - (a.value || 0));
  // Totals sum the DISPLAYED (whole-pound) per-item figures, not the raw
  // decimals, so the column visibly foots — a document whose line items
  // don't add up to the total is worthless to an insurer or an executor.
  const total = rows.reduce((s, r) => s + Math.round(Number(r.value) || 0), 0);

  // Adaptive acquisition column: only shown if the collector has
  // recorded at least one real purchase price (serves probate/estate
  // without cluttering an insurance-only collection). Gifts count as £0.
  const hasPaidVal = (r) => !r.is_gift && r.purchase_price != null && r.purchase_price !== "";
  const hasPaid = rows.some(r => r.is_gift === true || hasPaidVal(r));
  const totalPaid = rows.reduce((s, r) => s + (hasPaidVal(r) ? Math.round(Number(r.purchase_price)) : 0), 0);

  const paidCell = (r) => {
    if (r.is_gift) return "Gift";
    if (r.purchase_price == null || r.purchase_price === "") return "—";
    return apxGbp(r.purchase_price);
  };

  const bodyRows = rows.map((r, i) => {
    // Three honest states: a real figure; genuinely awaiting its first
    // valuation ("Pending"); or priced but with no market data found
    // ("No market value") — never conflate the last two.
    const valued = r.value != null && r.value > 0;
    const valCell = valued
      ? apxGbp(r.value)
      : (r.pending ? '<span class="pend">Pending</span>' : '<span class="pend">No market value</span>');
    // Tolerant of a year stored as a number OR a numeric string.
    const yr = parseInt(r.year, 10);
    const year = Number.isFinite(yr) ? yr : "—";
    const fmt = r.format ? apxEsc(r.format) : "LP";
    // Optional catalogue / label line under the title, if we hold it.
    const sub = [r.label, r.catalogue_no].filter(Boolean).map(apxEsc).join(" · ");
    return (
      '<tr>' +
        '<td class="c-num">' + (i + 1) + '</td>' +
        '<td class="c-item"><span class="it-artist">' + apxEsc(apxArtist(r)) + '</span>' +
          '<span class="it-title">' + apxEsc(apxTitle(r)) + '</span>' +
          (sub ? '<span class="it-sub">' + sub + '</span>' : '') +
        '</td>' +
        '<td class="c-year">' + year + '</td>' +
        '<td class="c-fmt">' + fmt + '</td>' +
        '<td class="c-cond">' + apxEsc(apxCondition(r)) + '</td>' +
        '<td class="c-basis">' + apxEsc(apxBasis(r)) + '</td>' +
        (hasPaid ? '<td class="c-paid num">' + paidCell(r) + '</td>' : '') +
        '<td class="c-val num">' + valCell + '</td>' +
      '</tr>'
    );
  }).join("");

  const totalRow =
    '<tr class="total-row">' +
      '<td></td>' +
      '<td class="c-item">Total estimated replacement value</td>' +
      '<td></td><td></td><td></td><td class="c-basis">' + meta.valuedCount + ' of ' + meta.count + ' valued</td>' +
      (hasPaid ? '<td class="c-paid num">' + apxGbp(totalPaid) + '</td>' : '') +
      '<td class="c-val num strong">' + apxGbp(total) + '</td>' +
    '</tr>';

  const paidHead = hasPaid ? '<th class="c-paid num">Paid</th>' : '';

  return '<div class="apx-doc"><div class="doc">' +
    // Letterhead
    '<div class="lh"><div class="wm">The <b>Lead-In</b></div>' +
      '<div class="lh-r">Collection Appraisal<br>' + apxEsc(meta.ref) + '</div></div>' +
    '<h1 class="h1">Vinyl Collection Appraisal</h1>' +
    '<p class="sub">Estimated replacement valuation of a private vinyl record collection, prepared for insurance, probate or claim purposes.</p>' +
    // Meta
    '<div class="meta">' +
      '<div><span class="k">Prepared for</span><span class="v">' + apxEsc(meta.name) + '</span></div>' +
      '<div><span class="k">Date prepared</span><span class="v">' + apxEsc(meta.dateLong) + '</span></div>' +
      '<div><span class="k">Records appraised</span><span class="v">' + meta.count + ' (' + meta.valuedCount + ' valued)</span></div>' +
      '<div><span class="k">Reference</span><span class="v">' + apxEsc(meta.ref) + '</span></div>' +
    '</div>' +
    // Headline
    '<div class="head-fig"><span class="lab">Total estimated replacement value</span><span class="amt">' + apxGbp(total) + '</span></div>' +
    // Table
    '<table><thead><tr>' +
      '<th class="c-num">#</th><th>Record</th><th>Year</th><th>Format</th><th>Condition</th><th>Valuation basis</th>' +
      paidHead + '<th class="num">Est. value</th>' +
    '</tr></thead><tbody>' + bodyRows + totalRow + '</tbody></table>' +
    // Notes
    '<div class="notes">' +
      '<h3>Methodology</h3>' +
      '<p>Each record is valued at its estimated <b>replacement cost</b> — the price of acquiring an equivalent copy on the open collector market, in the condition recorded here. Figures are derived from the Discogs marketplace price guide at the item’s media condition, corroborated where possible against a second independent marketplace (active eBay UK listings). Valuations are refreshed daily; the figures above are those held on the date of preparation. All amounts are in pounds sterling (GBP). Condition is shown as Media / Sleeve using the standard Goldmine grading scale (M, NM, VG+, VG, G, F, P).</p>' +
      '<div class="legend">' +
        '<span><b>Price guide · this pressing</b> — condition-matched guide for the exact pressing (highest confidence).</span>' +
        '<span><b>Corroborated · two marketplaces</b> — the guide figure was confirmed by a second, independent marketplace.</span>' +
        '<span><b>Price guide · release</b> — the release in general rather than the exact pressing.</span>' +
        '<span><b>Market-low estimate</b> — a conservative lowest-listed figure where no full guide exists.</span>' +
      '</div>' +
      '<h3>Important</h3>' +
      '<p class="disc">This document is a market-derived estimate prepared by The Lead-In (theleadin.com) as a convenience for its members. It is not a formal appraisal by a chartered valuer or auctioneer and carries no warranty. Collectible record values fluctuate with condition, demand and supply; realised prices may differ materially from the estimates shown. Condition grades are as entered by the collection’s owner and have not been independently inspected. Figures should be treated as indicative and, for high-value claims or estates, corroborated by an independent specialist valuation.</p>' +
    '</div>' +
    '<div class="foot"><span>Generated by The Lead-In</span><span>theleadin.com</span><span>' + apxEsc(meta.dateLong) + '</span></div>' +
    '</div></div>';
}

// Full standalone HTML document. Used two ways from the SAME builder:
//   • opts.toolbar = false  -> the on-screen <iframe srcDoc> preview.
//   • opts.toolbar = true   -> the page opened in a NEW TAB for saving as
//     PDF. This is the ONLY reliable "Save as PDF" path on iOS Safari:
//     printing an iframe, or a hidden-then-revealed print node in the SPA,
//     both render a BLANK PDF on iPhone (layout-snapshot / @media-print
//     timing bugs). A plain full-page document in its own tab prints fine
//     via the browser's native Print / Share → Save to Files.
// The toolbar is a screen-only bar (hidden in print) with a one-tap print
// button + instructions, plus a best-effort auto-print on load.
function apxBuildDoc(items, meta, opts) {
  opts = opts || {};
  const bar = opts.toolbar
    ? '<div class="apx-bar"><button type="button" class="apx-bar-btn" onclick="window.print()">Save as PDF / Print</button>' +
      '<span class="apx-bar-hint">On iPhone: tap the button (or Share → Print), then choose “Save to Files”. On a computer: choose “Save as PDF” as the printer.</span></div>'
    : "";
  const barCss = opts.toolbar
    ? ".apx-bar{position:sticky;top:0;z-index:5;display:flex;flex-wrap:wrap;gap:8px 14px;align-items:center;" +
        "padding:12px 16px;background:#17130f;color:#fff;font-family:'Helvetica Neue',Arial,sans-serif;}" +
      ".apx-bar-btn{background:#c8442a;color:#fff;border:none;border-radius:999px;padding:10px 20px;" +
        "font-size:15px;font-weight:600;cursor:pointer;}" +
      ".apx-bar-hint{font-size:12px;line-height:1.4;opacity:.85;flex:1 1 220px;min-width:200px;}" +
      "@media print{.apx-bar{display:none !important;}}"
    : "";
  // Best-effort auto-print in the new tab (button + Share remain the fallback
  // if inline script is blocked). Not added to the iframe preview.
  const autoPrint = opts.toolbar
    ? '<script>try{addEventListener("load",function(){setTimeout(function(){try{window.print();}catch(e){}},400);});}catch(e){}</script>'
    : "";
  return '<!doctype html><html lang="en"><head><meta charset="utf-8">' +
    '<meta name="viewport" content="width=device-width, initial-scale=1">' +
    '<title>Collection Appraisal — ' + apxEsc(meta.name) + '</title>' +
    '<style>' + apxPageCss() + 'html,body{margin:0;padding:0;background:#fdfbf6;}' + barCss + apxDocCss() + '</style>' +
    '</head><body>' + bar + apxDocMarkup(items, meta) + autoPrint + '</body></html>';
}

function AppraisalExport({ user, items, onClose }) {
  const [doc, setDoc] = useApxState("");        // full HTML for the iframe preview
  const [openUrl, setOpenUrl] = useApxState(""); // blob: URL of the printable doc (new tab)
  const [name, setName] = useApxState("");
  const onCloseRef = useApxRef(onClose);
  onCloseRef.current = onClose;

  // Only records that carry a value contribute to the headline total; the
  // rest are still listed (marked "Pending") so the document is a complete
  // inventory, not a filtered one.
  const list = (items || []).filter(Boolean);
  const valuedCount = list.filter(it => it.value != null && it.value > 0).length;

  // Resolve the collector's name for the letterhead: profiles.display_name,
  // else the local part of their email, else a neutral fallback. Best-effort;
  // the document builds regardless.
  useApxEffect(() => {
    let alive = true;
    (async () => {
      let n = "";
      try {
        if (window.BBR_supabase && user && user.id) {
          const { data } = await window.BBR_supabase
            .from("profiles").select("display_name").eq("id", user.id).maybeSingle();
          if (data && data.display_name) n = data.display_name;
        }
      } catch (e) { /* ignore — fall through to email */ }
      if (!n && user && user.email) n = user.email.split("@")[0];
      if (!n) n = "The collection owner";
      if (alive) setName(n);
    })();
    return () => { alive = false; };
  }, [user && user.id]);

  // Build the document once we have the name. new Date() is fine here — this
  // is the live browser at click time (the sandbox restriction is workflow-only).
  useApxEffect(() => {
    if (!name) return;
    const now = new Date();
    const dateLong = now.toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" });
    const yyyymmdd = now.toISOString().slice(0, 10).replace(/-/g, "");
    const ref = "LI-APR-" + yyyymmdd + "-" + String(list.length).padStart(3, "0");
    const meta = { name, dateLong, ref, count: list.length, valuedCount };
    setDoc(apxBuildDoc(list, meta, { toolbar: false }));
    // The printable doc (with the save toolbar) is served as a blob: URL so the
    // "Save as PDF" control can be a real <a target="_blank"> — a genuine link
    // click, which iOS never treats as a blocked pop-up.
    const url = URL.createObjectURL(new Blob([apxBuildDoc(list, meta, { toolbar: true })], { type: "text/html" }));
    setOpenUrl(url);
    return () => URL.revokeObjectURL(url);
  }, [name]);

  // Body-scroll lock (iOS-safe position:fixed pin), same as InviteFriend.
  useApxEffect(() => {
    const scrollY = window.scrollY || window.pageYOffset || 0;
    const b = document.body;
    const prev = { position: b.style.position, top: b.style.top, left: b.style.left, right: b.style.right, width: b.style.width, overflow: b.style.overflow };
    b.style.position = "fixed"; b.style.top = "-" + scrollY + "px"; b.style.left = "0";
    b.style.right = "0"; b.style.width = "100%"; b.style.overflow = "hidden";
    return () => {
      b.style.position = prev.position; b.style.top = prev.top; b.style.left = prev.left;
      b.style.right = prev.right; b.style.width = prev.width; b.style.overflow = prev.overflow;
      window.scrollTo(0, scrollY);
    };
  }, []);

  // Back-gesture pops the sheet instead of navigating the SPA (same as InviteFriend).
  useApxEffect(() => {
    const url = location.pathname + location.search + location.hash;
    history.pushState({ bbrAppraisal: true }, "", url);
    let popped = false;
    const onPop = () => { popped = true; onCloseRef.current(); };
    window.addEventListener("popstate", onPop);
    return () => {
      window.removeEventListener("popstate", onPop);
      if (!popped) history.back();
    };
  }, []);

  useApxEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  return (
    <div className="av2-backdrop" onClick={onClose}>
      <div className="av2-modal apx-modal" onClick={(e) => e.stopPropagation()}>
        <div className="av2-head">
          <div className="av2-head-left">
            <button className="av2-back-arrow" aria-label="Back to collection" onClick={onClose}>
              <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M12 4l-5 6 5 6" /></svg>
            </button>
            <span className="av2-head-title">Collection appraisal</span>
          </div>
          <button className="av2-x" aria-label="Close" onClick={onClose}>×</button>
        </div>
        <div className="av2-body apx-body">
          <p className="apx-lede">
            A dated, print-ready valuation of your collection — replacement value, condition and
            valuation basis for every record — formatted for an insurer, a probate file or a claim.
          </p>
          <div className="apx-preview">
            {doc
              ? <iframe className="apx-frame" title="Appraisal document preview" srcDoc={doc} />
              : <div className="apx-loading">Preparing your appraisal…</div>}
          </div>
          <div className="apx-actions">
            <a
              className={"apx-print" + (openUrl ? "" : " is-disabled")}
              href={openUrl || undefined}
              target="_blank"
              rel="noopener"
              aria-disabled={openUrl ? undefined : true}
              onClick={(e) => { if (!openUrl) e.preventDefault(); }}
            >
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M6 9V3h12v6"/><path d="M6 18H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg>
              Save as PDF / Print
            </a>
            <span className="apx-hint">Opens a clean copy in a new tab — then save it as a PDF (on iPhone: Share → Print, then “Save to Files”).</span>
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { AppraisalExport });
