# Fichier: services/pdf_service.py (VERSION FINALE - CHOIX MULTIPLES)

from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, Flowable
from reportlab.lib import colors
from reportlab.lib.colors import HexColor
from io import BytesIO
import html
import re
import logging
import os
from flask import current_app

logger = logging.getLogger(__name__)

# ========================================================================
# 1. CONFIGURATION & COULEURS
# ========================================================================
COLOR_BG_PAGE   = HexColor('#0f0f23') 
COLOR_BG_HEADER = HexColor('#1a1a2e') 
COLOR_BG_CARD   = HexColor('#1f1f3e')
COLOR_BORDER    = HexColor('#333366') 
COLOR_ACCENT    = HexColor('#6366f1') 
COLOR_TEXT      = HexColor('#ffffff') 
COLOR_SUBTEXT   = HexColor('#94a3b8') 

COLOR_SUCCESS = HexColor('#10b981') 
COLOR_WARNING = HexColor('#f59e0b') 
COLOR_DANGER  = HexColor('#ef4444') 

# Limites de sécurité
MAX_PRECONISATIONS = 20
MAX_TEXT_LENGTH = 1000
MAX_TITLE_LENGTH = 200
MAX_NAME_LENGTH = 150

# ✅ Configuration des scores MISE À JOUR
SCORE_MAP_CONFIG = {
    'points_2fa': 10, 'points_filtrage': 4, 'points_edr': 8, 'points_gestionnaire_mdp': 3,
    'points_sauvegarde_types': 12, 'points_3211': 8, 'points_frequence': 5, 'points_cloud': 2,
    'points_os_postes': 6, 'points_os_serveurs': 6, 'points_hyperviseur': 2,
    'points_parefeu': 8, 'points_vpn': 7,
    'points_phishing': 5, 'points_formation': 5, 'points_pra_pca': 5, 'points_audit': 4
}

STRUCTURE_QUESTIONS = {
    "PROTECTION DES ACCÈS": [
        ('q_2fa', "Authentification Double Facteur (MFA)"),
        ('q_gestionnaire_mdp', "Utilisation d'un gestionnaire de mots de passe"),
        ('q_filtrage_mail', "Solution de filtrage des emails"),
        ('q_edr', "Protection Antivirus / EDR")
    ],
    "SAUVEGARDES & DONNÉES": [
        ('q_sauvegarde_types', "Types de sauvegarde"),
        ('q_architecture_editeur', "Architecture (si éditeur spécialisé)"),
        ('q_3211', "Respect de la règle 3-2-1-1"),
        ('q_frequence', "Fréquence des sauvegardes"),
        ('q_cloud_rgpd', "Conformité Cloud RGPD")
    ],
    "INFRASTRUCTURE & SYSTÈMES": [
        ('q_os_postes', "Système d'exploitation (Postes)"),
        ('q_os_serveurs', "Système d'exploitation (Serveurs)"),
        ('q_hyperviseur', "Mise à jour Hyperviseur")
    ],
    "RÉSEAU & ACCÈS DISTANTS": [
        ('q_parefeu', "Type de Pare-feu"),
        ('q_vpn', "Sécurisation Accès Distants (VPN)")
    ],
    "HUMAIN & ORGANISATION": [
        ('q_phishing', "Campagnes de test Phishing"),
        ('q_formation', "Formation des utilisateurs"),
        ('q_pra_pca', "Plan de Reprise d'Activité (PRA)"),
        ('q_audit', "Audit externe récurrent")
    ]
}

CHAMPS_DETAILS = {
    'q_filtrage_mail': 'q_filtrage_mail_editeur',
    'q_edr': 'q_edr_editeur',
    'q_gestionnaire_mdp': 'q_gestionnaire_mdp_nom',
    'q_sauvegarde_types': 'q_sauvegarde_editeur',
    'q_os_postes': 'q_os_postes_editeur',
    'q_os_serveurs': 'q_os_serveurs_editeur',
    'q_hyperviseur': 'q_hyperviseur_editeur',
    'q_parefeu': 'q_parefeu_editeur'
}

