/* cst-report.jsx — the paid deliverable: a 2–5 page, chart-rich PDF report.
   Auto-paginates a list of content blocks across A4 pages drawn on canvas,
   then assembles them into a PDF via jsPDF (PNG fallback if jsPDF is absent).
   Console aesthetic. Pure drawing + builders. Exported to window.CST_REPORT. */

const _RC = window.CST;
const RFT = "'Bricolage Grotesque', ui-sans-serif, system-ui, sans-serif";
const R_INK = '#0B0D11', R_TEXT = '#ECEDF1', R_HAZE = 'rgba(236,237,241,0.62)', R_HAIR = 'rgba(236,237,241,0.12)';
const R_STATUS = { good: '#5FD08C', watch: '#FFC861', risk: '#FF7E6E', cold: '#7FB2C9' };

// page geometry (A4 portrait @ ~150dpi)
const PW = 1240, PH = 1754, PM = 96;
const CW = PW - PM * 2;           // content width
const TOP = 196, BOTTOM = PH - 132;

function rWrap(ctx, text, maxW) {
  const words = String(text == null ? '' : text).split(/\s+/);
  const lines = []; let line = '';
  for (const w of words) {
    const test = line ? line + ' ' + w : w;
    if (ctx.measureText(test).width > maxW && line) { lines.push(line); line = w; }
    else line = test;
  }
  if (line) lines.push(line);
  return lines.length ? lines : [''];
}
function rRound(ctx, x, y, w, h, r) {
  ctx.beginPath(); ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath();
}
function rSeed(seed) { let s = (seed >>> 0) || 1; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; }
function rHash(str) { let h = 2166136261 >>> 0; for (let i = 0; i < (str || '').length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; }

async function rFont() {
  try {
    if (document.fonts && document.fonts.load) {
      await Promise.all([
        document.fonts.load('800 120px "Bricolage Grotesque"'), document.fonts.load('700 40px "Bricolage Grotesque"'),
        document.fonts.load('600 28px "Bricolage Grotesque"'), document.fonts.load('500 24px "Bricolage Grotesque"'),
      ]); await document.fonts.ready;
    }
  } catch (e) {}
}

// ---------- chrome ----------
function rBackdrop(ctx, accent, seed) {
  ctx.fillStyle = R_INK; ctx.fillRect(0, 0, PW, PH);
  const g = ctx.createRadialGradient(PW / 2, 230, 30, PW / 2, 230, PW);
  g.addColorStop(0, accent + '1c'); g.addColorStop(0.5, 'rgba(15,17,21,0.12)'); g.addColorStop(1, 'rgba(11,13,17,0)');
  ctx.fillStyle = g; ctx.fillRect(0, 0, PW, PH);
  // faint node field along the top band
  const rnd = rSeed(seed); const N = 26; const pts = [];
  for (let i = 0; i < N; i++) pts.push({ x: rnd() * PW, y: rnd() * 300, r: 1.5 + rnd() * 3 });
  ctx.lineWidth = 1.2;
  for (let i = 0; i < N; i++) { const j = (i + 1) % N; ctx.strokeStyle = 'rgba(236,237,241,0.05)'; ctx.beginPath(); ctx.moveTo(pts[i].x, pts[i].y); ctx.lineTo(pts[j].x, pts[j].y); ctx.stroke(); }
  for (const p of pts) { const lit = rnd() > 0.7; ctx.fillStyle = lit ? accent : 'rgba(236,237,241,0.18)'; if (lit) { ctx.shadowColor = accent; ctx.shadowBlur = 12; } else ctx.shadowBlur = 0; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); }
  ctx.shadowBlur = 0;
  ctx.strokeStyle = accent + '33'; ctx.lineWidth = 2.5; rRound(ctx, 24, 24, PW - 48, PH - 48, 28); ctx.stroke();
}
function rLogo(ctx, x, y, scale, accent) {
  ctx.save(); ctx.translate(x, y); ctx.scale(scale, scale);
  ctx.strokeStyle = '#FFB454'; ctx.lineWidth = 3.4; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
  ctx.beginPath(); ctx.moveTo(23, 9); ctx.lineTo(15, 9); ctx.quadraticCurveTo(11, 9, 11, 13); ctx.lineTo(11, 20); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(33, 38); ctx.lineTo(41, 38); ctx.quadraticCurveTo(45, 38, 45, 34); ctx.lineTo(45, 27); ctx.stroke();
  ctx.fillStyle = '#ECEDF1'; ctx.font = `800 17px ${RFT}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText('CS', 28, 25); ctx.restore();
}
function rHeader(ctx, spec, pageNo, pageCount, accent) {
  rLogo(ctx, PM - 8, 70, 1.45, accent);
  ctx.textBaseline = 'alphabetic'; ctx.textAlign = 'left';
  ctx.fillStyle = R_TEXT; ctx.font = `700 28px ${RFT}`; ctx.fillText('CS Tech', PM + 74, 92);
  ctx.fillStyle = 'rgba(236,237,241,0.5)'; ctx.font = `600 14px ${RFT}`; ctx.fillText('OPERATING PARTNER', PM + 74, 116);
  ctx.textAlign = 'right'; ctx.fillStyle = accent; ctx.font = `700 20px ${RFT}`;
  ctx.fillText((spec.tool || '').toUpperCase() + ' · FULL REPORT', PW - PM, 96);
  ctx.strokeStyle = R_HAIR; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(PM, 150); ctx.lineTo(PW - PM, 150); ctx.stroke();
  // footer
  ctx.strokeStyle = R_HAIR; ctx.beginPath(); ctx.moveTo(PM, PH - 108); ctx.lineTo(PW - PM, PH - 108); ctx.stroke();
  ctx.textAlign = 'left'; ctx.fillStyle = accent; ctx.font = `700 18px ${RFT}`;
  ctx.fillText('Decide easier, better, faster.', PM, PH - 74);
  ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(236,237,241,0.5)'; ctx.font = `600 16px ${RFT}`;
  ctx.fillText(spec.shortlink || 'cstechpartner.com', PW / 2, PH - 74);
  ctx.textAlign = 'right'; ctx.fillStyle = 'rgba(236,237,241,0.5)'; ctx.font = `600 16px ${RFT}`;
  ctx.fillText(`${pageNo} / ${pageCount}`, PW - PM, PH - 74);
}

// ---------- block measurement ----------
function setFont(ctx, w, px) { ctx.font = `${w} ${px}px ${RFT}`; }
function blockH(ctx, b) {
  switch (b.type) {
    case 'hero': {
      setFont(ctx, 700, 40); const vl = rWrap(ctx, b.title, CW - 280).length;
      let h = Math.max(240, 70 + vl * 50);
      if (b.body) { setFont(ctx, 400, 24); h += rWrap(ctx, b.body, CW - 280).length * 34 + 8; }
      return h + 24;
    }
    case 'kv': return 64;
    case 'h': return 70;
    case 'p': { setFont(ctx, 400, 25); return rWrap(ctx, b.text, CW).length * 38 + 18; }
    case 'list': {
      setFont(ctx, 400, 24); let h = 6;
      (b.items || []).forEach((it) => { h += rWrap(ctx, it, CW - 56).length * 36 + 16; });
      return h + 8;
    }
    case 'radar': return 560;
    case 'bars': { return 56 + (b.rows || []).length * 66 + 10; }
    case 'gauge': {
      let h = 340;
      if (b.note) { setFont(ctx, 400, 23); const nl = rWrap(ctx, b.note, CW - 340).length; h = Math.max(h, 120 + nl * 34 + 30); }
      return h;
    }
    case 'stats': { const n = (b.cells || []).length; return Math.ceil(n / 2) * 132 + 16; }
    case 'matrix': {
      let h = 0; setFont(ctx, 400, 23);
      (b.rows || []).forEach((r) => {
        const rl = rWrap(ctx, r.read, CW - 70).length;
        const dl = rWrap(ctx, r.decision, CW - 90).length;
        h += 56 + rl * 32 + 18 + dl * 32 + 30;
      });
      return h;
    }
    case 'callout': { setFont(ctx, 700, 30); return 64 + rWrap(ctx, b.text, CW - 56).length * 40 + 30; }
    case 'divider': return 40;
    default: return 0;
  }
}

// ---------- block drawing (returns next y) ----------
function drawBlock(ctx, b, y, accent) {
  const x = PM;
  switch (b.type) {
    case 'hero': {
      const cx = x + 100, cy = y + 100, R = 92, sw = 18;
      ctx.lineWidth = sw; ctx.lineCap = 'round';
      ctx.strokeStyle = 'rgba(236,237,241,0.12)'; ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.stroke();
      ctx.strokeStyle = accent; ctx.shadowColor = accent; ctx.shadowBlur = 22;
      ctx.beginPath(); ctx.arc(cx, cy, R, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * (_RC.clamp(b.ring.pct) / 100)); ctx.stroke(); ctx.shadowBlur = 0;
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillStyle = R_TEXT; setFont(ctx, 800, 70); ctx.fillText(String(b.ring.value), cx, cy - 6);
      ctx.fillStyle = 'rgba(236,237,241,0.55)'; setFont(ctx, 600, 16); ctx.fillText(b.ring.unit || '', cx, cy + 34);
      ctx.textBaseline = 'alphabetic';
      // verdict text to the right
      const tx = x + 220, tw = CW - 220;
      ctx.textAlign = 'left'; ctx.fillStyle = R_TEXT; setFont(ctx, 700, 38);
      let yy = y + 44;
      rWrap(ctx, b.title, tw).forEach((ln) => { ctx.fillText(ln, tx, yy); yy += 50; });
      if (b.body) { ctx.fillStyle = R_HAZE; setFont(ctx, 400, 24); rWrap(ctx, b.body, tw).forEach((ln) => { ctx.fillText(ln, tx, yy + 6); yy += 34; }); }
      return y + blockH(ctx, b);
    }
    case 'kv': {
      ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(236,237,241,0.45)'; setFont(ctx, 600, 15);
      ctx.fillText((b.label || '').toUpperCase(), x, y + 18);
      ctx.fillStyle = R_TEXT; setFont(ctx, 600, 24); ctx.fillText(b.value || '', x, y + 50);
      return y + 64;
    }
    case 'h': {
      ctx.textAlign = 'left'; ctx.fillStyle = accent; setFont(ctx, 700, 22);
      ctx.fillText((b.text || '').toUpperCase(), x, y + 40);
      ctx.strokeStyle = accent + '40'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x, y + 56); ctx.lineTo(x + 64, y + 56); ctx.stroke();
      return y + 70;
    }
    case 'p': {
      ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(236,237,241,0.84)'; setFont(ctx, 400, 25);
      let yy = y + 26; rWrap(ctx, b.text, CW).forEach((ln) => { ctx.fillText(ln, x, yy); yy += 38; });
      return y + blockH(ctx, b);
    }
    case 'list': {
      let yy = y + 6;
      (b.items || []).forEach((it, i) => {
        ctx.textAlign = 'left'; ctx.fillStyle = accent; setFont(ctx, 700, 24);
        ctx.fillText(b.ordered ? (i + 1) + '.' : '→', x, yy + 24);
        ctx.fillStyle = 'rgba(236,237,241,0.84)'; setFont(ctx, 400, 24);
        const lines = rWrap(ctx, it, CW - 56);
        lines.forEach((ln, k) => { ctx.fillText(ln, x + 48, yy + 24 + k * 36); });
        yy += lines.length * 36 + 16;
      });
      return y + blockH(ctx, b);
    }
    case 'radar': {
      const data = b.factors || [];
      const N = data.length, cx = PW / 2, cy = y + 260, R = 210;
      const ang = (i) => (-Math.PI / 2) + i * (2 * Math.PI / N);
      const pt = (i, r) => [cx + Math.cos(ang(i)) * r, cy + Math.sin(ang(i)) * r];
      [0.34, 0.67, 1].forEach((f) => { ctx.strokeStyle = 'rgba(236,237,241,0.12)'; ctx.lineWidth = 1.5; ctx.beginPath(); data.forEach((_, i) => { const [px, py] = pt(i, R * f); i ? ctx.lineTo(px, py) : ctx.moveTo(px, py); }); ctx.closePath(); ctx.stroke(); });
      data.forEach((_, i) => { const [px, py] = pt(i, R); ctx.strokeStyle = 'rgba(236,237,241,0.10)'; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(px, py); ctx.stroke(); });
      ctx.beginPath(); data.forEach((d, i) => { const [px, py] = pt(i, R * Math.max(0.08, d.value / 100)); i ? ctx.lineTo(px, py) : ctx.moveTo(px, py); }); ctx.closePath();
      ctx.fillStyle = accent + '33'; ctx.fill(); ctx.strokeStyle = accent; ctx.lineWidth = 3; ctx.shadowColor = accent; ctx.shadowBlur = 10; ctx.stroke(); ctx.shadowBlur = 0;
      data.forEach((d, i) => { const [px, py] = pt(i, R * Math.max(0.08, d.value / 100)); ctx.fillStyle = accent; ctx.beginPath(); ctx.arc(px, py, 5, 0, Math.PI * 2); ctx.fill(); });
      data.forEach((d, i) => {
        const [lx, ly] = pt(i, R + 42); ctx.textAlign = Math.abs(lx - cx) < 10 ? 'center' : (lx > cx ? 'left' : 'right');
        ctx.fillStyle = 'rgba(236,237,241,0.8)'; setFont(ctx, 600, 21); ctx.fillText(d.label, lx, ly);
        ctx.fillStyle = accent; setFont(ctx, 700, 19); ctx.fillText(String(d.value), lx, ly + 26);
      });
      ctx.textAlign = 'left';
      return y + blockH(ctx, b);
    }
    case 'bars': {
      const rows = b.rows || []; const max = b.max || Math.max(1, ...rows.map((r) => r.value));
      let yy = y + 10;
      rows.forEach((r) => {
        ctx.textAlign = 'left'; ctx.fillStyle = r.emph ? R_TEXT : R_HAZE; setFont(ctx, r.emph ? 700 : 500, 22);
        ctx.fillText(r.label, x, yy + 20);
        ctx.textAlign = 'right'; ctx.fillStyle = r.color || accent; setFont(ctx, 700, 22); ctx.fillText(r.valueText, x + CW, yy + 20);
        yy += 32;
        ctx.fillStyle = 'rgba(236,237,241,0.09)'; rRound(ctx, x, yy, CW, r.emph ? 16 : 12, 8); ctx.fill();
        const w = Math.max(6, CW * _RC.clamp((r.value / max) * 100) / 100);
        ctx.fillStyle = r.color || accent; if (r.emph) { ctx.shadowColor = (r.color || accent); ctx.shadowBlur = 14; }
        rRound(ctx, x, yy, w, r.emph ? 16 : 12, 8); ctx.fill(); ctx.shadowBlur = 0;
        yy += (r.emph ? 16 : 12) + 22;
      });
      ctx.textAlign = 'left';
      return y + blockH(ctx, b);
    }
    case 'gauge': {
      const cx = x + 150, cy = y + 170, R = 130;
      const pct = _RC.clamp(b.value) / 100, tgt = b.target != null ? _RC.clamp(b.target) / 100 : null;
      ctx.lineWidth = 26; ctx.lineCap = 'round';
      ctx.strokeStyle = 'rgba(236,237,241,0.1)'; ctx.beginPath(); ctx.arc(cx, cy, R, Math.PI * 0.75, Math.PI * 2.25); ctx.stroke();
      ctx.strokeStyle = accent; ctx.shadowColor = accent; ctx.shadowBlur = 18;
      ctx.beginPath(); ctx.arc(cx, cy, R, Math.PI * 0.75, Math.PI * 0.75 + Math.PI * 1.5 * pct); ctx.stroke(); ctx.shadowBlur = 0;
      if (tgt != null) { const ta = Math.PI * 0.75 + Math.PI * 1.5 * tgt; ctx.strokeStyle = R_TEXT; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(cx + Math.cos(ta) * (R - 20), cy + Math.sin(ta) * (R - 20)); ctx.lineTo(cx + Math.cos(ta) * (R + 20), cy + Math.sin(ta) * (R + 20)); ctx.stroke(); }
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = R_TEXT; setFont(ctx, 800, 58); ctx.fillText(b.valueText || (Math.round(b.value) + '%'), cx, cy - 4);
      ctx.fillStyle = R_HAZE; setFont(ctx, 600, 16); ctx.fillText(b.label || '', cx, cy + 36); ctx.textBaseline = 'alphabetic';
      // legend to the right
      if (b.note) { ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(236,237,241,0.78)'; setFont(ctx, 400, 23); let yy = cy - 60; rWrap(ctx, b.note, CW - 340).forEach((ln) => { ctx.fillText(ln, x + 320, yy); yy += 34; }); }
      ctx.textAlign = 'left';
      return y + blockH(ctx, b);
    }
    case 'stats': {
      const cells = b.cells || []; const cw = (CW - 20) / 2, ch = 116;
      cells.forEach((c, i) => {
        const col = i % 2, row = Math.floor(i / 2);
        const bx = x + col * (cw + 20), by = y + row * 132;
        ctx.fillStyle = 'rgba(236,237,241,0.04)'; ctx.strokeStyle = c.emph ? accent + '55' : R_HAIR; ctx.lineWidth = 1.5;
        rRound(ctx, bx, by, cw, ch, 14); ctx.fill(); ctx.stroke();
        ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(236,237,241,0.5)'; setFont(ctx, 600, 15); ctx.fillText((c.label || '').toUpperCase(), bx + 22, by + 34);
        ctx.fillStyle = c.emph ? accent : R_TEXT; setFont(ctx, 800, 38); ctx.fillText(c.value, bx + 22, by + 78);
        if (c.sub) { ctx.fillStyle = R_HAZE; setFont(ctx, 400, 16); ctx.fillText(c.sub, bx + 22, by + 102); }
      });
      ctx.textAlign = 'left';
      return y + blockH(ctx, b);
    }
    case 'matrix': {
      let yy = y;
      (b.rows || []).forEach((r) => {
        const sc = R_STATUS[r.status] || accent;
        ctx.fillStyle = 'rgba(236,237,241,0.035)'; ctx.strokeStyle = R_HAIR; ctx.lineWidth = 1.5;
        const rl = rWrap(ctx, r.read, CW - 70); const dl = rWrap(ctx, r.decision, CW - 90);
        const rowH = 56 + rl.length * 32 + 18 + dl.length * 32 + 16;
        rRound(ctx, x, yy, CW, rowH, 14); ctx.fill(); ctx.stroke();
        ctx.fillStyle = sc; rRound(ctx, x, yy, 6, rowH, 3); ctx.fill();
        // glyph chip
        ctx.fillStyle = sc + '26'; rRound(ctx, x + 22, yy + 18, 40, 40, 9); ctx.fill();
        ctx.fillStyle = sc; ctx.textAlign = 'center'; setFont(ctx, 800, 22); ctx.textBaseline = 'middle'; ctx.fillText(r.glyph, x + 42, yy + 39); ctx.textBaseline = 'alphabetic';
        ctx.textAlign = 'left'; ctx.fillStyle = R_TEXT; setFont(ctx, 700, 23); ctx.fillText(r.label, x + 78, yy + 33);
        // status pill
        ctx.textAlign = 'right'; ctx.fillStyle = sc; setFont(ctx, 700, 15); ctx.fillText((r.statusLabel || r.status).toUpperCase(), x + CW - 22, yy + 33);
        ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(236,237,241,0.74)'; setFont(ctx, 400, 23);
        let ty = yy + 70; rl.forEach((ln) => { ctx.fillText(ln, x + 24, ty); ty += 32; });
        ty += 8; ctx.fillStyle = sc; setFont(ctx, 700, 15); ctx.fillText('NEXT DECISION', x + 24, ty); ty += 26;
        ctx.fillStyle = R_TEXT; setFont(ctx, 600, 23); dl.forEach((ln) => { ctx.fillText(ln, x + 24, ty); ty += 32; });
        yy += rowH + 14;
      });
      return y + blockH(ctx, b);
    }
    case 'callout': {
      const lines = rWrap(ctx, b.text, CW - 56);
      const h = 64 + lines.length * 40 + 30;
      ctx.fillStyle = accent + '12'; ctx.strokeStyle = accent + '55'; ctx.lineWidth = 2; rRound(ctx, x, y, CW, h - 10, 16); ctx.fill(); ctx.stroke();
      ctx.textAlign = 'left'; ctx.fillStyle = accent; setFont(ctx, 700, 16); ctx.fillText((b.label || 'THE BOTTOM LINE').toUpperCase(), x + 28, y + 40);
      ctx.fillStyle = R_TEXT; setFont(ctx, 700, 30); let yy = y + 86; lines.forEach((ln) => { ctx.fillText(ln, x + 28, yy); yy += 40; });
      return y + blockH(ctx, b);
    }
    case 'divider': {
      ctx.strokeStyle = R_HAIR; ctx.lineWidth = 1.5; ctx.setLineDash([6, 8]); ctx.beginPath(); ctx.moveTo(x, y + 20); ctx.lineTo(x + CW, y + 20); ctx.stroke(); ctx.setLineDash([]);
      return y + 40;
    }
    default: return y;
  }
}

// ---------- pagination + render ----------
function paginate(ctx, blocks) {
  const pages = []; let cur = []; let y = TOP;
  const splitList = (b) => {
    // split a long list into chunks that fit a page
    const parts = []; let items = []; let h = 6;
    setFont(ctx, 400, 24);
    (b.items || []).forEach((it) => {
      const ih = rWrap(ctx, it, CW - 56).length * 36 + 16;
      if (h + ih > BOTTOM - TOP - 80 && items.length) { parts.push({ ...b, items: items.slice() }); items = []; h = 6; }
      items.push(it); h += ih;
    });
    if (items.length) parts.push({ ...b, items });
    return parts;
  };
  for (const b of blocks) {
    if (b.type === 'pagebreak') { if (cur.length) pages.push(cur); cur = []; y = TOP; continue; }
    const h = blockH(ctx, b);
    if (y + h > BOTTOM && cur.length) { pages.push(cur); cur = []; y = TOP; }
    // split oversized lists across pages
    if (b.type === 'list' && h > BOTTOM - TOP) {
      const parts = splitList(b);
      for (const part of parts) {
        const ph = blockH(ctx, part);
        if (y + ph > BOTTOM && cur.length) { pages.push(cur); cur = []; y = TOP; }
        cur.push(part); y += ph + 18;
      }
      continue;
    }
    cur.push(b); y += h + (b.type === 'h' ? 6 : 18);
  }
  if (cur.length) pages.push(cur);
  return pages;
}

async function renderPages(spec) {
  await rFont();
  const accent = spec.accent || '#FFB454';
  const meas = document.createElement('canvas').getContext('2d');
  const pages = paginate(meas, spec.blocks || []);
  const canvases = [];
  pages.forEach((blocks, pi) => {
    const cv = document.createElement('canvas'); cv.width = PW; cv.height = PH;
    const ctx = cv.getContext('2d');
    rBackdrop(ctx, accent, rHash((spec.tool || '') + pi));
    rHeader(ctx, spec, pi + 1, pages.length, accent);
    let y = TOP;
    blocks.forEach((b) => { y = drawBlock(ctx, b, y, accent) + (b.type === 'h' ? 6 : 18); });
    canvases.push(cv);
  });
  return canvases;
}

// ---- self-contained PDF writer (no external lib): one full-page JPEG per page ----
function _strBytes(s) { const a = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) a[i] = s.charCodeAt(i) & 0xff; return a; }
async function _canvasJpeg(cv, q) {
  const blob = await new Promise((r) => cv.toBlob(r, 'image/jpeg', q || 0.9));
  return new Uint8Array(await blob.arrayBuffer());
}
async function buildImagePDF(canvases) {
  const PWpt = 595.28, PHpt = 841.89;
  const N = canvases.length;
  const imgs = []; for (const cv of canvases) imgs.push(await _canvasJpeg(cv, 0.9));
  const chunks = []; let len = 0; const offsets = [];
  const push = (d) => { const b = (typeof d === 'string') ? _strBytes(d) : d; chunks.push(b); len += b.length; };
  const obj = (n) => { offsets[n] = len; };
  push('%PDF-1.4\n%\xFF\xFF\xFF\xFF\n');
  obj(1); push('1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n');
  const kids = []; for (let p = 0; p < N; p++) kids.push(`${5 + p * 3} 0 R`);
  obj(2); push(`2 0 obj\n<< /Type /Pages /Kids [${kids.join(' ')}] /Count ${N} >>\nendobj\n`);
  for (let p = 0; p < N; p++) {
    const imgN = 3 + p * 3, contN = 4 + p * 3, pageN = 5 + p * 3;
    const cv = canvases[p], ib = imgs[p];
    obj(imgN);
    push(`${imgN} 0 obj\n<< /Type /XObject /Subtype /Image /Width ${cv.width} /Height ${cv.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${ib.length} >>\nstream\n`);
    push(ib); push('\nendstream\nendobj\n');
    const content = `q\n${PWpt} 0 0 ${PHpt} 0 0 cm\n/Im0 Do\nQ\n`;
    obj(contN);
    push(`${contN} 0 obj\n<< /Length ${content.length} >>\nstream\n${content}endstream\nendobj\n`);
    obj(pageN);
    push(`${pageN} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${PWpt} ${PHpt}] /Resources << /XObject << /Im0 ${imgN} 0 R >> >> /Contents ${contN} 0 R >>\nendobj\n`);
  }
  const total = 2 + N * 3, xrefStart = len;
  let xref = `xref\n0 ${total + 1}\n0000000000 65535 f \n`;
  for (let i = 1; i <= total; i++) xref += String(offsets[i]).padStart(10, '0') + ' 00000 n \n';
  push(xref);
  push(`trailer\n<< /Size ${total + 1} /Root 1 0 R >>\nstartxref\n${xrefStart}\n%%EOF`);
  const out = new Uint8Array(len); let o = 0; for (const c of chunks) { out.set(c, o); o += c.length; }
  return new Blob([out], { type: 'application/pdf' });
}

async function downloadReportPDF(spec, filename) {
  const canvases = await renderPages(spec);
  try {
    const blob = await buildImagePDF(canvases);
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob); a.download = (filename || 'cs-tech-report') + '.pdf';
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(a.href), 4000);
    return { pages: canvases.length, format: 'pdf' };
  } catch (e) {
    // fallback: download each page as PNG
    for (let i = 0; i < canvases.length; i++) {
      const blob = await new Promise((r) => canvases[i].toBlob(r, 'image/png'));
      const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
      a.download = `${filename || 'cs-tech-report'}-p${i + 1}.png`;
      document.body.appendChild(a); a.click(); a.remove();
      setTimeout(() => URL.revokeObjectURL(a.href), 4000);
    }
    return { pages: canvases.length, format: 'png' };
  }
}

window.CST_REPORT = { renderPages, downloadReportPDF, buildImagePDF, PW, PH };

// ---------- per-tool report specs (2–5 pages, chart-rich) ----------
const SHORT = (s, n) => { s = (s || '').trim(); return s.length > n ? s.slice(0, n - 1).trim() + '…' : s; };

function validateReportSpec(idea, focus, verdict, accent) {
  const V = window.CST_VALIDATE;
  const factors = V.BREAKDOWN_FACTORS.map((f) => ({ key: f.key, label: f.label, value: verdict.breakdown[f.key] }));
  const reads = V.BREAKDOWN_FACTORS.map((f) => {
    const v = verdict.breakdown[f.key]; const r = V.factorRead(f.key, v);
    return `${f.label} (${v}/100) — ${r.text}`;
  });
  return {
    tool: 'Idea check', accent, shortlink: window.CST_SHARE.SHORTLINK,
    blocks: [
      { type: 'hero', ring: { value: verdict.score, pct: verdict.score, unit: '/ 100' }, title: verdict.verdict, body: verdict.summary },
      { type: 'kv', label: 'The idea', value: SHORT(idea, 70) },
      focus ? { type: 'kv', label: 'Where it stands out', value: focus.label } : { type: 'divider' },
      { type: 'p', text: 'This report breaks down how promising your idea looks across the five things that decide whether a new venture takes off — and exactly what to do next.' },
      { type: 'pagebreak' },
      { type: 'h', text: 'How your idea scores' },
      { type: 'radar', factors },
      { type: 'bars', max: 100, rows: factors.map((f) => ({ label: f.label, value: f.value, valueText: f.value + '/100', emph: f.value >= 75 || f.value < 45, color: f.value >= 75 ? R_STATUS.good : f.value < 45 ? R_STATUS.risk : accent })) },
      { type: 'pagebreak' },
      { type: 'h', text: 'What each score means' },
      { type: 'list', items: reads },
      { type: 'h', text: 'Who to try it with first' },
      { type: 'p', text: verdict.market },
      { type: 'pagebreak' },
      { type: 'h', text: 'What to watch' },
      { type: 'list', ordered: true, items: verdict.risks || [] },
      { type: 'h', text: 'Your first steps' },
      { type: 'list', ordered: true, items: verdict.nextSteps || [] },
      { type: 'callout', label: 'The bottom line', text: verdict.verdict },
    ],
  };
}

function swotReportSpec(answers, board, accent) {
  const S = window.CST_SWOT;
  const sval = { good: 86, watch: 56, risk: 30 };
  const dimRows = S.DIMENSIONS.map((d) => {
    const x = board.dims[d.key];
    return { glyph: d.glyph, label: d.label, status: x.status, statusLabel: S.STATUS_LABEL[x.status], read: x.read, decision: x.decision };
  });
  const moves = S.DIMENSIONS
    .map((d) => ({ d, x: board.dims[d.key] }))
    .sort((a, b) => sval[a.x.status] - sval[b.x.status])
    .slice(0, 3)
    .map(({ d, x }) => `${d.label}: ${x.decision}`);
  return {
    tool: 'SWOT', accent, shortlink: window.CST_SHARE.SHORTLINK,
    blocks: [
      { type: 'hero', ring: { value: board.score, pct: board.score, unit: 'READY' }, title: board.headline },
      { type: 'kv', label: 'The business', value: SHORT(answers.ctx, 70) },
      { type: 'h', text: 'Where you stand, by dimension' },
      { type: 'bars', max: 100, rows: dimRows.map((r) => ({ label: r.label, value: sval[r.status], valueText: r.statusLabel, emph: r.status !== 'good', color: R_STATUS[r.status] })) },
      { type: 'pagebreak' },
      { type: 'h', text: 'Your living decision board' },
      { type: 'matrix', rows: dimRows },
      { type: 'pagebreak' },
      { type: 'h', text: 'Your next three moves' },
      { type: 'list', ordered: true, items: moves },
      { type: 'callout', label: 'The bottom line', text: board.headline },
    ],
  };
}

function priceReportSpec(result, accent) {
  const P = window.CST;
  const link = window.CST_SHARE.SHORTLINK;
  if (result.mode === 'job') {
    const r = result; const max = Math.max(r.scenarioPrice, r.price, r.costPerUnit, 1) * 1.05;
    const isHour = r.basis === 'hour';
    return {
      tool: 'Price-It', accent, shortlink: link,
      blocks: [
        { type: 'hero', ring: { value: P.moneyFull(r.effHourly), pct: Math.max(4, Math.round(r.marginPct)), unit: '/ HR KEPT' }, title: r.headline },
        { type: 'kv', label: 'How you charge', value: `${P.moneyFull(r.price)} per ${r.unitLabel} · ${Math.round(r.hours)} hrs/mo of your time` },
        { type: 'stats', cells: [
          { label: 'Effective hourly', value: P.moneyFull(r.effHourly), sub: 'your time, after costs', emph: true },
          { label: 'Monthly profit', value: P.money(r.monthlyProfit), sub: `${Math.round(r.marginPct)}% margin`, emph: true },
          { label: isHour ? 'Your rate' : 'Profit / ' + r.unitLabel, value: isHour ? P.moneyFull(r.price) + '/hr' : P.moneyFull(r.profitPerUnit), sub: isHour ? `${Math.round(r.hours)} billable hrs/mo` : `cost ${P.moneyFull(r.costPerUnit)}` },
          { label: 'Monthly revenue', value: P.money(r.monthlyRevenue), sub: `cost ${P.money(r.monthlyCost)}` },
        ] },
        { type: 'pagebreak' },
        { type: 'h', text: 'The math, in plain sight' },
        { type: 'p', text: `Across a typical month you bring in ${P.moneyFull(r.monthlyRevenue)} and spend ${P.moneyFull(r.monthlyCost)} to deliver, leaving ${P.moneyFull(r.monthlyProfit)} of profit. Spread over the ${Math.round(r.hours)} hours of your own time, that's an effective ${P.moneyFull(r.effHourly)} an hour — the number that matters whatever you charge by.` },
        { type: 'bars', max, rows: [
          { label: `Cost to deliver (per ${r.unitLabel})`, value: r.costPerUnit, valueText: P.moneyFull(r.costPerUnit), color: 'rgba(236,237,241,0.4)' },
          { label: `You charge (per ${r.unitLabel})`, value: r.price, valueText: P.moneyFull(r.price), emph: true, color: r.flag === 'losing' ? R_STATUS.risk : r.flag === 'under' ? R_STATUS.watch : R_STATUS.good },
          { label: 'If you raised 20%', value: r.scenarioPrice, valueText: P.moneyFull(r.scenarioPrice), emph: true, color: accent },
        ] },
        { type: 'gauge', value: Math.round(r.marginPct), valueText: Math.round(r.marginPct) + '%', label: 'profit margin', note: `Your time is really earning ${P.moneyFull(r.effHourly)}/hr. A 20% price rise would lift that to ${P.moneyFull(r.scenarioHourly)}/hr and monthly profit to ${P.moneyFull(r.scenarioProfit)}.` },
        { type: 'pagebreak' },
        { type: 'h', text: 'The blunt truth' },
        { type: 'callout', label: 'Verdict', text: (r.take && r.take.verdict) || r.headline },
        { type: 'h', text: 'How to fix it' },
        { type: 'list', ordered: true, items: (r.take && r.take.levers) || [] },
      ],
    };
  }
  const r = result; const max = Math.max(r.high, r.sdeHi, r.revHi) * 1.05 || 1;
  return {
    tool: 'Price-It', accent, shortlink: link,
    blocks: [
      { type: 'hero', ring: { value: P.money(r.likely), pct: r.profitMargin != null ? Math.round(r.profitMargin) : 50, unit: r.industry.label.toUpperCase() }, title: r.headline },
      { type: 'stats', cells: [
        { label: 'On profit (SDE)', value: `${P.money(r.sdeLo)}–${P.money(r.sdeHi)}`, sub: `${r.industry.sde[0]}–${r.industry.sde[1]}× SDE` },
        { label: 'On revenue', value: `${P.money(r.revLo)}–${P.money(r.revHi)}`, sub: `${r.industry.rev[0]}–${r.industry.rev[1]}× revenue` },
        { label: 'Effective multiple', value: r.effMultiple ? r.effMultiple.toFixed(1) + '×' : '—', sub: 'of owner profit', emph: true },
        { label: 'Profit margin', value: r.profitMargin != null ? Math.round(r.profitMargin) + '%' : '—', sub: 'profit ÷ revenue' },
      ] },
      { type: 'pagebreak' },
      { type: 'h', text: 'Two ways to value it' },
      { type: 'p', text: `Buyers pay a multiple of owner profit (SDE) — at ${r.industry.sde[0]}–${r.industry.sde[1]}× that's ${P.money(r.sdeLo)}–${P.money(r.sdeHi)}. As a cross-check, ${r.industry.rev[0]}–${r.industry.rev[1]}× revenue gives ${P.money(r.revLo)}–${P.money(r.revHi)}. The likely figure blends both, profit-led.` },
      { type: 'bars', max, rows: [
        { label: 'On profit (SDE × multiple)', value: r.sdeMid, valueText: `${P.money(r.sdeLo)}–${P.money(r.sdeHi)}`, emph: true, color: accent },
        { label: 'On revenue (× multiple)', value: r.revMid, valueText: `${P.money(r.revLo)}–${P.money(r.revHi)}`, color: R_STATUS.cold },
        { label: 'Blended — likely', value: r.likely, valueText: P.money(r.likely), emph: true, color: R_STATUS.good },
      ] },
      { type: 'gauge', value: r.profitMargin != null ? Math.round(r.profitMargin) : 0, valueText: (r.profitMargin != null ? Math.round(r.profitMargin) : '—') + '%', label: 'profit margin', note: 'Profit margin is the single biggest lever on your multiple — lift it before chasing revenue.' },
      { type: 'pagebreak' },
      { type: 'h', text: 'The blunt truth' },
      { type: 'callout', label: 'Verdict', text: (r.take && r.take.verdict) || r.headline },
      { type: 'h', text: 'How to raise the number' },
      { type: 'list', ordered: true, items: (r.take && r.take.levers) || [] },
      { type: 'p', text: 'A rule-of-thumb band, not a formal appraisal — your real number depends on growth, risk, and who is buying.' },
    ],
  };
}

Object.assign(window.CST_REPORT, { validateReportSpec, swotReportSpec, priceReportSpec });
