// FEATURE 2: PDF Export -- pure HTML/CSS template generators, no I/O

// FEATURE 2: PDF Export — template generators + puppeteer renderer
// ============================================================

/**
 * Generate a health score ring SVG (inline, no external deps)
 * @param {number} score  0–100
 * @param {string} color  CSS color
 */
function scoreRingSvg(score, color) {
  const r = 54, cx = 64, cy = 64;
  const circ = 2 * Math.PI * r;
  const fill = circ * (1 - score / 100);
  const clr = color || (score >= 80 ? '#22c55e' : score >= 60 ? '#f59e0b' : '#ef4444');
  return `<svg viewBox="0 0 128 128" width="128" height="128" xmlns="http://www.w3.org/2000/svg">
    <circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#e5e7eb" stroke-width="12"/>
    <circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="${clr}" stroke-width="12"
      stroke-dasharray="${circ}" stroke-dashoffset="${fill}"
      stroke-linecap="round" transform="rotate(-90 ${cx} ${cy})"/>
    <text x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="central"
      font-family="system-ui,sans-serif" font-size="26" font-weight="700" fill="${clr}">${score}</text>
    <text x="${cx}" y="${cy + 20}" text-anchor="middle"
      font-family="system-ui,sans-serif" font-size="10" fill="#6b7280">/100</text>
  </svg>`;
}

/** Shared CSS for all PDF templates */
function pdfBaseCSS() {
  return `
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, 'Segoe UI', system-ui, Arial, sans-serif; color: #1e1b4b; background: #fff; font-size: 11pt; line-height: 1.5; }
    @page { size: A4; margin: 18mm 16mm; }
    .page { page-break-after: always; min-height: 247mm; padding: 0; }
    .page:last-child { page-break-after: avoid; }
    h1 { font-size: 22pt; font-weight: 700; color: #4f46e5; }
    h2 { font-size: 14pt; font-weight: 600; color: #3730a3; margin-top: 1.4em; margin-bottom: 0.5em; border-bottom: 2px solid #e0e7ff; padding-bottom: 4px; }
    h3 { font-size: 11pt; font-weight: 600; color: #4338ca; margin-top: 1em; margin-bottom: 0.3em; }
    p  { margin-bottom: 0.5em; }
    table { width: 100%; border-collapse: collapse; margin: 0.6em 0; font-size: 9.5pt; }
    th { background: #4f46e5; color: #fff; padding: 6px 10px; text-align: left; font-weight: 600; }
    td { padding: 5px 10px; border-bottom: 1px solid #e0e7ff; vertical-align: top; }
    tr:nth-child(even) td { background: #f5f3ff; }
    .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 8.5pt; font-weight: 600; }
    .badge-green { background: #dcfce7; color: #166534; }
    .badge-red   { background: #fee2e2; color: #991b1b; }
    .badge-yellow{ background: #fef9c3; color: #854d0e; }
    .badge-gray  { background: #f3f4f6; color: #374151; }
    .badge-indigo{ background: #e0e7ff; color: #3730a3; }
    .cover { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; padding: 40px 0 20px; min-height: 247mm; }
    .cover-logo { font-size: 11pt; font-weight: 700; color: #4f46e5; letter-spacing: -0.5px; margin-bottom: 60px; display: flex; align-items: center; gap: 8px; }
    .cover-logo span { color: #1e1b4b; }
    .cover-asn  { font-size: 11pt; font-weight: 500; color: #6b7280; margin-bottom: 6px; }
    .cover-name { font-size: 28pt; font-weight: 700; color: #1e1b4b; margin-bottom: 8px; line-height: 1.2; }
    .cover-sub  { font-size: 13pt; color: #4f46e5; font-weight: 500; margin-bottom: 30px; }
    .cover-meta { font-size: 9pt; color: #9ca3af; margin-top: auto; padding-top: 20px; border-top: 1px solid #e0e7ff; width: 100%; }
    .cover-stripe { position: fixed; right: -8mm; top: 0; width: 28mm; height: 100vh; background: linear-gradient(180deg, #4f46e5 0%, #6366f1 50%, #818cf8 100%); opacity: 0.08; pointer-events: none; }
    .metric-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 1em 0; }
    .metric-card { background: #f5f3ff; border: 1px solid #e0e7ff; border-radius: 8px; padding: 12px 14px; }
    .metric-card .label { font-size: 8pt; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
    .metric-card .value { font-size: 18pt; font-weight: 700; color: #4f46e5; }
    .metric-card .unit  { font-size: 8pt; color: #9ca3af; }
    .score-row { display: flex; align-items: center; gap: 20px; margin: 0.8em 0; }
    .check-list { list-style: none; padding: 0; }
    .check-list li { display: flex; align-items: flex-start; gap: 8px; padding: 5px 0; border-bottom: 1px solid #f3f4f6; font-size: 9.5pt; }
    .check-icon { width: 16px; height: 16px; flex-shrink: 0; margin-top: 2px; }
    .check-pass { color: #16a34a; }
    .check-fail { color: #dc2626; }
    .check-warn { color: #d97706; }
    .section-intro { font-size: 9.5pt; color: #6b7280; margin-bottom: 1em; font-style: italic; }
    footer { position: fixed; bottom: 0; left: 0; right: 0; font-size: 8pt; color: #9ca3af; padding: 6px 16mm; border-top: 1px solid #e0e7ff; display: flex; justify-content: space-between; }
  `;
}