# ✅ MAP_REPONSES ÉTENDU POUR CHECKBOXES
MAP_REPONSES = {
    # Réponses classiques
    'oui': 'Oui / En place', 'non': 'Non / Absent', 'partiel': 'Partiellement',
    'n_a': 'Non Applicable', 'aucun': 'Aucun dispositif',
    'outil_specialise': 'Solution dédiée', 'microsoft_base': 'Microsoft 365 (Base)',
    'edr': 'EDR Managé', 'antivirus': 'Antivirus classique',
    
    # ✅ Types de sauvegardes (checkboxes)
    'editeur_specialise': '📦 Éditeur spécialisé',
    'cloud': '☁️ Cloud',
    'nas': '🗄️ NAS',
    'bande': '📼 Bandes magnétiques',
    'usb': '💾 Disque USB',
    'aucune': '❌ Aucune sauvegarde',
    
    # ✅ Architecture éditeur
    'hybride': '🔁 Hybride (Local + Cloud)',
    'multi_sites': '🏢 Multi-sites',
    'full_cloud': '☁️ Full Cloud',
    
    # ✅ Fréquences (checkboxes)
    'journaliere': 'Quotidienne', 
    'hebdomadaire': 'Hebdomadaire', 
    'mensuelle': 'Mensuelle',
    
    'annuelle': 'Annuelle',
    'sans_cloud': 'Pas de Cloud', 
    
    # ✅ OS (checkboxes)
    'recent': '✅ Récents et à jour', 
    'unsupported_soon': '⚠️ Fin de vie proche', 
    'obsolete': '❌ Obsolètes',
    
    'ngfw_gere': 'Pare-feu Nouvelle Génération', 
    'routeur_base': 'Box Opérateur / Routeur simple',
    'vpn_gere': 'VPN Sécurisé', 
    'rdp_direct': 'RDP Ouvert (Critique)', 
    'rdp_ouvert': 'RDP Ouvert (Critique)',
    'trimestriel': 'Trimestriel', 
    'annuel': 'Annuel', 
    'jamais': 'Jamais',
    'teste': 'Oui, testé régulièrement', 
    'existe': 'Oui, mais non testé', 
    'inexistant': 'Inexistant'
}

# ========================================================================
# 2. FONCTIONS DE SÉCURITÉ
# ========================================================================

def safe_int(val, default=0):
    try:
        return int(float(val)) 
    except (ValueError, TypeError):
        return default

def safe_str(val, default=""):
    if val is None:
        return default
    return str(val)

def sanitize_text(text, max_length=MAX_TEXT_LENGTH):
    if text is None:
        return ""
    text = str(text)[:max_length]
    text = html.escape(text)
    text = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', text)
    text = re.sub(r'<[^>]*>', '', text)
    text = re.sub(r'\s+', ' ', text)
    return text.strip()

def validate_form_data(donnees):
    if not isinstance(donnees, dict):
        return {}
    
    validated = {}
    champs_nb = [
        'nb_postes_fixes', 'nb_postes_portables', 'nb_postes_critiques',
        'nb_serveurs_physiques', 'nb_serveurs_virtuels', 'nb_serveurs_critiques'
    ]
    for key in champs_nb:
        val = safe_int(donnees.get(key, 0))
        validated[key] = max(0, min(val, 9999)) 
    
    vol = sanitize_text(donnees.get('volumetrie', 'Non spécifié'), max_length=50)
    validated['volumetrie'] = vol if vol else 'Non spécifié'
    
    for key, val in donnees.items():
        if key.endswith('_editeur') or key.endswith('_nom'):
             validated[key] = sanitize_text(val, max_length=100)

    for key, val in donnees.items():
        if key.startswith('q_'):
            validated[key] = val 
    
    return validated

