// pdf.jsx - real PDF generation (Schedule of Assets + Policy Certificate)
// Drawn programmatically with jsPDF (vector text + shapes), so the export is a
// crisp, selectable-text PDF that downloads in one click and renders identically
// everywhere - no screenshotting, no print dialog. Money uses "NGN" since the ₦
// glyph isn't guaranteed across embedded fonts.
//
// BRAND TYPE: the same system as the app — Space Grotesk for display, Manrope
// for body text, JetBrains Mono for figures. The TTFs are self-hosted in
// /fonts/pdf and embedded into each document; if they can't be fetched the
// generator degrades to built-in fonts so a download always succeeds.

const PAGE = { w: 595.28, h: 841.89, m: 40 };
const INK = "#16182e", MUTED = "#5b6280", FAINT = "#8a90af", LINE = "#e4e6f0", SOFT = "#f5f6fb";
const GREEN = "#0f9d58", AMBER = "#d98605", RED = "#e0392b";

function _accentHex() {
  const v = getComputedStyle(document.documentElement).getPropertyValue("--accent").trim();
  return v || "#5b3df5";
}
function _today() {
  return new Date().toLocaleDateString("en-NG", { day: "2-digit", month: "long", year: "numeric" });
}
function _ref(prefix) { return prefix + "-" + Math.random().toString(36).slice(2, 8).toUpperCase(); }

// Turn an owner's name into a safe, readable filename fragment, e.g.
// "ABC Logistics Ltd." -> "ABC-Logistics-Ltd", "Adé Okafor" -> "Ade-Okafor".
// Strips punctuation/currency glyphs and collapses whitespace to single hyphens.
function _slug(name, fallback = "VaultCova") {
  const clean = String(name || "")
    .normalize("NFKD")            // separate accents from letters
    .replace(/[^\w\s-]/g, "")     // drop punctuation, currency glyphs, accents, ()
    .trim().replace(/\s+/g, "-").replace(/-+/g, "-") // spaces -> single hyphen
    .slice(0, 60).replace(/^-+|-+$/g, "");
  return clean || fallback;
}

// Company identity — set by the app from /api/settings (ops manages it in
// /admin/company), with safe fallbacks so documents render before it loads.
function _company() {
  const c = (typeof window !== "undefined" && window.VC_COMPANY) || {};
  return {
    name: c.name || "VaultCova",
    legalName: c.legalName || "VaultCova Technologies Ltd.",
    tagline: c.tagline || "The Insurance Operating System",
    email: c.email || "",
    phone: c.phone || "",
    address: c.address || "Lagos, Nigeria",
    website: c.website || "",
    rcNumber: c.rcNumber || "",
    regulator: c.regulator || "NAICOM",
  };
}

function _rgb(hex) {
  const h = hex.replace("#", "");
  return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
}
const money = (n) => "NGN " + Math.round(n || 0).toLocaleString("en-NG");

function _fit(doc, str, maxW) {
  str = String(str || "");
  if (doc.getTextWidth(str) <= maxW) return str;
  let s = str;
  while (s.length > 1 && doc.getTextWidth(s + "...") > maxW) s = s.slice(0, -1);
  return s + "...";
}

/* ============ Brand fonts (embedded TTFs) ============ */
// Fetched once per session from /fonts/pdf, cached as base64, registered into
// every new document. All four are needed before we claim "brand" — otherwise
// mixed families would look worse than a clean helvetica fallback.
const _FONT_FILES = [
  ["SpaceGrotesk-Bold.ttf", "SpaceGrotesk", "bold"],
  ["Manrope-Regular.ttf", "Manrope", "normal"],
  ["Manrope-Bold.ttf", "Manrope", "bold"],
  ["JetBrainsMono-Bold.ttf", "JetBrainsMono", "bold"],
];
let _fontData = null;   // { file: base64 } once loaded
let _fontPromise = null;

function _b64(buf) {
  const bytes = new Uint8Array(buf);
  let bin = "";
  const CHUNK = 0x8000;
  for (let i = 0; i < bytes.length; i += CHUNK) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
  return btoa(bin);
}

function _ensureFonts() {
  if (_fontPromise) return _fontPromise;
  _fontPromise = Promise.all(
    _FONT_FILES.map(([file]) =>
      fetch("/fonts/pdf/" + file)
        .then((r) => (r.ok ? r.arrayBuffer() : Promise.reject(new Error("missing " + file))))
        .then((buf) => [file, _b64(buf)])
    )
  )
    .then((pairs) => { _fontData = Object.fromEntries(pairs); })
    .catch(() => { _fontData = null; }); // degrade to built-ins, never block a download
  return _fontPromise;
}

