// ============================================================
// Nami Order Portal — pdfmake parallel renderer (Option B)
//
// Builds a complete Service Order package PDF using pdfmake's
// declarative docDefinition model. Exposes window.NAMI_PDFMAKE.generate
// for the toolbar to call. Pdfmake handles its own pagination, fixed
// headers/footers, and table flow — no chunker or @media print CSS
// involved.
//
// Note on fonts: pdfmake's default vfs ships with Roboto. Switching to
// Gilroy would require fetching the OTF files and registering them in
// pdfMake.vfs as base64. For the spike we use Roboto so the comparison
// is about pagination quality / output fidelity, not font face.
// ============================================================

(function () {
  // ---- Gilroy font loader -------------------------------------------------
  // pdfmake ships with Roboto in its default vfs. To match the
  // browser-print path's typography, we fetch the Gilroy OTFs and
  // append them to pdfMake.vfs at runtime, then register the family.
  //
  // Caveat: Nami's Gilroy bundle ships Regular/Medium/SemiBold/Bold/ExtraBold
  // but no italic faces. Italics fall back to Regular/Bold (the slant
  // distinction will be lost, but the text still renders in Gilroy).
  let gilroyReady = null;
  const ensureGilroy = async () => {
    if (gilroyReady) return gilroyReady;
    gilroyReady = (async () => {
      if (!window.pdfMake) return false;
      // Load Regular (400) + SemiBold (600) + Bold (700). pdfmake's
      // font definition only has `normal` / `bold` slots per family, so
      // we register TWO families to expose SemiBold:
      //   - Gilroy        → normal=Regular, bold=Bold
      //   - GilroyMed     → normal=Regular, bold=SemiBold (slimmer "bold")
      // Use `font: "GilroyMed", bold: true` for SemiBold runs.
      const faces = [
        { file: "Gilroy-Regular.otf" },
        { file: "Gilroy-SemiBold.otf" },
        { file: "Gilroy-Bold.otf" },
      ];
      try {
        for (const f of faces) {
          if (window.pdfMake.vfs && window.pdfMake.vfs[f.file]) continue;
          const resp = await fetch(`assets/fonts/${f.file}`);
          if (!resp.ok) throw new Error(`Failed to load ${f.file}: ${resp.status}`);
          const buf = await resp.arrayBuffer();
          const bytes = new Uint8Array(buf);
          let binary = "";
          const CHUNK = 0x8000;
          for (let i = 0; i < bytes.length; i += CHUNK) {
            binary += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
          }
          window.pdfMake.vfs = window.pdfMake.vfs || {};
          window.pdfMake.vfs[f.file] = btoa(binary);
        }
        window.pdfMake.fonts = window.pdfMake.fonts || {};
        window.pdfMake.fonts.Gilroy = {
          normal: "Gilroy-Regular.otf",
          bold: "Gilroy-Bold.otf",
          italics: "Gilroy-Regular.otf",
          bolditalics: "Gilroy-Bold.otf",
        };
        window.pdfMake.fonts.GilroyMed = {
          normal: "Gilroy-Regular.otf",
          bold: "Gilroy-SemiBold.otf",
          italics: "Gilroy-Regular.otf",
          bolditalics: "Gilroy-SemiBold.otf",
        };
        return true;
      } catch (err) {
        console.warn("Gilroy load failed, falling back to Roboto:", err);
        return false;
      }
    })();
    return gilroyReady;
  };

  const C = {
    navy: "#021F3E",
    alloy: "#0B4A8E",
    blueBright: "#1374DE",
    aqua: "#67D6E0",
    accentRed: "#D83A58",
    darkGrey: "#516372",
    strokes: "#DDDDDD",
    smoke: "#F5F8FA",
    alice: "#EFF4F7",
    white: "#FFFFFF",
  };

  // ---- SVG asset cache ----------------------------------------------------
  // pdfmake renders SVG natively (via svg-to-pdfkit), so we can embed the
  // real Nami wordmark and a CSS-equivalent gradient hero. Fetch the
  // wordmark SVGs once and inline them.
  const svgCache = {};
  const loadSvg = async (path) => {
    if (svgCache[path]) return svgCache[path];
    try {
      const r = await fetch(path);
      if (!r.ok) throw new Error(`${path} ${r.status}`);
      let text = await r.text();
      // svg-to-pdfkit can't reliably resolve forward `clip-path` URL
      // references when the matching `<clipPath>` is defined in `<defs>`
      // later in the document (e.g. Figma export pattern). The wordmark
      // doesn't need clipping — the path data is already inside the
      // viewBox — so strip both the references AND the defs to avoid
      // any clipping artefacts.
      text = text.replace(/\s*clip-path="url\([^)]+\)"/g, "");
      text = text.replace(/<clipPath[\s\S]*?<\/clipPath>/g, "");
      text = text.replace(/<defs>\s*<\/defs>/g, "");
      svgCache[path] = text;
      return svgCache[path];
    } catch (err) {
      console.warn("SVG load failed:", path, err);
      svgCache[path] = null;
      return null;
    }
  };

  // 135° hero gradient matching the CSS `--gradient-hero` token:
  // linear-gradient(135deg, #021F3E 0%, #0B4A8E 40%, #1374DE 75%, #67D6E0 100%).
  // In SVG the 135° direction maps to x1=0 y1=0 → x2=1 y2=1 (top-left → bottom-right).
  const COVER_GRADIENT_SVG = `
    <svg xmlns="http://www.w3.org/2000/svg" width="612" height="792" viewBox="0 0 612 792" preserveAspectRatio="none">
      <defs>
        <linearGradient id="namiHero" x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%"   stop-color="#021F3E"/>
          <stop offset="40%"  stop-color="#0B4A8E"/>
          <stop offset="75%"  stop-color="#1374DE"/>
          <stop offset="100%" stop-color="#67D6E0"/>
        </linearGradient>
      </defs>
      <rect x="0" y="0" width="612" height="792" fill="url(#namiHero)"/>
    </svg>
  `;

  // Frosted-glass-style rounded rect (matches the cover summary card +
  // doc inventory in the browser-print path). 7% white fill + 16% white
  // stroke approximates the CSS `rgba(255,255,255,0.07)` /
  // `rgba(255,255,255,0.16)` over the navy gradient.
  const frostedFrameSvg = (w, h, r = 6) => `
    <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
      <rect x="0.5" y="0.5" width="${w - 1}" height="${h - 1}" rx="${r}" ry="${r}"
            fill="white" fill-opacity="0.07"
            stroke="white" stroke-opacity="0.18" stroke-width="1"/>
    </svg>
  `;

  const fmtDate = (iso) => {
    if (!iso) return "—";
    const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso));
    const d = m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : new Date(iso);
    return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
  };
  const money = (n) => n == null ? "$0" : `$${Math.round(n).toLocaleString("en-US")}`;
  const fmtImp = (n) => n == null ? "—" : `${(n / 1_000_000).toFixed(n < 10_000_000 ? 1 : 0)}M`;
  const computeEndDate = (startISO, termYears) => {
    if (!startISO || !termYears) return null;
    const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(startISO));
    if (!m) return null;
    const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
    d.setFullYear(d.getFullYear() + termYears);
    d.setDate(d.getDate() - 1);
    const yyyy = d.getFullYear(), mm = String(d.getMonth() + 1).padStart(2, "0"), dd = String(d.getDate()).padStart(2, "0");
    return `${yyyy}-${mm}-${dd}`;
  };
  const titlecase = (s) => s ? s[0].toUpperCase() + s.slice(1) : "";

  // Skip the top edge (above first row) and bottom edge (below last
  // row) of a pdfmake table — matches the HTML/print convention where
  // only interior row dividers are drawn. Use as the `hLineWidth`
  // function in any table layout that should have inter-row dividers
  // without an outer top/bottom frame.
  const innerOnlyHLine = (width = 0.5) => (i, node) =>
    i === 0 || i === node.table.body.length ? 0 : width;

  // ---- Markdown → pdfmake content nodes -----------------------------------
  // Mirrors public/legal-md.js: handles ##/###/#### headings, paragraphs,
  // unordered lists, GFM tables, inline bold/italic, autolinks.
  const inlineToContent = (raw) => {
    // Returns a single string OR an array of text-runs with styling for
    // pdfmake's text node syntax.
    const out = [];
    const boldParts = String(raw).split(/(\*\*[^*]+\*\*)/g);
    for (const part of boldParts) {
      if (!part) continue;
      if (part.startsWith("**") && part.endsWith("**")) {
        out.push({ text: part.slice(2, -2), bold: true });
      } else {
        const itParts = part.split(/(\*[^*\n]+\*)/g);
        for (const sub of itParts) {
          if (!sub) continue;
          if (/^\*[^*\n]+\*$/.test(sub)) {
            out.push({ text: sub.slice(1, -1), italics: true });
          } else {
            const urlParts = sub.split(/(https?:\/\/[^\s)]+)/g);
            for (const u of urlParts) {
              if (!u) continue;
              if (/^https?:\/\//.test(u)) {
                out.push({ text: u, link: u, color: C.alloy });
              } else {
                // Convert `\<space>` (markdown hard break) to a newline.
                out.push(u.replace(/\\ /g, "\n"));
              }
            }
          }
        }
      }
    }
    // Collapse if only one plain string.
    if (out.length === 1 && typeof out[0] === "string") return out[0];
    return out;
  };

  const parseTableRow = (line) =>
    line.replace(/^\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());

  const markdownToContent = (md) => {
    if (!md) return [];
    const lines = String(md).replace(/\r\n?/g, "\n").split("\n");
    const out = [];
    let i = 0;
    let para = [];

    const flushPara = () => {
      if (para.length) {
        out.push({ text: inlineToContent(para.join(" ")), margin: [0, 0, 0, 6], alignment: "justify" });
        para = [];
      }
    };

    while (i < lines.length) {
      const line = lines[i];
      const t = line.trim();

      if (!t) { flushPara(); i++; continue; }
      // Explicit `<!-- pagebreak -->` marker → force pdfmake page break.
      if (/^<!--\s*pagebreak\s*-->$/i.test(t)) {
        flushPara();
        out.push({ text: "", pageBreak: "after" });
        i++; continue;
      }
      if (/^-{3,}$/.test(t)) {
        flushPara();
        out.push({ canvas: [{ type: "line", x1: 0, y1: 0, x2: 500, y2: 0, lineColor: C.strokes }], margin: [0, 6, 0, 6] });
        i++; continue;
      }

      const h = t.match(/^(#{2,6})\s+(.*)$/);
      if (h) {
        flushPara();
        const lvl = h[1].length;
        const headingText = h[2];
        if (lvl === 2) {
          // H2: uppercase, blue alloy, border-bottom — section header
          out.push({
            text: headingText.toUpperCase(),
            color: C.alloy,
            bold: true,
            fontSize: 10.5,
            characterSpacing: 1.2,
            margin: [0, 14, 0, 4],
          });
          out.push({ canvas: [{ type: "line", x1: 0, y1: 0, x2: 500, y2: 0, lineColor: C.strokes }], margin: [0, 0, 0, 6] });
        } else if (lvl === 3) {
          out.push({ text: headingText, color: C.navy, bold: true, fontSize: 11, margin: [0, 8, 0, 4] });
        } else {
          out.push({ text: headingText, color: C.navy, bold: true, fontSize: 10.5, margin: [0, 6, 0, 3] });
        }
        i++; continue;
      }

      // Table — header row + separator + body rows
      if (t.startsWith("|") && i + 1 < lines.length && /^\s*\|?\s*[:\-\s|]+\|?\s*$/.test(lines[i + 1])) {
        flushPara();
        const headerCells = parseTableRow(t);
        i += 2;
        const bodyRows = [];
        while (i < lines.length && lines[i].trim().startsWith("|")) {
          bodyRows.push(parseTableRow(lines[i].trim()));
          i++;
        }
        const colCount = headerCells.length;
        out.push({
          table: {
            headerRows: 1,
            widths: Array(colCount).fill("*"),
            body: [
              headerCells.map((c) => ({ text: c.toUpperCase(), bold: true, fontSize: 9, color: C.darkGrey, fillColor: C.alice, characterSpacing: 0.6 })),
              ...bodyRows.map((row) => row.map((c) => ({ text: inlineToContent(c), fontSize: 10 }))),
            ],
          },
          layout: {
            hLineWidth: innerOnlyHLine(0.5),
            vLineWidth: () => 0,
            hLineColor: () => C.strokes,
            paddingTop: () => 6,
            paddingBottom: () => 6,
            paddingLeft: () => 6,
            paddingRight: () => 6,
          },
          margin: [0, 4, 0, 8],
        });
        continue;
      }

      // Bullet list
      if (/^[-*]\s+/.test(t)) {
        flushPara();
        const items = [];
        while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) {
          items.push(lines[i].trim().replace(/^[-*]\s+/, ""));
          i++;
        }
        out.push({
          ul: items.map((it) => ({ text: inlineToContent(it), margin: [0, 0, 0, 2] })),
          margin: [0, 0, 0, 6],
        });
        continue;
      }

      para.push(t);
      i++;
    }
    flushPara();
    return out;
  };

  // ---- Page chrome: header + footer (pdfmake injects them per page) -----
  const buildHeader = (docLabel) => (currentPage, pageCount, pageSize) => ({
    columns: [
      { text: "NAMI", bold: true, fontSize: 11, characterSpacing: 1, color: C.navy, alignment: "left" },
      { text: docLabel.toUpperCase(), fontSize: 8.5, characterSpacing: 0.8, bold: true, color: C.alloy, alignment: "right" },
    ],
    margin: [56, 28, 56, 0],
  });

  const buildHeaderWithRule = (docLabel) => (currentPage, pageCount, pageSize) => ({
    stack: [
      {
        columns: [
          { text: "NAMI", bold: true, fontSize: 11, characterSpacing: 1, color: C.navy, alignment: "left" },
          { text: docLabel.toUpperCase(), fontSize: 8.5, characterSpacing: 0.8, bold: true, color: C.alloy, alignment: "right" },
        ],
      },
      { canvas: [{ type: "line", x1: 0, y1: 6, x2: pageSize.width - 112, y2: 6, lineColor: C.strokes }] },
    ],
    margin: [56, 28, 56, 0],
  });

  const buildFooter = (isDraft) => (currentPage, pageCount) => ({
    columns: [
      { text: "Confidential — Nami ML Inc.", fontSize: 9, color: C.darkGrey, alignment: "left" },
      {
        alignment: "right",
        columns: [
          {
            width: "*",
            text: "",
          },
          ...(isDraft ? [{
            width: "auto",
            text: "DRAFT",
            color: C.white,
            fillColor: C.accentRed,
            bold: true,
            fontSize: 8,
            characterSpacing: 1,
            margin: [6, 2, 6, 2],
          }] : []),
          {
            width: "auto",
            text: ` ${currentPage} of ${pageCount}`,
            fontSize: 9,
            color: C.darkGrey,
            margin: [4, 2, 0, 2],
          },
        ],
      },
    ],
    margin: [56, 0, 56, 24],
  });

  // ---- Page builders ----------------------------------------------------
  const buildCover = (deal, amendments, isDraft, pages, assets) => {
    const inventory = pages;
    const wordmarkWhite = assets?.wordmarkWhite || null;
    const CARD_W = 500;
    // Compute heights for the frosted-box backgrounds drawn behind
    // each card. These are rendered as IN-FLOW canvas rects, with the
    // table content overlapping via a negative top-margin — so the
    // alignment auto-tracks pdfmake's natural flow.
    // Empirically tuned to what pdfmake actually renders (its row
    // height calculation runs a bit larger than the font-size × line-
    // height arithmetic suggests). Generous enough that Signatures
    // doesn't escape the frame, snug enough that the box hugs content.
    const SUMMARY_H = 122;
    const INVENTORY_H = 22 + Math.max(1, inventory.length) * 44;

    return [
      {
        // Full-bleed cover gradient (matches CSS --gradient-hero) —
        // 135° linear gradient #021F3E → #0B4A8E → #1374DE → #67D6E0.
        svg: COVER_GRADIENT_SVG,
        width: 612,
        height: 792,
        absolutePosition: { x: 0, y: 0 },
      },
      // Cover content flows naturally inside page margins; no
      // absolutePosition so pdfmake handles pagination correctly.
      wordmarkWhite
        ? { svg: wordmarkWhite, fit: [70, 18], margin: [0, 14, 0, 14] }
        : { text: "NAMI", bold: true, fontSize: 14, characterSpacing: 1.4, color: C.white, margin: [0, 14, 0, 14] },
      { text: "SERVICE ORDER PACKAGE", fontSize: 9, characterSpacing: 1.6, bold: true, color: C.aqua, margin: [0, 0, 0, 4] },
      { text: "Subscription Services Agreement", fontSize: 26, bold: true, color: C.white, margin: [0, 0, 0, 4], lineHeight: 1.05 },
      { text: deal.customer || "Customer", fontSize: 14, bold: true, color: C.white, margin: [0, 0, 0, 2] },
      {
        text: isDraft
          ? "Effective TBD — TBD. Prepared by Nami ML Inc."
          : deal.endDate
            ? `Effective ${fmtDate(deal.startDate)} — ${fmtDate(deal.endDate)}. Prepared by Nami ML Inc.`
            : `Effective ${fmtDate(deal.startDate)}. Prepared by Nami ML Inc.`,
        fontSize: 9.5,
        color: "#cad8e8",
        margin: [0, 0, 0, 12],
      },

      // Summary card — frosted background canvas (in flow, takes
      // SUMMARY_H of vertical space), then table content overlapping
      // via negative top-margin.
      // Frosted summary-card frame: real translucent white SVG over the
      // gradient (pdfmake's canvas can't do fillOpacity, so we use SVG
      // which does).
      { svg: frostedFrameSvg(CARD_W, SUMMARY_H, 6) },
      {
        // Match HTML version: labels are LIGHTER (not bold) tracked-out
        // uppercase in a faded blue-white; values are 13pt semibold in
        // pure white. Less heavy than the previous fontSize 11 bold.
        table: {
          widths: ["*", "*"],
          body: [
            [
              { stack: [{ text: "PLAN", fontSize: 8, characterSpacing: 1, color: "#a4b6cc" }, { text: titlecase(deal.trim || "premier"), fontSize: 12, bold: true, font: "GilroyMed", color: C.white, margin: [0, 2, 0, 0] }], border: [false, false, false, false] },
              { stack: [{ text: "TERM", fontSize: 8, characterSpacing: 1, color: "#a4b6cc" }, { text: `${deal.termYears || 1} year${(deal.termYears || 1) === 1 ? "" : "s"}`, fontSize: 12, bold: true, font: "GilroyMed", color: C.white, margin: [0, 2, 0, 0] }], border: [false, false, false, false] },
            ],
            [
              { stack: [{ text: "TOTAL CONTRACT VALUE", fontSize: 8, characterSpacing: 1, color: "#a4b6cc" }, { text: money(deal.tcv), fontSize: 12, bold: true, font: "GilroyMed", color: C.white, margin: [0, 2, 0, 0] }], border: [false, false, false, false] },
              { stack: [{ text: "VOLUME COMMIT", fontSize: 8, characterSpacing: 1, color: "#a4b6cc" }, { text: fmtImp((deal.commits || [])[0]), fontSize: 12, bold: true, font: "GilroyMed", color: C.white, margin: [0, 2, 0, 0] }], border: [false, false, false, false] },
            ],
            [
              { stack: [{ text: "SUPPORT LEVEL", fontSize: 8, characterSpacing: 1, color: "#a4b6cc" }, { text: titlecase(deal.support || "standard"), fontSize: 12, bold: true, font: "GilroyMed", color: C.white, margin: [0, 2, 0, 0] }], border: [false, false, false, false] },
              { stack: [{ text: "GOVERNING LAW", fontSize: 8, characterSpacing: 1, color: "#a4b6cc" }, { text: ({ NY: "New York", CA: "California", DE: "Delaware" }[(deal.variants || {})["v1-governing-law"]]) || "New York", fontSize: 12, bold: true, font: "GilroyMed", color: C.white, margin: [0, 2, 0, 0] }], border: [false, false, false, false] },
            ],
          ],
        },
        layout: {
          hLineWidth: () => 0,
          vLineWidth: () => 0,
          // Tighter than the previous 6/6 to match HTML's compact
          // row rhythm inside the summary card.
          paddingTop: () => 3,
          paddingBottom: () => 3,
          paddingLeft: () => 14,
          paddingRight: () => 14,
        },
        margin: [0, -SUMMARY_H, 0, 10],
      },

      // Frosted doc-inventory frame.
      { svg: frostedFrameSvg(CARD_W, INVENTORY_H, 6) },
      {
        table: {
          widths: ["*", 56],
          body: [
            [
              // Header row — no cell border (layout handles separators).
              { text: "DOCUMENTS IN THIS PACKAGE", fontSize: 8, characterSpacing: 1.4, color: "#b8c6d8", colSpan: 2, border: [false, false, false, false] },
              {},
            ],
            ...inventory.map((doc) => [
              // Font sizes tuned to match HTML's 12.5px SemiBold for
              // the label (≈ 10pt) and 10.5px regular for the sub
              // (≈ 8pt). Layout draws the inter-row dividers; cell
              // borders OFF to avoid the double-line artefact.
              { stack: [{ text: doc.label, fontSize: 10, bold: true, font: "GilroyMed", color: C.white }, { text: doc.sub, fontSize: 8, color: "#a4b6cc", margin: [0, 1, 0, 0] }], border: [false, false, false, false] },
              { text: `p. ${doc.page}`, fontSize: 10, font: "GilroyMed", bold: true, color: "#cad8e8", alignment: "right", border: [false, false, false, false], margin: [0, 4, 0, 0] },
            ]),
          ],
        },
        layout: {
          // Interior-only dividers, bright enough to read against the
          // brighter parts of the cover gradient. HTML uses
          // rgba(255,255,255,0.10) — we approximate with a near-white
          // line at a thicker stroke.
          hLineWidth: (i, node) => {
            if (i === 0 || i === 1 || i === node.table.body.length) return 0;
            return 1;
          },
          vLineWidth: () => 0,
          hLineColor: () => "#c8d4e6",
          paddingTop: () => 4,
          paddingBottom: () => 4,
          paddingLeft: () => 14,
          paddingRight: () => 14,
        },
        margin: [0, -INVENTORY_H, 0, 12],
      },

      // Force page break after cover so the gradient bg doesn't bleed
      // onto the Service Order page. The footer (incl. cover-specific
      // styling) is rendered by the top-level `footer:` callback so it
      // has access to the correct pageCount.
      { text: "", pageBreak: "after" },
    ];
  };

  const buildServiceOrderCustomer = (deal) => {
    const PRICING = window.NAMI_PRICING;
    const trims = window.NAMI_DATA.TRIM_TIERS;
    const trim = trims[deal.trim || "premier"];
    const features = PRICING.featuresForTrim(trim.key);
    const supportLabel = titlecase(deal.support || "standard");
    const supportFeatures = PRICING.SUPPORT_FEATURES[deal.support || "standard"] || [];
    const half = Math.ceil(features.length / 2);
    const halfS = Math.ceil(supportFeatures.length / 2);
    return [
      { text: "Service Order", fontSize: 22, bold: true, color: C.navy, margin: [0, 8, 0, 2] },
      { canvas: [{ type: "rect", x: 0, y: 0, w: 56, h: 3, color: C.blueBright }], margin: [0, 0, 0, 14] },
      {
        text: `Customer subscribes to Nami Services for the Subscription Term and Fees specified below, and Nami agrees to provide the Nami Services specified pursuant to the Nami Master Services Agreement (the "MSA") as modified by the Amendment Addendum attached hereto. In the event of conflict between this Service Order and the MSA, the MSA shall govern except as modified by the Amendment Addendum.`,
        fontSize: 10, color: C.darkGrey, alignment: "justify", margin: [0, 0, 0, 14],
      },

      { text: "CUSTOMER", color: C.alloy, bold: true, fontSize: 10.5, characterSpacing: 1.2, margin: [0, 4, 0, 4] },
      { canvas: [{ type: "line", x1: 0, y1: 0, x2: 500, y2: 0, lineColor: C.strokes }], margin: [0, 0, 0, 6] },
      {
        table: {
          widths: [120, "*"],
          body: [
            [{ text: "Legal Entity", color: C.darkGrey }, deal.customerLegal || deal.customer || "—"],
            [{ text: "Address", color: C.darkGrey }, deal.customerAddress || "—"],
            [{ text: "Primary Contact", color: C.darkGrey }, `${deal.primaryContact?.name || "—"}${deal.primaryContact?.email ? ` — ${deal.primaryContact.email}` : ""}`],
            [{ text: "A/P Email", color: C.darkGrey }, deal.billingContact?.email || "—"],
            [{ text: "Authorized Signer", color: C.darkGrey }, `${deal.contractAdmin?.name || "—"}${deal.contractAdmin?.title ? ` — ${deal.contractAdmin.title}` : ""}${deal.contractAdmin?.email ? ` — ${deal.contractAdmin.email}` : ""}`],
            [{ text: "Legal Notices", color: C.darkGrey }, deal.noticeAddress || "—"],
          ],
        },
        layout: {
          hLineWidth: innerOnlyHLine(0.5),
          vLineWidth: () => 0,
          hLineColor: () => C.strokes,
          paddingTop: () => 6,
          paddingBottom: () => 6,
        },
        fontSize: 10.5,
        margin: [0, 0, 0, 16],
      },

      { text: "SERVICES DESCRIPTION", color: C.alloy, bold: true, fontSize: 10.5, characterSpacing: 1.2, margin: [0, 8, 0, 4] },
      { canvas: [{ type: "line", x1: 0, y1: 0, x2: 500, y2: 0, lineColor: C.strokes }], margin: [0, 0, 0, 8] },

      { text: `Nami ${trim.name} — Subscription Services`, color: C.navy, bold: true, fontSize: 11, margin: [0, 4, 0, 4] },
      {
        columns: [
          { ul: features.slice(0, half).map((f) => ({ text: f, fontSize: 10.5 })), width: "*" },
          { ul: features.slice(half).map((f) => ({ text: f, fontSize: 10.5 })), width: "*" },
        ],
        columnGap: 18,
        margin: [0, 0, 0, 12],
      },

      { text: `${supportLabel} - Service Level`, color: C.navy, bold: true, fontSize: 11, margin: [0, 4, 0, 4] },
      {
        columns: [
          { ul: supportFeatures.slice(0, halfS).map((f) => ({ text: f, fontSize: 10.5 })), width: "*" },
          { ul: supportFeatures.slice(halfS).map((f) => ({ text: f, fontSize: 10.5 })), width: "*" },
        ],
        columnGap: 18,
      },

      { text: "", pageBreak: "after" },
    ];
  };

  const buildServiceOrderFees = (deal, isDraft) => {
    const PRICING = window.NAMI_PRICING;
    const trims = window.NAMI_DATA.TRIM_TIERS;
    const trim = trims[deal.trim || "premier"];
    const commits = deal.commits || [trim.minCommit * 1_000_000];
    const cpm = deal.cpm || trim.cpmRepresentative;
    const { tcv, subscriptionTotal, conciergeTotal, carrierTotal, billingDiscountAmount, multiyrDiscountAmount, psTotal } =
      PRICING.computeDealTcv(deal);
    const billing = deal.cadence || "annual";
    const v11 = (deal.variants || {})["v11-payment-terms"];
    const netTerms = deal.netTerms || (v11 === "net45" ? "Net 45" : v11 === "net60" ? "Net 60" : "Net 30");
    const netDays = parseInt((netTerms || "").replace(/\D/g, ""), 10) || 30;
    const netWords = netDays === 30 ? "thirty (30)" : netDays === 45 ? "forty-five (45)" : netDays === 60 ? "sixty (60)" : `${netDays}`;
    const termYears = deal.termYears || 1;
    const carrierPerYear = (deal.addons || []).includes("carrier-grade") ? PRICING.CARRIER_PRICE : 0;
    const showConc = conciergeTotal > 0;
    const showCarr = carrierTotal > 0;
    const startStr = fmtDate(deal.startDate);
    const currencyWord = (deal.currency || "USD") === "USD" ? "United States Dollars" : (deal.currency || "USD");

    let firstPara;
    if (billing === "usage") {
      firstPara = `Fees will be invoiced monthly in arrears based on actual measured impressions during the preceding calendar month at the CPM rate of $${cpm.toFixed(2)} per 1,000 impressions set forth above, and are payable Net ${netWords} days from the date of invoice. All Fees are denominated in ${currencyWord}.`;
    } else if (billing === "quarterly") {
      firstPara = `The Total Contract Value set forth above will be divided into four (4) equal quarterly installments${termYears > 1 ? " per year of the Subscription Term" : ""} and invoiced quarterly in advance. The first installment will be invoiced on the Start Date (${startStr}); each subsequent installment will be invoiced no less than thirty (30) days prior to the start of the next calendar quarter. Each installment is payable Net ${netWords} days from the date of invoice. Usage in excess of the annual committed volume in any year will be invoiced monthly in arrears at the CPM rate of $${cpm.toFixed(2)} per 1,000 impressions set forth above and is payable Net ${netWords} days from the date of invoice. All Fees are denominated in ${currencyWord}.`;
    } else {
      const y2 = termYears === 3
        ? ` Subscription Fees for Year 2 and Year 3 will each be invoiced no less than thirty (30) days prior to the applicable anniversary of the Start Date and are payable Net ${netWords} days from the date of invoice.`
        : termYears === 2
          ? ` Subscription Fees for Year 2 will be invoiced no less than thirty (30) days prior to the applicable anniversary of the Start Date and are payable Net ${netWords} days from the date of invoice.`
          : "";
      firstPara = `Year 1 Subscription Fees will be invoiced on the Start Date (${startStr}) and are payable Net ${netWords} days from the date of invoice.${y2} Usage in excess of the annual committed volume in any year will be invoiced monthly in arrears at the CPM rate of $${cpm.toFixed(2)} per 1,000 impressions set forth above and is payable Net ${netWords} days from the date of invoice. All Fees are denominated in ${currencyWord}.`;
    }

    // Fees table
    const headerCols = ["Year", "Volume commit", "Subscription"];
    if (showConc) headerCols.push("Concierge");
    if (showCarr) headerCols.push("Carrier");
    headerCols.push("Subtotal");
    const widths = ["auto", "*", "auto"];
    if (showConc) widths.push("auto");
    if (showCarr) widths.push("auto");
    widths.push("auto");

    const rows = commits.map((c, i) => {
      const sub = (c / 1000) * cpm;
      const conc = PRICING.conciergePriceFor(sub, deal);
      const subtotal = sub + conc + carrierPerYear;
      const r = [`Year ${i + 1}`, fmtImp(c), money(sub)];
      if (showConc) r.push(money(conc));
      if (showCarr) r.push(money(carrierPerYear));
      r.push({ text: money(subtotal), alignment: "right" });
      return r;
    });

    const spanWidth = headerCols.length - 1;
    const totalsRow = (label, value, opts = {}) => {
      const cells = [{ text: label, colSpan: spanWidth, ...opts }];
      for (let i = 1; i < spanWidth; i++) cells.push({});
      cells.push({ text: value, alignment: "right", ...opts });
      return cells;
    };

    const body = [
      headerCols.map((h) => ({ text: h.toUpperCase(), bold: true, fontSize: 9, color: C.darkGrey, fillColor: C.alice, characterSpacing: 0.6 })),
      ...rows,
      totalsRow("Subtotal before discounts", money(subscriptionTotal + conciergeTotal + carrierTotal), { bold: true, fillColor: C.smoke }),
    ];
    if (billingDiscountAmount > 0) {
      body.push(totalsRow(
        billing === "annual" ? "Commit-upfront savings (25% off)" :
        billing === "quarterly" ? "Quarterly billing (10% off)" : "Billing discount",
        `−${money(billingDiscountAmount)}`,
      ));
    }
    if (multiyrDiscountAmount > 0) {
      body.push(totalsRow("Three-year term (5% off)", `−${money(multiyrDiscountAmount)}`));
    }
    if (psTotal > 0) {
      body.push(totalsRow("Professional Services (full price)", money(psTotal)));
    }
    body.push(totalsRow("Total Contract Value", money(tcv), { bold: true, fontSize: 11 }));

    return [
      { text: "SUBSCRIPTION TERM", color: C.alloy, bold: true, fontSize: 10.5, characterSpacing: 1.2, margin: [0, 8, 0, 4] },
      { canvas: [{ type: "line", x1: 0, y1: 0, x2: 500, y2: 0, lineColor: C.strokes }], margin: [0, 0, 0, 6] },
      {
        table: {
          widths: [120, "*"],
          body: [
            [{ text: "Term length", color: C.darkGrey }, `${termYears} year${termYears === 1 ? "" : "s"}`],
            [{ text: "Start date", color: C.darkGrey }, isDraft ? "TBD" : fmtDate(deal.startDate)],
            [{ text: "End date", color: C.darkGrey }, isDraft ? "TBD" : fmtDate(deal.endDate)],
            [{ text: "Renewal", color: C.darkGrey }, "Auto-renew for successive 12-month period per MSA §8.1."],
          ],
        },
        layout: { hLineWidth: innerOnlyHLine(0.5), vLineWidth: () => 0, hLineColor: () => C.strokes, paddingTop: () => 6, paddingBottom: () => 6 },
        fontSize: 10.5,
        margin: [0, 0, 0, 16],
      },

      { text: "VOLUME COMMITMENT & SUBSCRIPTION FEES", color: C.alloy, bold: true, fontSize: 10.5, characterSpacing: 1.2, margin: [0, 8, 0, 4] },
      { canvas: [{ type: "line", x1: 0, y1: 0, x2: 500, y2: 0, lineColor: C.strokes }], margin: [0, 0, 0, 6] },
      {
        table: { headerRows: 1, widths, body },
        layout: { hLineWidth: innerOnlyHLine(0.5), vLineWidth: () => 0, hLineColor: () => C.strokes, paddingTop: () => 6, paddingBottom: () => 6, paddingLeft: () => 6, paddingRight: () => 6 },
        fontSize: 10,
        margin: [0, 0, 0, 4],
      },
      { text: `CPM rate: $${cpm.toFixed(2)} per 1,000 impressions. Usage above the committed volume is invoiced monthly in arrears at the same CPM.`, fontSize: 9.5, color: C.darkGrey, margin: [0, 0, 0, 14] },

      { text: "PAYMENT TERMS", color: C.alloy, bold: true, fontSize: 10.5, characterSpacing: 1.2, margin: [0, 8, 0, 4] },
      { canvas: [{ type: "line", x1: 0, y1: 0, x2: 500, y2: 0, lineColor: C.strokes }], margin: [0, 0, 0, 6] },
      { text: firstPara, fontSize: 10.5, alignment: "justify", margin: [0, 0, 0, 8] },
      { text: `Customer shall pay undisputed invoices within Net ${netWords} days of receipt. In the event Customer disputes any portion of an invoice, Customer shall notify Nami in writing of the disputed items prior to the due date and shall pay all undisputed amounts in accordance with this Service Order. The parties shall use commercially reasonable efforts to resolve any disputed items promptly.`, fontSize: 10.5, alignment: "justify", margin: [0, 0, 0, 8] },
      { text: "Past-due amounts accrue interest at the lesser of 1.5% per month or the maximum rate permitted by applicable law, calculated from the original due date until paid in full, as set forth in MSA §1.3.", fontSize: 9.5, alignment: "justify" },

      { text: "", pageBreak: "after" },
    ];
  };

  const buildAmendment = (deal, amendments) => {
    if (!amendments?.length) return [];
    return [
      { text: "Amendment Addendum", fontSize: 22, bold: true, color: C.navy, margin: [0, 8, 0, 2] },
      { canvas: [{ type: "rect", x: 0, y: 0, w: 56, h: 3, color: C.blueBright }], margin: [0, 0, 0, 14] },
      {
        text: `This Amendment Addendum supplements the Nami Master Services Agreement entered into between Nami ML Inc. and ${deal.customerLegal || deal.customer || "Customer"} effective ${fmtDate(deal.startDate)}. Capitalized terms not defined here carry the meanings in the MSA. In the event of conflict between this Amendment Addendum and the MSA, this Amendment Addendum prevails. All other provisions of the MSA remain in full force and effect.`,
        fontSize: 10, color: C.darkGrey, alignment: "justify", margin: [0, 0, 0, 16],
      },
      ...amendments.map((a, i) => {
        const num = (a.id || "").match(/^v(\d+)/)?.[1] || "";
        return {
          stack: [
            { text: `Amendment §${i + 1} — ${a.category}`, fontSize: 10, bold: true, color: C.navy, margin: [0, 0, 0, 2] },
            { text: `MODIFIES ${(a.section || "").toUpperCase()}`, fontSize: 9, characterSpacing: 0.6, bold: true, color: C.darkGrey, margin: [0, 0, 0, 6] },
            { text: `"${a.text}"`, italics: true, fontSize: 10.5, lineHeight: 1.55 },
          ],
          fillColor: C.smoke,
          margin: [0, 0, 0, 10],
          // pdfmake doesn't support per-stack border-left; approximate with margin
        };
      }),
      {
        text: [
          { text: "Standard terms preserved. ", bold: true, color: C.navy },
          "Apart from the modifications set forth above, all terms of the Nami Master Services Agreement, the Data Protection Addendum, and the Service Level Addendum remain in full force and effect.",
        ],
        fontSize: 10.5,
        fillColor: C.smoke,
        margin: [0, 8, 0, 0],
      },
      { text: "", pageBreak: "after" },
    ];
  };

  const buildSLASummary = (deal) => {
    const isConcierge = (deal.support || "standard") === "concierge";
    const supportLabel = isConcierge ? "Concierge" : "Standard";
    return [
      { text: "Service Level Summary", fontSize: 22, bold: true, color: C.navy, margin: [0, 8, 0, 2] },
      { canvas: [{ type: "rect", x: 0, y: 0, w: 56, h: 3, color: C.blueBright }], margin: [0, 0, 0, 14] },
      {
        text: [
          `Customer's Service Level under the Nami Service Level Addendum is `,
          { text: supportLabel, bold: true },
          `. The ${supportLabel} Service Level provides 24×7×365 support with the response-time commitments and uptime guarantees set forth below. This Service Level Addendum is incorporated into and forms part of the Agreement.`,
        ],
        fontSize: 10, color: C.darkGrey, alignment: "justify", margin: [0, 0, 0, 14],
      },
      { text: "INITIAL RESPONSE TIMES", color: C.alloy, bold: true, fontSize: 10.5, characterSpacing: 1.2, margin: [0, 4, 0, 4] },
      { canvas: [{ type: "line", x1: 0, y1: 0, x2: 500, y2: 0, lineColor: C.strokes }], margin: [0, 0, 0, 6] },
      {
        table: {
          headerRows: 1,
          widths: [60, "*", 100],
          body: [
            [
              { text: "PRIORITY", bold: true, fontSize: 9, color: C.darkGrey, fillColor: C.alice, characterSpacing: 0.6 },
              { text: "DESCRIPTION", bold: true, fontSize: 9, color: C.darkGrey, fillColor: C.alice, characterSpacing: 0.6 },
              { text: "INITIAL RESPONSE", bold: true, fontSize: 9, color: C.darkGrey, fillColor: C.alice, characterSpacing: 0.6 },
            ],
            [{ text: "P1", bold: true }, "Catastrophic — production down, no workaround", isConcierge ? "15 minutes" : "2 hours"],
            [{ text: "P2", bold: true }, "Major — significant impact, reduced capacity", isConcierge ? "1 hour" : "4 hours"],
            [{ text: "P3", bold: true }, "Medium — partial or non-critical functions impaired", isConcierge ? "8 hours" : "1 business day"],
            [{ text: "P4", bold: true }, "Minor — cosmetic, documentation, enhancement", isConcierge ? "1 business day" : "3 business days"],
          ],
        },
        layout: { hLineWidth: innerOnlyHLine(0.5), vLineWidth: () => 0, hLineColor: () => C.strokes, paddingTop: () => 6, paddingBottom: () => 6, paddingLeft: () => 6, paddingRight: () => 6 },
        fontSize: 10,
        margin: [0, 0, 0, 14],
      },
      { text: "UPTIME & SERVICE CREDITS", color: C.alloy, bold: true, fontSize: 10.5, characterSpacing: 1.2, margin: [0, 4, 0, 4] },
      { canvas: [{ type: "line", x1: 0, y1: 0, x2: 500, y2: 0, lineColor: C.strokes }], margin: [0, 0, 0, 6] },
      {
        table: {
          widths: [180, "*"],
          body: [
            [{ text: "Minimum uptime", color: C.darkGrey }, `${isConcierge ? "99.999%" : "99.99%"} per calendar month`],
            [{ text: "Service credit cap", color: C.darkGrey }, `Up to ${isConcierge ? "50%" : "25%"} of fees attributable to a calendar month`],
            ...(isConcierge ? [
              [{ text: "CSM model", color: C.darkGrey }, "Dedicated — single point of contact, named individual"],
              [{ text: "Kickstart Pro Services", color: C.darkGrey }, "100 hours included annually"],
              [{ text: "Quarterly business reviews", color: C.darkGrey }, "Yes — with Nami leadership"],
            ] : []),
          ],
        },
        layout: { hLineWidth: innerOnlyHLine(0.5), vLineWidth: () => 0, hLineColor: () => C.strokes, paddingTop: () => 6, paddingBottom: () => 6 },
        fontSize: 10.5,
        margin: [0, 0, 0, 14],
      },
      { text: "The summary above is for reference. The full text of the Nami Service Level Addendum (included in this package) is the authoritative version and governs in the event of any conflict with this summary.", fontSize: 9.5, color: C.darkGrey, alignment: "justify" },
      { text: "", pageBreak: "after" },
    ];
  };

  const buildFullLegal = (docLabel, markdown) => {
    if (!markdown) return [];
    const cleaned = String(markdown)
      .replace(/^\s*##\s+[^\n]+\n+/, "")
      .replace(/^\s*\*\*Effective Date:?\*\*[^\n]*\n+/i, "");
    return [
      { text: docLabel, fontSize: 22, bold: true, color: C.navy, margin: [0, 8, 0, 2] },
      { canvas: [{ type: "rect", x: 0, y: 0, w: 56, h: 3, color: C.blueBright }], margin: [0, 0, 0, 14] },
      ...markdownToContent(cleaned),
      { text: "", pageBreak: "after" },
    ];
  };

  const buildReferences = () => [
    { text: "Reference Documents", fontSize: 22, bold: true, color: C.navy, margin: [0, 8, 0, 2] },
    { canvas: [{ type: "rect", x: 0, y: 0, w: 56, h: 3, color: C.blueBright }], margin: [0, 0, 0, 14] },
    { text: "The following Nami reference documents are incorporated into this contract package by reference at their published URLs and are subject to the change-notification mechanics described in the MSA and DPA respectively. Customer's contractual rights with respect to these documents are governed by the Master Services Agreement and Data Protection Addendum included in this package.", fontSize: 10, color: C.darkGrey, alignment: "justify", margin: [0, 0, 0, 14] },
    {
      stack: [
        { text: "Nami Privacy Policy", bold: true, fontSize: 11, color: C.navy, margin: [0, 0, 0, 3] },
        { text: [{ text: "Referenced in MSA §5 (Security and Privacy). Published at " }, { text: "www.nami.ml/privacy", link: "https://www.nami.ml/privacy", color: C.blueBright, decoration: "underline" }], fontSize: 9.5, color: C.darkGrey },
      ],
      margin: [0, 0, 0, 10],
    },
    {
      stack: [
        { text: "Nami Subprocessor List", bold: true, fontSize: 11, color: C.navy, margin: [0, 0, 0, 3] },
        { text: [{ text: "Referenced in DPA §6 (Subprocessors). Published at " }, { text: "www.nami.ml/legal/subprocessors/", link: "https://www.nami.ml/legal/subprocessors/", color: C.blueBright, decoration: "underline" }], fontSize: 9.5, color: C.darkGrey },
      ],
    },
    { text: "", pageBreak: "after" },
  ];

  const buildSignatures = (deal, amendments) => [
    { text: "Signatures", fontSize: 22, bold: true, color: C.navy, margin: [0, 8, 0, 2] },
    { canvas: [{ type: "rect", x: 0, y: 0, w: 56, h: 3, color: C.blueBright }], margin: [0, 0, 0, 14] },
    { text: "IN WITNESS WHEREOF, the parties have caused this Agreement to be duly executed as of the Effective Date. Each party represents and warrants that the signatory below has authority to bind the party to the Agreement.", fontSize: 10, color: C.darkGrey, alignment: "justify", margin: [0, 0, 0, 10] },
    { text: `By signing below, each party agrees to be bound by this Service Order, the Master Services Agreement, Data Protection Addendum, and Service Level Addendum included in this package,${amendments?.length ? " the Amendment Addendum," : ""} and the reference documents identified herein, as a single integrated Agreement.`, fontSize: 10, color: C.darkGrey, alignment: "justify", margin: [0, 0, 0, 18] },
    {
      columns: [
        {
          stack: [
            { text: `Customer — ${deal.customer}`, bold: true, color: C.navy, fontSize: 11, margin: [0, 0, 0, 16] },
            { canvas: [{ type: "line", x1: 0, y1: 0, x2: 220, y2: 0, lineColor: C.navy, lineWidth: 1 }], margin: [0, 16, 0, 6] },
            { columns: [{ width: 50, text: "Name:", color: C.darkGrey, fontSize: 10 }, { width: "*", text: deal.contractAdmin?.name || "_______________________", fontSize: 10.5 }], margin: [0, 0, 0, 2] },
            { columns: [{ width: 50, text: "Title:", color: C.darkGrey, fontSize: 10 }, { width: "*", text: deal.contractAdmin?.title || "_______________________", fontSize: 10.5 }], margin: [0, 0, 0, 2] },
            { columns: [{ width: 50, text: "Date:", color: C.darkGrey, fontSize: 10 }, { width: "*", text: "_______________________", fontSize: 10.5 }] },
          ],
        },
        { width: 18, text: "" },
        {
          stack: [
            { text: "Nami ML Inc.", bold: true, color: C.navy, fontSize: 11, margin: [0, 0, 0, 16] },
            { canvas: [{ type: "line", x1: 0, y1: 0, x2: 220, y2: 0, lineColor: C.navy, lineWidth: 1 }], margin: [0, 16, 0, 6] },
            { columns: [{ width: 50, text: "Name:", color: C.darkGrey, fontSize: 10 }, { width: "*", text: "_______________________", fontSize: 10.5 }], margin: [0, 0, 0, 2] },
            { columns: [{ width: 50, text: "Title:", color: C.darkGrey, fontSize: 10 }, { width: "*", text: "_______________________", fontSize: 10.5 }], margin: [0, 0, 0, 2] },
            { columns: [{ width: 50, text: "Date:", color: C.darkGrey, fontSize: 10 }, { width: "*", text: "_______________________", fontSize: 10.5 }] },
          ],
        },
      ],
    },
  ];

  // ---- Public entry point -------------------------------------------------
  window.NAMI_PDFMAKE = {
    get available() { return !!window.pdfMake; },
    async generate({ deal: rawDeal, amendments, legalText, isDraft, download = true }) {
      if (!window.pdfMake) throw new Error("pdfmake not loaded");
      // Preload Gilroy + Nami wordmark SVGs in parallel so the doc
      // renders with the real brand assets. The earlier "hang" was
      // actually a React regen loop, not an SVG/font issue.
      const [gilroyOk, wordmarkNavy, wordmarkWhite] = await Promise.all([
        ensureGilroy(),
        loadSvg("assets/nami-wordmark-navy.svg"),
        loadSvg("assets/nami-wordmark-white.svg"),
      ]);
      const brandFont = gilroyOk ? "Gilroy" : "Roboto";

      const deal = {
        ...rawDeal,
        endDate: rawDeal.endDate || computeEndDate(rawDeal.startDate, rawDeal.termYears || 1),
      };
      const dealAmendments = amendments || [];
      const draftStates = new Set(["draft", "review", "pending_legal", "approved", "legal"]);
      const draft = isDraft != null ? isDraft : draftStates.has(deal.status);

      // Static inventory page numbers — best-effort estimates. pdfmake
      // doesn't expose final page numbers at build time, so for the
      // spike we approximate; future work could do a two-pass build.
      const hasAmend = !!dealAmendments.length;
      let p = 2;
      const soPage = p++; const soFeesPage = p++;
      const amendPage = hasAmend ? p++ : null;
      const slaSummaryPage = p++;
      const msaPage = p++;
      const dpaPage = msaPage + 8;
      const slaPage = dpaPage + 6;
      const refsPage = slaPage + 3;
      const sigPage = refsPage + 1;
      const inventory = [
        { label: "Service Order", sub: "Customer-specific commercial terms", page: soPage },
        ...(hasAmend ? [{
          label: "Amendment Addendum",
          sub: `${dealAmendments.length} pre-approved modification${dealAmendments.length === 1 ? "" : "s"} to the standard MSA`,
          page: amendPage,
        }] : []),
        { label: "Service Level Summary", sub: `${(deal.support || "standard")[0].toUpperCase() + (deal.support || "standard").slice(1)} tier — full SLA follows`, page: slaSummaryPage },
        { label: "Master Services Agreement", sub: "Standard Nami enterprise MSA", page: msaPage },
        { label: "Data Protection Addendum", sub: "SCCs, UK IDTA, CCPA/CPRA", page: dpaPage },
        { label: "Service Level Addendum", sub: "Standard and Concierge", page: slaPage },
        { label: "Reference Documents", sub: "Privacy Policy, Subprocessor List", page: refsPage },
        { label: "Signatures", sub: "Execution page for the entire Agreement package", page: sigPage },
      ];

      // The header changes per section. pdfmake supports a single page
      // header function, so we route the doc-label based on currentPage
      // and a precomputed map.
      const sectionMap = []; // built as we append content; entry = { startPage, label }
      // For the spike we use a simpler approach: per-section pageBreak
      // boundaries are managed by inserting `pageBreak: 'after'` between
      // sections, then a header function looks up the current section
      // by counting page breaks. Easier: just use a single static
      // header per page, and accept that the cover/section page header
      // label is best-effort. For sharpness, we set the header dynamically
      // via a docLabel map computed below.

      const docLabels = [];
      const pushPage = (label) => docLabels.push(label);
      // Cover
      pushPage(null);
      // Service Order Customer
      pushPage("Service Order");
      // Service Order Fees
      pushPage("Service Order — Fees & Terms");
      // Amendment
      if (hasAmend) pushPage("Amendment Addendum");
      // SLA Summary
      pushPage("Service Level Summary");
      // MSA chunks - we don't know how many. pdfmake will fill in.
      // We let header callback return MSA by default after the SO sections.
      // For simplicity, use a stack of regions:
      const regions = [
        { count: 1, label: null }, // cover
        { count: 1, label: "Service Order" },
        { count: 1, label: "Service Order — Fees & Terms" },
        ...(hasAmend ? [{ count: 1, label: "Amendment Addendum" }] : []),
        { count: 1, label: "Service Level Summary" },
        { count: 999, label: "Master Services Agreement", until: "msa-end" },
        { count: 999, label: "Data Protection Addendum", until: "dpa-end" },
        { count: 999, label: "Service Level Addendum", until: "sla-end" },
        { count: 1, label: "Reference Documents" },
        { count: 1, label: "Signatures" },
      ];

      // Build content with section markers we'll use server-side… no,
      // simplest: don't try to route per-section header dynamically.
      // pdfmake header() can return null/empty for cover. Use a single
      // header function that picks label by examining a region table.
      // We'll just emit the docLabel near the page via a `headlineText`
      // field embedded in content, and the header() function reads a
      // shared mutable index — but that's racy.
      //
      // Compromise: set the header label statically to MASTER SERVICES
      // AGREEMENT after the fixed section pages, since the legal docs
      // are the bulk of the page count. For now header just reads
      // "NAMI Order Portal" generically except on cover where it's
      // suppressed. This is a known spike limitation — note in summary.

      const fixedSectionCount = 4 + (hasAmend ? 1 : 0); // cover + SO×2 + [amend] + SLA summary
      const fixedLabels = [
        null,                              // cover
        "Service Order",                   // SO customer
        "Service Order — Fees & Terms",    // SO fees
        ...(hasAmend ? ["Amendment Addendum"] : []),
        "Service Level Summary",
      ];

      const docDefinition = {
        pageSize: "LETTER",
        pageMargins: [56, 72, 56, 60],
        defaultStyle: { font: brandFont, fontSize: 10.5, color: C.navy, lineHeight: 1.4 },
        info: {
          title: `Service Order — ${deal.customer || "Nami"}`,
          author: "Nami ML Inc.",
          subject: "Subscription Services Agreement",
        },
        header: (currentPage, pageCount, pageSize) => {
          if (currentPage === 1) return null;
          // Best-effort label based on a static region table.
          let label;
          if (currentPage <= fixedLabels.length) {
            label = fixedLabels[currentPage - 1];
          } else {
            // After the fixed sections, label transitions through the
            // legal docs. We don't know exact boundaries, so pick the
            // most likely doc by page position.
            const afterFixed = currentPage - fixedLabels.length;
            // Heuristic: total ~22-25 legal pages → MSA first ~10, DPA next ~8, SLA next ~3, then refs+sigs (2).
            if (afterFixed <= 10) label = "Master Services Agreement";
            else if (afterFixed <= 18) label = "Data Protection Addendum";
            else if (afterFixed <= 22) label = "Service Level Addendum";
            else if (afterFixed === pageCount - currentPage + currentPage - 1 - fixedLabels.length) label = "Signatures";
            else label = "Master Services Agreement"; // fallback
            // Last two pages are References + Signatures
            if (currentPage === pageCount - 1) label = "Reference Documents";
            if (currentPage === pageCount) label = "Signatures";
          }
          if (!label) return null;
          return {
            stack: [
              {
                columns: [
                  wordmarkNavy
                    ? { svg: wordmarkNavy, width: 56, alignment: "left" }
                    : { text: "NAMI", bold: true, fontSize: 11, characterSpacing: 1, color: C.navy, alignment: "left" },
                  { text: label.toUpperCase(), fontSize: 8.5, characterSpacing: 0.8, bold: true, color: C.alloy, alignment: "right", margin: [0, 4, 0, 0] },
                ],
              },
              { canvas: [{ type: "line", x1: 0, y1: 6, x2: 500, y2: 6, lineColor: C.strokes }] },
            ],
            margin: [56, 28, 56, 0],
          };
        },
        footer: (currentPage, pageCount) => {
          // Page count was dropped (too fragile to keep in sync with
          // pdfmake's flowed pagination). DRAFT shows as plain red
          // text in the slot the page count used to occupy.
          const isCover = currentPage === 1;
          const textColor = isCover ? "#cad8e8" : C.darkGrey;
          return {
            columns: [
              { width: "*", text: "Confidential — Nami ML Inc.", fontSize: 9, color: textColor, alignment: "left" },
              ...(draft ? [{
                width: "auto",
                text: "DRAFT",
                color: C.accentRed,
                bold: true,
                fontSize: 10,
                characterSpacing: 1.2,
                alignment: "right",
              }] : []),
            ],
            margin: [56, 0, 56, 24],
          };
        },
        content: [
          ...buildCover(deal, dealAmendments, draft, inventory, { wordmarkWhite }),
          ...buildServiceOrderCustomer(deal),
          ...buildServiceOrderFees(deal, draft),
          ...buildAmendment(deal, dealAmendments),
          ...buildSLASummary(deal),
          ...buildFullLegal("Master Services Agreement", legalText?.MSA),
          ...buildFullLegal("Data Protection Addendum", legalText?.DPA),
          ...buildFullLegal("Service Level Addendum", legalText?.SLA),
          ...buildReferences(),
          ...buildSignatures(deal, dealAmendments),
        ],
      };

      const filename = `Nami Order Portal — ${deal.customer || "package"} (pdfmake).pdf`;
      if (download) {
        window.pdfMake.createPdf(docDefinition).download(filename);
        return;
      }
      return new Promise((resolve, reject) => {
        let pdf;
        try {
          pdf = window.pdfMake.createPdf(docDefinition);
        } catch (err) {
          reject(err);
          return;
        }
        try {
          pdf.getBlob((blob) => {
            if (!blob) {
              reject(new Error("pdfmake.getBlob returned no blob"));
              return;
            }
            resolve(blob);
          });
        } catch (err) {
          reject(err);
        }
      });
    },
  };
})();