# ✅ NOUVELLE FONCTION : Convertir liste en texte lisible
def format_liste_reponses(valeur):
    """
    Convertit une liste de réponses (checkboxes) en texte lisible.
    Exemples:
    - ['cloud', 'nas'] → "☁️ Cloud, 🗄️ NAS"
    - ['journaliere'] → "Quotidienne"
    - 'oui' → "Oui / En place"
    """
    if isinstance(valeur, list):
        if not valeur:
            return "Non spécifié"
        # Convertir chaque élément et joindre
        elements_lisibles = [MAP_REPONSES.get(v, v) for v in valeur]
        return ", ".join(elements_lisibles)
    else:
        # Valeur simple (string)
        return MAP_REPONSES.get(valeur, "Non spécifié") if valeur else "Non spécifié"

# ========================================================================
# 3. ELEMENTS GRAPHIQUES
# ========================================================================

class CyberGauge(Flowable):
    def __init__(self, score, width=120, height=120):
        Flowable.__init__(self)
        self.score = max(0, min(100, safe_int(score)))
        self.width = width
        self.height = height
        self.radius = 45

    def draw(self):
        self.canv.saveState()
        self.canv.translate(self.width/2, self.height/2)
        if self.score >= 80: color = COLOR_SUCCESS
        elif self.score >= 50: color = COLOR_WARNING
        else: color = COLOR_DANGER
        
        self.canv.setStrokeColor(HexColor('#333366'))
        self.canv.setLineWidth(6)
        self.canv.circle(0, 0, self.radius, stroke=1, fill=0)
        
        if self.score > 0:
            angle = 360 * (self.score / 100)
            self.canv.setStrokeColor(color)
            self.canv.setLineWidth(6)
            p = self.canv.beginPath()
            p.arc(-self.radius, -self.radius, self.radius, self.radius, 90, -angle)
            self.canv.drawPath(p, stroke=1, fill=0)
        
        self.canv.setFillColor(colors.white)
        self.canv.setFont("Helvetica-Bold", 22)
        self.canv.drawCentredString(0, -8, str(self.score))
        self.canv.setFillColor(COLOR_SUBTEXT)
        self.canv.setFont("Helvetica", 9)
        self.canv.drawCentredString(0, -20, "/ 100")
        self.canv.restoreState()

class CyberProgressBar(Flowable):
    def __init__(self, score, max_score, width=100, height=6):
        Flowable.__init__(self)
        self.score = score 
        self.max_score = max(1, safe_int(max_score, 5))
        self.width = width
        self.height = height

    def draw(self):
        if self.score is None: 
            color = HexColor('#4b5563'); pct = 0
        else:
            val = max(0, safe_int(self.score))
            pct = min(1.0, val / self.max_score)
            if pct >= 0.8: color = COLOR_SUCCESS
            elif pct >= 0.4: color = COLOR_WARNING
            else: color = COLOR_DANGER

        self.canv.setFillColor(HexColor('#111122'))
        self.canv.roundRect(0, 0, self.width, self.height, 3, fill=1, stroke=0)
        
        fill_width = self.width * pct
        if fill_width > 0:
            self.canv.setFillColor(color)
            self.canv.roundRect(0, 0, fill_width, self.height, 3, fill=1, stroke=0)

def draw_background(canvas, doc, company_name="MCyber Consulting"):
    canvas.saveState()
    canvas.setFillColor(COLOR_BG_PAGE)
    canvas.rect(0, 0, A4[0], A4[1], fill=True, stroke=False)
    canvas.setStrokeColor(COLOR_ACCENT)
    canvas.setLineWidth(2)
    canvas.line(0, 0, A4[0], 0) 
    canvas.setFont("Helvetica", 8)
    canvas.setFillColor(HexColor('#444466'))
    canvas.drawCentredString(A4[0]/2, 1*cm, f"{company_name} - Page {doc.page}")
    canvas.restoreState()