// Registers the brand set into a document. Returns true when brand type is live.
function _registerFonts(doc) {
  if (!_fontData) return false;
  try {
    for (const [file, family, style] of _FONT_FILES) {
      doc.addFileToVFS(file, _fontData[file]);
      doc.addFont(file, family, style);
    }
    return true;
  } catch (e) { return false; }
}

// The one place font families are chosen. kind: "display" | "body" | "mono".
function _font(doc, kind, style = "normal") {
  if (doc.__brand) {
    if (kind === "display") { doc.setFont("SpaceGrotesk", "bold"); return; } // display is always bold
    if (kind === "mono") { doc.setFont("JetBrainsMono", "bold"); return; }   // figures are always bold
    doc.setFont("Manrope", style === "bold" ? "bold" : "normal");
    return;
  }
  if (kind === "mono") { doc.setFont("courier", "bold"); return; }
  doc.setFont("helvetica", kind === "display" || style === "bold" ? "bold" : "normal");
}

function _newDoc() {
  const { jsPDF } = window.jspdf;
  const doc = new jsPDF({ unit: "pt", format: "a4", compress: true });
  doc.__brand = _registerFonts(doc);
  return doc;
}

// The vault-dial mark.
function _mark(doc, x, y, size, accent) {
  doc.setFillColor(..._rgb(accent));
  doc.roundedRect(x, y, size, size, size * 0.28, size * 0.28, "F");
  const cx = x + size / 2, cy = y + size / 2;
  doc.setDrawColor(255, 255, 255); doc.setLineWidth(size * 0.055);
  doc.circle(cx, cy, size * 0.27, "S");
  doc.setFillColor(255, 255, 255); doc.circle(cx, cy, size * 0.08, "F");
  const t = size * 0.1, off = size * 0.15;
  doc.line(cx, y + off, cx, y + off + t);
  doc.line(cx, y + size - off - t, cx, y + size - off);
  doc.line(x + off, cy, x + off + t, cy);
  doc.line(x + size - off - t, cy, x + size - off, cy);
}

function _header(doc, accent, docType, ref) {
  _mark(doc, PAGE.m, 40, 28, accent);
  _font(doc, "display"); doc.setFontSize(16); doc.setTextColor(..._rgb(INK));
  doc.text("Vault", PAGE.m + 38, 53);
  const vw = doc.getTextWidth("Vault");
  doc.setTextColor(..._rgb(accent)); doc.text("Cova", PAGE.m + 38 + vw, 53);
  _font(doc, "body"); doc.setFontSize(7.5); doc.setTextColor(..._rgb(MUTED));
  doc.text("THE INSURANCE OPERATING SYSTEM", PAGE.m + 38, 63);
  const right = PAGE.w - PAGE.m;
  _font(doc, "display"); doc.setFontSize(11); doc.setTextColor(..._rgb(INK));
  doc.text(docType.toUpperCase(), right, 50, { align: "right" });
  _font(doc, "mono"); doc.setFontSize(8.5); doc.setTextColor(..._rgb(MUTED));
  doc.text(ref, right, 62, { align: "right" });
  doc.setDrawColor(..._rgb(INK)); doc.setLineWidth(1.5);
  doc.line(PAGE.m, 78, right, 78);
}

function _metaRow(doc, items, y) {
  const colW = (PAGE.w - 2 * PAGE.m) / items.length;
  items.forEach(([label, value], i) => {
    const x = PAGE.m + i * colW;
    _font(doc, "body", "bold"); doc.setFontSize(7); doc.setTextColor(..._rgb(FAINT));
    doc.text(label.toUpperCase(), x, y);
    _font(doc, "body", "bold"); doc.setFontSize(10.5); doc.setTextColor(..._rgb(INK));
    doc.text(_fit(doc, value, colW - 8), x, y + 14);
  });
}

function _footer(doc, lines) {
  const co = _company();
  const y = PAGE.h - 54;
  doc.setDrawColor(..._rgb(LINE)); doc.setLineWidth(0.8);
  doc.line(PAGE.m, y, PAGE.w - PAGE.m, y);
  _font(doc, "body", "bold"); doc.setFontSize(8); doc.setTextColor(..._rgb(INK));
  doc.text(co.legalName, PAGE.m, y + 14);
  _font(doc, "body"); doc.setFontSize(7.5); doc.setTextColor(..._rgb(MUTED));
  // Contact line is built from whatever the company profile has set (no hardcoding).
  const contact = [co.address, co.phone, co.email, co.website].filter(Boolean).join("  ·  ");
  if (contact) doc.text(_fit(doc, contact, PAGE.w - 2 * PAGE.m - 180), PAGE.m, y + 24);
  doc.text(lines, PAGE.w - PAGE.m, y + 14, { align: "right" });
}

