// Construit et imaginé par Jean-François Avart
// pdf-report.js - Version améliorée avec table des matières et graphiques
(function () {
  const { jsPDF } = window.jspdf || {};
  const charts = window.charts || {};
  if (!jsPDF || !(jsPDF.API && jsPDF.API.autoTable)) {
    void 0;
    return;
  }

  // Configuration PDF
  const CONFIG = {
    MARGIN_X: 25,
    MARGIN_Y: 20,
    MAX_CHART_HEIGHT: 55,
    CHART_MARGIN: 8,
    LINE_HEIGHT: 5,
    TABLE_FONT_SIZE: 8,
    SUBTITLE_FONT_SIZE: 12,
    TITLE_FONT_SIZE: 12,
    MAIN_TITLE_FONT_SIZE: 16,
    TOC_FONT_SIZE: 10,
    BODY_TOP: 28,
    COLORS: {
      primary: [59, 130, 246],      // Bleu
      success: [34, 197, 94],       // Vert
      warning: [249, 115, 22],      // Orange
      danger: [239, 68, 68],        // Rouge
      muted: [107, 114, 128],       // Gris
      headerBg: [241, 245, 249],    // Fond en-tête
      alternateBg: [248, 250, 252]  // Fond alterné
    }
  };

  const MATRIX_COLORS = {
    insignifiant: [239, 243, 244],
    mineur: [220, 252, 231],
    significatif: [254, 243, 199],
    grave: [254, 215, 170],
    critique: [254, 202, 202]
  };

  function tt(key, params) {
    return typeof window.t === 'function' ? window.t(key, params) : key;
  }

  function getLabels() {
    return {
      impact: { 1: tt('impactLevels.1'), 2: tt('impactLevels.2'), 3: tt('impactLevels.3'), 4: tt('impactLevels.4'), 5: tt('impactLevels.5') },
      probability: { 1: tt('probabilityLevels.1'), 2: tt('probabilityLevels.2'), 3: tt('probabilityLevels.3'), 4: tt('probabilityLevels.4'), 5: tt('probabilityLevels.5') },
      threatStatus: { reduction: tt('treatments.reduction'), acceptation: tt('treatments.acceptation'), transfert: tt('treatments.transfert'), evitement: tt('treatments.evitement') },
      projectRiskStatus: { red: tt('projectRiskStatuses.red'), yellow: tt('projectRiskStatuses.yellow'), green: tt('projectRiskStatuses.green'), todo: tt('projectRiskStatuses.todo'), suspended: tt('projectRiskStatuses.suspended') },
      nis2Status: { todo: tt('nis2Statuses.todo'), in_progress: tt('nis2Statuses.inProgress'), suspended: tt('nis2Statuses.suspended'), done: tt('nis2Statuses.done') },
      nis2Type: { service: tt('nis2ActionTypes.service'), projet: tt('nis2ActionTypes.projet'), initiative: tt('nis2ActionTypes.initiative'), programme: tt('nis2ActionTypes.programme') },
      nis2Urgency: { high: tt('nis2Urgency.high'), medium: tt('nis2Urgency.medium'), standard: tt('nis2Urgency.standard') },
      projectRiskPriority: { high: tt('projectRisks.priorityHigh'), medium: tt('projectRisks.priorityMedium'), low: tt('projectRisks.priorityLow') },
      actionPriority: { critical: tt('actionPriorities.critical'), high: tt('actionPriorities.high'), medium: tt('actionPriorities.medium'), low: tt('actionPriorities.low') },
      actionLinkType: { control: tt('actionLinkTypes.control', 'Contrôle'), risk: tt('actionLinkTypes.risk', 'Risque'), nc: tt('actionLinkTypes.nc', 'Non-conformité'), audit: tt('actionLinkTypes.audit', 'Audit'), threat: tt('actionLinkTypes.threat', 'Menace'), continuous: tt('actionLinkTypes.continuous', 'Amélioration continue') }
    };
  }

  // FIX-PDF-RECYF (audit 2026-06-10) : mesures ReCyF dans le périmètre du degré courant
  // (EI obj. 1-15 / EE obj. 1-20), même filtre que recyfControlsInScope() de bundle.js.
  function getRecyfControlsInScope() {
    const controls = window.CSI_FrameworkMapper
      ? window.CSI_FrameworkMapper.getAppDataControls('recyf2025')
      : (window.appData?.recyfControls || []);
    const levelOrder = { importante: 1, essentielle: 2 };
    const maxOrder = levelOrder[window.appData?.recyfLevel || 'importante'] || 1;
    return (Array.isArray(controls) ? controls : []).filter(c => (levelOrder[c.level] || 99) <= maxOrder);
  }

  // FIX-PDF-RECYF (audit 2026-06-10) : libellé localisé du statut de conformité ReCyF.
  function getRecyfConformityLabel(value) {
    const opt = (window.RECYF_CONFORMITY_OPTIONS || []).find(o => o.value === value);
    if (!opt) return value || '';
    const lang = window.CSI_i18n ? window.CSI_i18n.getLanguage() : 'fr';
    // GOV-NIS2 S5 (M18) : trilingue — le NL retombait sur le français.
    if (lang === 'en' && opt.labelEn) return opt.labelEn;
    if (lang === 'nl' && opt.labelNl) return opt.labelNl;
    return opt.label;
  }

  // GOV-NIS2 S5 (M16) : statut CyFun restitué en libellé localisé — le PDF imprimait la
  // clé interne ('implemente') sous un en-tête pourtant traduit.
  function getNis2ControlStatusLabel(value) {
    const map = {
      non_implemente: tt('controlStatuses.nonImplemente'),
      en_cours: tt('controlStatuses.enCours'),
      implemente: tt('controlStatuses.implemente'),
      verifie: tt('controlStatuses.verifie')
    };
    return map[value] || value || '';
  }

  function getReportSections() {
    // FIX-PDF-RECYF (audit 2026-06-10) : les sections NIS2 figées sur les données CyFun
    // ignoraient le référentiel France. Quand appData.nis2Referential === 'recyf2025',
    // on remplace les 3 sections CyFun (contrôles, budget, socle — onglets masqués en UI
    // sous ReCyF) par la section « Mesures ReCyF » lisant appData.recyfControls.
    const isRecyf = (window.appData?.nis2Referential || 'cyfun2025') === 'recyf2025';
    const nis2Sections = isRecyf
      ? [{ id: 'recyf-controls', title: tt('recyfControls.title'), dataKey: 'recyfControls' }]
      : [
          { id: 'nis2-controls', title: tt('reportSections.nis2Controls'), dataKey: 'nis2Controls' },
          { id: 'nis2-socle', title: tt('reportSections.nis2Socle'), dataKey: 'soclePillars' }
        ];
    // Budget Cybersécurité : désormais décorrélé de NIS2 → inclus dans le rapport quel
    // que soit le référentiel actif (auparavant absent en mode ReCyF/France).
    nis2Sections.push({ id: 'nis2-program', title: tt('reportSections.nis2Program'), dataKey: 'nis2Plan' });
    return [
      { id: 'controls', title: tt('reportSections.controls'), dataKey: 'controls' },
      { id: 'soa', title: tt('reportSections.soa'), dataKey: 'soa' },
      ...nis2Sections,
      { id: 'actions', title: tt('reportSections.actions'), dataKey: 'actions' },
      { id: 'risks', title: tt('reportSections.risks'), dataKey: 'risks' },
      { id: 'threats-section', title: tt('reportSections.threats'), dataKey: 'threats' },
      { id: 'nonconformities', title: tt('reportSections.nonconformities'), dataKey: 'nonconformities' },
      { id: 'audits', title: tt('reportSections.audits'), dataKey: 'audits' },
      { id: 'managementReviews', title: tt('reportSections.managementReviews'), dataKey: 'reviews' },
      { id: 'docReview', title: tt('reportSections.docReview'), dataKey: 'documentReview' },
      { id: 'documents', title: tt('reportSections.documents'), dataKey: 'documents' },
      { id: 'stakeholders', title: tt('reportSections.stakeholders'), dataKey: 'stakeholders' },
      { id: 'kpis', title: tt('reportSections.kpis'), dataKey: 'kpis' },
      { id: 'critical-assets', title: tt('reportSections.criticalAssets'), dataKey: 'criticalAssets' },
      { id: 'project-risks', title: tt('reportSections.projectRisks'), dataKey: 'projectRisks' }
    ];
  }

  // État global pour la pagination
  let totalPages = 0;
  let tocEntries = [];

  // FIX-PDF-PAGINATION (audit 2026-06-10) : le total de pages n'est connu qu'en fin de
  // génération. Les pieds de page écrivent ce placeholder, remplacé par le vrai total via
  // doc.putTotalPages() (jsPDF) après le rendu complet — fin de l'estimation fausse
  // « 3 + sections × 2 ».
  // FIX-PDF-PLACEHOLDER (revue audit 2026-06-11) : jeton improbable au lieu de '{nb}'.
  // doc.putTotalPages() remplace la chaîne dans TOUTES les pages ; un '{nb}' littéral
  // saisi par l'utilisateur (titre, justification…) aurait été corrompu en numéro de page.
  const TOTAL_PAGES_PLACEHOLDER = 'CSI_TOTAL_PAGES';

  // ============================================================
  // Utilitaires
  // ============================================================

  function formatItem(item) {
    if (item === null || item === undefined) return '';
    if (typeof item === 'string' || typeof item === 'number') return String(item);
    if (item.title) return item.title;
    if (item.name) return item.name;
    try { return JSON.stringify(item); } catch { return String(item); }
  }

  function formatDate(value) {
    if (!value) return '';
    const date = new Date(value);
    const locale = window.CSI_i18n ? { fr: 'fr-FR', en: 'en-GB' }[window.CSI_i18n.getLanguage()] || 'fr-FR' : 'fr-FR';
    return Number.isFinite(date.getTime()) ? date.toLocaleDateString(locale) : String(value);
  }

  function getNis2PhaseLabel(phaseId) {
    if (!phaseId) return tt('pdfTableHeaders.outOfPhase');
    const phases = (window.appData?.nis2Plan?.phases || []);
    const direct = phases.find(p => p.id === String(phaseId));
    if (direct && direct.label) return direct.label;
    const numeric = parseInt(String(phaseId).replace(/[^0-9]/g, ""), 10);
    if (Number.isFinite(numeric)) return `Phase ${numeric}`;
    return `Phase ${phaseId}`;
  }

  function getDataCount(section) {
    // FIX-PDF-RECYF (audit 2026-06-10) : compteur ReCyF limité au périmètre du degré courant.
    if (section.id === 'recyf-controls') return getRecyfControlsInScope().length;
    const data = window.appData?.[section.dataKey];
    if (!data) return 0;
    if (Array.isArray(data)) return data.length;
    if (section.id === 'nis2-program') {
      // Budget Cybersécurité : nombre de lignes budgétaires
      return Array.isArray(data.budgetLines) ? data.budgetLines.length : 0;
    }
    return 0;
  }

  // ============================================================
  // Dessin d'en-tête et pied de page
  // ============================================================

  function addHeader(doc, title, date) {
    const pageWidth = doc.internal.pageSize.getWidth();

    // Ligne de séparation
    doc.setDrawColor(200);
    doc.setLineWidth(0.3);
    doc.line(CONFIG.MARGIN_X, 14, pageWidth - CONFIG.MARGIN_X, 14);

    // Titre à gauche
    doc.setFont('helvetica', 'normal');
    doc.setFontSize(9);
    doc.setTextColor(100);
    doc.text(title, CONFIG.MARGIN_X, 10);

    // Date à droite
    doc.text(date, pageWidth - CONFIG.MARGIN_X, 10, { align: 'right' });
  }

  function addFooter(doc, pageNum, totalPages) {
    const pageWidth = doc.internal.pageSize.getWidth();
    const pageHeight = doc.internal.pageSize.getHeight();

    // Ligne de séparation
    doc.setDrawColor(200);
    doc.setLineWidth(0.3);
    doc.line(CONFIG.MARGIN_X, pageHeight - 12, pageWidth - CONFIG.MARGIN_X, pageHeight - 12);

    // Numéro de page centré
    doc.setFont('helvetica', 'normal');
    doc.setFontSize(9);
    doc.setTextColor(100);
    // FIX-PDF-PAGINATION (audit 2026-06-10) : placeholder remplacé en fin de génération par
    // doc.putTotalPages() ; si l'API n'existe pas (vendor exotique), on omet le total plutôt
    // que d'afficher une estimation fausse.
    const total = (typeof doc.putTotalPages === 'function') ? TOTAL_PAGES_PLACEHOLDER : (totalPages > 0 ? totalPages : null);
    const pageText = total ? tt('pdfReportLabels.pageOf', {current: pageNum, total: total}) : tt('pdfReportLabels.page', {current: pageNum});
    doc.text(pageText, pageWidth / 2, pageHeight - 7, { align: 'center' });

    // Mention en bas à droite
    doc.setFontSize(7);
    doc.text('Cyber-Assistant', pageWidth - CONFIG.MARGIN_X, pageHeight - 7, { align: 'right' });
  }

  // ============================================================
  // Page de couverture
  // ============================================================

  function drawCoverPage(doc, appTitle, subtitle, date) {
    const pageWidth = doc.internal.pageSize.getWidth();
    const pageHeight = doc.internal.pageSize.getHeight();

    // Fond décoratif en haut
    doc.setFillColor(59, 130, 246);
    doc.rect(0, 0, pageWidth, 60, 'F');

    // Titre principal
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(28);
    doc.setTextColor(255);
    doc.text(tt('pdfReportLabels.coverTitle'), pageWidth / 2, 35, { align: 'center' });

    // Sous-titre
    doc.setFontSize(14);
    doc.text(subtitle || tt('pdfReportLabels.coverSubtitle'), pageWidth / 2, 48, { align: 'center' });

    // Titre de l'application
    doc.setTextColor(50);
    doc.setFontSize(20);
    doc.text(appTitle || 'Cyber-Assistant', pageWidth / 2, 100, { align: 'center' });

    // Date de génération
    doc.setFont('helvetica', 'normal');
    doc.setFontSize(12);
    doc.setTextColor(100);
    doc.text(tt('pdfReportLabels.generatedOn') + ' ' + date, pageWidth / 2, 120, { align: 'center' });

    // Statistiques rapides
    const stats = getQuickStats();
    let y = 160;
    doc.setFontSize(11);
    doc.setTextColor(60);

    const statsLines = [
      `${tt('pdfReportLabels.conformityISO')} : ${stats.conformity}%`,
      `${tt('pdfReportLabels.implementedControls')} : ${stats.implementedControls} / ${stats.totalControls}`,
      `${tt('pdfReportLabels.actionsInProgress')} : ${stats.actionsInProgress}`,
      `${tt('pdfReportLabels.criticalRisks')} : ${stats.criticalRisks}`
    ];

    statsLines.forEach(line => {
      doc.text(line, pageWidth / 2, y, { align: 'center' });
      y += 8;
    });

    // Footer
    doc.setFontSize(9);
    doc.setTextColor(150);
    doc.text(tt('pdfReportLabels.confidential'), pageWidth / 2, pageHeight - 20, { align: 'center' });
  }

  function getQuickStats() {
    const allControls = window.appData?.controls || [];
    const soaEntries = Array.isArray(window.appData?.soa) ? window.appData.soa : [];
    const soaMap = new Map(soaEntries.map(e => [e.controlId, e]));
    const controls = allControls.filter(c => {
      const soaEntry = soaMap.get(c.id);
      return !soaEntry || soaEntry.applicable !== false;
    });
    const implemented = controls.filter(c => c.status === 'implemente' || c.status === 'verifie').length;
    const conformity = controls.length > 0 ? Math.round((implemented / controls.length) * 100) : 0;
    const actions = window.appData?.actions || [];
    const inProgress = actions.filter(a => a.progress > 0 && a.progress < 100).length;
    const risks = window.appData?.risks || [];
    const critical = risks.filter(r => r.level === 'Critique').length;

    return {
      conformity,
      implementedControls: implemented,
      totalControls: controls.length,
      actionsInProgress: inProgress,
      criticalRisks: critical
    };
  }

  // ============================================================
  // Table des matières
  // ============================================================

  // FIX-PDF-TOC (audit 2026-06-10) : la TdM est désormais dessinée APRÈS le rendu des
  // sections (sur la page 2 réservée par l'appelant via doc.setPage(2)), quand tocEntries
  // contient les vrais numéros de page — avant, elle était rendue en premier et affichait
  // « — » pour toutes les sections.
  function drawTableOfContents(doc, sections, date, appTitle) {
    addHeader(doc, appTitle, date);

    const pageWidth = doc.internal.pageSize.getWidth();
    let y = CONFIG.BODY_TOP + 5;

    // Titre
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(16);
    doc.setTextColor(50);
    doc.text(tt('pdfReportLabels.tableOfContents'), CONFIG.MARGIN_X, y);
    y += 15;

    // Entrées
    doc.setFont('helvetica', 'normal');
    doc.setFontSize(CONFIG.TOC_FONT_SIZE);

    // Dashboard
    const dashboardLabel = '1. ' + tt('pdfReportLabels.dashboardAndIndicators');
    doc.setTextColor(59, 130, 246);
    doc.textWithLink(dashboardLabel, CONFIG.MARGIN_X, y, { pageNumber: 3 });
    doc.setTextColor(150);
    const dotsWidth1 = pageWidth - CONFIG.MARGIN_X * 2 - doc.getTextWidth(dashboardLabel) - 15;
    doc.text('.'.repeat(Math.floor(dotsWidth1 / 2)), CONFIG.MARGIN_X + doc.getTextWidth(dashboardLabel) + 5, y);
    doc.text('3', pageWidth - CONFIG.MARGIN_X, y, { align: 'right' });
    y += 8;

    // Sections
    sections.forEach((section, index) => {
      const num = index + 2;
      const count = getDataCount(section);
      const label = `${num}. ${section.title} (${count})`;

      // FIX-PDF-TOC (audit 2026-06-10) : tocEntries est rempli (rendu des sections déjà
      // effectué) → vrais numéros de page + lien cliquable, comme l'entrée Dashboard.
      const pageNum = tocEntries.find(e => e.id === section.id)?.page;
      doc.setTextColor(59, 130, 246);
      if (typeof pageNum === 'number') {
        doc.textWithLink(label, CONFIG.MARGIN_X, y, { pageNumber: pageNum });
      } else {
        doc.text(label, CONFIG.MARGIN_X, y);
      }

      // Points de conduite
      doc.setTextColor(150);
      const labelWidth = doc.getTextWidth(label);
      const dotsWidth = pageWidth - CONFIG.MARGIN_X * 2 - labelWidth - 15;
      if (dotsWidth > 10) {
        doc.text('.'.repeat(Math.floor(dotsWidth / 2)), CONFIG.MARGIN_X + labelWidth + 5, y);
      }

      doc.text(String(pageNum || '—'), pageWidth - CONFIG.MARGIN_X, y, { align: 'right' });

      y += 7;
      // FIX-PDF-TOC (audit 2026-06-10) : plus de doc.addPage() ici — la TdM est dessinée
      // sur la page 2 réservée après le rendu complet, un addPage créerait une page en FIN
      // de document. Les entrées (≤ 17 sections, 7 mm chacune) tiennent toujours sur la page.
    });

    addFooter(doc, 2, totalPages);
  }

  // ============================================================
  // Dashboard avec graphiques
  // ============================================================

  async function drawDashboard(doc, date, appTitle) {
    doc.addPage();
    addHeader(doc, appTitle, date);

    const pageWidth = doc.internal.pageSize.getWidth();
    const pageHeight = doc.internal.pageSize.getHeight();
    let y = CONFIG.BODY_TOP + 5;

    // Titre
    doc.setFont('helvetica', 'bold');
    doc.setFontSize(14);
    doc.setTextColor(50);
    doc.text(tt('pdfReportLabels.dashboardTitle'), CONFIG.MARGIN_X, y);
    y += 12;

    // KPIs en ligne
    const stats = getQuickStats();
    const kpis = [
      { label: tt('pdfReportLabels.conformity'), value: stats.conformity + '%', color: CONFIG.COLORS.primary },
      { label: tt('pdfReportLabels.controls'), value: `${stats.implementedControls}/${stats.totalControls}`, color: CONFIG.COLORS.success },
      { label: tt('pdfReportLabels.actions'), value: String(stats.actionsInProgress), color: CONFIG.COLORS.warning },
      { label: tt('pdfReportLabels.criticalRisks'), value: String(stats.criticalRisks), color: CONFIG.COLORS.danger }
    ];

    const kpiWidth = (pageWidth - 2 * CONFIG.MARGIN_X - 30) / 4;
    kpis.forEach((kpi, i) => {
      const x = CONFIG.MARGIN_X + i * (kpiWidth + 10);

      // Fond
      doc.setFillColor(...CONFIG.COLORS.headerBg);
      doc.roundedRect(x, y, kpiWidth, 20, 2, 2, 'F');

      // Valeur
      doc.setFont('helvetica', 'bold');
      doc.setFontSize(14);
      doc.setTextColor(...kpi.color);
      doc.text(kpi.value, x + kpiWidth / 2, y + 10, { align: 'center' });

      // Label
      doc.setFont('helvetica', 'normal');
      doc.setFontSize(8);
      doc.setTextColor(100);
      doc.text(kpi.label, x + kpiWidth / 2, y + 17, { align: 'center' });
    });
    y += 30;

    // Graphiques
    const chartWidth = (pageWidth - 2 * CONFIG.MARGIN_X - 10) / 2;

    async function addChartImage(chart, x, yPos, w, h) {
      if (!chart || !chart.canvas) return 0;
      try {
        const canvas = chart.canvas;
        if (!canvas.width || !canvas.height) {
          if (typeof chart.update === 'function') chart.update();
          await new Promise(r => setTimeout(r, 100));
        }

        const img = chart.toBase64Image();
        const ratio = canvas.height / canvas.width;
        let imgW = w;
        let imgH = imgW * ratio;
        if (imgH > h) {
          imgH = h;
          imgW = imgH / ratio;
        }

        doc.addImage(img, 'PNG', x + (w - imgW) / 2, yPos, imgW, imgH);
        return imgH;
      } catch (err) {
        void 0;
        return 0;
      }
    }

    // Radar ISO27002
    if (charts.capabilityRadar) {
      doc.setFont('helvetica', 'bold');
      doc.setFontSize(10);
      doc.setTextColor(60);
      doc.text(tt('pdfReportLabels.maturityISO'), CONFIG.MARGIN_X, y);
      await addChartImage(charts.capabilityRadar, CONFIG.MARGIN_X, y + 5, chartWidth, CONFIG.MAX_CHART_HEIGHT);
    }

    // Radar NIS2
    if (charts.nis2Radar) {
      doc.text(tt('pdfReportLabels.maturityNIS2'), CONFIG.MARGIN_X + chartWidth + 10, y);
      await addChartImage(charts.nis2Radar, CONFIG.MARGIN_X + chartWidth + 10, y + 5, chartWidth, CONFIG.MAX_CHART_HEIGHT);
    }
    y += CONFIG.MAX_CHART_HEIGHT + 15;

    // Nouvelle page si nécessaire
    if (y + CONFIG.MAX_CHART_HEIGHT + 20 > pageHeight - 20) {
      addFooter(doc, doc.internal.getNumberOfPages(), totalPages);
      doc.addPage();
      addHeader(doc, appTitle, date);
      y = CONFIG.BODY_TOP + 5;
    }

    // Graphique des risques et matrice
    if (charts.risks) {
      doc.setFont('helvetica', 'bold');
      doc.setFontSize(10);
      doc.setTextColor(60);
      doc.text(tt('pdfReportLabels.riskDistribution'), CONFIG.MARGIN_X, y);
      await addChartImage(charts.risks, CONFIG.MARGIN_X, y + 5, chartWidth, CONFIG.MAX_CHART_HEIGHT);
    }

    // Matrice de risques
    doc.text(tt('pdfReportLabels.riskMatrix'), CONFIG.MARGIN_X + chartWidth + 10, y);
    drawRiskMatrix(doc, CONFIG.MARGIN_X + chartWidth + 20, y + 8, 45);
    y += CONFIG.MAX_CHART_HEIGHT + 15;

    // Graphique CMM si espace disponible
    if (y + CONFIG.MAX_CHART_HEIGHT < pageHeight - 30 && charts.cmm) {
      doc.setFont('helvetica', 'bold');
      doc.setFontSize(10);
      doc.text(tt('pdfReportLabels.maturityCMM'), CONFIG.MARGIN_X, y);
      await addChartImage(charts.cmm, CONFIG.MARGIN_X, y + 5, chartWidth, CONFIG.MAX_CHART_HEIGHT);
    }

    addFooter(doc, doc.internal.getNumberOfPages(), totalPages);
  }

  function drawRiskMatrix(doc, x, y, size) {
    const cellSize = size / 5;
    const risks = window.appData?.risks || [];

    for (let impact = 5; impact >= 1; impact--) {
      for (let prob = 1; prob <= 5; prob++) {
        const col = prob - 1;
        const row = 5 - impact;
        const px = x + col * cellSize;
        const py = y + row * cellSize;
        const score = impact * prob;

        let level = 'insignifiant';
        if (score >= 20) level = 'critique';
        else if (score >= 15) level = 'grave';
        else if (score >= 10) level = 'significatif';
        else if (score >= 5) level = 'mineur';

        const color = MATRIX_COLORS[level];
        doc.setFillColor(...color);
        doc.rect(px, py, cellSize, cellSize, 'F');
        doc.setDrawColor(180);
        doc.setLineWidth(0.2);
        doc.rect(px, py, cellSize, cellSize);

        // Score
        doc.setTextColor(80);
        doc.setFontSize(7);
        doc.text(String(score), px + cellSize / 2, py + cellSize / 2 - 1, { align: 'center' });

        // Nombre de risques
        const count = risks.filter(r => r.impact === impact && r.probability === prob).length;
        if (count > 0) {
          doc.setFontSize(6);
          doc.setTextColor(59, 130, 246);
          doc.text(`(${count})`, px + cellSize / 2, py + cellSize / 2 + 3, { align: 'center' });
        }
      }
    }

    // Labels axes
    doc.setFontSize(5);
    doc.setTextColor(100);
    for (let prob = 1; prob <= 5; prob++) {
      doc.text(String(prob), x + (prob - 0.5) * cellSize, y + size + 3, { align: 'center' });
    }
    for (let impact = 1; impact <= 5; impact++) {
      doc.text(String(impact), x - 3, y + (5 - impact + 0.5) * cellSize, { align: 'center' });
    }
    doc.text(tt('pdfReportLabels.probability'), x + size / 2, y + size + 7, { align: 'center' });
    doc.text(tt('pdfReportLabels.impact'), x - 8, y + size / 2, { align: 'center', angle: 90 });
  }

  // ============================================================
  // Génération des sections
  // ============================================================

  function getSectionTableConfig(section) {
    // FIX-PDF-NIS2-CRASH (audit 2026-06-10) : le literal `configs` est construit en
    // entier à chaque appel avec le `data` de la SEULE section courante. Quand la
    // section est 'nis2-program', data = appData.nis2Plan (un OBJET) et les entrées
    // sœurs faisaient `(data || []).map(...)` → TypeError systématique dès que la
    // section « Programme NIS2 » (cochée par défaut) était incluse dans le rapport.
    // Toutes les entrées tabulaires utilisent désormais Array.isArray(data).
    const data = window.appData?.[section.dataKey];
    const LABELS = getLabels();

    const configs = {
      'controls': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.num'), tt('pdfTableHeaders.title'), tt('pdfTableHeaders.status'), tt('pdfTableHeaders.cmm')]],
        rows: (Array.isArray(data) ? data : []).map((c, i) => [
          i + 1,
          c.numero || '',
          c.title || '',
          c.status || '',
          c.cmm || ''
        ])
      },
      'soa': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.reference'), tt('pdfTableHeaders.title'), tt('pdfTableHeaders.applicable'), tt('pdfTableHeaders.justification')]],
        rows: (() => {
          const controls = window.appData?.controls || [];
          const entries = window.appData?.soa || [];
          const entryMap = new Map(entries.map(e => [e.controlId, e]));
          return controls.sort((a, b) => window.SortUtils.reference(a.reference || '', b.reference || ''))
            .map((c, i) => {
              const entry = entryMap.get(c.id) || {};
              return [i + 1, c.reference || '', c.title || '', entry.applicable === false ? tt('pdfTableHeaders.no') : tt('pdfTableHeaders.yes'), entry.justification || ''];
            });
        })()
      },
      'nis2-controls': {
        // GOV-NIS2 : réf CCB + maturités doc/impl à la place du seul CMM dérivé (≤7 col).
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.reference'), tt('pdfTableHeaders.function'), tt('pdfTableHeaders.category'), tt('pdfTableHeaders.status'), tt('pdfTableHeaders.docMaturity'), tt('pdfTableHeaders.implMaturity')]],
        rows: (Array.isArray(data) ? data : []).map((c, i) => [i + 1, c.ref || '', c.fonction || '', c.categorie || '', getNis2ControlStatusLabel(c.status), (c.docMaturity != null ? c.docMaturity : 0), (c.implMaturity != null ? c.implMaturity : 0)])
      },
      // FIX-PDF-RECYF (audit 2026-06-10) : section « Mesures ReCyF » (référentiel France) —
      // conformité par mesure (modèle ANSSI), affichée à la place des sections CyFun.
      'recyf-controls': {
        head: [[tt('pdfTableHeaders.hash'), tt('recyfControls.ref'), tt('recyfControls.objective'), tt('recyfControls.theme'), tt('recyfControls.conformity'), tt('recyfControls.justification')]],
        rows: getRecyfControlsInScope().map((c, i) => [
          i + 1,
          c.ref || '',
          // GOV-NIS2 S5 : même format que le CSV et l'écran (« Obj. X — libellé »).
          (c.objectiveId != null ? 'Obj. ' + c.objectiveId + ' — ' : '') + (c.objectiveLabel || ''),
          c.theme || '',
          getRecyfConformityLabel(c.conformity || 'non_evalue'),
          c.justification || ''
        ]),
        columnStyles: {
          0: { cellWidth: 8, halign: 'center' },
          1: { cellWidth: 16 },
          2: { cellWidth: 45 },
          3: { cellWidth: 35 },
          4: { cellWidth: 24 },
          5: { cellWidth: 32 }
        }
      },
      'nis2-program': (() => {
        // Budget Cybersécurité : lignes budgétaires, colonnes d'années dynamiques + total
        const plan = data || {};
        const years = (Array.isArray(plan.budgetYears) && plan.budgetYears.length) ? plan.budgetYears : ['2026', '2027', '2028'];
        const lines = Array.isArray(plan.budgetLines) ? plan.budgetLines : [];
        const cats = Array.isArray(plan.budgetCategories) ? plan.budgetCategories : [];
        const catLabel = (v) => { const c = cats.find(x => (x.value || x.label) === v); return c ? (c.label || c.value) : (v || tt('budgetCategories.divers', 'Divers')); };
        const stLabel = (s) => { s = String(s || '').toLowerCase(); if (s === 'engage') return tt('budgetStatus.engage', 'Engagé'); if (s === 'realise') return tt('budgetStatus.realise', 'Réalisé'); return tt('budgetStatus.planifie', 'Planifié'); };
        const money = (n) => { n = Number(n) || 0; try { return new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(n) + ' €'; } catch (e) { return n + ' €'; } };
        const amt = (line, yr) => { const v = parseFloat(String((line.amounts || {})[yr] != null ? (line.amounts || {})[yr] : 0).replace(',', '.')); return Number.isFinite(v) ? v : 0; };
        const head = [[tt('nis2Program.lineLabel', 'Intitulé'), tt('nis2Program.lineCategory', 'Catégorie'), tt('nis2Program.lineStatus', 'Statut')].concat(years).concat([tt('nis2Program.lineTotal', 'Total')])];
        const rows = [];
        const grandYear = years.map(() => 0);
        let grand = 0;
        lines.forEach(line => {
          const amts = years.map(yr => amt(line, yr));
          const total = amts.reduce((a, b) => a + b, 0);
          amts.forEach((v, i) => { grandYear[i] += v; });
          grand += total;
          rows.push([line.label || '', catLabel(line.category), stLabel(line.status)].concat(amts.map(money)).concat([money(total)]));
        });
        if (rows.length) {
          rows.push([tt('nis2Program.grandTotal', 'Total général'), '', ''].concat(grandYear.map(money)).concat([money(grand)]));
        } else {
          rows.push(['—', '—', '—'].concat(years.map(() => '—')).concat(['—']));
        }
        // La colonne 0 est l'INTITULÉ (texte libre), pas un index # : sans columnStyles
        // explicite, drawSection lui appliquerait son défaut 10mm centré (illisible).
        const columnStyles = { 0: { halign: 'left' }, 1: { halign: 'left' }, 2: { halign: 'left' } };
        years.forEach((_, i) => { columnStyles[3 + i] = { halign: 'right' }; });
        columnStyles[3 + years.length] = { halign: 'right' };
        return { head, rows, columnStyles };
      })(),
      'nis2-socle': {
        head: [[tt('pdfTableHeaders.pillar'), tt('pdfTableHeaders.title'), tt('pdfTableHeaders.element'), tt('pdfTableHeaders.owner')]],
        rows: (() => {
          const pillars = Array.isArray(data) ? data : [];
          const rows = [];
          pillars.forEach((p, idx) => {
            (p.items || ['']).forEach(item => {
              rows.push([`${tt('pdfTableHeaders.pillar')} ${idx + 1}`, p.title || '', item || '', p.owner || '']);
            });
          });
          return rows.length ? rows : [['—', '—', '—', '—']];
        })()
      },
      'actions': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.action'), tt('pdfTableHeaders.link'), tt('pdfTableHeaders.priority'), tt('pdfTableHeaders.progression'), tt('pdfTableHeaders.dueDate'), tt('pdfTableHeaders.owner')]],
        rows: (Array.isArray(data) ? data : []).map((a, i) => [
          i + 1,
          a.title || '',
          LABELS.actionLinkType[a.linkType] || a.linkType || '',
          LABELS.actionPriority[a.priority] || a.priority || '',
          Number.isFinite(a.progress) ? `${a.progress}%` : '',
          formatDate(a.dueDate),
          a.assignedTo || ''
        ])
      },
      'risks': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.risk'), tt('pdfTableHeaders.impact'), tt('pdfTableHeaders.prob'), tt('pdfTableHeaders.score'), tt('pdfTableHeaders.level'), tt('pdfTableHeaders.treatment')]],
        rows: (Array.isArray(data) ? data : []).map((r, i) => {
          const score = (r.impact || 0) * (r.probability || 0);
          const level = r.level || (score >= 20 ? 'Critique' : score >= 15 ? 'Grave' : score >= 10 ? 'Significatif' : score >= 5 ? 'Mineur' : 'Insignifiant');
          return [i + 1, r.title || '', r.impact || '', r.probability || '', score, level, r.treatment || ''];
        })
      },
      'threats-section': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.threat'), tt('pdfTableHeaders.impact'), tt('pdfTableHeaders.prob'), tt('pdfTableHeaders.score'), tt('pdfTableHeaders.status')]],
        rows: (Array.isArray(data) ? data : []).map((t, i) => {
          const score = (t.impact || 0) * (t.likelihood || t.probability || 0);
          return [i + 1, t.title || t.name || '', t.impact || '', t.likelihood || t.probability || '', score, LABELS.threatStatus[t.status] || t.status || ''];
        })
      },
      'nonconformities': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.title'), tt('pdfTableHeaders.type'), tt('pdfTableHeaders.status'), tt('pdfTableHeaders.detectionDate'), tt('pdfTableHeaders.owner')]],
        rows: (Array.isArray(data) ? data : []).map((nc, i) => [i + 1, nc.title || '', nc.type || '', nc.status || '', formatDate(nc.detectedDate), nc.owner || ''])
      },
      'audits': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.title'), tt('pdfTableHeaders.type'), tt('pdfTableHeaders.date'), tt('pdfTableHeaders.auditor'), tt('pdfTableHeaders.status')]],
        rows: (Array.isArray(data) ? data : []).map((a, i) => [i + 1, a.title || '', a.type || '', formatDate(a.plannedDate), a.auditor || '', a.status || ''])
      },
      'managementReviews': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.date'), tt('pdfTableHeaders.participants'), tt('pdfTableHeaders.decisions')]],
        rows: (Array.isArray(data) ? data : []).map((r, i) => [i + 1, formatDate(r.date), r.participants || '', r.decisions || ''])
      },
      'docReview': {
        head: [[tt('pdfTableHeaders.reference'), tt('pdfTableHeaders.justification'), tt('pdfTableHeaders.tool')]],
        rows: (Array.isArray(data) ? data : []).map(d => [d.reference || '', d.justification || '', d.tool || ''])
      },
      'documents': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.docLevel'), tt('pdfTableHeaders.docType'), tt('pdfTableHeaders.docTool'), tt('pdfTableHeaders.docLink')]],
        rows: (Array.isArray(data) ? data : []).map((d, i) => [i + 1, d.category || '', d.type || '', d.tool || '', d.link || ''])
      },
      'stakeholders': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.name'), tt('pdfTableHeaders.role'), tt('pdfTableHeaders.contact')]],
        rows: (Array.isArray(data) ? data : []).map((s, i) => [i + 1, s.name || '', s.role || '', s.contact || ''])
      },
      'kpis': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.title'), tt('pdfTableHeaders.description'), tt('pdfTableHeaders.progression')]],
        rows: (Array.isArray(data) ? data : []).map((k, i) => [i + 1, k.title || '', k.description || '', Number.isFinite(k.progress) ? `${k.progress}%` : ''])
      },
      'critical-assets': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.priority'), tt('pdfTableHeaders.beneficiary'), tt('pdfTableHeaders.product'), tt('pdfTableHeaders.rto'), tt('pdfTableHeaders.rpo'), tt('pdfTableHeaders.availability')]],
        // FIX-REVUE ACTIFS (2026-09) : rto/rpo/availability résolus clé→libellé (paliers du
        // lot 3, classes de reprise) via le résolveur exposé par le bundle — sans lui, le PDF
        // imprimait « lt4h » brut. Repli identité si le bundle n'est pas chargé (robustesse).
        rows: (Array.isArray(data) ? data : []).map((a, i) => {
          const rf = (field, v) => (typeof window._csiAssetFieldValue === 'function' ? window._csiAssetFieldValue(field)(v) : (v || ''));
          return [
            i + 1,
            a.priority || '',
            a.beneficiary || '',
            [a.productCode, a.productName].filter(Boolean).join(' - '),
            rf('rto', a.rto),
            rf('rpo', a.rpo),
            rf('availability', a.availability)
          ];
        })
      },
      'project-risks': {
        head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.project'), tt('pdfTableHeaders.beneficiary'), tt('pdfTableHeaders.description'), tt('pdfTableHeaders.priority'), tt('pdfTableHeaders.date'), tt('pdfTableHeaders.status'), tt('pdfTableHeaders.recommendations')]],
        rows: (Array.isArray(data) ? data : []).map((r, i) => {
          const recos = r.recommendations || [];
          const pending = recos.filter(rc => !rc.implemented).length;
          const desc = r.description || '';
          const shortDesc = desc.length > 120 ? desc.substring(0, 117) + '...' : desc;
          const name = (r.projectName || '') + (r.sourceAnalysisId ? ' *' : '');
          return [
            i + 1,
            name,
            r.beneficiary || '',
            shortDesc,
            r.priority ? LABELS.projectRiskPriority[r.priority] || r.priority : '',
            formatDate(r.completionDate),
            LABELS.projectRiskStatus[r.status] || r.status || '',
            recos.length ? `${recos.length} (${pending} ${tt('pdfTableHeaders.pending')})` : '0'
          ];
        }),
        columnStyles: {
          0: { cellWidth: 8, halign: 'center' },
          1: { cellWidth: 26 },
          2: { cellWidth: 20 },
          3: { cellWidth: 32 },
          4: { cellWidth: 18 },
          5: { cellWidth: 18, halign: 'center' },
          6: { cellWidth: 18, halign: 'center' },
          7: { cellWidth: 18, halign: 'center' }
        },
        _statusColumn: 6,
        _statusColorMap: {
          red: [239, 68, 68], yellow: [249, 115, 22], green: [34, 197, 94],
          todo: [93, 135, 143], suspended: [31, 184, 205]
        },
        _rawData: data
      }
    };

    return configs[section.id] || { head: [['#', tt('pdfReportLabels.noData')]], rows: [['-', tt('pdfReportLabels.noData')]] };
  }

  function drawSection(doc, section, date, appTitle) {
    const pageNum = doc.internal.getNumberOfPages();
    tocEntries.push({ id: section.id, title: section.title, page: pageNum });

    const config = getSectionTableConfig(section);

    if (!config.rows.length) {
      config.rows = [config.head[0].map(() => '—')];
    }

    let firstPage = true;

    const tableOpts = {
      startY: CONFIG.BODY_TOP + 15,
      margin: { top: CONFIG.BODY_TOP + 15, left: CONFIG.MARGIN_X, right: CONFIG.MARGIN_X, bottom: 20 },
      head: config.head,
      body: config.rows,
      styles: {
        fontSize: CONFIG.TABLE_FONT_SIZE,
        font: 'helvetica',
        cellPadding: 2,
        overflow: 'linebreak'
      },
      headStyles: {
        fillColor: CONFIG.COLORS.headerBg,
        textColor: [50, 50, 50],
        fontStyle: 'bold',
        halign: 'center'
      },
      alternateRowStyles: {
        fillColor: CONFIG.COLORS.alternateBg
      },
      columnStyles: config.columnStyles || { 0: { cellWidth: 10, halign: 'center' } },
      didDrawPage: function(data) {
        addHeader(doc, appTitle, date);
        addFooter(doc, data.pageNumber, totalPages);

        // Titre de section
        doc.setFont('helvetica', 'bold');
        doc.setFontSize(12);
        doc.setTextColor(50);
        const title = firstPage ? section.title : section.title + ' ' + tt('pdfReportLabels.continued');
        doc.text(title, CONFIG.MARGIN_X, CONFIG.BODY_TOP + 8);

        // Compteur
        if (firstPage) {
          const count = getDataCount(section);
          doc.setFont('helvetica', 'normal');
          doc.setFontSize(9);
          doc.setTextColor(100);
          doc.text(`(${count} ${count > 1 ? tt('pdfReportLabels.elementsPlural') : tt('pdfReportLabels.elements')})`, CONFIG.MARGIN_X + doc.getTextWidth(title) + 5, CONFIG.BODY_TOP + 8);
        }

        firstPage = false;
      }
    };

    // Status color indicator for project-risks
    if (config._statusColumn !== undefined && config._statusColorMap && config._rawData) {
      const statusCol = config._statusColumn;
      const colorMap = config._statusColorMap;
      const rawData = config._rawData || [];
      tableOpts.didDrawCell = function(data) {
        if (data.section === 'body' && data.column.index === statusCol) {
          const rowIdx = data.row.index;
          const risk = rawData[rowIdx];
          if (risk) {
            const statusKey = risk.status ? String(risk.status).toLowerCase().replace(/[àâ]/g, 'a').replace(/[éèêë]/g, 'e') : '';
            const normalizedKey = statusKey === 'rouge' ? 'red' : statusKey === 'jaune' ? 'yellow' : statusKey === 'vert' ? 'green' : statusKey === 'a_realiser' || statusKey === 'todo' ? 'todo' : statusKey === 'suspendue' || statusKey === 'suspended' ? 'suspended' : statusKey;
            const color = colorMap[normalizedKey];
            if (color) {
              const cx = data.cell.x + 2;
              const cy = data.cell.y + data.cell.height / 2;
              doc.setFillColor(color[0], color[1], color[2]);
              doc.circle(cx, cy, 1.5, 'F');
            }
          }
        }
      };
    }

    doc.autoTable(tableOpts);

    // Project-risks: detailed recommendations sub-table + auto-sync footnote
    if (section.id === 'project-risks' && config._rawData && config._rawData.length > 0) {
      const rawData = config._rawData;
      const hasRecos = rawData.some(r => (r.recommendations || []).length > 0);
      const hasAutoSync = rawData.some(r => r.sourceAnalysisId);
      if (hasRecos) {
        doc.addPage();
        addHeader(doc, '', '');
        doc.setFont('helvetica', 'bold');
        doc.setFontSize(11);
        doc.setTextColor(50);
        doc.text(tt('pdfTableHeaders.detailedRecommendations'), CONFIG.MARGIN_X, CONFIG.BODY_TOP + 8);
        const recoRows = [];
        rawData.forEach((r, idx) => {
          (r.recommendations || []).forEach(rc => {
            recoRows.push([
              idx + 1,
              r.projectName || '',
              rc.title || '',
              rc.recommendation || '',
              rc.implemented ? tt('pdfTableHeaders.yes') : tt('pdfTableHeaders.no')
            ]);
          });
        });
        if (recoRows.length > 0) {
          doc.autoTable({
            startY: CONFIG.BODY_TOP + 14,
            margin: { top: CONFIG.BODY_TOP + 14, left: CONFIG.MARGIN_X, right: CONFIG.MARGIN_X, bottom: 20 },
            head: [[tt('pdfTableHeaders.hash'), tt('pdfTableHeaders.project'), tt('pdfTableHeaders.title'), tt('pdfTableHeaders.description'), tt('pdfTableHeaders.status')]],
            body: recoRows,
            styles: { fontSize: CONFIG.TABLE_FONT_SIZE - 0.5, font: 'helvetica', cellPadding: 2, overflow: 'linebreak' },
            headStyles: { fillColor: CONFIG.COLORS.headerBg, textColor: [50, 50, 50], fontStyle: 'bold', halign: 'center' },
            alternateRowStyles: { fillColor: CONFIG.COLORS.alternateBg },
            columnStyles: { 0: { cellWidth: 8, halign: 'center' }, 1: { cellWidth: 28 }, 2: { cellWidth: 35 }, 3: { cellWidth: 65 }, 4: { cellWidth: 18, halign: 'center' } }
          });
        }
      }
      if (hasAutoSync) {
        const y = doc.lastAutoTable ? doc.lastAutoTable.finalY + 8 : doc.internal.pageSize.getHeight() - 25;
        doc.setFont('helvetica', 'italic');
        doc.setFontSize(7);
        doc.setTextColor(120);
        doc.text(tt('pdfTableHeaders.autoSyncNote'), CONFIG.MARGIN_X, y);
      }
    }
  }

  // ============================================================
  // Fonction principale
  // ============================================================

  window.generatePdfReport = async function generatePdfReport(selectedIds = null) {
    // Bloquer l'export en mode démo
    const isDemo = (() => {
      const meta = document.querySelector('meta[name="csi-mode"]');
      return meta && meta.content === 'demo';
    })();
    if (isDemo) {
      if (window.showToast) {
        window.showToast(tt('pdfReportLabels.pdfExportDisabled'), 'warning');
      } else {
        alert(tt('pdfReportLabels.pdfExportDisabled'));
      }
      return;
    }

    // Réinitialiser l'état
    tocEntries = [];

    // Préparer le dashboard
    const dashboard = document.getElementById('dashboard');
    let restore = null;
    if (dashboard && !dashboard.classList.contains('module--active')) {
      const activeModule = document.querySelector('.module--active');
      if (activeModule) activeModule.classList.remove('module--active');
      dashboard.classList.add('module--active');
      dashboard.style.position = 'absolute';
      dashboard.style.left = '-9999px';
      dashboard.style.visibility = 'hidden';
      restore = activeModule;
    }

    if (window.updateDashboard) {
      window.updateDashboard();
      await new Promise(r => setTimeout(r, 200));
    }

    // Filtrer les sections
    const selected = Array.isArray(selectedIds) && selectedIds.length ? new Set(selectedIds) : null;
    const sections = getReportSections().filter(sec => !selected || selected.has(sec.id));

    // Informations du rapport
    const pdfLocale = window.CSI_i18n ? { fr: 'fr-FR', en: 'en-GB' }[window.CSI_i18n.getLanguage()] || 'fr-FR' : 'fr-FR';
    const date = new Date().toLocaleDateString(pdfLocale);
    const appTitle = document.getElementById('appTitle')?.innerText.trim() || 'Cyber-Assistant';
    const subtitle = document.querySelector('.header__subtitle')?.innerText.trim() || '';

    // Créer le document
    const doc = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' });

    // FIX-PDF-PAGINATION (audit 2026-06-10) : plus d'estimation « 3 + sections × 2 » —
    // les pieds de page utilisent un placeholder remplacé par putTotalPages() à la fin.
    totalPages = 0;

    // Page de couverture
    drawCoverPage(doc, appTitle, subtitle, date);

    // FIX-PDF-TOC (audit 2026-06-10) : réserver la page 2 pour la table des matières,
    // dessinée APRÈS le rendu des sections (tocEntries alors rempli de vrais numéros).
    doc.addPage();

    // Dashboard
    await drawDashboard(doc, date, appTitle);

    // Sections
    for (const section of sections) {
      doc.addPage();
      drawSection(doc, section, date, appTitle);
    }

    // Mettre à jour le nombre total de pages
    totalPages = doc.internal.getNumberOfPages();

    // FIX-PDF-TOC (audit 2026-06-10) : table des matières sur la page réservée,
    // avec les numéros de page réels collectés pendant le rendu des sections.
    doc.setPage(2);
    drawTableOfContents(doc, sections, date, appTitle);

    // FIX-PDF-PAGINATION (audit 2026-06-10) : remplacer le placeholder « total de pages »
    // de tous les pieds de page par la vraie valeur, maintenant connue.
    if (typeof doc.putTotalPages === 'function') {
      doc.putTotalPages(TOTAL_PAGES_PLACEHOLDER);
    }

    // Sauvegarder
    doc.save('SMSI_Report_' + new Date().toISOString().slice(0, 10) + '.pdf');

    // Restaurer l'interface
    if (restore) {
      dashboard.classList.remove('module--active');
      dashboard.style.position = '';
      dashboard.style.left = '';
      dashboard.style.visibility = '';
      restore.classList.add('module--active');
    }

    return doc;
  };

  window.generatePdfReportWithFeedback = async function generatePdfReportWithFeedback() {
    try {
      if (typeof window.openModal === "function") {
        const reportSections = getReportSections();
        const fields = reportSections.map((s) => ({
          name: s.id,
          label: `${s.title} (${getDataCount(s)})`,
          type: "checkbox",
          value: true
        }));

        window.openModal(tt('pdfReportLabels.sectionsTitle'), fields, async (data) => {
          const chosen = getReportSections().filter((s) => data[s.id]).map((s) => s.id);
          if (!chosen.length) {
            if (window.showToast) window.showToast(tt('pdfReportLabels.selectAtLeastOne'), "warning");
            return false;
          }
          await window.generatePdfReport(chosen);
          if (window.showToast) window.showToast(tt('pdfReportLabels.pdfGeneratedSuccess'), 'success');
        }, null, { allowReadOnly: true });
      } else {
        await window.generatePdfReport();
        if (window.showToast) window.showToast(tt('pdfReportLabels.pdfGeneratedSuccess'), 'success');
      }
    } catch (error) {
      void 0;
      if (window.showToast) window.showToast(t('common.error', 'Erreur') + ': ' + error.message, 'error');
    }
  };

  // Exposer les sections pour usage externe
  Object.defineProperty(window, 'PDF_REPORT_SECTIONS', { get: getReportSections });

  void 0;
})();