# ========================================================================
# 4. GÉNÉRATION DU PDF
# ========================================================================
def get_logo_path(user):
    """
    Détermine le chemin du logo à utiliser selon la hiérarchie white-label.
    Retourne le chemin absolu du logo ou None.
    """
    logo_filename = None
    company_name = "MCyber Consulting"
    
    if user.role == 'manager' or user.role == 'admin':
        # Manager : utilise ses propres paramètres
        if user.show_logo_on_reports and user.logo_filename:
            logo_filename = user.logo_filename
        
        if user.show_company_name_on_reports and user.company_name:
            company_name = user.company_name
    
    elif user.role == 'user':
        # User : hérite du manager si activé
        if user.use_manager_branding and user.manager:
            if user.manager.show_logo_on_reports and user.manager.logo_filename:
                logo_filename = user.manager.logo_filename
            
            if user.manager.show_company_name_on_reports and user.manager.company_name:
                company_name = user.manager.company_name
    
    # Construire le chemin du logo
    if logo_filename:
        logo_path = os.path.join(current_app.root_path, 'static/uploads/logos', logo_filename)
        if os.path.exists(logo_path):
            return logo_path, company_name
    
    # Logo par défaut MCyber
    default_logo = os.path.join(current_app.root_path, 'static/images/logo.png')
    if os.path.exists(default_logo):
        return default_logo, company_name
    
    return None, company_name

def generer_pdf_rapport(rapport, scores_blocs, preconisations, donnees_formulaire=None):
    if donnees_formulaire is None:
        donnees_formulaire = {}
    else:
        donnees_formulaire = validate_form_data(donnees_formulaire)

    from flask_login import current_user
    
    nom_client = sanitize_text(rapport.nom_client, max_length=MAX_NAME_LENGTH)
    if not nom_client: nom_client = "Client Inconnu"
    
    try:
        role = getattr(current_user, 'role', '')
        uid = getattr(current_user, 'id', None)
        if role == 'user' and rapport.user_id != uid:
            raise PermissionError("Accès non autorisé")
        if role == 'manager' and rapport.user_id != uid:
            from models import User
            owner = User.query.get(rapport.user_id)
            if not owner or owner.manager_id != uid: nom_client = "CLIENT CONFIDENTIEL"
    except Exception:
        pass

    buffer = BytesIO()
    doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=1.5*cm, leftMargin=1.5*cm, topMargin=1*cm, bottomMargin=2*cm, title=f"Audit - {nom_client}")
    story = []
    
    # Styles
    s_titre_gros = ParagraphStyle('T1', fontName='Helvetica-Bold', fontSize=28, textColor=colors.white, leading=32)
    s_sous_titre = ParagraphStyle('T2', fontName='Helvetica', fontSize=12, textColor=COLOR_ACCENT, spaceAfter=15)
    s_section = ParagraphStyle('H2', fontName='Helvetica-Bold', fontSize=14, textColor=colors.white, spaceBefore=20, spaceAfter=10)
    s_h3 = ParagraphStyle('H3', fontName='Helvetica-Bold', fontSize=11, textColor=COLOR_ACCENT, spaceBefore=10, spaceAfter=5)
    s_normal = ParagraphStyle('N', fontName='Helvetica', fontSize=10, textColor=HexColor('#cbd5e1'), leading=14)
    s_label = ParagraphStyle('L', fontName='Helvetica-Bold', fontSize=8, textColor=HexColor('#64748b'), textTransform='uppercase')
    s_q_cell = ParagraphStyle('QC', fontName='Helvetica', fontSize=9, textColor=HexColor('#e2e8f0'))
    s_r_cell = ParagraphStyle('RC', fontName='Helvetica-Bold', fontSize=9, textColor=colors.white, alignment=TA_RIGHT)