// Schedule header: VaultLog is the PRIMARY title (in the brand colour), with
// "Schedule of Assets" as the secondary line, and a VaultCova attribution.
function _scheduleHeader(doc, accent, ref) {
  _mark(doc, PAGE.m, 40, 28, accent);
  _font(doc, "display"); doc.setFontSize(21); doc.setTextColor(..._rgb(accent));
  doc.text("VaultLog", PAGE.m + 38, 55);
  _font(doc, "body", "bold"); doc.setFontSize(10); doc.setTextColor(..._rgb(MUTED));
  doc.text("Schedule of Assets", PAGE.m + 38, 68);
  const right = PAGE.w - PAGE.m;
  const co = _company();
  _font(doc, "body"); doc.setFontSize(8); doc.setTextColor(..._rgb(FAINT));
  doc.text("Powered by " + co.name, right, 50, { align: "right" });
  _font(doc, "mono"); doc.setFontSize(8.5); doc.setTextColor(..._rgb(MUTED));
  doc.text(ref, right, 62, { align: "right" });
  doc.setDrawColor(..._rgb(INK)); doc.setLineWidth(1.5);
  doc.line(PAGE.m, 78, right, 78);
}

function _statusMeta(status) {
  return { insured: [GREEN, "Insured"], partial: [AMBER, "Underinsured"], lapsing: [AMBER, "Lapsing"] }[status] || [RED, "Uninsured"];
}

/* ============ Schedule of Assets ============
   opts (all optional):
     byLocation  — group the register into per-place sections with subtotals
     statement   — array from /api/valuations: per-asset value history for a
                   period. When present, a "Value statement" section is appended
                   (bank-statement style: every value change with its running
                   figure) plus each item's 12-month average used for cover.
     from, to    — ISO strings bounding the statement period (for labelling). */