/**
 * Build the PDF HTML for a single ASN.
 * @param {object} data   Full /api/validate response
 * @param {object} aspa   /api/aspa response (may be null)
 * @param {string} format 'report' | 'executive' | 'technical'
 */
function buildPdfHtml(data, aspa, format) {
  const asn      = data.asn || '?';
  const name     = data.name || 'Unknown Network';
  const score    = Math.round(data.health_score || 0);
  const ts       = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
  const checks   = (data.score_breakdown || []);
  const rel      = data.relationships || {};
  const counts   = rel.counts || {};
  // validations is an object keyed by check name; rpki details live in rpki_completeness
  const validations  = (data.validations && typeof data.validations === 'object' && !Array.isArray(data.validations)) ? data.validations : {};
  const rpkiDetails  = (validations.rpki_completeness?.details || []);
  const totalPfx     = data.meta?.total_prefixes || 0;

  // Derived stats — use rpki_completeness for RPKI metrics
  const rpki           = validations.rpki_completeness || {};
  const rpkiCoverage   = rpki.coverage_pct ?? 0;
  const rpkiTotal      = rpki.total_checked || rpkiDetails.length || 0;
  const rpkiValid      = rpki.with_roa || rpkiDetails.filter(v => v.status === 'valid').length;
  const rpkiInvalid    = rpkiDetails.filter(v => v.status === 'invalid').length;
  const rpkiNotFound   = rpkiDetails.filter(v => v.status === 'not_found').length;

  const passedChecks = checks.filter(c => c.status === 'pass').length;
  const failedChecks = checks.filter(c => c.status !== 'pass' && c.status !== 'info').length;
  const scoreColor   = score >= 80 ? '#16a34a' : score >= 60 ? '#d97706' : '#dc2626';
  const aspaDirect   = aspa?.direct?.has_aspa ?? null;
  const aspaProviders= aspa?.direct?.provider_asns?.length || 0;

  // Check list HTML
  function checkIcon(passed) {
    if (passed) return '<svg class="check-icon check-pass" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
    return '<svg class="check-icon check-fail" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>';
  }

  const checksHtml = checks.map(c => {
    const isPassed = c.status === 'pass';
    const isInfo   = c.status === 'info';
    const icon     = checkIcon(isPassed || isInfo);
    const statusBadge = isPassed ? 'badge-green' : isInfo ? 'badge-indigo' : 'badge-red';
    const statusLabel = isPassed ? 'PASS' : isInfo ? 'INFO' : 'FAIL';
    return `
    <li>
      ${icon}
      <div>
        <span style="font-weight:600">${escHtml(c.check || '')}</span>
        <span class="badge ${statusBadge}" style="margin-left:6px">${statusLabel}</span>
        ${c.earned !== undefined ? `<span class="badge badge-indigo" style="margin-left:4px">${c.earned}/${c.weight}pt</span>` : ''}
      </div>
    </li>`;
  }).join('');

  // Upstream providers table
  const upstreamRows = (rel.upstreams || []).slice(0, 10).map(u => `
    <tr><td>AS${u.asn}</td><td>${escHtml(u.name || '')}</td><td>${u.power ?? '-'}</td></tr>
  `).join('');

  // Prefix validations table (sample from rpki_completeness.details)
  const pfxRows = rpkiDetails.slice(0, 20).map(v => {
    const badge = v.status === 'valid' ? 'badge-green' : v.status === 'invalid' ? 'badge-red' : 'badge-gray';
    return `<tr><td>${escHtml(v.prefix)}</td>
      <td><span class="badge ${badge}">${v.status}</span></td>
      <td>${v.validating_roas ?? '-'}</td>
      <td>—</td></tr>`;
  }).join('');

  // Recommendations
  const failedItems = checks.filter(c => c.status !== 'pass' && c.status !== 'info');
  const recHtml = failedItems.length === 0
    ? '<p style="color:#16a34a;font-weight:500">✓ No critical issues found. Network health is excellent.</p>'
    : failedItems.map((c, i) => `
      <div style="margin:8px 0; padding:10px 14px; background:#fef2f2; border-left:3px solid #ef4444; border-radius:4px;">
        <div style="font-weight:600;color:#991b1b">${i+1}. ${escHtml(c.check || '')}</div>
        ${c.detail ? `<div style="font-size:9pt;color:#6b7280;margin-top:3px">${escHtml(c.detail)}</div>` : ''}
      </div>`).join('');

  // ── Executive pages (always present) ───────────────────────────────────────
  const coverPage = `
  <div class="page cover">
    <div class="cover-stripe"></div>
    <div class="cover-logo">⬡ PeerCortex <span>Network Intelligence</span></div>
    <div class="cover-asn">AS${asn}</div>
    <div class="cover-name">${escHtml(name)}</div>
    <div class="cover-sub">${formatLabel(format)} Report</div>
    <div style="margin:20px 0; display:flex; align-items:center; gap:16px;">
      ${scoreRingSvg(score)}
      <div>
        <div style="font-size:9pt;color:#6b7280;text-transform:uppercase;letter-spacing:0.5px">Health Score</div>
        <div style="font-size:26pt;font-weight:700;color:${scoreColor}">${score}/100</div>
        <div style="font-size:9pt;color:#6b7280">${passedChecks} passed · ${failedChecks} failed</div>
      </div>
    </div>
    <div class="cover-meta">
      Generated by PeerCortex · ${ts} · peercortex.org
      &nbsp;&nbsp;|&nbsp;&nbsp; ${totalPfx} prefixes analysed
      &nbsp;&nbsp;|&nbsp;&nbsp; RPKI coverage ${rpkiCoverage}%
    </div>
  </div>`;

  const summaryPage = `
  <div class="page">
    <h2>Executive Summary</h2>
    <p class="section-intro">Key performance indicators for AS${asn} (${escHtml(name)}).</p>
    <div class="metric-grid">
      <div class="metric-card"><div class="label">Health Score</div><div class="value" style="color:${scoreColor}">${score}</div><div class="unit">out of 100</div></div>
      <div class="metric-card"><div class="label">RPKI Coverage</div><div class="value">${rpkiCoverage}%</div><div class="unit">${rpkiValid}/${rpkiTotal} prefixes</div></div>
      <div class="metric-card"><div class="label">Upstreams</div><div class="value">${counts.upstreams || 0}</div><div class="unit">providers</div></div>
      <div class="metric-card"><div class="label">Downstreams</div><div class="value">${counts.downstreams || 0}</div><div class="unit">customers</div></div>
      <div class="metric-card"><div class="label">Peers</div><div class="value">${counts.peers || 0}</div><div class="unit">BGP sessions</div></div>
      <div class="metric-card"><div class="label">ASPA Status</div>
        <div class="value" style="font-size:12pt;margin-top:4px">
          <span class="badge ${aspaDirect === true ? 'badge-green' : aspaDirect === false ? 'badge-red' : 'badge-gray'}">
            ${aspaDirect === true ? 'Deployed' : aspaDirect === false ? 'Not Deployed' : 'Unknown'}
          </span>
        </div>
        <div class="unit">${aspaProviders} providers</div>
      </div>
    </div>
    <h3>Status Overview</h3>
    <ul class="check-list">${checksHtml}</ul>
  </div>`;

  // ── Recommendations page ────────────────────────────────────────────────────
  const recsPage = `
  <div class="page">
    <h2>Recommendations</h2>
    <p class="section-intro">Action items to improve network health and security posture.</p>
    ${recHtml}
    ${failedItems.length > 0 ? `
    <h3 style="margin-top:1.5em">Remediation Priority</h3>
    <table>
      <tr><th>#</th><th>Check</th><th>Impact</th><th>Effort</th></tr>
      ${failedItems.map((c, i) => `
        <tr>
          <td>${i+1}</td>
          <td>${escHtml(c.check || '')}</td>
          <td><span class="badge badge-red">High</span></td>
          <td><span class="badge badge-yellow">Medium</span></td>
        </tr>`).join('')}
    </table>` : ''}
  </div>`;

  // ── Detail pages (included for 'report' and 'technical') ───────────────────
  const rpkiPage = `
  <div class="page">
    <h2>RPKI Compliance</h2>
    <p class="section-intro">Route Origin Authorization (ROA) validation results for announced prefixes.</p>
    <div class="metric-grid">
      <div class="metric-card"><div class="label">Coverage</div><div class="value">${rpkiCoverage}%</div><div class="unit">${rpkiValid} valid ROAs</div></div>
      <div class="metric-card"><div class="label">Invalid</div><div class="value" style="color:#dc2626">${rpkiInvalid}</div><div class="unit">prefix${rpkiInvalid !== 1 ? 'es' : ''}</div></div>
      <div class="metric-card"><div class="label">Not Found</div><div class="value" style="color:#d97706">${rpkiNotFound}</div><div class="unit">no ROA</div></div>
    </div>
    <h3>ASPA (Autonomous System Provider Authorization)</h3>
    <table>
      <tr><th>Attribute</th><th>Value</th></tr>
      <tr><td>ASPA Object Present</td><td><span class="badge ${aspaDirect ? 'badge-green' : 'badge-red'}">${aspaDirect ? 'Yes' : 'No'}</span></td></tr>
      <tr><td>Provider Count</td><td>${aspaProviders}</td></tr>
      <tr><td>Providers</td><td>${(aspa?.direct?.provider_asns || []).slice(0, 8).map(a => `AS${a}`).join(', ') || '—'}</td></tr>
    </table>
    ${pfxRows ? `
    <h3>Prefix Validation Sample (first 20)</h3>
    <table>
      <tr><th>Prefix</th><th>Status</th><th>Max Length</th><th>ROA Origin</th></tr>
      ${pfxRows}
    </table>` : ''}
  </div>`;

  const bgpPage = `
  <div class="page">
    <h2>BGP Topology</h2>
    <p class="section-intro">AS relationship overview and upstream connectivity analysis.</p>
    <div class="metric-grid">
      <div class="metric-card"><div class="label">Upstreams</div><div class="value">${counts.upstreams || 0}</div><div class="unit">transit providers</div></div>
      <div class="metric-card"><div class="label">Downstreams</div><div class="value">${counts.downstreams || 0}</div><div class="unit">customers</div></div>
      <div class="metric-card"><div class="label">Peers</div><div class="value">${counts.peers || 0}</div><div class="unit">BGP peers</div></div>
    </div>
    ${upstreamRows ? `
    <h3>Top Upstream Providers</h3>
    <table>
      <tr><th>ASN</th><th>Name</th><th>Power Score</th></tr>
      ${upstreamRows}
    </table>` : '<p style="color:#9ca3af">No upstream data available.</p>'}
  </div>`;

  const footer = `
  <footer>
    <span>PeerCortex · AS${asn} · ${escHtml(name)}</span>
    <span>Generated ${ts} · peercortex.org</span>
  </footer>`;

  // ── Extra pages for technical / report formats ─────────────────────────────

  // Technical: full prefix table (no row cap) + all upstream rows + check scores
  const allPfxRows = rpkiDetails.map(v => {
    const badge = v.status === 'valid' ? 'badge-green' : v.status === 'invalid' ? 'badge-red' : 'badge-gray';
    return `<tr><td>${escHtml(v.prefix)}</td>
      <td><span class="badge ${badge}">${v.status}</span></td>
      <td>${v.validating_roas ?? '-'}</td>
      <td>${v.max_length ?? '-'}</td></tr>`;
  }).join('');

  const allUpstreamRows = (rel.upstreams || []).map(u =>
    `<tr><td>AS${u.asn}</td><td>${escHtml(u.name || '')}</td><td>${u.power ?? '-'}</td></tr>`
  ).join('');

  const checkScoreRows = checks.map(c =>
    `<tr>
      <td>${escHtml(c.check || '')}</td>
      <td><span class="badge ${c.status === 'pass' ? 'badge-green' : c.status === 'info' ? 'badge-indigo' : 'badge-red'}">${c.status?.toUpperCase()}</span></td>
      <td>${c.earned ?? '-'}</td><td>${c.weight ?? '-'}</td>
      <td style="font-size:8pt;color:#6b7280">${escHtml(c.detail || '')}</td>
     </tr>`
  ).join('');

  const technicalHeaderPage = `
  <div class="page" style="padding-top:2cm">
    <div style="border-left:4px solid #6366f1;padding-left:16px;margin-bottom:24px">
      <div style="font-size:8pt;text-transform:uppercase;letter-spacing:1px;color:#6b7280">PeerCortex · Technical Analysis Report</div>
      <div style="font-size:22pt;font-weight:700;margin:4px 0">AS${asn} — ${escHtml(name)}</div>
      <div style="font-size:9pt;color:#6b7280">Generated ${ts} · peercortex.org · ${totalPfx} prefixes · RPKI ${rpkiCoverage}%</div>
    </div>
    <div class="metric-grid">
      <div class="metric-card"><div class="label">Health Score</div><div class="value" style="color:${scoreColor}">${score}/100</div><div class="unit">${passedChecks} pass · ${failedChecks} fail</div></div>
      <div class="metric-card"><div class="label">RPKI Coverage</div><div class="value">${rpkiCoverage}%</div><div class="unit">${rpkiValid} valid · ${rpkiInvalid} invalid · ${rpkiNotFound} missing</div></div>
      <div class="metric-card"><div class="label">Upstreams</div><div class="value">${counts.upstreams || 0}</div><div class="unit">transit providers</div></div>
      <div class="metric-card"><div class="label">Peers / Downstreams</div><div class="value">${counts.peers || 0} / ${counts.downstreams || 0}</div><div class="unit">BGP relationships</div></div>
      <div class="metric-card"><div class="label">ASPA</div>
        <div class="value" style="font-size:11pt;margin-top:6px"><span class="badge ${aspaDirect === true ? 'badge-green' : 'badge-red'}">${aspaDirect === true ? 'Deployed' : 'Not deployed'}</span></div>
        <div class="unit">${aspaProviders} providers registered</div>
      </div>
      <div class="metric-card"><div class="label">Prefixes Analysed</div><div class="value">${totalPfx}</div><div class="unit">${rpkiDetails.length} with RPKI data</div></div>
    </div>
  </div>`;

  const rpkiFullPage = `
  <div class="page">
    <h2>RPKI Compliance — Full Prefix Table</h2>
    <p class="section-intro">${rpkiDetails.length} prefixes with RPKI validation data. Coverage: ${rpkiCoverage}% · Valid: ${rpkiValid} · Invalid: ${rpkiInvalid} · Not Found: ${rpkiNotFound}</p>
    <h3>ASPA Object</h3>
    <table>
      <tr><th>Attribute</th><th>Value</th></tr>
      <tr><td>ASPA Present</td><td><span class="badge ${aspaDirect ? 'badge-green' : 'badge-red'}">${aspaDirect ? 'Yes' : 'No'}</span></td></tr>
      <tr><td>Provider Count</td><td>${aspaProviders}</td></tr>
      <tr><td>Registered Providers</td><td style="font-size:8pt">${(aspa?.direct?.provider_asns || []).map(a => `AS${a}`).join(', ') || '—'}</td></tr>
    </table>
    ${allPfxRows ? `
    <h3 style="margin-top:1.5em">All Prefixes</h3>
    <table>
      <tr><th>Prefix</th><th>RPKI Status</th><th>Validating ROAs</th><th>Max Length</th></tr>
      ${allPfxRows}
    </table>` : '<p style="color:#9ca3af">No RPKI prefix data available.</p>'}
  </div>`;

  const bgpFullPage = `
  <div class="page">
    <h2>BGP Topology — Full Relationship Table</h2>
    <p class="section-intro">Complete AS relationship data. Upstreams: ${counts.upstreams || 0} · Peers: ${counts.peers || 0} · Downstreams: ${counts.downstreams || 0}</p>
    ${allUpstreamRows ? `
    <h3>All Upstream Providers</h3>
    <table>
      <tr><th>ASN</th><th>Name</th><th>Power Score</th></tr>
      ${allUpstreamRows}
    </table>` : '<p style="color:#9ca3af">No upstream data available.</p>'}
    ${(rel.peers || []).length > 0 ? `
    <h3 style="margin-top:1.5em">Peer Sample (top 20)</h3>
    <table>
      <tr><th>ASN</th><th>Name</th></tr>
      ${(rel.peers || []).slice(0, 20).map(p => `<tr><td>AS${p.asn}</td><td>${escHtml(p.name || '')}</td></tr>`).join('')}
    </table>` : ''}
  </div>`;

  const checkDetailPage = `
  <div class="page">
    <h2>Check Score Detail</h2>
    <p class="section-intro">All ${checks.length} health checks with individual scores and diagnostic notes.</p>
    <table>
      <tr><th>Check</th><th>Status</th><th>Earned</th><th>Weight</th><th>Detail</th></tr>
      ${checkScoreRows}
    </table>
    <div style="margin-top:1.5em;padding:12px;background:#f9fafb;border:1px solid #e5e7eb;font-size:8pt;color:#6b7280">
      <strong>Scoring:</strong> Total = sum of earned points across all checks. Maximum possible: ${checks.reduce((s, c) => s + (c.weight || 0), 0)} points.
      Current score: ${checks.reduce((s, c) => s + (c.earned || 0), 0)} / ${checks.reduce((s, c) => s + (c.weight || 0), 0)} = ${score}/100
    </div>
  </div>`;

  // Report: add data provenance and ASPA deep-dive
  const aspaPage = `
  <div class="page">
    <h2>ASPA — Provider Authorization Analysis</h2>
    <p class="section-intro">Autonomous System Provider Authorization (ASPA) is a BGP security mechanism that cryptographically links an ASN to its upstream providers in the RPKI.</p>
    <div class="metric-grid">
      <div class="metric-card"><div class="label">ASPA Deployed</div>
        <div class="value" style="font-size:12pt;margin-top:6px"><span class="badge ${aspaDirect === true ? 'badge-green' : 'badge-red'}">${aspaDirect === true ? 'Yes' : 'No'}</span></div>
      </div>
      <div class="metric-card"><div class="label">Providers Listed</div><div class="value">${aspaProviders}</div><div class="unit">in ASPA object</div></div>
      <div class="metric-card"><div class="label">RPKI Coverage</div><div class="value">${rpkiCoverage}%</div><div class="unit">of announced prefixes</div></div>
    </div>
    <h3>What ASPA Protects Against</h3>
    <table>
      <tr><th>Threat</th><th>ASPA Mitigates?</th><th>Notes</th></tr>
      <tr><td>Route leaks (valley-free violations)</td><td><span class="badge badge-green">Yes</span></td><td>Providers verify upstream path</td></tr>
      <tr><td>BGP hijacks (origin AS spoofing)</td><td><span class="badge badge-indigo">Partial</span></td><td>Combined with ROV</td></tr>
      <tr><td>AS-path prepending attacks</td><td><span class="badge badge-green">Yes</span></td><td>Invalid paths rejected</td></tr>
      <tr><td>Forged-origin hijacks</td><td><span class="badge badge-red">No</span></td><td>ROA + ROV required</td></tr>
    </table>
    ${aspaDirect === false ? `
    <div style="margin-top:1.5em;padding:14px;background:#fef2f2;border-left:3px solid #ef4444;border-radius:4px">
      <div style="font-weight:600;color:#991b1b;margin-bottom:6px">Recommendation: Deploy ASPA</div>
      <div style="font-size:9pt;color:#6b7280">
        1. Identify all upstream transit providers (${counts.upstreams || 0} detected)<br>
        2. Create an ASPA object in your RIR portal listing their ASNs<br>
        3. Sign with your RPKI key — takes effect within hours<br>
        4. Verify with: <code>rpki-client -v</code> or RIPE RPKI Validator
      </div>
    </div>` : `
    <div style="margin-top:1.5em;padding:14px;background:#f0fdf4;border-left:3px solid #16a34a;border-radius:4px">
      <div style="font-weight:600;color:#15803d">ASPA is deployed — listed providers:</div>
      <div style="font-size:9pt;color:#374151;margin-top:6px">${(aspa?.direct?.provider_asns || []).map(a => `AS${a}`).join(' · ') || '—'}</div>
    </div>`}
  </div>`;

  const provenancePage = `
  <div class="page">
    <h2>Data Provenance</h2>
    <p class="section-intro">All data sources, cache ages, and methodology used to generate this report.</p>
    <table>
      <tr><th>Data Source</th><th>What it provides</th><th>Update Frequency</th></tr>
      <tr><td>Cloudflare RPKI Feed</td><td>ROA + ASPA objects</td><td>Every 4 hours</td></tr>
      <tr><td>PeeringDB</td><td>IXP presence, peering policy, facilities</td><td>Daily (03:00 UTC)</td></tr>
      <tr><td>RIPE Stat</td><td>AS relationships, prefix visibility, BGP data</td><td>Real-time / cached 15 min</td></tr>
      <tr><td>RIPE Atlas</td><td>Active measurement probes by ASN</td><td>Cached 12 hours</td></tr>
      <tr><td>bgproutes.io</td><td>BGP route visibility fallback</td><td>Real-time</td></tr>
      <tr><td>bgp.he.net</td><td>AS relationship graph</td><td>Cached 24 hours</td></tr>
    </table>
    <h3 style="margin-top:1.5em">Report Metadata</h3>
    <table>
      <tr><th>Field</th><th>Value</th></tr>
      <tr><td>ASN</td><td>AS${asn}</td></tr>
      <tr><td>Network Name</td><td>${escHtml(name)}</td></tr>
      <tr><td>Report Format</td><td>${formatLabel(format)}</td></tr>
      <tr><td>Generated</td><td>${ts}</td></tr>
      <tr><td>Platform</td><td>PeerCortex · peercortex.org</td></tr>
      <tr><td>Prefixes Analysed</td><td>${totalPfx}</td></tr>
      <tr><td>RPKI Objects Checked</td><td>${rpkiDetails.length}</td></tr>
      <tr><td>Health Checks Run</td><td>${checks.length}</td></tr>
    </table>
    <div style="margin-top:1.5em;font-size:8pt;color:#9ca3af;border-top:1px solid #e5e7eb;padding-top:12px">
      This report is generated automatically based on publicly available BGP and RPKI data.
      Data may be delayed by cache TTLs. Not a substitute for real-time monitoring.
      MIT License · github.com/renefichtmueller/PeerCortex
    </div>
  </div>`;

  // ── Compose pages by format ────────────────────────────────────────────────
  let bodyContent;
  if (format === 'executive') {
    // 3 pages: clean stakeholder summary — no deep technical data
    bodyContent = coverPage + summaryPage + recsPage;
  } else if (format === 'technical') {
    // ~8 pages: engineer-focused, dense, no decorative cover, full data tables
    bodyContent = technicalHeaderPage + rpkiFullPage + bgpFullPage + checkDetailPage + recsPage;
  } else {
    // 'report' — full stakeholder + technical, ~10 pages
    bodyContent = coverPage + summaryPage + rpkiPage + bgpPage + aspaPage + recsPage + provenancePage;
  }

  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <title>PeerCortex — AS${asn} ${formatLabel(format)}</title>
  <style>${pdfBaseCSS()}</style>
</head>
<body>
  ${bodyContent}
  ${footer}
</body>
</html>`;
}

/**
 * Build comparison PDF HTML for two ASNs.
 */
function buildComparisonPdfHtml(d1, aspa1, l1, d2, aspa2, l2) {
  const ts    = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });

  function col(d, aspa) {
    const asn   = d.asn || '?';
    const name  = d.name || 'Unknown';
    const score = Math.round(d.health_score || 0);
    const rel   = d.relationships || {};
    const counts= rel.counts || {};
    const vals  = (d.validations && !Array.isArray(d.validations)) ? d.validations : {};
    const rpki  = vals.rpki_completeness || {};
    const rpkiV = rpki.with_roa || 0;
    const rpkiT = rpki.total_checked || 0;
    const rpkiC = rpki.coverage_pct ?? (rpkiT > 0 ? Math.round((rpkiV / rpkiT) * 100) : 0);
    const sc    = score >= 80 ? '#16a34a' : score >= 60 ? '#d97706' : '#dc2626';
    const aspaDep = aspa?.direct?.has_aspa ?? null;
    const checks= (d.score_breakdown || []);
    const passC = checks.filter(c => c.status === 'pass').length;
    const failC = checks.filter(c => c.status !== 'pass' && c.status !== 'info').length;

    return `
      <td style="width:50%;padding:0 10px;vertical-align:top;border-right:2px solid #e0e7ff;">
        <div style="text-align:center;padding:16px 0 12px;">
          ${scoreRingSvg(score, sc)}
          <div style="font-size:10pt;color:#6b7280;margin-top:4px">AS${asn}</div>
          <div style="font-size:13pt;font-weight:700;color:#1e1b4b">${escHtml(name)}</div>
        </div>
        <table>
          <tr><th colspan="2" style="background:#4f46e5">Key Metrics</th></tr>
          <tr><td>Health Score</td><td style="font-weight:700;color:${sc}">${score}/100</td></tr>
          <tr><td>RPKI Coverage</td><td>${rpkiC}% (${rpkiV}/${rpkiT})</td></tr>
          <tr><td>Upstreams</td><td>${counts.upstreams || 0}</td></tr>
          <tr><td>Downstreams</td><td>${counts.downstreams || 0}</td></tr>
          <tr><td>Peers</td><td>${counts.peers || 0}</td></tr>
          <tr><td>ASPA</td><td><span class="badge ${aspaDep ? 'badge-green' : 'badge-red'}">${aspaDep ? 'Deployed' : 'Not Deployed'}</span></td></tr>
          <tr><td>Checks Passed</td><td style="color:#16a34a;font-weight:600">${passC} / ${checks.length}</td></tr>
          <tr><td>Checks Failed</td><td style="color:#dc2626;font-weight:600">${failC}</td></tr>
          <tr><td>Total Prefixes</td><td>${d.meta?.total_prefixes || 0}</td></tr>
        </table>
      </td>`;
  }

  const colHtml = col(d1, aspa1) + col(d2, aspa2);

  // ── Peering Intelligence: common IXPs + common facilities ─────────────────
  const ixList1 = (l1?.ix_presence?.connections || []).map(x => ({ id: x.ix_id, name: x.ix_name, city: x.city || '', speed: x.speed_mbps }));
  const ixList2 = (l2?.ix_presence?.connections || []).map(x => ({ id: x.ix_id, name: x.ix_name, city: x.city || '', speed: x.speed_mbps }));
  const ixIds1  = new Set(ixList1.map(x => x.id));
  const ixIds2  = new Set(ixList2.map(x => x.id));
  const commonIxIds = [...ixIds1].filter(id => ixIds2.has(id));
  const commonIxps  = commonIxIds.slice(0, 20).map(id => {
    const x1 = ixList1.find(x => x.id === id);
    const x2 = ixList2.find(x => x.id === id);
    return { id, name: x1?.name || 'Unknown', city: x1?.city || '', speed1: x1?.speed, speed2: x2?.speed };
  });

  const facList1 = (l1?.facilities?.list || []).map(f => ({ id: f.fac_id, name: f.name, city: f.city || '', country: f.country || '' }));
  const facList2 = (l2?.facilities?.list || []).map(f => ({ id: f.fac_id, name: f.name, city: f.city || '', country: f.country || '' }));
  const facIds1  = new Set(facList1.map(f => f.id));
  const facIds2  = new Set(facList2.map(f => f.id));
  const commonFacIds = [...facIds1].filter(id => facIds2.has(id));
  const commonFacs   = commonFacIds.slice(0, 15).map(id => facList1.find(f => f.id === id));

  // IXPs of ASN1 where ASN2 is NOT present (opportunity IXPs for ASN2)
  const oppsFor2 = ixList1.filter(x => !ixIds2.has(x.id)).slice(0, 8);
  const oppsFor1 = ixList2.filter(x => !ixIds1.has(x.id)).slice(0, 8);

  const commonIxHtml = commonIxps.length > 0
    ? `<table>
        <tr><th>IXP Name</th><th>City</th><th>AS${d1.asn} Speed</th><th>AS${d2.asn} Speed</th></tr>
        ${commonIxps.map(x => `
        <tr>
          <td style="font-weight:500">${escHtml(x.name)}</td>
          <td>${escHtml(x.city)}</td>
          <td>${x.speed1 ? (x.speed1/1000).toLocaleString()+'G' : '—'}</td>
          <td>${x.speed2 ? (x.speed2/1000).toLocaleString()+'G' : '—'}</td>
        </tr>`).join('')}
      </table>`
    : '<p style="color:#6b7280;font-style:italic">No common IXPs found — these networks do not share any Internet Exchange Points.</p>';

  const commonFacHtml = commonFacs.length > 0
    ? `<table>
        <tr><th>Datacenter / Colocation Facility</th><th>City</th><th>Country</th></tr>
        ${commonFacs.map(f => `
        <tr>
          <td style="font-weight:500">${escHtml(f?.name||'')}</td>
          <td>${escHtml(f?.city||'')}</td>
          <td>${escHtml(f?.country||'')}</td>
        </tr>`).join('')}
      </table>`
    : '<p style="color:#6b7280;font-style:italic">No shared colocation facilities found.</p>';

  const oppsHtml = (asns, opps) => opps.length === 0
    ? `<p style="color:#16a34a;font-size:9pt">Already maximally peered on these exchanges — or no data available.</p>`
    : opps.map(x => `<div style="padding:4px 0;border-bottom:1px solid #f3f4f6;font-size:9pt">
        <span style="font-weight:600">${escHtml(x.name)}</span>
        ${x.city ? `<span style="color:#6b7280"> · ${escHtml(x.city)}</span>` : ''}
        ${x.speed ? `<span class="badge badge-indigo" style="margin-left:6px">${(x.speed/1000).toLocaleString()}G</span>` : ''}
      </div>`).join('');

  const peeringPage = (commonIxps.length + commonFacs.length > 0 || oppsFor1.length + oppsFor2.length > 0) ? `
  <div class="page">
    <h2>Peering Intelligence</h2>
    <p class="section-intro">Common Internet Exchange Points and colocation facilities — potential peering locations between AS${d1.asn} and AS${d2.asn}.</p>

    <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:6px">
      <div style="background:#e0e7ff;border-radius:6px;padding:10px 14px;text-align:center">
        <div style="font-size:20pt;font-weight:700;color:#4f46e5">${commonIxps.length}</div>
        <div style="font-size:9pt;color:#6b7280">Common IXPs</div>
      </div>
      <div style="background:#e0e7ff;border-radius:6px;padding:10px 14px;text-align:center">
        <div style="font-size:20pt;font-weight:700;color:#4f46e5">${commonFacs.length}</div>
        <div style="font-size:9pt;color:#6b7280">Shared Facilities</div>
      </div>
    </div>

    <h3>Common Internet Exchange Points (direct peering possible)</h3>
    ${commonIxHtml}

    <h3 style="margin-top:1.2em">Shared Colocation Facilities</h3>
    ${commonFacHtml}

    ${oppsFor1.length + oppsFor2.length > 0 ? `
    <h3 style="margin-top:1.2em">Expansion Opportunities</h3>
    <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:6px">
      <div>
        <div style="font-weight:600;color:#4f46e5;margin-bottom:6px;font-size:9.5pt">AS${d2.asn} could join (AS${d1.asn} is there)</div>
        ${oppsHtml(d2.asn, oppsFor2)}
      </div>
      <div>
        <div style="font-weight:600;color:#4f46e5;margin-bottom:6px;font-size:9.5pt">AS${d1.asn} could join (AS${d2.asn} is there)</div>
        ${oppsHtml(d1.asn, oppsFor1)}
      </div>
    </div>` : ''}
  </div>` : '';

  // Side-by-side check comparison
  const allChecks = [...new Set([
    ...(d1.score_breakdown || []).map(c => c.check),
    ...(d2.score_breakdown || []).map(c => c.check),
  ])];
  const checkRows = allChecks.map(checkName => {
    const c1 = (d1.score_breakdown || []).find(c => c.check === checkName);
    const c2 = (d2.score_breakdown || []).find(c => c.check === checkName);
    const isP1 = c1?.status === 'pass';
    const isP2 = c2?.status === 'pass';
    const icon = p => p ? '✓' : '✗';
    const st   = p => p ? 'color:#16a34a;font-weight:700' : 'color:#dc2626;font-weight:700';
    return `<tr>
      <td>${escHtml(checkName)}</td>
      <td style="${st(isP1)}">${c1 ? icon(isP1) : '—'}</td>
      <td style="${st(isP2)}">${c2 ? icon(isP2) : '—'}</td>
    </tr>`;
  }).join('');

  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <title>PeerCortex — AS${d1.asn} vs AS${d2.asn} Comparison</title>
  <style>${pdfBaseCSS()}</style>
</head>
<body>
  <div class="page cover">
    <div class="cover-stripe"></div>
    <div class="cover-logo">⬡ PeerCortex <span>Network Intelligence</span></div>
    <div style="margin-top:40px">
      <div style="font-size:9pt;color:#6b7280;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">ASN Comparison Report</div>
      <div style="font-size:24pt;font-weight:700;color:#1e1b4b">AS${d1.asn} vs AS${d2.asn}</div>
      <div style="font-size:13pt;color:#4f46e5;margin-top:4px">${escHtml(d1.name)} ← vs → ${escHtml(d2.name)}</div>
    </div>
    <table style="margin-top:40px"><tr>${colHtml}</tr></table>
    <div class="cover-meta" style="margin-top:auto;padding-top:16px">Generated by PeerCortex · ${ts} · peercortex.org</div>
  </div>
  <div class="page">
    <h2>Head-to-Head Health Checks</h2>
    <table>
      <tr><th>Check</th><th>AS${d1.asn}</th><th>AS${d2.asn}</th></tr>
      ${checkRows}
    </table>
  </div>
  ${peeringPage}
  <footer>
    <span>PeerCortex · AS${d1.asn} vs AS${d2.asn}</span>
    <span>Generated ${ts} · peercortex.org</span>
  </footer>
</body>
</html>`;
}

function formatLabel(fmt) {
  return fmt === 'executive' ? 'Executive Summary' : fmt === 'technical' ? 'Technical Analysis' : 'Full Report';
}

function escHtml(s) {
  if (!s) return '';
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

module.exports = { scoreRingSvg, pdfBaseCSS, buildPdfHtml, buildComparisonPdfHtml, formatLabel, escHtml };