# ========================================================================
    # HEADER AVEC LOGO ET UUID
    # ========================================================================
    from reportlab.platypus import Image as RLImage
    
    date_str = rapport.date_creation.strftime('%d/%m/%Y') if rapport.date_creation else "N/A"
    short_id = str(rapport.id)[:8].upper()
    
    # Récupération logo + nom entreprise
    from models import User
    author = User.query.get(rapport.user_id)
    logo_path, company_name = get_logo_path(author) if author else (None, "MCyber Consulting")
    
    # Construction colonne gauche
    header_items = []
    
    # Logo si disponible
    if logo_path:
        try:
            logo_img = RLImage(logo_path, width=1.5*cm, height=1.5*cm, kind='proportional')
            header_items.append([logo_img])
            header_items.append([Spacer(1, 8)])
        except Exception as e:
            logger.warning(f"Impossible de charger le logo: {e}")
    
    # Textes du header
    header_items.extend([
        [Paragraph("RAPPORT DE DIAGNOSTIC", s_label)],
        [Paragraph(nom_client.upper(), s_titre_gros)],
        [Spacer(1, 5)],
        [Paragraph(f"Réf: #{short_id} • {date_str}", s_sous_titre)],
        [Paragraph(f"<font size=8 color=#64748b>{company_name}</font>", s_normal)]
    ])
    
    col_gauche = Table(header_items, colWidths=[12*cm])
    gauge = CyberGauge(safe_int(rapport.score_total), width=100, height=100)
    
    # Tableau banner
    tbl_banner = Table([[col_gauche, gauge]], colWidths=[12*cm, 5*cm])
    tbl_banner.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,-1), COLOR_BG_HEADER), 
        ('VALIGN', (0,0), (-1,-1), 'MIDDLE'), 
        ('ALIGN', (1,0), (1,0), 'CENTER'), 
        ('PADDING', (0,0), (-1,-1), 20), 
        ('BOX', (0,0), (-1,-1), 1, HexColor('#333355'))
    ]))
    
    # Ajouter au story
    story.append(tbl_banner)
    story.append(Spacer(1, 1.5*cm))

    # ========================================================================
    # SCORES DÉTAILLÉS
    # ========================================================================
    story.append(Paragraph("ANALYSE DÉTAILLÉE", s_section))
    label_map = {
        'points_2fa': 'Double authentification', 
        'points_filtrage': 'Filtrage des mails',
        'points_edr': 'Protection Endpoints', 
        'points_gestionnaire_mdp': 'Gestion mots de passe',
        'points_sauvegarde_types': 'Types de sauvegarde',
        'points_3211': 'Règle 3-2-1-1',
        'points_frequence': 'Fréquence des sauvegardes', 
        'points_cloud': 'Cloud / RGPD',
        'points_os_postes': 'OS Postes', 
        'points_os_serveurs': 'OS Serveurs',
        'points_hyperviseur': 'Hyperviseur', 
        'points_parefeu': 'Pare-feu',
        'points_vpn': 'Accès distant',
        'points_phishing': 'Test phishing', 
        'points_formation': 'Formation continue',
        'points_pra_pca': 'PRA/PCA', 
        'points_audit': 'Audit récurrent'
    }
    
    items = list(scores_blocs.items())
    rows_data = []
    for i in range(0, len(items), 2):
        row_cells = []
        for j in range(2):
            if i + j < len(items):
                key, score = items[i+j]
                nom = label_map.get(key, key)
                max_pts = SCORE_MAP_CONFIG.get(key, 5)
                val_int = safe_int(score) if score is not None else 0
                txt_val = f"{val_int} / {max_pts}" if score is not None else "N/A"
                bar = CyberProgressBar(val_int if score is not None else None, max_score=max_pts, width=130)
                
                pct = val_int / max_pts if max_pts > 0 and score is not None else 0
                c_sc = COLOR_SUCCESS if pct >= 0.8 else (COLOR_WARNING if pct >= 0.4 else COLOR_DANGER) if score is not None else HexColor('#64748b')

                top_line = Table([[Paragraph(nom, ParagraphStyle('CT', fontSize=9, textColor=HexColor('#e2e8f0'))), Paragraph(f"<b>{txt_val}</b>", ParagraphStyle('CS', fontSize=9, textColor=c_sc, alignment=TA_RIGHT))]], colWidths=[100, 40])
                t_cell = Table([[top_line], [bar]], colWidths=[150])
                t_cell.setStyle(TableStyle([('BACKGROUND', (0,0), (-1,-1), COLOR_BG_CARD), ('PADDING', (0,0), (-1,-1), 8), ('LINEBELOW', (0,0), (-1,-1), 2, COLOR_BG_PAGE)]))
                row_cells.append(t_cell)
            else:
                row_cells.append("")
        rows_data.append(row_cells)
    if rows_data:
        t_grid = Table(rows_data, colWidths=[9*cm, 9*cm])
        t_grid.setStyle(TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'), ('ALIGN', (0,0), (-1,-1), 'CENTER'), ('BOTTOMPADDING', (0,0), (-1,-1), 5)]))
        story.append(t_grid)
    story.append(Spacer(1, 1*cm))

    # PRECONISATIONS
    story.append(Paragraph("PLAN D'ACTION", s_section))
    if not preconisations:
        msg = Table([[Paragraph("Aucune anomalie critique détectée. Bravo !", s_normal)]], colWidths=[17*cm])
        msg.setStyle(TableStyle([('BACKGROUND', (0,0), (-1,-1), HexColor('#064e3b')), ('BOX', (0,0), (-1,-1), 1, COLOR_SUCCESS), ('PADDING', (0,0), (-1,-1), 15)]))
        story.append(msg)
    else:
        preconisations_limitees = preconisations[:MAX_PRECONISATIONS]
        if len(preconisations) > MAX_PRECONISATIONS:
            story.append(Paragraph(f"<font color={COLOR_WARNING}>Note : Seules les {MAX_PRECONISATIONS} premières préconisations sont affichées.</font>", s_normal))
            story.append(Spacer(1, 0.3*cm))
        for p in preconisations_limitees:
            gravite = sanitize_text(p.get('gravite', 'Moyenne'), max_length=20)
            titre = sanitize_text(p.get('titre', 'Sans titre'), max_length=MAX_TITLE_LENGTH)
            details = sanitize_text(p.get('details', ''), max_length=MAX_TEXT_LENGTH)
            if gravite == 'Critique': border, bg, icon = COLOR_DANGER, HexColor('#450a0a'), "✕"
            elif gravite == 'Élevée': border, bg, icon = COLOR_WARNING, HexColor('#451a03'), "!"
            else: border, bg, icon = COLOR_ACCENT, HexColor('#1e1b4b'), "i"
            head = Table([[Paragraph(f"<font color={border} size=12><b>{icon}</b></font>", s_normal), Paragraph(f"<b>{titre}</b>", ParagraphStyle('PT', fontSize=10, textColor=colors.white)), Paragraph(f"<font color={border}>{gravite.upper()}</font>", ParagraphStyle('PG', fontSize=8, alignment=TA_RIGHT))]], colWidths=[1*cm, 12*cm, 3*cm])
            card = Table([[head], [Paragraph(details, s_normal)]], colWidths=[17*cm])
            card.setStyle(TableStyle([('BACKGROUND', (0,0), (0,0), bg), ('BACKGROUND', (0,1), (-1,-1), COLOR_BG_CARD), ('BOX', (0,0), (-1,-1), 0.5, border), ('PADDING', (0,0), (-1,-1), 10), ('LINEBELOW', (0,0), (0,0), 0.5, border)]))
            story.append(card)
            story.append(Spacer(1, 0.4*cm))

    # ANNEXE RÉCAPITULATIF
    story.append(PageBreak())
    story.append(Paragraph("ANNEXE : RÉCAPITULATIF DES RÉPONSES", s_section))

    try:
        nb_postes = safe_int(donnees_formulaire.get('nb_postes_fixes', 0)) + safe_int(donnees_formulaire.get('nb_postes_portables', 0))
        nb_critiques_p = safe_int(donnees_formulaire.get('nb_postes_critiques', 0))
        
        nb_serveurs = safe_int(donnees_formulaire.get('nb_serveurs_physiques', 0)) + safe_int(donnees_formulaire.get('nb_serveurs_virtuels', 0))
        nb_critiques_s = safe_int(donnees_formulaire.get('nb_serveurs_critiques', 0))
        
        vol = sanitize_text(donnees_formulaire.get('volumetrie', 'Non spécifié'), max_length=50)
    except Exception as e:
        logger.warning(f"Erreur extraction volumétrie: {e}")
        nb_postes = "?"
        nb_serveurs = "?"
        vol = "Non spécifié"

    # Tableau Volumétrie
    volumetrie_data = [
        ["Postes de travail (Total)", str(nb_postes)],
        ["Dont postes critiques", str(nb_critiques_p)],
        ["Serveurs (Total)", str(nb_serveurs)],
        ["Dont serveurs critiques", str(nb_critiques_s)],
        ["Volume de données estimé", vol]
    ]

    rows_vol = []
    for q, r in volumetrie_data:
        rows_vol.append([Paragraph(q, s_q_cell), Paragraph(r, s_r_cell)])
    
    t_vol = Table(rows_vol, colWidths=[12*cm, 5*cm])
    t_vol.setStyle(TableStyle([('BACKGROUND', (0,0), (-1,-1), COLOR_BG_CARD), ('BOX', (0,0), (-1,-1), 0.5, COLOR_BORDER), ('INNERGRID', (0,0), (-1,-1), 0.25, COLOR_BORDER), ('PADDING', (0,0), (-1,-1), 8)]))
    story.append(Paragraph("Volumétrie & Parc", s_h3))
    story.append(t_vol)
    story.append(Spacer(1, 0.5*cm))

    # ✅ Questions par catégorie AVEC SUPPORT CHECKBOXES
    for categorie, questions in STRUCTURE_QUESTIONS.items():
        rows = []
        for q_key, q_label in questions:
            # Récupération réponse principale
            val_brute = donnees_formulaire.get(q_key)
            
            # ✅ NOUVEAU : Support des listes (checkboxes)
            val_lisible = format_liste_reponses(val_brute)
            
            # Récupération du champ "Détails" associé
            detail_key = CHAMPS_DETAILS.get(q_key)
            if detail_key:
                detail_val = sanitize_text(donnees_formulaire.get(detail_key, ''), max_length=80)
                if detail_val:
                    val_lisible += f" <font color='#94a3b8'>({detail_val})</font>"
            
            rows.append([Paragraph(sanitize_text(q_label), s_q_cell), Paragraph(val_lisible, s_r_cell)])
        
        if rows:
            story.append(Paragraph(sanitize_text(categorie), s_h3))
            t_cat = Table(rows, colWidths=[12*cm, 5*cm])
            t_cat.setStyle(TableStyle([('BACKGROUND', (0,0), (-1,-1), COLOR_BG_CARD), ('BOX', (0,0), (-1,-1), 0.5, COLOR_BORDER), ('INNERGRID', (0,0), (-1,-1), 0.25, COLOR_BORDER), ('PADDING', (0,0), (-1,-1), 8)]))
            story.append(t_cat)
            story.append(Spacer(1, 0.3*cm))

    try:
        # ✅ Passer company_name au callback
        def draw_bg_with_company(canvas, doc):
            draw_background(canvas, doc, company_name)
        
        doc.build(story, onFirstPage=draw_bg_with_company, onLaterPages=draw_bg_with_company)
        buffer.seek(0)
        logger.info(f"PDF généré avec succès pour rapport {rapport.id}")
        return buffer
    except Exception as e:
        logger.error(f"Erreur PDF: {e}", exc_info=True)
        raise ValueError("Erreur lors de la génération du PDF") from e