async function downloadSchedule(assets, profile, userName, ownerAddress, opts = {}) {
  const D = window.VC_DATA;
  if (!window.jspdf) return; // jsPDF must be loaded
  await _ensureFonts();
  const accent = _accentHex();
  const p = D.portfolio(assets);
  const bizName = (typeof window !== "undefined" && window.VC_BUSINESS) || "";
  const docName = profile === "business" && bizName ? bizName : userName;
  const owner = profile === "business" ? docName : userName + " (Personal)";
  const address = String(ownerAddress || (typeof window !== "undefined" && window.VC_ADDRESS) || "").trim();
  const byLocation = !!opts.byLocation && assets.some((a) => (a.place || "").trim());
  const statement = Array.isArray(opts.statement) ? opts.statement.filter((s) => s && (s.lines || []).length) : [];
  const fmtDay = (iso) => { const d = new Date(iso); return isNaN(d) ? "" : d.toLocaleDateString("en-NG", { day: "2-digit", month: "short", year: "numeric" }); };
  const doc = _newDoc();

  _scheduleHeader(doc, accent, "Ref " + _ref("SOA"));
  _metaRow(doc, [
    ["Prepared for", owner],
    ["Account type", profile === "business" ? "Business" : "Personal"],
    ["Date issued", _today()],
    ["Total assets", String(assets.length)],
  ], 100);
  if (address) {
    _font(doc, "body", "bold"); doc.setFontSize(7); doc.setTextColor(..._rgb(FAINT));
    doc.text("ADDRESS", PAGE.m, 122);
    _font(doc, "body"); doc.setFontSize(9.5); doc.setTextColor(..._rgb(INK));
    doc.text(_fit(doc, address, PAGE.w - 2 * PAGE.m), PAGE.m, 133);
  }

  // summary cards
  const cy = address ? 148 : 128, cgap = 12, cw = (PAGE.w - 2 * PAGE.m - 2 * cgap) / 3, ch = 56;
  [["Total replacement value", money(p.replacement), INK],
   ["Total sum insured", money(p.insured), GREEN],
   ["Coverage gap", money(p.gap), p.gap ? RED : GREEN]].forEach(([l, v, c], i) => {
    const x = PAGE.m + i * (cw + cgap);
    doc.setFillColor(..._rgb(SOFT)); doc.setDrawColor(..._rgb(LINE)); doc.setLineWidth(0.8);
    doc.roundedRect(x, cy, cw, ch, 8, 8, "FD");
    _font(doc, "body", "bold"); doc.setFontSize(7); doc.setTextColor(..._rgb(FAINT));
    doc.text(l.toUpperCase(), x + 12, cy + 20);
    _font(doc, "mono"); doc.setFontSize(11.5); doc.setTextColor(..._rgb(c));
    doc.text(_fit(doc, v, cw - 22), x + 12, cy + 40);
  });

  // register columns
  const cols = [
    { label: "Asset", x: PAGE.m, align: "left" },
    { label: "Insurer", x: 215, align: "left" },
    { label: "Replacement", x: 360, align: "right" },
    { label: "Sum insured", x: 448, align: "right" },
    { label: "Gap", x: 512, align: "right" },
    { label: "Status", x: 555, align: "right" },
  ];
  let y = cy + ch + 34;
  _font(doc, "display"); doc.setFontSize(11); doc.setTextColor(..._rgb(INK));
  doc.text(byLocation ? "Asset register — by location" : "Asset register", PAGE.m, y - 16);

  const pageBreak = (needed = 90) => { if (y > PAGE.h - needed) { doc.addPage(); y = 60; return true; } return false; };
  const drawHead = () => {
    _font(doc, "body", "bold"); doc.setFontSize(7.5); doc.setTextColor(..._rgb(FAINT));
    cols.forEach((c) => doc.text(c.label.toUpperCase(), c.x, y, { align: c.align }));
    doc.setDrawColor(..._rgb("#d4d8e8")); doc.setLineWidth(1);
    doc.line(PAGE.m, y + 6, PAGE.w - PAGE.m, y + 6);
    y += 20;
  };
  const drawRow = (a) => {
    if (pageBreak()) drawHead();
    const gap = Math.max(0, a.replacement - a.sumInsured);
    const [sc, sl] = _statusMeta(a.status);
    _font(doc, "body", "bold"); doc.setFontSize(9.5); doc.setTextColor(..._rgb(INK));
    doc.text(_fit(doc, a.name, 168), cols[0].x, y);
    _font(doc, "body"); doc.setFontSize(7.5); doc.setTextColor(..._rgb(MUTED));
    doc.text((D.CATEGORIES[a.category]?.label || "") + (a.trackValue ? " · Tracked" : "") + (a.bundle ? " · Bundled" : ""), cols[0].x, y + 10);
    _font(doc, "body"); doc.setFontSize(9); doc.setTextColor(..._rgb(INK));
    doc.text(_fit(doc, a.insurer || "-", 130), cols[1].x, y);
    _font(doc, "mono"); doc.setFontSize(8);
    doc.text(money(a.replacement), cols[2].x, y, { align: "right" });
    doc.text(money(a.sumInsured), cols[3].x, y, { align: "right" });
    doc.setTextColor(..._rgb(gap ? RED : GREEN));
    doc.text(money(gap).replace("NGN ", ""), cols[4].x, y, { align: "right" });
    _font(doc, "body", "bold"); doc.setFontSize(7.5); doc.setTextColor(..._rgb(sc));
    doc.text(sl, cols[5].x, y, { align: "right" });
    doc.setDrawColor(..._rgb(LINE)); doc.setLineWidth(0.6);
    doc.line(PAGE.m, y + 16, PAGE.w - PAGE.m, y + 16);
    y += 28;
  };
  const subtotalRow = (label, items) => {
    const rep = items.reduce((s, a) => s + (a.replacement || 0), 0);
    const ins = items.reduce((s, a) => s + (a.sumInsured || 0), 0);
    doc.setDrawColor(..._rgb("#d4d8e8")); doc.setLineWidth(0.8); doc.line(PAGE.m, y - 4, PAGE.w - PAGE.m, y - 4);
    y += 10;
    _font(doc, "body", "bold"); doc.setFontSize(8.5); doc.setTextColor(..._rgb(MUTED));
    doc.text(label, cols[0].x, y);
    _font(doc, "mono"); doc.setFontSize(8); doc.setTextColor(..._rgb(INK));
    doc.text(money(rep), cols[2].x, y, { align: "right" });
    doc.text(money(ins), cols[3].x, y, { align: "right" });
    doc.text(money(Math.max(0, rep - ins)).replace("NGN ", ""), cols[4].x, y, { align: "right" });
    y += 22;
  };

  if (byLocation) {
    // Group by place (preserve highest-value first), unplaced last.
    const map = new Map();
    assets.forEach((a) => { const k = (a.place || "").trim() || " "; if (!map.has(k)) map.set(k, []); map.get(k).push(a); });
    const groups = [...map.entries()].map(([k, items]) => ({ name: k === " " ? "Not assigned to a place" : k, items, value: items.reduce((s, a) => s + (a.replacement || 0), 0) }))
      .sort((a, b) => (a.name === "Not assigned to a place" ? 1 : b.name === "Not assigned to a place" ? -1 : b.value - a.value));
    groups.forEach((g) => {
      pageBreak(120);
      _font(doc, "display"); doc.setFontSize(10.5); doc.setTextColor(..._rgb(accent));
      doc.text(_fit(doc, g.name, 360), PAGE.m, y);
      _font(doc, "body"); doc.setFontSize(8.5); doc.setTextColor(..._rgb(FAINT));
      doc.text(g.items.length + (g.items.length === 1 ? " item" : " items"), PAGE.w - PAGE.m, y, { align: "right" });
      y += 12; drawHead();
      g.items.forEach(drawRow);
      subtotalRow("Subtotal — " + g.name, g.items);
    });
  } else {
    drawHead();
    assets.forEach(drawRow);
  }

  // grand totals
  pageBreak();
  doc.setDrawColor(..._rgb(INK)); doc.setLineWidth(1.2); doc.line(PAGE.m, y - 4, PAGE.w - PAGE.m, y - 4);
  y += 12;
  _font(doc, "body", "bold"); doc.setFontSize(9.5); doc.setTextColor(..._rgb(INK));
  doc.text("Totals (" + assets.length + " assets)", cols[0].x, y);
  _font(doc, "mono"); doc.setFontSize(8.5);
  doc.text(money(p.replacement), cols[2].x, y, { align: "right" });
  doc.text(money(p.insured), cols[3].x, y, { align: "right" });
  doc.text(money(p.gap).replace("NGN ", ""), cols[4].x, y, { align: "right" });

  // ---- Value statement (bank-statement style) for tracked assets ----
  if (statement.length) {
    doc.addPage(); y = 60;
    const periodLabel = (opts.from && opts.to) ? (fmtDay(opts.from) + " – " + fmtDay(opts.to)) : "All time";
    _font(doc, "display"); doc.setFontSize(13); doc.setTextColor(..._rgb(INK));
    doc.text("Value statement", PAGE.m, y);
    _font(doc, "body"); doc.setFontSize(8.5); doc.setTextColor(..._rgb(MUTED));
    doc.text("Period: " + periodLabel + "  ·  how tracked values moved, like a bank statement", PAGE.m, y + 13);
    y += 34;

    const sc = [
      { label: "Date", x: PAGE.m, align: "left" },
      { label: "Note", x: 150, align: "left" },
      { label: "Value", x: 430, align: "right" },
      { label: "Change", x: 555, align: "right" },
    ];
    statement.forEach((s) => {
      pageBreak(150);
      // asset heading
      _font(doc, "display"); doc.setFontSize(10.5); doc.setTextColor(..._rgb(INK));
      doc.text(_fit(doc, s.name, 340), PAGE.m, y);
      _font(doc, "body"); doc.setFontSize(8); doc.setTextColor(..._rgb(FAINT));
      const sub = [D.CATEGORIES[s.category]?.label, s.place].filter(Boolean).join(" · ");
      if (sub) doc.text(_fit(doc, sub, 200), PAGE.w - PAGE.m, y, { align: "right" });
      y += 14;
      // column head
      _font(doc, "body", "bold"); doc.setFontSize(7); doc.setTextColor(..._rgb(FAINT));
      sc.forEach((c) => doc.text(c.label.toUpperCase(), c.x, y, { align: c.align }));
      doc.setDrawColor(..._rgb("#d4d8e8")); doc.setLineWidth(0.8); doc.line(PAGE.m, y + 5, PAGE.w - PAGE.m, y + 5);
      y += 17;
      // opening balance
      let prev = s.opening;
      _font(doc, "body"); doc.setFontSize(8.5); doc.setTextColor(..._rgb(MUTED));
      doc.text("Opening", sc[0].x, y);
      doc.text("Value at period start", sc[1].x, y);
      _font(doc, "mono"); doc.setFontSize(8); doc.setTextColor(..._rgb(INK));
      doc.text(money(s.opening), sc[2].x, y, { align: "right" });
      y += 18;
      // each change
      (s.lines || []).forEach((ln) => {
        if (pageBreak(90)) {
          _font(doc, "body", "bold"); doc.setFontSize(7); doc.setTextColor(..._rgb(FAINT));
          sc.forEach((c) => doc.text(c.label.toUpperCase(), c.x, y, { align: c.align }));
          y += 14;
        }
        const change = ln.value - prev;
        _font(doc, "body"); doc.setFontSize(8.5); doc.setTextColor(..._rgb(INK));
        doc.text(fmtDay(ln.at), sc[0].x, y);
        _font(doc, "body"); doc.setFontSize(8); doc.setTextColor(..._rgb(MUTED));
        doc.text(_fit(doc, ln.note || (ln.source === "baseline" ? "Baseline" : "Revaluation"), 270), sc[1].x, y);
        _font(doc, "mono"); doc.setFontSize(8); doc.setTextColor(..._rgb(INK));
        doc.text(money(ln.value), sc[2].x, y, { align: "right" });
        doc.setTextColor(..._rgb(change > 0 ? GREEN : change < 0 ? RED : MUTED));
        doc.text((change > 0 ? "+" : "") + money(change).replace("NGN ", ""), sc[3].x, y, { align: "right" });
        doc.setDrawColor(..._rgb(LINE)); doc.setLineWidth(0.5); doc.line(PAGE.m, y + 6, PAGE.w - PAGE.m, y + 6);
        y += 18; prev = ln.value;
      });
      // closing + averages
      doc.setDrawColor(..._rgb(INK)); doc.setLineWidth(1); doc.line(PAGE.m, y - 2, PAGE.w - PAGE.m, y - 2);
      y += 12;
      _font(doc, "body", "bold"); doc.setFontSize(8.5); doc.setTextColor(..._rgb(INK));
      doc.text("Closing value", sc[0].x, y);
      _font(doc, "mono"); doc.setFontSize(8.5); doc.text(money(s.current), sc[2].x, y, { align: "right" });
      y += 14;
      _font(doc, "body", "bold"); doc.setFontSize(8); doc.setTextColor(..._rgb(accent));
      doc.text("12-month average (used for annual cover)", sc[0].x, y);
      _font(doc, "mono"); doc.setFontSize(8); doc.setTextColor(..._rgb(accent));
      doc.text(money(s.avg12), sc[2].x, y, { align: "right" });
      y += 26;
    });
  }

  const note = statement.length
    ? "Values are user-declared. The 12-month average is a time-weighted figure for annual cover, not a contract of insurance."
    : "Replacement costs are user-declared. Not a contract of insurance.";
  _footer(doc, note + "\nGenerated by Cova · " + _today());
  doc.save(_slug(docName) + (statement.length ? "-Value-Statement.pdf" : "-Schedule-of-Assets.pdf"));
}

/* ============ Policy Certificate (individual or bundle) ============ */
async function downloadCertificate(items, profile, userName, bundleName, ownerAddress) {
  const D = window.VC_DATA;
  if (!window.jspdf) return;
  await _ensureFonts();
  const accent = _accentHex();
  const isBundle = items.length > 1;
  const insurer = items[0]?.insurer || "Leadway Assurance";
  const sumInsured = items.reduce((s, a) => s + a.sumInsured, 0);
  const premium = items.reduce((s, a) => s + (a.premium || 0), 0);
  const bizName = (typeof window !== "undefined" && window.VC_BUSINESS) || "";
  const docName = profile === "business" && bizName ? bizName : userName;
  const owner = docName;
  const address = String(ownerAddress || (typeof window !== "undefined" && window.VC_ADDRESS) || "").trim();
  const polNo = _ref(isBundle ? "VC-BDL" : "VC-POL");
  const doc = _newDoc();

  _header(doc, accent, isBundle ? "Bundled Policy Certificate" : "Policy Certificate", "Policy " + polNo);
  _metaRow(doc, [
    ["Policyholder", owner],
    ["Underwritten by", insurer],
    ["Effective", _today()],
    ["Expires", new Date(Date.now() + 3.15e10).toLocaleDateString("en-NG", { day: "2-digit", month: "long", year: "numeric" })],
  ], 100);

  // insured assets
  let y = 150;
  if (address) {
    _font(doc, "body", "bold"); doc.setFontSize(7); doc.setTextColor(..._rgb(FAINT));
    doc.text("POLICYHOLDER ADDRESS", PAGE.m, 122);
    _font(doc, "body"); doc.setFontSize(9.5); doc.setTextColor(..._rgb(INK));
    doc.text(_fit(doc, address, PAGE.w - 2 * PAGE.m), PAGE.m, 133);
    y = 158;
  }
  _font(doc, "display"); doc.setFontSize(11); doc.setTextColor(..._rgb(INK));
  doc.text(isBundle ? "Bundle: " + (bundleName || "Asset bundle") + " - " + items.length + " assets covered" : "Insured asset", PAGE.m, y);
  y += 18;
  _font(doc, "body", "bold"); doc.setFontSize(7.5); doc.setTextColor(..._rgb(FAINT));
  doc.text("ASSET", PAGE.m, y);
  doc.text("REPLACEMENT VALUE", 400, y, { align: "right" });
  doc.text("SUM INSURED", 555, y, { align: "right" });
  doc.setDrawColor(..._rgb("#d4d8e8")); doc.setLineWidth(1); doc.line(PAGE.m, y + 6, PAGE.w - PAGE.m, y + 6);
  y += 20;
  items.forEach((a) => {
    _font(doc, "body", "bold"); doc.setFontSize(9.5); doc.setTextColor(..._rgb(INK));
    doc.text(_fit(doc, a.name, 240), PAGE.m, y);
    _font(doc, "body"); doc.setFontSize(7.5); doc.setTextColor(..._rgb(MUTED));
    doc.text(D.CATEGORIES[a.category]?.label || "", PAGE.m, y + 10);
    _font(doc, "mono"); doc.setFontSize(8.5); doc.setTextColor(..._rgb(INK));
    doc.text(money(a.replacement), 400, y, { align: "right" });
    doc.text(money(a.sumInsured), 555, y, { align: "right" });
    doc.setDrawColor(..._rgb(LINE)); doc.setLineWidth(0.6); doc.line(PAGE.m, y + 16, PAGE.w - PAGE.m, y + 16);
    y += 28;
  });

  // cover details grid
  y += 8;
  _font(doc, "body", "bold"); doc.setFontSize(9); doc.setTextColor(..._rgb(FAINT));
  doc.text("COVER DETAILS", PAGE.m, y); y += 14;
  const details = [
    ["Type of cover", isBundle ? "Bundled multi-asset policy" : "Single-asset policy"],
    ["Sum insured", money(sumInsured)],
    ["Policy excess", "10% of each claim"],
    ["Premium (annual)", money(premium)],
    ["Geographic scope", "Nigeria"],
    ["Regulator", "NAICOM"],
  ];
  details.forEach(([k, v], i) => {
    const col = i % 2, row = Math.floor(i / 2);
    const x = PAGE.m + col * 258, ry = y + row * 22;
    _font(doc, "body"); doc.setFontSize(9); doc.setTextColor(..._rgb(MUTED));
    doc.text(k, x, ry + 12);
    if (v.startsWith("NGN")) { _font(doc, "mono"); doc.setFontSize(8.5); } else { _font(doc, "body", "bold"); }
    doc.setTextColor(..._rgb(INK));
    doc.text(v, x + 242, ry + 12, { align: "right" });
    doc.setDrawColor(..._rgb(LINE)); doc.setLineWidth(0.5); doc.line(x, ry + 18, x + 242, ry + 18);
  });
  y += 3 * 22 + 16;

  // sum-insured banner
  doc.setFillColor(..._rgb(accent)); doc.roundedRect(PAGE.m, y, PAGE.w - 2 * PAGE.m, 56, 10, 10, "F");
  doc.setTextColor(255, 255, 255);
  _font(doc, "body", "bold"); doc.setFontSize(7.5);
  doc.text("ANNUAL SUM INSURED", PAGE.m + 18, y + 22);
  doc.text("VAULTREWARDS (UP TO 7%)", PAGE.w - PAGE.m - 18, y + 22, { align: "right" });
  _font(doc, "mono"); doc.setFontSize(16);
  doc.text(money(sumInsured), PAGE.m + 18, y + 42);
  doc.text(money(Math.round(premium * 0.07)), PAGE.w - PAGE.m - 18, y + 42, { align: "right" });
  y += 80;

  // signatures + stamp
  doc.setDrawColor(..._rgb(INK)); doc.setLineWidth(1);
  doc.line(PAGE.m, y, PAGE.m + 150, y);
  doc.line(PAGE.m + 180, y, PAGE.m + 330, y);
  _font(doc, "body", "bold"); doc.setFontSize(9); doc.setTextColor(..._rgb(INK));
  doc.text("For " + insurer, PAGE.m, y + 12);
  doc.text(_company().legalName, PAGE.m + 180, y + 12);
  _font(doc, "body"); doc.setFontSize(7.5); doc.setTextColor(..._rgb(MUTED));
  doc.text("Authorised signatory", PAGE.m, y + 22);
  doc.text("Issued via Cova", PAGE.m + 180, y + 22);
  // stamp
  const sx = PAGE.w - PAGE.m - 44, sy = y - 6;
  doc.setDrawColor(..._rgb(accent)); doc.setLineWidth(2); doc.circle(sx, sy, 30, "S");
  doc.setTextColor(..._rgb(accent)); _font(doc, "display"); doc.setFontSize(10);
  doc.text("COVERED", sx, sy - 1, { align: "center" });
  doc.setFontSize(6.5); doc.text("VAULTCOVA", sx, sy + 9, { align: "center" });

  _footer(doc, "Evidence the listed asset(s) are insured under the stated policy.\nSubject to the full policy wording. Generated " + _today());
  doc.save(_slug(docName) + (isBundle ? "-Bundled-Policy-Certificate" : "-Policy-Certificate") + ".pdf");
}

/* ============ Payment Receipt ============ */
async function downloadInvoice(inv, userName, profile) {
  if (!window.jspdf) return;
  await _ensureFonts();
  const accent = _accentHex();
  const doc = _newDoc();

  _header(doc, accent, "Payment Receipt", inv.id || _ref("INV"));
  _metaRow(doc, [
    ["Billed to", userName || "Account holder"],
    ["Account type", profile === "business" ? "Business" : "Personal"],
    ["Date", inv.date || _today()],
    ["Status", inv.status || "Paid"],
  ], 100);

  // amount banner
  let y = 132;
  doc.setFillColor(..._rgb(SOFT)); doc.setDrawColor(..._rgb(LINE)); doc.setLineWidth(0.8);
  doc.roundedRect(PAGE.m, y, PAGE.w - 2 * PAGE.m, 62, 10, 10, "FD");
  _font(doc, "body", "bold"); doc.setFontSize(7.5); doc.setTextColor(..._rgb(FAINT));
  doc.text("AMOUNT PAID", PAGE.m + 16, y + 22);
  _font(doc, "mono"); doc.setFontSize(19); doc.setTextColor(..._rgb(INK));
  doc.text(money(inv.amt), PAGE.m + 16, y + 46);
  _font(doc, "body", "bold"); doc.setFontSize(10); doc.setTextColor(..._rgb(GREEN));
  doc.text((inv.status || "Paid").toUpperCase(), PAGE.w - PAGE.m - 16, y + 40, { align: "right" });
  y += 86;

  // line item
  _font(doc, "body", "bold"); doc.setFontSize(7.5); doc.setTextColor(..._rgb(FAINT));
  doc.text("DESCRIPTION", PAGE.m, y);
  doc.text("AMOUNT", PAGE.w - PAGE.m, y, { align: "right" });
  doc.setDrawColor(..._rgb("#d4d8e8")); doc.setLineWidth(1); doc.line(PAGE.m, y + 6, PAGE.w - PAGE.m, y + 6);
  y += 22;
  _font(doc, "body"); doc.setFontSize(9.5); doc.setTextColor(..._rgb(INK));
  doc.text("Monthly insurance premium", PAGE.m, y);
  _font(doc, "mono"); doc.setFontSize(9);
  doc.text(money(inv.amt), PAGE.w - PAGE.m, y, { align: "right" });
  doc.setDrawColor(..._rgb(INK)); doc.setLineWidth(1.2); doc.line(PAGE.m, y + 14, PAGE.w - PAGE.m, y + 14);
  y += 30;
  _font(doc, "body", "bold"); doc.setFontSize(10); doc.setTextColor(..._rgb(INK));
  doc.text("Total paid", PAGE.m, y);
  _font(doc, "mono"); doc.setFontSize(9.5);
  doc.text(money(inv.amt), PAGE.w - PAGE.m, y, { align: "right" });

  _footer(doc, "This is a payment receipt, not a contract of insurance.\nGenerated by Cova · " + _today());
  doc.save(_slug(userName) + "-Receipt-" + (inv.id || "invoice") + ".pdf");
}

// Kick the font fetch off early so the first download is instant.
if (typeof window !== "undefined" && window.fetch) _ensureFonts();

Object.assign(window, { downloadSchedule, downloadCertificate, downloadInvoice });
