#!/usr/bin/env python3
"""PGS 15:2025 Compliance Scanner - Flask Web Application v2"""

import os, sqlite3, json, requests, re
from flask import Flask, render_template, request, jsonify, send_file, session
from datetime import datetime

app = Flask(__name__)
app.secret_key = 'pgs15-scanner-secret-2026'

# Static files behind nginx reverse proxy at /pgs15-scanner/
# Use APPLICATION_ROOT so url_for() generates correct paths
app.config['APPLICATION_ROOT'] = '/pgs15-scanner'
app.config['PREFERRED_URL_SCHEME'] = 'https'
DB_PATH = os.path.join(os.path.dirname(__file__), 'data', 'scanner.db')

# ═══════════════════════════════════════════════════════════════════
# PGS 15:2025 DATA
# ═══════════════════════════════════════════════════════════════════

ONDERGRENS = {
    '3':   {'I': 1, 'II': 25, 'III': 50},
    '4.1': {'I': 1, 'II': 50, 'III': 50},
    '4.2': {'I': 1, 'II': 50, 'III': 50},
    '4.3': {'I': 1, 'II': 50, 'III': 50},
    '5.1': {'I': 1, 'II': 50, 'III': 50},
    '6.1': {'I': 1, 'II': 50, 'III': 50},
    '8':   {'I': 1, 'II': 250, 'III': 250},
    '9':   {'I': 1, 'II': 250, 'III': 250},
}

CLP_ROUTES = {
    # PGS 15:2025 — CLP-only routes (Tabel 2 + Tabel 6)
    'acute_tox_1': {'label': 'Acute Tox. Cat. 1', 'ondergrens': 1, 'C': 1000, 'B': None, 'A': 1000},
    'acute_tox_2_3': {'label': 'Acute Tox. Cat. 2/3', 'ondergrens': 1, 'C': 10000, 'B': None, 'A': 10000},
    'cmr_vast': {'label': 'CMR (vaste vorm)', 'ondergrens': 25, 'C': None, 'B': None, 'A': None},  # ALLEEN Basis
    'cmr_vloeibaar': {'label': 'CMR (vloeibaar)', 'ondergrens': 1, 'C': None, 'B': None, 'A': None},  # ALLEEN Basis
}

# ═══ PGS 15:2025 Tabel 6 — Niveaus (Publicatie 565) ═══
# Format: (ondergrens, C_start, B_threshold, A_threshold)
#   C_start = hoeveelheid waarboven C-niveau geldt (= ondergrens, None = geen C)
#   B_threshold = None = geen B-niveau
#   A_threshold = None = geen A-niveau
# 60min WBDBO waarden als standaard; 90min varianten in commentaar.
NIVEAUS = {
    # ADR 3: VG I/II/III identiek in 2025 (geen VG I onderscheid meer)
    ('3', 'I'):    (1, 1, None, 10000),       # 90min: A>20000
    ('3', 'II'):   (25, 25, None, 10000),      # 90min: A>20000
    ('3', 'III'):  (50, 50, None, 10000),      # 90min: A>20000
    # ADR 4.1
    ('4.1', 'I'):  (1, None, 10000, 20000),    # VG I: maatwerk, geen C
    ('4.1', 'II'): (50, None, 50, 20000),      # geen C; B vanaf ondergrens
    ('4.1', 'III'):(50, None, 50, None),       # geen C; B vanaf ondergrens; geen A
    # ADR 4.2
    ('4.2', 'I'):  (50, None, 50, 20000),      # VG I+II: geen C; B <10ton
    ('4.2', 'II'): (50, None, 50, 20000),      # VG I+II: geen C
    ('4.2', 'III'):(50, None, 50, None),       # geen C; geen A
    # ADR 4.3
    ('4.3', 'I'):  (50, None, 50, 20000),      # geen C; B <10ton
    ('4.3', 'II'): (50, None, 50, 20000),      # geen C
    ('4.3', 'III'):(50, None, 50, None),       # geen C; geen A
    # ADR 5.1: GEEN A-niveau in 2025!
    ('5.1', 'II'): (50, 50, 2500, None),       # C<2.5ton, B>2.5ton
    ('5.1', 'III'):(50, 50, 2500, None),       # C<2.5ton, B>2.5ton
    # ADR 5.2
    ('5.2', 'I'):  (30, 30, None, None),       # C<1ton, geen B/A
    # ADR 6.1: VG I/II/III — geen B-niveau, direct C→A
    ('6.1', 'I'):  (1, 1, None, 1000),         # C<1ton, A>1ton
    ('6.1', 'II'): (50, 50, None, 10000),      # 90min: A>20000
    ('6.1', 'III'):(50, 50, None, 10000),      # 90min: A>20000
    # ADR 8 VG I: afhankelijk van flashpoint (behandeld in determine_niveau)
    ('8', 'I'):    (250, 250, 10000, 10000),   # placeholder; FP logica in func
    # ADR 8+9 VG II+III: brandbaar vs onbrandbaar (behandeld in determine_niveau)
    ('8', 'II'):   (250, 250, 10000, None),    # brandbaar: C<10ton, B>10ton, geen A
    ('8', 'III'):  (250, 250, 10000, None),    # brandbaar: C<10ton, B>10ton, geen A
    ('9', 'II'):   (250, 250, 10000, None),    # brandbaar: C<10ton, B>10ton, geen A
    ('9', 'III'):  (250, 250, 10000, None),    # brandbaar: C<10ton, B>10ton, geen A
}

# ═══ SCENARIO'S (PGS 15:2025 Hoofdstuk 4) ═══
SCENARIOS = {
    'S1': 'Verpakking wordt aangereden waardoor de gevaarlijke stof vrijkomt uit de verpakking',
    'S2': 'Verpakking valt en faalt waardoor de stof vrijkomt uit de verpakking',
    'S3': 'Door het bezwijken van (een deel) van de stelling of de opslaghal vallen de verpakkingen en komen de gevaarlijke stoffen vrij',
    'S4': 'Doordat de verpakkingen met gevaarlijke stoffen worden overbelast (verkeerde stapeling) faalt de verpakking en komt de stof vrij',
    'S5': 'Door veroudering/degradatie van de verpakking of een onjuiste/beschadigde verpakking komen de gevaarlijke stoffen vrij',
    'S6': 'Door over- of onderdruk faalt een verpakking en komen de gevaarlijke stoffen vrij',
    'S7': 'Door fouten bij het openen van een verpakking voor monstername komen de gevaarlijke stoffen vrij',
    'S8': 'Verpakte gevaarlijke stoffen raken betrokken bij een brand in de opslagvoorziening',
    'S9': 'Verpakte gevaarlijke stoffen raken betrokken bij een brand van buiten de opslagvoorziening, maar binnen de begrenzing van de locatie waar de activiteit wordt verricht',
    'S10': 'Verpakte gevaarlijke stoffen raken betrokken bij een brand van buiten de begrenzing van de locatie waar de activiteit wordt verricht',
    'S11': 'Vrijkomen gevaarlijke stof uit de verpakking',
    'S12': 'Verspreiding damp/gas of vloeistof',
    'S13': 'Brand in opslagvoorziening',
    'S14': 'Escalatie van een brand binnen de begrenzing van de locatie waar de activiteit wordt verricht',
    'S15': 'Escalatie van een brand naar de omgeving',
    'S16': 'Overige stofspecifieke scenario\'s',
}

DOELEN = {
    'Basis': {
        'doelen': [
            'D1 — Voorkomen dat een aanrijding plaatsvindt',
            'D2 — Zeker stellen dat opslaglocaties voorzien zijn van een fysieke bescherming',
            'D3 — Zeker stellen dat verpakte gevaarlijke stoffen op een juiste wijze in de juiste opslagvoorziening worden opgeslagen',
            'D4 — Voorkomen dat na lekkage een gevaarlijke situatie ontstaat',
            'D5 — Zeker stellen dat adequaat wordt gereageerd bij noodsituaties ter voorkoming van effecten op medewerkers',
            'D6 — Zeker stellen dat hulpdiensten adequaat kunnen optreden',
            'D7 — Voorkomen verspreiding',
            'D8 — Voorkomen van ontsteking',
            'D9 — Voorkomen van brand in de opslagvoorziening',
            'D10 — Voorkomen van escalatie van een brand (passief) - compartimenteren',
            'D11 — Voorkomen van escalatie (actief) - beheersen',
            'D12 — Zeker stellen dat een brand (semi)automatisch geblust kan worden',
            'D13 — Zeker stellen dat aanvullende stofspecifieke risico\'s worden beheerst',
        ],
        'scenario': [
            'S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8', 'S9', 'S10',
            'S11', 'S12', 'S13', 'S14', 'S15', 'S16',
        ],
    },
    'C': {
        'doelen': [
            'D1 — Voorkomen dat een aanrijding plaatsvindt',
            'D2 — Zeker stellen dat opslaglocaties voorzien zijn van een fysieke bescherming',
            'D3 — Zeker stellen dat verpakte gevaarlijke stoffen op een juiste wijze in de juiste opslagvoorziening worden opgeslagen',
            'D4 — Voorkomen dat na lekkage een gevaarlijke situatie ontstaat',
            'D5 — Zeker stellen dat adequaat wordt gereageerd bij noodsituaties ter voorkoming van effecten op medewerkers',
            'D6 — Zeker stellen dat hulpdiensten adequaat kunnen optreden',
            'D7 — Voorkomen verspreiding',
            'D8 — Voorkomen van ontsteking',
            'D9 — Voorkomen van brand in de opslagvoorziening',
            'D10 — Voorkomen van escalatie van een brand (passief) - compartimenteren',
            'D11 — Voorkomen van escalatie (actief) - beheersen',
            'D12 — Zeker stellen dat een brand (semi)automatisch geblust kan worden',
            'D13 — Zeker stellen dat aanvullende stofspecifieke risico\'s worden beheerst',
        ],
        'scenario': [
            'S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8', 'S9', 'S10',
            'S11', 'S12', 'S13', 'S14', 'S15', 'S16',
        ],
    },
    'B': {
        'doelen': [
            'D1 — Voorkomen dat een aanrijding plaatsvindt',
            'D2 — Zeker stellen dat opslaglocaties voorzien zijn van een fysieke bescherming',
            'D3 — Zeker stellen dat verpakte gevaarlijke stoffen op een juiste wijze in de juiste opslagvoorziening worden opgeslagen',
            'D4 — Voorkomen dat na lekkage een gevaarlijke situatie ontstaat',
            'D5 — Zeker stellen dat adequaat wordt gereageerd bij noodsituaties ter voorkoming van effecten op medewerkers',
            'D6 — Zeker stellen dat hulpdiensten adequaat kunnen optreden',
            'D7 — Voorkomen verspreiding',
            'D8 — Voorkomen van ontsteking',
            'D9 — Voorkomen van brand in de opslagvoorziening',
            'D10 — Voorkomen van escalatie van een brand (passief) - compartimenteren',
            'D11 — Voorkomen van escalatie (actief) - beheersen',
            'D12 — Zeker stellen dat een brand (semi)automatisch geblust kan worden',
            'D13 — Zeker stellen dat aanvullende stofspecifieke risico\'s worden beheerst',
        ],
        'scenario': [
            'S1-S16 — Alle oorzaak- en gevolgscenario\'s zijn van toepassing',
        ],
    },
    'A': {
        'doelen': [
            'D1 — Voorkomen dat een aanrijding plaatsvindt',
            'D2 — Zeker stellen dat opslaglocaties voorzien zijn van een fysieke bescherming',
            'D3 — Zeker stellen dat verpakte gevaarlijke stoffen op een juiste wijze in de juiste opslagvoorziening worden opgeslagen',
            'D4 — Voorkomen dat na lekkage een gevaarlijke situatie ontstaat',
            'D5 — Zeker stellen dat adequaat wordt gereageerd bij noodsituaties ter voorkoming van effecten op medewerkers',
            'D6 — Zeker stellen dat hulpdiensten adequaat kunnen optreden',
            'D7 — Voorkomen verspreiding',
            'D8 — Voorkomen van ontsteking',
            'D9 — Voorkomen van brand in de opslagvoorziening',
            'D10 — Voorkomen van escalatie van een brand (passief) - compartimenteren',
            'D11 — Voorkomen van escalatie (actief) - beheersen',
            'D12 — Zeker stellen dat een brand (semi)automatisch geblust kan worden',
            'D13 — Zeker stellen dat aanvullende stofspecifieke risico\'s worden beheerst',
        ],
        'scenario': [
            'S1-S16 — Alle oorzaak- en gevolgscenario\'s zijn van toepassing',
        ],
    },
}

# ═══ MAATREGELEN (PGS 15:2025 Hoofdstuk 7) ═══
# Alle M-codes met officiële titels. Toelichtingen zijn samenvattingen.
MAATREGELEN_ALLE = {
    'MW1': ('Zorgplicht basisveiligheid', 'Er is een basisveiligheidsniveau aanwezig dat bestaat uit beschermende maatregelen volgens wet- en regelgeving, bewezen en geaccepteerde goede praktijken, good housekeeping en maatregelen goed vakmanschap.'),
    'M2': ('Opslaan van verpakte gevaarlijke stoffen en CMR-/CLP-stoffen', 'Verpakte gevaarlijke stoffen en CMR-/CLP-stoffen boven de ondergrenzen worden opgeslagen in een daarvoor bestemde opslagvoorziening.'),
    'M3': ('Vereist veiligheidsniveau', 'Op basis van het soort en de hoeveelheid gevaarlijke stoffen wordt bepaald welk veiligheidsniveau (C, B of A) vereist is.'),
    'M4': ('Vereist veiligheidsniveau ADR-klasse 4.3 VG II en III', 'Voor ADR-klasse 4.3 VG II en III geldt dat het vereiste veiligheidsniveau wordt bepaald op basis van de opgeslagen hoeveelheden en de specifieke eigenschappen van deze stoffen, conform de richtlijn in Tabel 6.'),
    'M5': ('Vereist veiligheidsniveau ADR-klasse 4.3 VG II en III - aanvullend 1', 'Aanvullende eis voor ADR-klasse 4.3 VG II en III: naast het basisveiligheidsniveau gelden extra maatregelen afhankelijk van de hoeveelheid en de reactieve eigenschappen van de stoffen.'),
    'M6': ('Vereist veiligheidsniveau ADR-klasse 4.3 VG II en III - aanvullend 2', 'Tweede aanvullende eis voor ADR-klasse 4.3 VG II en III: wanneer de stoffen in grotere hoeveelheden worden opgeslagen, gelden strengere veiligheidseisen inclusief specifieke scheidingseisen.'),
    'M7': ('Binnenopslag gasflessen tot 250 l', 'Gasflessen met een waterinhoud tot 250 l worden opgeslagen in een opslagvoorziening die voldoet aan de eisen voor binnenopslag, inclusief voldoende ventilatie en bescherming tegen hittebronnen.'),
    'M8': ('Binnenopslag gasflessen vanaf 250 l', 'Gasflessen met een waterinhoud vanaf 250 l worden opgeslagen volgens strengere eisen voor binnenopslag, inclusief specifieke eisen aan ventilatie, brandwerendheid en scheiding van andere gevaarlijke stoffen.'),
    'M9': ('Gasflessen met medische of medicinale gassen', 'Gasflessen met medische of medicinale gassen worden apart opgeslagen van andere gevaarlijke stoffen en zijn duidelijk gemarkeerd als medische gassen, conform de specifieke eisen voor deze categorie.'),
    'M10': ('Speciale gasmengsels voor kalibratie-/laboratoriumdoeleinden', 'Speciale gasmengsels voor kalibratie- of laboratoriumdoeleinden mogen in beperkte hoeveelheden worden opgeslagen, mits de verpakking intact is en de stoffen gescheiden worden opgeslagen van onverenigbare combinaties.'),
    'M11': ('Uitsluitend opslag volgens Wet vervoer gevaarlijke stoffen', 'Stoffen die uitsluitend onder het regime van de Wet vervoer gevaarlijke stoffen vallen, mogen worden opgeslagen indien wordt voldaan aan de specifieke eisen van die wetgeving.'),
    'M12': ('Opslag (tank)containers en parkeren vervoerseenheden', '(Tank)containers en geparkeerde vervoerseenheden met gevaarlijke stoffen worden opgeslagen op een daarvoor bestemde locatie die voldoet aan de eisen voor (tank)containeropslag uit deze richtlijn.'),
    'M13': ('Opslag ADR-klasse 6.2 cat. I1 en I2', 'Stoffen van ADR-klasse 6.2 categorie I1 en I2 (infectieuze stoffen) worden opgeslagen volgens specifieke hygiëne- en veiligheidseisen, inclusief gescheiden opslag van andere gevaarlijke stoffen.'),
    'M14': ('Werkvoorraad', 'Een werkvoorraad is een beperkte hoeveelheid gevaarlijke stoffen die noodzakelijk is voor de directe werkzaamheden. De werkvoorraad is beperkt tot maximaal de hoeveelheid voor één werkdag en wordt veilig opgeslagen.'),
    'M15': ('Maximale omvang van opslagvoorziening', 'Een opslagvoorziening is een onafhankelijk brandcompartiment. De maximale oppervlakte is 2.500 m² met een blussende VBB-installatie, of 1.000 m² (tot 20.000 kg) respectievelijk 2.500 m² (meer dan 20.000 kg) zonder VBB-installatie.'),
    'M16': ('Maximumvloeroppervlakte opslag spuitbussen en gaspatronen', 'Het vloeroppervlak voor opslag van spuitbussen of gaspatronen is maximaal 1.900 m² bij veiligheidsniveau A, maximaal 300 m² in een separaat brandcompartiment, of maximaal 100 m² in overige gevallen.'),
    'M17': ('Opslag op een verdieping', 'Op de eerste verdieping is maximaal één opslagruimte met maximaal 2.500 kg. In een kelder en op hogere verdiepingen is dit maximaal 500 kg, tenzij uitsluitend onbrandbare stoffen worden opgeslagen.'),
    'M18': ('Maximale waterinhoud cilinderpakket', 'De totale waterinhoud van een cilinderpakket (gasflessenbatterij) is maximaal 1.000 l voor giftige gassen van ADR-klasse 2 en maximaal 3.000 l in overige gevallen.'),
    'M19': ('Maximale opslaghoeveelheid organische peroxiden', 'Het opslaan van maximaal 1.000 kg organische peroxiden per opslagvoorziening is toegelaten als deze verpakt zijn als limited quantities (LQ) volgens hoofdstuk 3.4 van het ADR.'),
    'M20': ('Lege en ongereinigde verpakkingen', 'Voor lege, ongereinigde verpakkingen van ADR-klasse 3 en 6.1 gelden alle relevante maatregelen uit deze richtlijn, behalve M30 en M35. Voor andere klassen gelden specifieke voorwaarden waaronder dedicated opslag in vakken tot 300 m².'),
    'M21': ('Wbdbo', 'De Wbdbo tussen een opslagvoorziening en andere ruimten, brandbare objecten of locatiegrenzen bedraagt ten minste 60 min of 90 min, afhankelijk van de opgeslagen hoeveelheden en het veiligheidsniveau, bepaald volgens NEN 6068.'),
    'M22': ('Wbdbo bij de plaatsing van gasflessen tegen de gevelwand', 'Gasflessen tegen een gevelwand vereisen een Wbdbo van ten minste 60 min tot 4 m hoogte en 2 m aan weerszijden. Bij niet-voldoen zijn aanvullende maatregelen nodig zoals zijmuren of een dakconstructie (bushokje).'),
    'M23': ('Voorkomen van escalatie naar naastgelegen opslagvoorziening', 'Wanneer meerdere opslagvoorzieningen naast of boven elkaar liggen, mogen incidenten zich niet verplaatsen naar aangrenzende opslagvoorzieningen. Dit geldt niet voor dedicated opslag van onbrandbare stoffen van ADR-klasse 8 en 9.'),
    'M24': ('Brandwerendheid van een draagconstructie', 'De brandwerendheid tot bezwijken van de draagconstructie is ten minste gelijk aan de Wbdbo-waarde. Bij meer dan 2.500 kg gelden voor boven- en onderliggende constructies dezelfde brandwerendheidseisen.'),
    'M25': ('Criteria brandwerendheid', 'Voor brandwerendheid gelden NEN 6069-criteria: R voor draagconstructies, REI voor dragende wanden en vloeren, RE voor daken, EI voor niet-dragende wanden en EI1 voor deuren.'),
    'M26': ('Afdekking draagconstructie, wanden en dak', 'De noodzakelijke afdekking van draagconstructie, wanden en dak voldoet aan brandklasse A2 (volledige diepte) of A1 (eerste 10 mm) volgens NEN-EN 13501-1. Voor vloeren geldt A2fl respectievelijk A1fl.'),
    'M27': ('Constructie van een dak', 'Het dak van een opslagvoorziening bestaat uit niet-brandgevaarlijk materiaal volgens NEN 6063, om te voorkomen dat het dak gemakkelijk in brand raakt door vliegvuur vanuit de omgeving.'),
    'M28': ('Eisen aan ondergrond (tank)containers', 'De vloer waar (tank)containers worden opgeslagen is vervaardigd van onbrandbaar materiaal, heeft voldoende stabiliteit en is geëgaliseerd om veilige opslag te waarborgen.'),
    'M29': ('Voorkomen van een verspreiding van een lekkage', 'In de vloer van een opslagvoorziening zitten geen openingen met directe verbinding naar riolering of oppervlaktewater. Bij (tank)containers zijn maatregelen aanwezig om lekkageverspreiding via water of riool te voorkomen.'),
    'M30': ('Opvangvoorziening', 'Een opslagvoorziening heeft een opvangvoorziening voor gelekte of gemorste gevaarlijke vloeistof met een capaciteit van ten minste 110% van de grootste verpakking of 10% van de totale inhoud. Voor veiligheidsniveau A, B en C geldt Tabel 8.'),
    'M31': ('Afschot terrein', 'Het terreingedeelte voor geparkeerde vervoerseenheden is uitgevoerd met afschot richting een goot of afsluitbaar bedrijfsriool, zodat bij lekkage vloeistof wordt opgevangen en niet onder andere eenheden verspreidt.'),
    'M32': ('Bluswateropvangcapaciteit', 'De bluswateropvangcapaciteit voor veiligheidsniveau A wordt bepaald volgens PGS 14. De werkelijke grootte is minimaal 100% (ADR 6.1 en milieugevaarlijk), 50% (ADR 8) of 25% (ADR 3) van de nominale capaciteit.'),
    'M33': ('Normering en eisen brandveiligheidsopslagkast', 'Een brandveiligheidsopslagkast voldoet ten minste aan NEN-EN 14470-1 (eerste gebruik na 1-1-2006) of NEN 2678. Gasflessen-kasten vereisen minimaal 60 min brandwerendheid. Kasten groter dan 250 kg gelden als reguliere opslagvoorziening.'),
    'M34': ('Productcertificaat brandveiligheidsopslagkast', 'Er moet een productcertificaat aanwezig zijn voor de brandveiligheidsopslagkast waaruit blijkt dat deze voldoet aan NEN-EN 14470-1, zichtbaar voor zowel gebruiker als toezichthouders.'),
    'M35': ('Onverenigbare combinaties', 'Gevaarlijke stoffen die met elkaar gevaarlijke reacties kunnen aangaan (sterke temperatuurs-/drukverhoging of giftigere/brandbaardere gassen) worden gescheiden opgeslagen. Niet van toepassing op LQ- en EQ-verpakkingen.'),
    'M36': ('Gescheiden opslag in stellingen', 'Opslag in stellingen voldoet aan de scheidingseisen van M35. Stoffen die met elkaar kunnen reageren mogen niet boven elkaar in stellingen worden geplaatst. Corrosieve stoffen vereisen extra aandacht voor onderliggende verpakkingen.'),
    'M37': ('Gasflessen met gelijksoortige gevaarseigenschappen', 'Gasflessen met gassen met gelijksoortige gevaarseigenschappen worden gegroepeerd opgeslagen. Lege gasflessen apart opgeslagen hoeven niet gegroepeerd te worden. Groepering verkleint de kans op verwisseling.'),
    'M38': ('Afscheiding opslagvoorziening spuitbussen of gaspatronen', 'Een opslagvoorziening voor spuitbussen of gaspatronen is voorzien van gaaswerk of andere afscheiding zodat deze de opslag niet kan verlaten. Niet van toepassing bij ruimtevullend blussysteem of brandveiligheidsopslagkasten.'),
    'M39': ('Plaatsing (tank)containers op basis van ADR-klasse', '(Tank)containers met dezelfde ADR-klasse mogen boven en naast elkaar; verschillende ADR-klassen moeten minimaal 2,5 m (één containerbreedte) gescheiden worden opgeslagen.'),
    'M40': ('Opslag in vakken', 'In opslagvoorzieningen met veiligheidsniveau A of B wordt in vakken opgeslagen. Vakken zijn maximaal 300 m², gescheiden door een gangpad van 3,5 m of een scheidingsconstructie met 30 min brandwerendheid.'),
    'M41': ('Vakindeling veiligheidsniveau A', 'Bij veiligheidsniveau A met blusinstallatie wordt de vakindeling bepaald door het UPD. Bij bedrijfsbrandweer is vakindeling vereist als het scenario dit noodzakelijk maakt. In hoogstapelmagazijnen bepaalt de blusinstallatie de vakindeling.'),
    'M42': ('Vakindeling veiligheidsniveau B', 'Bij veiligheidsniveau B wordt vakindeling toegepast bij meer dan 300 m² brandbare stoffen. Vloeistoffen met vlampunt onder 100°C in niet-metalen verpakking vereisen voorzieningen tegen uitstromen naar aangrenzende vakken.'),
    'M43': ('Gescheiden opslag ADR-klasse 4.1 VG II en III', 'ADR-klasse 4.1 VG II en III met gevaarsaspecten D of DT mogen alleen met elkaar worden opgeslagen, niet met andere stoffen. Stoffen met gevaarsaspect SR2 worden altijd apart opgeslagen.'),
    'M44': ('Opslag ADR-klasse 4.2 VG III en 4.3 VG III in combinatie met ADR-klasse 3 VGIII', 'Bij meer dan 10.000 kg van ADR-klasse 4.2/4.3 VG III gecombineerd met ADR-klasse 3 VG III is veiligheidsniveau A vereist. ADR-klasse 4-stoffen en ADR-klasse 3-stoffen worden 30 min brandwerend gescheiden.'),
    'M45': ('Controle verpakte stoffen in opslagvoorziening', 'Verpakte gevaarlijke stoffen worden bij ontvangst en periodiek gecontroleerd op juiste en leesbare gevaarsetiketten, beschadiging en lekkage. De controlefrequentie en registratie van afwijkingen zijn vastgelegd in een procedure.'),
    'M46': ('Gebruik gemotoriseerde transportmiddelen', 'Bewegingen en verblijfsduur van gemotoriseerde transportmiddelen in een opslagvoorziening zijn tot een minimum beperkt en beperken zich tot noodzakelijke werkzaamheden.'),
    'M47': ('Ingangscontrole (tank)containers', 'De buitenkant van (tank)containers wordt visueel geïnspecteerd voordat zij in de stapeling worden geplaatst, om lekkages en onregelmatigheden vast te stellen.'),
    'M48': ('Geschikte verpakking', 'De verpakking voldoet aan de volgende voorwaarden: niets kan onvoorzien ontsnappen, het materiaal wordt niet aangetast door de inhoud, en de verpakking is bestand tegen normale behandeling.'),
    'M49': ('Wijze van opslag van acuut toxische stoffen', 'Acuut toxische stoffen en ADR-klasse 6.1 VG I/II zijn uitsluitend in UN-gekeurde verpakkingen aanwezig. Handelingen zijn verboden tenzij noodzakelijk voor logistiek. Opslaghoogte maximaal 1,80 m voor VG I-stoffen.'),
    'M50': ('Weerbestendige verpakking', 'De verpakking van in de buitenlucht opgeslagen gevaarlijke stoffen is bestand tegen weersinvloeden zoals regen, vorst en UV-straling.'),
    'M51': ('Bescherming tegen inregenen', 'Een open container met verpakte gevaarlijke stoffen die niet waterdicht zijn verpakt, is voorzien van bescherming tegen inregenen om aantasting van de verpakking en inhoud te voorkomen.'),
    'M52': ('Deugdelijke constructie van pallets en maximale stapeling', 'Pallets met gevaarlijke stoffen die gestapeld zijn, zijn van een deugdelijke constructie. Per verpakkingstype is een maximale stapeling vastgesteld afhankelijk van gewicht en sterkte van de verpakking.'),
    'M53': ('Stapelen van verpakte stoffen', 'Het stapelen gebeurt volgens de gebruiksaanwijzing van de verpakkingsleverancier, rekening houdend met de sterkte van de (om)verpakking. Breekbare enkelvoudige verpakkingen worden niet gestapeld.'),
    'M54': ('Wijze van opslaan gasflessen', 'Gasflessen worden niet gestapeld tenzij een voorziening dit toelaat. Gasflessen worden staand opgeslagen, behalve lege flessen of flessen in een voorziening die rocketeren voorkomt.'),
    'M55': ('Stapelen van spuitbussen en gaspatronen', 'De stapelhoogte voor spuitbussen en gaspatronen is maximaal 3,60 m zonder stellingen. Bij veiligheidsniveau A geldt de stapelhoogte uit het UPD.'),
    'M56': ('Bescherming tegen omvallen gasflessen', 'Gasflessen zijn tegen omvallen beschermd door vastzetten met ketting, beugel of spanband. Stabiele cilinders (zoals propaan/butaan) hoeven niet vastgezet. Bij opslag in een vak gelden specifieke beschermingseisen aan drie zijden en voorzijde.'),
    'M57': ('Afstand tot dakplaten van verpakte gevaarlijke stoffen', 'De ruimte tussen opgeslagen goederen en de onderzijde van de dakplaten is ten minste 0,50 m voor licht ontvlambare en acuut toxische stoffen en spuitbussen/gaspatronen, nodig voor luchtcirculatie en opwarming door zonnestraling.'),
    'M58': ('Toegelaten handelingen met verpakte gevaarlijke stoffen', 'Handelingen met gevaarlijke stoffen in een opslagvoorziening zijn verboden, behalve werkzaamheden voor monstername, lekkage-/calamiteitenbestrijding en ompakwerkzaamheden waarbij de primaire verpakking niet wordt geopend.'),
    'M59': ('Lekbak monsternamepunt acuut toxische stoffen', 'Een monsternamepunt voor acuut toxische stoffen of ADR-klasse 3 VG I/II is voorzien van een productbestendige en vloeistofdichte lekbak. Een morsing wordt direct opgeruimd. Bij meerdere vloeistoffen is de lekbak gecompartimenteerd.'),
    'M60': ('Openen afsluiters gasflessen', 'In een opslagvoorziening worden afsluiters van gasflessen niet geopend, tenzij het gaat om gasflessen gekoppeld aan een installatie buiten de opslagvoorziening via vaste leidingen.'),
    'M61': ('Opstelling (tank) containers', '(Tank)containers en vervoerseenheden met gevaarlijke stoffen zijn zodanig opgesteld dat ze altijd voor inspectie bereikbaar zijn en kunnen worden afgevoerd naar de calamiteitenplaats (M125).'),
    'M62': ('Stapeling in buitenste rijen', '(Tank)containers met gevaarlijke stoffen worden in de buitenste rijen van de stapeling geplaatst om bereikbaarheid bij calamiteiten te waarborgen.'),
    'M63': ('Stapeling open top containers', 'Op een open top container wordt geen andere container gestapeld tenzij deze door twistlocks worden gekoppeld. Uitzondering: stapeling zonder stoten onder een brugkraan of in een automatische stack.'),
    'M64': ('Opstelling (tank)containers - aanvullend', 'Een (tank)container met giftige gassen (etiket 2.3), ADR-klasse 6.1 VG I of ADR-klasse 8 met etiket 6.1 wordt op het maaiveld geplaatst op minimaal 5 m afstand van brandbare vloeistoffen en gassen.'),
    'M65': ('Opslag van aanverwante en koopmansgoederen', 'In een opslagvoorziening mogen aanverwante stoffen of koopmansgoederen aanwezig zijn bij maximaal 10 ton (60 min Wbdbo) of 20 ton (90 min Wbdbo), zonder VG I-stoffen, met spanningsvrije elektrische componenten.'),
    'M66': ('Opslag van aanverwante en koopmansgoederen veiligheidsniveau B en C', 'Bij veiligheidsniveau B of C worden aanverwante stoffen en koopmansgoederen behandeld als gevaarlijke stoffen. Bij het bepalen van het veiligheidsniveau worden de eigenschappen van aanverwante stoffen meegenomen.'),
    'M67': ('Opslag van aanverwante en koopmansgoederen veiligheidsniveau A', 'Bij veiligheidsniveau A wordt in het UPD rekening gehouden met alle opgeslagen stoffen of is er een bedrijfsbrandweer. Koopmansgoederen worden gescheiden opgeslagen van gevaarlijke stoffen in aparte vakken, tenzij het UPD anders bepaalt.'),
    'M68': ('Eisen aan machines en installaties', 'Machines en installaties in een opslagvoorziening zijn strikt noodzakelijk voor het logistiek proces, staan op minimaal 3,5 m afstand van gevaarlijke stoffen, worden periodiek onderhouden en zijn buiten bedrijfstijd uitgeschakeld.'),
    'M69': ('Voorkomen van opwarming spuitbussen en gaspatronen', 'Opwarming van spuitbussen en gaspatronen boven 50°C door zonnestraling of warmtebronnen wordt voorkomen. Opslag boven of binnen 1 m van kachels is verboden tenzij de oppervlaktetemperatuur nooit hoger dan 60°C wordt.'),
    'M70': ('Verwarming', 'De verbrandingsruimte van verwarmingstoestellen staat niet in open verbinding met de opslagvoorziening. Onderdelen hebben geen hogere oppervlaktetemperatuur dan 200°C en geen contact met opgeslagen stoffen.'),
    'M71': ('Opslag van pallets', 'Losse (lege) brandbare pallets zijn niet aanwezig in een opslagvoorziening om escalatie van brand te voorkomen.'),
    'M72': ('Opslag van pallets - eisen', 'In afwijking op M71 mogen beperkte aantallen losse pallets aanwezig zijn: maximaal 24 standaardpallets of 5% van palletplaatsen, op maaiveldniveau, maximaal 1,8 m hoog, in een apart vak gescheiden door 2,4 m of 30 min brandwerende constructie.'),
    'M73': ('Opslag van pallets - UPD', 'Als pallets zijn meegenomen in de risicoafweging van het UPD (M99), zijn M71 en M72 niet van toepassing. De voorwaarden uit het UPD zijn dan leidend.'),
    'M74': ('Materieel voor het vervoeren van (tank)containers', 'Materieel voor het vervoeren van (tank)containers is ontworpen, onderhouden en wordt gebruikt zodanig dat een veilige behandeling is gewaarborgd. Voor kranen en hijsmiddelen gelden Arbowet-verplichtingen.'),
    'M75': ('Constructie van stellingen', 'Een stelling voor opslag van gevaarlijke stoffen is bestand tegen de stoffen, stabiel, wordt niet zwaarder belast dan het ontwerp en de geschiktheid is aantoonbaar. NEN-EN 15512+A1 kan worden gebruikt voor palletstellingen.'),
    'M76': ('Manoeuvreerruimte tussen opslagvakken', 'Tussen opslagvakken en stellingen is voldoende manoeuvreerruimte voor de gebruikte transportmiddelen om veilig gebruik te waarborgen.'),
    'M77': ('Aanrijdbeveiliging van stellingen', 'Bij risico op aanrijden is een stelling voorzien van vrijstaande aanrijdbeschermers op hoeken van stellinggangen en -onderdoorgangen.'),
    'M78': ('Aanrijdbeveiliging van stellingen - afwijken', 'In afwijking op M77 is aanrijdbeveiliging niet noodzakelijk bij een (semi) geautomatiseerd magazijnsysteem als softwarematig is geborgd dat aanrijding wordt voorkomen, geen andere voertuigbewegingen plaatsvinden, en het systeem failsafe is.'),
    'M79': ('Aanrijdbeveiliging van gasflessen', 'Bij risico op aanrijden van gasflessen door frequente voertuigbewegingen is de opslagvoorziening voorzien van een aanrijdbeveiliging.'),
    'M80': ('Aanrijdbeveiliging van (tank)containers', '(Tank)containers en vervoerseenheden met gevaarlijke stoffen zijn tegen aanrijding beschermd door technische maatregelen (aanrijdbescherming) of organisatorische maatregelen (routering van voertuigen).'),
    'M81': ('Beschadigingen aan de stelling', 'Bij blijvende vervorming van een stellingonderdeel worden onmiddellijk passende maatregelen genomen, zoals vrijmaken van opslag. De stelling wordt pas weer in gebruik genomen na vervanging/reparatie en beoordeling volgens M82.'),
    'M82': ('Periodieke inspectie van de stellingconstructie', 'Een stellingconstructie wordt ten minste jaarlijks visueel geïnspecteerd op doelmatigheid, juist gebruik en beschadigingen. Resultaten worden geregistreerd en minimaal vijf jaar bewaard.'),
    'M83': ('Herkeuringstermijn van gasflessen', 'De herkeuringstermijn van gasflessen is niet verstreken. Bij overschrijding mag maximaal tweemaal de termijn worden opgeslagen mits aantoonbaar door lange gebruiksperiode en de fles in goede staat verkeert volgens NEN-EN 1968.'),
    'M84': ('Controle van brandkranen', 'Brandkranen worden elke drie jaar door een deskundige gecontroleerd op waterdruk en wateropbrengst. De meetmethode wordt in overleg met de veiligheidsregio vastgesteld. Brandkranen worden tweemaal per jaar doorgespoeld.'),
    'M85': ('Deskundig personeel', 'Bij opslag van meer dan 2.500 kg is tijdens werkzaamheden minimaal één door het bedrijf aangestelde deskundige aanwezig met aantoonbare vakbekwaamheid op het gebied van de aanwezige stoffen en incidentbestrijding.'),
    'M86': ('Instructie van medewerkers', 'Personeel met toegang tot de opslagvoorziening is op de hoogte van de aard en gevaarsaspecten van de opgeslagen stoffen, de te nemen maatregelen bij onregelmatigheden en het interne noodplan.'),
    'M87': ('Opleiding en instructie van bestuurders van transportmiddelen', 'Bestuurders van transportmiddelen zijn opgeleid en geïnstrueerd voor veilig gebruik van het transportmiddel, de gevaarsaspecten van gevaarlijke stoffen en bedrijfsinterne noodprocedures bij calamiteiten.'),
    'M88': ('Niet toegankelijk voor onbevoegden', 'Een opslagvoorziening mag niet ongecontroleerd toegankelijk zijn voor onbevoegden. Hieraan is voldaan bij afdoende afscherming door muren, hekken of sloten rond de locatie.'),
    'M89': ('Failsafe of behoud van functie', 'Besturingen van veiligheidssystemen zijn failsafe of met functiebehoud uitgevoerd, waaronder automatisch sluitende deuren, automatische magazijnsystemen, brand- en gasdetectie en VBB-systemen.'),
    'M90': ('Brandwerendheid (Wbo) op basis van afstand', 'De Wbdbo kan in de buitenlucht behaald worden met afstand. Bij meer dan 5 m geldt NEN 6068-berekening met stralingscriteria (15 kW/m² op bouwwerk, 10 kW/m² op opgeslagen stoffen). Praktische benadering: 5 m = 30 min reductie, 15 m = 60 min reductie.'),
    'M91': ('Voorkomen van aanstraling op basis van afstand - gasflessen', 'Warmtestralingsbelasting voor buiten opgeslagen gasflessen is maximaal 10 kW/m², in elk geval voldaan bij 15 m afstand. Bij kleinere afstanden zijn aanvullende maatregelen nodig zoals koeling (10 l/min/m² binnen 20 min) of brandwerende scheiding.'),
    'M92': ('Opslag van brandbare gassen zwaarder dan lucht', 'Gasflessen met brandbare gassen zwaarder dan lucht (zoals propaan/butaan) worden op minimaal 5 m afstand van kelderopeningen, putten, straatkolken en aanzuigopeningen van ventilatiesystemen beneden 1,5 m opgeslagen.'),
    'M93': ('Afstand tot stoffen van ADR-klasse 7', 'De afstand van een (tank)container met gevaarlijke stoffen tot een (tank)container met ADR-klasse 7-stoffen is ten minste 50 m. Voor stukgoedcontainers geldt een afstand van ten minste 25 m.'),
    'M94': ('Ruimte rond geparkeerde voertuigen met gevaarlijke stoffen', 'Rond een geparkeerd voertuig met gevaarlijke stoffen wordt een ruimte van 2 m vrijgehouden. Dit geldt niet voor voertuigen met lading uit dezelfde gevarenklasse, behalve ADR-klasse 3 en brandbare gassen klasse 2.'),
    'M95': ('Draagbaar blustoestel', 'Per 200 m² is ten minste één draagbaar blustoestel aanwezig per opslagvoorziening, gevuld met ten minste 5 kg of 5 l blusstof, beschermd tegen weersinvloeden en geschikt voor beginnende brand van opgeslagen stoffen.'),
    'M96': ('VBB-systeem', 'In een opslagvoorziening met veiligheidsniveau A is een geschikt VBB-systeem (vastopgesteld brandbeheersings- en blussysteem) aanwezig met detectie- en blusfunctie, gereed vóór start activiteit, conform het UPD (M99).'),
    'M97': ('Bedrijfsbrandweer', 'In afwijking van M96 kan een bedrijfsbrandweer worden ingezet voor defensief blussen van buitenaf. Toepassing vereist een aanvraag via de omgevingsvergunning, een positief advies van de veiligheidsregio en voldoende personele en materiële inzetcapaciteit.'),
    'M98': ('Criteria beoordeling specifieke scenario\'s bedrijfsbrandweer', ''),
    'M99': ('Uitgangspuntendocument (UPD)', 'Voor een opslagvoorziening met VBB-systeem is een UPD opgesteld met uitgangspunten voor ontwerp, uitvoering, beheer en inspectie. Het UPD is goedgekeurd door het bevoegd gezag vóór aanleg en bevat doelstellingen, stoffenlijst, brandscenario\'s en gekozen systeem.'),
    'M100': ('Beoordeling van het UPD', 'Het UPD wordt ter goedkeuring aangeboden aan het bevoegd gezag. Het beoordelingsrapport wordt opgesteld door een type-A-inspectieinstelling, geaccrediteerd volgens NEN-EN-ISO/IEC 17020. Bij bedrijfsbrandweer is ook een positieve beoordeling van de veiligheidsregio vereist.'),
    'M101': ('Ingebruikname opslagvoorziening', 'Een opslagvoorziening met veiligheidsniveau A en VBB-systeem wordt pas in gebruik genomen na een initieel inspectierapport door een type-A-instelling conform het goedgekeurde UPD. Bij bedrijfsbrandweer moet eerst worden voldaan aan de aanwijzing.'),
    'M102': ('Jaarlijkse controle van VBB-systeem', 'Na ingebruikname wordt het VBB-systeem elke twaalf maanden geïnspecteerd door een type-A-instelling conform het CCV-inspectieschema, getoetst aan het goedgekeurde UPD. Het inspectierapport is beschikbaar in de opslagvoorziening.'),
    'M103': ('Bluswatervoorziening en bereikbaarheid', 'Voor opslagvoorzieningen met veiligheidsniveau A, B en C is een bluswatervoorziening vereist, bereikbaar en afgestemd op het materieel van de (overheids)brandweer. Voor niveau A is aanvullend koelwater bepaald op basis van uitgewerkte scenario\'s.'),
    'M104': ('Bluswatervoorziening en bereikbaarheid binnenopslag', 'Als een opslagvoorziening in een ander bouwwerk ligt, is een bluswatervoorziening aanwezig met een opbrengst van 1.500 l/min binnen 100 m en 15 min opbouwtijd. Er is een veilige inzetplaats op maximaal 25 m van de interne brandscheiding.'),
    'M105': ('Blusleidingen en brandkranen', 'Binnen de locatie zijn blusleidingen en brandkranen aanwezig. Brandkranen onderling maximaal 200 m (80 m bij opstallen), waterlevering minimaal 1.500 l/min per kraan bij gebruik van twee kranen. Brandkranen worden altijd vrijgehouden.'),
    'M106': ('Uitvoering van blusleidingen', 'Blusleidingen zijn als ringleiding aangelegd met blokafsluiters. Ondergrondse stalen leidingen zijn corrosievast uitgevoerd; bestaande leidingen zijn corrosiebeschermd en aantoonbaar functioneel via inspectie en onderhoud.'),
    'M107': ('Normering voor brandkranen', 'Ondergrondse brandkranen voldoen aan NEN-EN 14339. Bovengrondse brandkranen geïnstalleerd na 1 maart 2008 voldoen aan NEN-EN 14384; daarvoor geldt DIN 3222 of NEN-EN 14384.'),
    'M108': ('Schuimvormend middel (SVM)', 'Bij aanwezigheid van gevaarlijke stoffen in (tank)containers zorgt de beheerder voor schuimvormend middel (SVM), middelen om schuim op te brengen en veilige inzet van (bedrijfs)brandweer. Hoeveelheid en locatie worden afgestemd met de veiligheidsregio.'),
    'M109': ('Bereikbaarheid', 'De opslagvoorziening en calamiteiten-voorzieningen zijn zodanig bereikbaar dat vereiste middelen effectief kunnen worden ingezet. Toegangsdeuren en aansluitpunten voor blussystemen worden vrijgehouden.'),
    'M110': ('Bereikbaarheid terrein', 'Het terrein is altijd via twee zo ver mogelijk uit elkaar gelegen ingangen toegankelijk voor hulpdiensten. De minimumbreedte van toegangswegen is 3,5 m.'),
    'M111': ('Vrijkomen van dampen', 'Als onbedoeld dampen kunnen vrijkomen, zijn doeltreffende maatregelen genomen. In een buitenopslag is natuurlijke ventilatie altijd gewaarborgd. Bij binnenopslag is voldoende ventilatie aanwezig.'),
    'M112': ('Eisen aan vloer', 'De vloer van een opslagvoorziening is vloeistofdicht en bestand tegen de opgeslagen stoffen, of er zijn doeltreffende maatregelen genomen om te voorkomen dat gemorste of gelekte stoffen in de bodem of het riool terechtkomen.'),
    'M113': ('Intern noodplan en/of noodprocedures', 'De beheerder stelt een intern noodplan op met procedures voor detectie, waarschuwing, ontruiming en bestrijding van incidenten met gevaarlijke stoffen, passend bij de omvang en complexiteit van de locatie.'),
    'M114': ('Intern noodplan - evaluatie', 'Het intern noodplan wordt periodiek geëvalueerd en zonodig bijgewerkt, bijvoorbeeld na een incident, wijziging van opgeslagen stoffen of veranderingen in de bedrijfsvoering.'),
    'M115': ('Nooddouche en oogspoelvoorziening', 'Op locaties waar wordt gewerkt met gevaarlijke stoffen is een nooddouche en oogspoelvoorziening aanwezig, aangesloten op het waterleidingnet met voldoende capaciteit, bereikbaar binnen 10 seconden loopafstand.'),
    'M116': ('Informatievoorziening hulpdiensten', 'Informatie over de aard, hoeveelheid en locatie van de opgeslagen gevaarlijke stoffen wordt beschikbaar gesteld aan hulpdiensten, conform de eisen van de veiligheidsregio en het Besluit kwaliteit leefomgeving.'),
    'M117': ('Informatievoorziening hulpdiensten - aanvullende informatie', 'Aanvullend op M116 wordt specifieke informatie verstrekt over de aanwezige installaties, bluswatervoorzieningen, bereikbaarheid en andere relevante aspecten voor een effectieve inzet van hulpdiensten.'),
    'M118': ('Wijze van beschikbaar stellen informatie', 'De wijze waarop informatie aan hulpdiensten wordt beschikbaar gesteld is afgestemd met de veiligheidsregio, bijvoorbeeld via een digitaal portaal, papieren documentatie of eeninrichtingsplan.'),
    'M119': ('Opruimen gemorste gevaarlijke stoffen', 'Gemorste of gelekte gevaarlijke stoffen worden direct en op correcte wijze opgeruimd met geschikte middelen. Het opruimprotocol is vastgelegd en personeel is getraind in de juiste werkwijze.'),
    'M120': ('Lekkage-instructie', 'Er is een lekkage-instructie aanwezig die beschrijft welke acties moeten worden ondernomen bij een lekkage van gevaarlijke stoffen, inclusief het gebruik van persoonlijke beschermingsmiddelen en het alarmeren van hulpdiensten.'),
    'M121': ('Calamiteiteninstructie', 'Er is een calamiteiteninstructie aanwezig die beschrijft welke acties moeten worden ondernomen bij brand, explosie of andere calamiteiten, inclusief ontruimingsprocedures en alarmeringsprotocollen.'),
    'M122': ('Calamiteitenploeg', 'Er is een calamiteitenploeg aanwezig of beschikbaar, bestaande uit getraind personeel dat in staat is om eerste maatregelen te nemen bij incidenten met gevaarlijke stoffen.'),
    'M123': ('Middelen voor incidentbestrijding', 'Er zijn voldoende middelen voor incidentbestrijding aanwezig, zoals absorptiemateriaal, persoonlijke beschermingsmiddelen, blustoestellen en lekbakken, afgestemd op de soorten gevaarlijke stoffen'),
    'M124': ('Beschadigde of lekkende gasflessen', 'Beschadigde of lekkende gasflessen worden onmiddellijk veiliggesteld en afgevoerd naar een veilige locatie. Er is een procedure voor het omgaan met defecte gasflessen, inclusief communicatie met leveranciers en hulpdiensten.'),
    'M125': ('Calamiteitenplaats', 'Er is een calamiteitenplaats aanwezig op het terrein waar (tank)containers met lekkage veilig kunnen worden geplaatst, voorzien van een opvangvoorziening en op voldoende afstand van andere opslagvoorzieningen en locatiegrenzen.'),
    'M126': ('Omvang en bereikbaarheid calamiteitenplaats', 'De calamiteitenplaats is voldoende groot en bereikbaar voor hulpdiensten, voorzien van een opvangvoorziening voor de inhoud van de grootste (tank)container, en ligt op veilige afstand van gebouwen en opslagvoorzieningen.'),
    'M127': ('Verplaatsing (tank)container naar calamiteitenplaats', 'Een (tank)container met lekkage kan veilig en snel worden verplaatst naar de calamiteitenplaats met geschikt materieel en getraind personeel.'),
    'M128': ('Verrijdbare opvangbak', 'Er is een verrijdbare opvangbak beschikbaar die kan worden ingezet bij lekkage van (tank)containers of vervoerseenheden, groot genoeg om de inhoud van de grootste verpakking op te vangen.'),
    'M129': ('Opstelling geparkeerde voertuigen met gevaarlijke stoffen', 'Geparkeerde voertuigen met gevaarlijke stoffen worden op een daarvoor bestemde plaats opgesteld, op voldoende afstand van gebouwen, opslagvoorzieningen en locatiegrenzen, en zodanig dat ze bij calamiteiten kunnen worden verplaatst.'),
    'M130': ('Opslagvoorziening - vluchtroute', 'Een opslagvoorziening heeft voldoende vluchtroutes, zodanig dat bij een calamiteit alle aanwezigen de opslagvoorziening snel en veilig kunnen verlaten.'),
    'M131': ('Vluchtroutes', 'Vluchtroutes zijn duidelijk gemarkeerd, vrij toegankelijk en leiden direct naar de buitenlucht of een veilige ruimte. De breedte en aantal vluchtroutes zijn afgestemd op het aantal personen en de oppervlakte van de opslagvoorziening.'),
    'M132': ('Toegangsdeur', 'Toegangsdeuren tot de opslagvoorziening zijn van buitenaf te openen zonder sleutel of gereedschap, of zijn voorzien van paniekbeslag, zodat bij nood de opslagvoorziening direct kan worden verlaten.'),
    'M133': ('Noodverlichting en vluchtrouteaanduiding', 'Er is noodverlichting en vluchtrouteaanduiding aanwezig conform de geldende normen, zodat bij stroomuitval of rookontwikkeling de vluchtroutes zichtbaar blijven en personen veilig kunnen vluchten.'),
    'M134': ('Rook- en vuurverbod', 'Roken en open vuur zijn verboden in en nabij de opslagvoorziening. Hieraan wordt voldaan door duidelijke bordeling en handhaving van het rook- en vuurverbod.'),
    'M135': ('Veiligheidssignalering', 'Veiligheidssignalering conform NEN-EN-ISO 7010 is aanwezig in en bij de opslagvoorziening, waaronder verbods-, gebods-, waarschuwings- en brandblussignalering.'),
    'M136': ('Gevaaraspecten op etiketten', 'Alle gevaaraspecten op etiketten van opgeslagen stoffen zijn goed leesbaar en in de Nederlandse taal of met pictogrammen begrijpelijk. Etiketten worden bij ontvangst en periodiek gecontroleerd.'),
    'M137': ('Veiligheidsinformatiebladen', 'Veiligheidsinformatiebladen (VIB/SDS) van alle opgeslagen gevaarlijke stoffen zijn beschikbaar voor medewerkers en hulpdiensten, conform de REACH-verordening.'),
    'M138': ('Opschrift gasflessen', 'Gasflessen zijn voorzien van een duidelijk opschrift met de gasnaam of gasformule, conform de eisen uit het ADR, zodat de inhoud te allen tijde identificeerbaar is.'),
    'M139': ('Keurmerk gasflessen', 'Gasflessen dragen een geldig keurmerk waaruit blijkt dat de fles is gekeurd conform de van toepassing zijnde normen (zoals NEN-EN 1968 of NEN-EN 1802).'),
    'M140': ('Niet-hervulbare gasflessen', 'Niet-hervulbare gasflessen worden niet opgeslagen na de door de fabrikant aangegeven maximale gebruiksduur. Beschadigde of verroeste niet-hervulbare flessen worden direct afgevoerd.'),
    'M141': ('Ontwerplevensduur gasflessen', 'De ontwerplevensduur van gasflessen wordt in acht genomen. Gasflessen die de ontwerplevensduur hebben overschreden, worden niet meer opgeslagen of gebruikt, tenzij na keuring de verdere geschiktheid is aangetoond.'),
    'M142': ('Zichtbaarheid gevaarsetiket', 'Het gevaarsetiket op de verpakking is te allen tijde goed zichtbaar. Bij opslag in stellingen of pallets worden de etiketten zodanig geplaatst dat inspectie en identificatie mogelijk blijven.'),
    'M143': ('Opslagvoorziening organische peroxiden', 'Een opslagvoorziening voor organische peroxiden voldoet aan specifieke eisen: temperatuurbeheersing, gescheiden opslag van andere stoffen en een blussysteem dat koeling van naastgelegen opslag waarborgt.'),
    'M144': ('Toegestane stoffen voor tijdelijke opslag', 'In tijdelijke opslag mogen uitsluitend de stoffen worden opgeslagen die zijn toegestaan volgens de systematiek van deze richtlijn, conform de specifieke categorieën en hoeveelheden.'),
    'M145': ('Gevaarlijke stoffen voor gebruik binnen de locatie', 'Gevaarlijke stoffen die bestemd zijn voor gebruik binnen de locatie (bijv. onderhoud, laboratorium) mogen in beperkte hoeveelheid in tijdelijke opslag aanwezig zijn.'),
    'M146': ('Stoffen uitgezonderd van tijdelijke opslag', 'Bepaalde stoffen zijn uitgezonderd van opslag in tijdelijke opslag, zoals ontplofbare stoffen (ADR-klasse 1), radioactieve stoffen (ADR-klasse 7) en lithiumhoudende energiedragers.'),
    'M147': ('Maximale hoeveelheid stoffen in tijdelijke opslag', 'De maximale hoeveelheid gevaarlijke stoffen in tijdelijke opslag is beperkt tot de in de richtlijn vastgelegde grenzen, afhankelijk van het type tijdelijke opslag (met of zonder personeel).'),
    'M148': ('Maximale hoeveelheid brandbare vloeistoffen in tijdelijke opslag', 'De maximale hoeveelheid brandbare vloeistoffen in tijdelijke opslag is beperkt tot de specifieke grenzen die in de richtlijn zijn vastgelegd voor brandbare vloeistoffen van diverse ADR-klassen.'),
    'M149': ('Samengestelde zending', 'Een samengestelde zending (diverse gevaarlijke stoffen in één zending) mag in tijdelijke opslag worden opgeslagen mits wordt voldaan aan de scheidingseisen en maximale hoeveelheden per stof.'),
    'M150': ('Scheiding van gevaarlijke stoffen', 'Gevaarlijke stoffen in tijdelijke opslag worden gescheiden opgeslagen volgens de scheidingseisen uit Bijlage E, rekening houdend met onverenigbare combinaties.'),
    'M151': ('Tijdelijke opslag van uitsluitend gelimiteerde en/of vrijgestelde hoeveelheden', 'Tijdelijke opslag van uitsluitend gelimiteerde hoeveelheden (LQ) en/of vrijgestelde hoeveelheden (EQ) mag onder versoepelde voorwaarden, mits de totale hoeveelheid binnen de gestelde grenzen blijft.'),
    'M152': ('Tijdelijke opslag van uitsluitend LQ/EQ', 'Tijdelijke opslag van uitsluitend LQ/EQ-verpakkingen mag onder specifieke voorwaarden, waarbij de verkleinde risico\'s door de beperkte verpakkingseenheden worden meegewogen.'),
    'M153': ('Randvoorwaarden voor opslag in tijdelijke opslag (max. 10 ton zonder personeel)', 'Voor tijdelijke opslag tot maximaal 10 ton zonder permanente aanwezigheid van personeel gelden specifieke randvoorwaarden voor opvang, bereikbaarheid en signalering.'),
    'M154': ('Uitvoering tijdelijke opvangvoorziening (max. 10 ton zonder personeel)', 'De uitvoering van de tijdelijke opvangvoorziening (max. 10 ton zonder personeel) omvat een vloeistichte bodem, afschot naar opvang en bescherming tegen weersinvloeden.'),
    'M155': ('Maximaal aanwezige hoeveelheid in tijdelijke opslag (max. 10 ton zonder personeel)', 'De maximaal aanwezige hoeveelheid in tijdelijke opslag (max. 10 ton zonder personeel) is beperkt tot 10.000 kg per brandcompartiment.'),
    'M156': ('Wijze van opslag binnen de tijdelijke opslagvoorziening (max. 10 ton zonder personeel)', 'De wijze van opslag binnen de tijdelijke opslagvoorziening (max. 10 ton zonder personeel) voldoet aan de basisveiligheidseisen: deugdelijke verpakking, correcte stapeling en vrijhouden van vluchtroutes.'),
    'M157': ('Randvoorwaarden voor opslag in tijdelijke opslag (max. 10 ton met personeel)', 'Voor tijdelijke opslag tot maximaal 10 ton met permanente aanwezigheid van personeel gelden strengere randvoorwaarden, waaronder de aanwezigheid van een deskundige en uitgebreidere veiligheidsvoorzieningen.'),
    'M158': ('Uitvoering tijdelijke opslagvoorziening (max. 10 ton met personeel)', 'De uitvoering van de tijdelijke opslagvoorziening (max. 10 ton met personeel) omvat brandwerende constructie, opvangvoorziening, ventilatie en toegangsbeveiliging.'),
    'M159': ('Tijdelijke opslag - aanwezigheid deskundig personeel (max. 10 ton met personeel)', 'Tijdelijke opslag met maximaal 10 ton met personeel vereist de aanwezigheid van deskundig personeel dat op de hoogte is van de gevaren en maatregelen bij incidenten.'),
    'M160': ('Maximaal aanwezige hoeveelheid in tijdelijke opslag (max. 10 ton met personeel)', 'De maximaal aanwezige hoeveelheid in tijdelijke opslag (max. 10 ton met personeel) is beperkt tot 10.000 kg per brandcompartiment, met een correcte registratie van de aanwezige stoffen.'),
    'M161': ('Afwijking van maximale hoeveelheid (max. 10 ton met personeel)', 'In specifieke gevallen kan worden afgeweken van de maximale hoeveelheid (max. 10 ton met personeel) als aanvullende veiligheidsmaatregelen worden getroffen en dit is vastgelegd in de vergunning.'),
    'M162': ('Randvoorwaarden voor opslag in tijdelijke opslag (10-30 ton met personeel)', 'Voor tijdelijke opslag van 10 tot 30 ton met personeel gelden de zwaarste randvoorwaarden, waaronder uitgebreide brandveiligheidsvoorzieningen, bluswatervoorziening en continue aanwezigheid van deskundig personeel.'),
    'M163': ('Uitvoering tijdelijke opvangvoorziening (10-30 ton met personeel)', 'De uitvoering van de tijdelijke opvangvoorziening (10-30 ton met personeel) omvat een robuuste opvangconstructie, voldoende capaciteit voor product- en bluswateropvang en brandwerende scheiding.'),
    'M164': ('Tijdelijke opslag - aanwezigheid deskundig personeel (10-30 ton met personeel)', 'Tijdelijke opslag van 10-30 ton vereist continue aanwezigheid van voldoende en adequaat opgeleid personeel dat in staat is om eerste maatregelen bij incidenten te nemen.'),
    'M165': ('Maximaal aanwezige hoeveelheid in tijdelijke opslag (10-30 ton met personeel)', 'De maximaal aanwezige hoeveelheid in tijdelijke opslag (10-30 ton met personeel) is beperkt tot 30.000 kg per brandcompartiment met strikte registratie en scheidingseisen.'),
    'M166': ('Voorkomen snelle branduitbreiding tijdelijke opslag (10-30 ton met personeel)', 'Voor tijdelijke opslag van 10-30 ton met personeel zijn voorzieningen getroffen om snelle branduitbreiding te voorkomen, zoals vakindeling, blusmiddelen en brandwerende scheidingen.'),
    'M167': ('Opleiding en instructie bestuurders - tijdelijke opslag', 'Bestuurders van transportmiddelen op locaties met tijdelijke opslag zijn specifiek opgeleid en geïnstrueerd over de gevaren van de opgeslagen stoffen en de te volgen procedures bij calamiteiten.'),
}

# Backward compat: keep old list-based accessors
MAATREGELEN_BASIS = [(k, v[0], v[1]) for k, v in MAATREGELEN_ALLE.items() if k.startswith('MW') or k in []]  # MW1 only for basis
MAATREGELEN_C = []
MAATREGELEN_B = []
MAATREGELEN_A = []

# The old MAATREGELEN_* lists are now replaced by MAATREGELEN_ALLE dict.
# Functions that need list format can use: [(k,v[0],v[1]) for k,v in MAATREGELEN_ALLE.items()]


# ═══════════════════════════════════════════════════════════════════
# DOELSTELLING → SCENARIO → MAATREGEL MAPPING
# ═══════════════════════════════════════════════════════════════════
# Per doelstelling: relevante scenario's en maatregelen per niveau.
# Maatregelen die op meerdere doelstellingen van toepassing zijn, worden
# bij alle relevante doelstellingen getoond.

DOELSTELLING_MAP = {
    'D1': {
        'titel': 'Voorkomen dat een aanrijding plaatsvindt',
        'omschrijving': 'Dit doel is gericht op het voorkomen van aanrijdingen waardoor een verpakking of stelling beschadigd raakt of kan falen.',
        'scenario': ['S1', 'S3'],
        'maatregelen': {
            'Basis': ['M2', 'M14', 'M20', 'M46', 'M76', 'M87'],
            'Binnen': ['M7', 'M8'],
            'Container': ['M11', 'M12', 'M74'],
            'Tijdelijk': ['M87', 'M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10'],
        },
    },
    'D2': {
        'titel': 'Zeker stellen dat opslaglocaties voorzien zijn van een fysieke bescherming',
        'omschrijving': 'Beschermen van opslaglocaties tegen fysieke impact ter voorkoming van beschadiging en mogelijk falen.',
        'scenario': ['S1', 'S3'],
        'maatregelen': {
            'Basis': ['M2', 'M14', 'M20', 'M77', 'M78'],
            'Binnen': ['M7', 'M8'],
            'Container': ['M11', 'M12', 'M80'],
            'Tijdelijk': ['M77', 'M78', 'M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10', 'M79'],
        },
    },
    'D3': {
        'titel': 'Zeker stellen dat verpakte gevaarlijke stoffen op een juiste wijze in de juiste opslagvoorziening worden opgeslagen',
        'omschrijving': 'Zeker stellen dat de verpakte gevaarlijke stoffen op een juiste wijze behandeld worden en op de juiste locatie worden opgeslagen.',
        'scenario': ['S2', 'S3', 'S4', 'S5', 'S6', 'S7'],
        'maatregelen': {
            'Basis': ['M2', 'M14', 'M20', 'M36', 'M45', 'M48', 'M52', 'M53', 'M58', 'M75', 'M81', 'M82', 'M85', 'M87'],
            'Binnen': ['M7', 'M8', 'M57'],
            'Buiten': ['M50'],
            'Container': ['M11', 'M12', 'M47', 'M51', 'M61', 'M63', 'M74'],
            'Tijdelijk': ['M50', 'M52', 'M53', 'M58', 'M75', 'M81', 'M82', 'M85', 'M87', 'M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10', 'M54', 'M56', 'M60', 'M83', 'M138', 'M139', 'M141'],
            'ADR_2_spuitbussen': ['M38', 'M55', 'M57', 'M69'],
            'ADR_6_1_VG_I_II': ['M49'],
            'ADR_3': ['M49'],
            'ADR_4_1': ['M49'],
            'ADR_4_2': ['M49'],
            'ADR_4_3': ['M49'],
            'ADR_5_1': ['M49'],
            'acuut_toxisch': ['M49'],
        },
    },
    'D4': {
        'titel': 'Voorkomen dat na lekkage een gevaarlijke situatie ontstaat',
        'omschrijving': 'Na een lekkage de situatie niet laten verslechteren: opvangen, opruimen, ongewenste reacties voorkomen.',
        'scenario': ['S11'],
        'maatregelen': {
            'Basis': ['M2', 'M14', 'M20', 'M30', 'M35', 'M36', 'M111', 'M119', 'M120'],
            'Binnen': ['M7', 'M8'],
            'Container': ['M11', 'M12', 'M39', 'M123'],
            'Tijdelijk': ['M119', 'M120', 'M144', 'M145', 'M146', 'M149', 'M150', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10', 'M112', 'M124'],
            'ADR_3_VG_I_II': ['M59'],
            'ADR_3': ['M59'],
            'ADR_4_1': ['M59'],
            'ADR_4_2': ['M59'],
            'ADR_4_3': ['M59'],
            'ADR_5_1': ['M59'],
            'ADR_6_1': ['M59'],
            'ADR_8': ['M30', 'M35'],
            'acuut_toxisch': ['M59'],
        },
    },
    'D5': {
        'titel': 'Zeker stellen dat adequaat wordt gereageerd bij noodsituaties ter voorkoming van effecten op medewerkers',
        'omschrijving': 'Noodorganisatie, competentie en informatie medewerkers, bereikbaarheid en vluchtwegen.',
        'scenario': ['S11'],
        'maatregelen': {
            'Basis': ['M2', 'M14', 'M20', 'M85', 'M86', 'M88', 'M89', 'M113', 'M114', 'M115', 'M120', 'M121', 'M130', 'M131', 'M132', 'M133', 'M135', 'M136', 'M137'],
            'Binnen': ['M7', 'M8'],
            'Container': ['M11', 'M12', 'M61', 'M62', 'M64', 'M122', 'M123', 'M125', 'M126', 'M127', 'M128', 'M129', 'M142'],
            'Tijdelijk': ['M85', 'M88', 'M115', 'M120', 'M121', 'M131', 'M132', 'M133', 'M135', 'M136', 'M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10', 'M37', 'M138', 'M140'],
            'ADR_6_1': ['M85', 'M115', 'M121'],
            'ADR_8': ['M115', 'M120', 'M121'],
        },
    },
    'D6': {
        'titel': 'Zeker stellen dat hulpdiensten adequaat kunnen optreden',
        'omschrijving': 'Informatie, bereikbaarheid en aanwezigheid voldoende bluswater voor hulpdiensten.',
        'scenario': ['S11', 'S13', 'S14', 'S15'],
        'maatregelen': {
            'Basis': ['M2', 'M14', 'M20', 'M89', 'M109', 'M113', 'M114', 'M116', 'M117', 'M118', 'M121', 'M135', 'M136', 'M137'],
            'A': ['M40', 'M41'],
            'B': ['M40', 'M42'],
            'Binnen': ['M7', 'M8'],
            'Container': ['M11', 'M12', 'M62', 'M64', 'M84', 'M110', 'M125', 'M126', 'M129', 'M142'],
            'Tijdelijk': ['M109', 'M116', 'M121', 'M135', 'M136', 'M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10', 'M37', 'M138', 'M140'],
            'ADR_6_1': ['M89', 'M113', 'M114', 'M116', 'M117', 'M135', 'M136', 'M137'],
            'ADR_3': ['M40', 'M41', 'M42'],
            'ADR_8': ['M113', 'M116', 'M135', 'M136'],
        },
    },
    'D7': {
        'titel': 'Voorkomen verspreiding',
        'omschrijving': 'Voorkomen dat vrijgekomen gevaarlijke stof zich verspreidt buiten de ruimte/locatie waar de verpakking lek raakt.',
        'scenario': ['S12'],
        'maatregelen': {
            'Basis': ['M2', 'M14', 'M20', 'M23', 'M29', 'M30', 'M89'],
            'Binnen': ['M7', 'M8'],
            'Container': ['M11', 'M12', 'M31', 'M108'],
            'Tijdelijk': ['M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10', 'M92'],
            'ADR_8': ['M23', 'M29', 'M30', 'M89', 'M111'],
            'ADR_6_1': ['M23', 'M29', 'M89'],
        },
    },
    'D8': {
        'titel': 'Voorkomen van ontsteking',
        'omschrijving': 'Beheersing van ontstekingsbronnen en brandgevaarlijke activiteiten, aanwezigheid brandbare materialen, blussen beginnende brand.',
        'scenario': ['S13'],
        'maatregelen': {
            'Basis': ['M2', 'M14', 'M20', 'M68', 'M95', 'M111', 'M134'],
            'Binnen': ['M7', 'M8', 'M70'],
            'Container': ['M11', 'M12', 'M108'],
            'Tijdelijk': ['M68', 'M70', 'M95', 'M134', 'M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M166', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10', 'M70', 'M92', 'M112'],
            'ADR_2_spuitbussen': ['M70'],
            'ADR_3': ['M70', 'M95', 'M111', 'M134'],
            'ADR_4_1': ['M95', 'M134'],
            'ADR_4_2': ['M95', 'M134'],
            'ADR_4_3': ['M95', 'M134'],
            'ADR_5_1': ['M95', 'M134'],
            'ADR_5_2': ['M68', 'M95'],
        },
    },
    'D9': {
        'titel': 'Voorkomen van brand in de opslagvoorziening',
        'omschrijving': 'Voorkomen van een brand van enige omvang die kan leiden tot escalatie naar opgesloten verpakte gevaarlijke stoffen.',
        'scenario': ['S8'],
        'maatregelen': {
            'Basis': ['M2', 'M14', 'M20', 'M68', 'M71', 'M72', 'M95', 'M134'],
            'A': ['M73'],
            'Binnen': ['M7', 'M8'],
            'Container': ['M11', 'M12', 'M28'],
            'Tijdelijk': ['M68', 'M71', 'M72', 'M73', 'M95', 'M134', 'M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M166', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10'],
            'ADR_3': ['M68', 'M71', 'M72', 'M73', 'M95', 'M134'],
            'ADR_4_1': ['M71', 'M72', 'M73', 'M95', 'M134'],
            'ADR_4_2': ['M71', 'M72', 'M95', 'M134'],
            'ADR_4_3': ['M71', 'M72', 'M95', 'M134'],
            'ADR_5_1': ['M68', 'M71', 'M72', 'M73', 'M95', 'M134'],
            'ADR_5_2': ['M68', 'M71', 'M72'],
            'ADR_8': ['M71', 'M72'],
        },
    },
    'D10': {
        'titel': 'Voorkomen van escalatie van een brand (passief) - compartimenteren',
        'omschrijving': 'Beheersen van de situatie bij brand: scheiding/brandwerendheid, beperken brandlast.',
        'scenario': ['S8', 'S9', 'S10', 'S14'],
        'maatregelen': {
            'Basis': ['M2', 'M3', 'M14', 'M15', 'M17', 'M20', 'M21', 'M23', 'M24', 'M25', 'M26', 'M27', 'M65', 'M89'],
            'A': ['M6', 'M40', 'M41', 'M67'],
            'B': ['M40', 'M42', 'M66'],
            'C': ['M66'],
            'Binnen': ['M7', 'M8'],
            'Buiten': ['M90'],
            'BVK': ['M33', 'M34'],
            'Container': ['M11', 'M12', 'M64', 'M93', 'M94'],
            'Tijdelijk': ['M144', 'M145', 'M146', 'M147', 'M148', 'M149', 'M151', 'M152', 'M153', 'M154', 'M155', 'M156', 'M157', 'M158', 'M159', 'M160', 'M161', 'M162', 'M163', 'M164', 'M165', 'M166', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10', 'M18', 'M22', 'M91'],
            'ADR_2_spuitbussen': ['M16'],
            'ADR_4_3_VG_II_III': ['M4', 'M5', 'M6'],
            'ADR_3': ['M3', 'M6', 'M15', 'M17', 'M21', 'M23', 'M24', 'M25', 'M26', 'M27', 'M65'],
            'ADR_4_1': ['M15', 'M17', 'M21', 'M23', 'M24', 'M25', 'M26', 'M27', 'M65'],
            'ADR_4_2': ['M15', 'M17', 'M21', 'M23', 'M24', 'M25', 'M26', 'M27', 'M65'],
            'ADR_4_3': ['M4', 'M5', 'M6', 'M15', 'M17', 'M21', 'M23', 'M24', 'M25', 'M26', 'M27', 'M65'],
            'ADR_5_1': ['M3', 'M6', 'M15', 'M17', 'M21', 'M23', 'M24', 'M25', 'M26', 'M27', 'M65'],
            'ADR_5_2': ['M15', 'M17', 'M21', 'M23', 'M24', 'M25', 'M26', 'M27'],
            'ADR_8': ['M23', 'M24', 'M25', 'M26', 'M27'],
        },
    },
    'D11': {
        'titel': 'Voorkomen van escalatie (actief) - beheersen',
        'omschrijving': 'Blussen van een brand en voorkomen dat een brand uitbreidt: detectie, blusvoorzieningen, bereikbaarheid.',
        'scenario': ['S8', 'S9', 'S14'],
        'maatregelen': {
            'Basis': ['M2', 'M3', 'M14', 'M20', 'M89'],
            'A': ['M6', 'M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102', 'M103'],
            'B': ['M103'],
            'C': ['M103'],
            'Binnen': ['M7', 'M8', 'M104'],
            'Container': ['M11', 'M12', 'M84', 'M105', 'M106', 'M107'],
            'Tijdelijk': ['M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M166', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10'],
            'ADR_4_3_VG_II_III': ['M4', 'M5', 'M6'],
            'ADR_3': ['M3', 'M6', 'M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102', 'M103'],
            'ADR_4_1': ['M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102', 'M103'],
            'ADR_4_2': ['M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102', 'M103'],
            'ADR_4_3': ['M4', 'M5', 'M6', 'M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102', 'M103'],
            'ADR_5_1': ['M3', 'M6', 'M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102', 'M103'],
            'ADR_5_2': ['M96', 'M97', 'M98'],
        },
    },
    'D12': {
        'titel': 'Zeker stellen dat een brand (semi)automatisch geblust kan worden',
        'omschrijving': 'Snel detecteren en ((semi)automatisch of bedrijfsbrandweer) blussen van een brand in een beginstadium.',
        'scenario': ['S15'],
        'maatregelen': {
            'Basis': ['M2', 'M3', 'M14', 'M20', 'M89'],
            'A': ['M32', 'M67', 'M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102'],
            'Binnen': ['M7', 'M8'],
            'Container': ['M11', 'M12', 'M84'],
            'Tijdelijk': ['M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10'],
            'ADR_3': ['M32', 'M67', 'M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102'],
            'ADR_4_1': ['M32', 'M67', 'M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102'],
            'ADR_4_2': ['M32', 'M67', 'M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102'],
            'ADR_4_3': ['M32', 'M67', 'M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102'],
            'ADR_5_1': ['M32', 'M67', 'M96', 'M97', 'M98', 'M99', 'M100', 'M101', 'M102'],
            'ADR_5_2': ['M67'],
        },
    },
    'D13': {
        'titel': 'Zeker stellen dat aanvullende stofspecifieke risico\'s worden beheerst',
        'omschrijving': 'Specifieke risico\'s van stoffen zoals infectueuze stoffen, organische peroxiden, brandbare vaste stoffen.',
        'scenario': ['S16'],
        'maatregelen': {
            'Basis': ['M2', 'M14', 'M20'],
            'Binnen': ['M7', 'M8'],
            'Container': ['M11', 'M12'],
            'Tijdelijk': ['M144', 'M145', 'M146', 'M149', 'M151', 'M152', 'M153', 'M157', 'M159', 'M162', 'M164', 'M167'],
            'ADR_2_gasflessen': ['M7', 'M8', 'M9', 'M10'],
            'ADR_2_spuitbussen': ['M38'],
            'ADR_4_1_VG_II_III': ['M43'],
            'ADR_4_2_4_3_VG_III': ['M44'],
            'ADR_5_2': ['M19', 'M143'],
            'ADR_6_2': ['M13'],
            'ADR_6_1': ['M13', 'M49'],
            'ADR_8': ['M30', 'M35'],
            'ADR_9': ['M30'],
        },
    },
}

# Niveau-order: alle 13 doelstellingen gelden op alle niveaus
DOELSTELLING_PER_NIVEAU = {
    'Basis': ['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8', 'D9', 'D10', 'D11', 'D12', 'D13'],
    'C': ['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8', 'D9', 'D10', 'D11', 'D12', 'D13'],
    'B': ['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8', 'D9', 'D10', 'D11', 'D12', 'D13'],
    'A': ['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8', 'D9', 'D10', 'D11', 'D12', 'D13'],
}


def build_compliance(strengste_niveau, opslag_type='binnen', 
                      adr_klasse='', vg='', is_cmr=False, cmr_type='',
                      is_acute_tox=False, acute_tox_cat='',
                      is_brandbaar_k8=False, clp_classificatie=''):
    """Bouw gestructureerde compliance-toetsing per doelstelling.

    Filtert doelstellingen EN maatregelen op basis van:
    - ADR-klasse en VG van de stof
    - CLP-classificatie (CMR, acute tox, etc.)
    - Opslagtype (binnen/buiten/container/tijdelijk)
    - Huidig veiligheidsniveau
    
    Alleen relevante doelstellingen en maatregelen worden getoond.
    """
    niveaus_actief = ['Basis']
    if strengste_niveau in ('C', 'B', 'A'):
        niveaus_actief.append('C')
    if strengste_niveau in ('B', 'A'):
        niveaus_actief.append('B')
    if strengste_niveau == 'A':
        niveaus_actief.append('A')

    # Bepaal welke sub-categorieen van toepassing zijn
    opslag_map = {'binnen': 'Binnen', 'buiten': 'Buiten', 'container': 'Container', 'tijdelijk': 'Tijdelijk'}
    opslag_key = opslag_map.get(opslag_type, 'Binnen')

    # ADR-specifieke categorieen
    adr_cats = set()
    if adr_klasse:
        adr_cat_map = {
            '2': 'ADR_2_gasflessen', '3': 'ADR_3', '4.1': 'ADR_4_1',
            '4.2': 'ADR_4_2', '4.3': 'ADR_4_3', '5.1': 'ADR_5_1',
            '5.2': 'ADR_5_2', '6.1': 'ADR_6_1', '6.2': 'ADR_6_2',
            '8': 'ADR_8', '9': 'ADR_9',
        }
        cat = adr_cat_map.get(adr_klasse)
        if cat:
            adr_cats.add(cat)
        if adr_klasse == '6.1' and vg in ('I', 'II'):
            adr_cats.add('ADR_6_1_VG_I_II')

    # CLP-specifieke categorieen
    clp_cats = set()
    if is_acute_tox and acute_tox_cat:
        clp_cats.add('acuut_toxisch')
    if is_cmr:
        clp_cats.add('cmr')
    if is_brandbaar_k8:
        clp_cats.add('brandbaar_k8')

    # Regel 1: Stoffen buiten scope → alleen MW1 + M2 (basiszorgplicht)
    if strengste_niveau == 'Buiten scope':
        return [{
            'code': 'MW1',
            'titel': 'Basiszorgplicht',
            'omschrijving': 'Deze stof valt onder de basiszorgplicht. Zorg voor veilige opslag volgens de algemene veiligheidsvoorschriften en good housekeeping. PGS 15 veiligheidsniveaus (C/B/A) zijn niet van toepassing omdat de hoeveelheid onder de drempelwaarde blijft.',
            'scenario': [],
            'maatregelen': ['MW1', 'M2'],
        }]

    # Regel 2: Alleen relevante categorieën
    # 'Basis' altijd + niveau-specifieke (C/B/A) + ADR-specifieke + CLP-specifieke
    # 'Binnen'/'Buiten'/'Container'/'Tijdelijk' NIET standaard — deze bevatten
    # maatregelen die alleen gelden voor specifieke opslagcontexten (bv. gasflessen)
    active_cats = {'Basis'} | adr_cats | clp_cats

    # Verzamel relevante maatregelen per doelstelling
    compliance = []
    seen = set()
    doel_codes = []
    for nivo in niveaus_actief:
        for dc in DOELSTELLING_PER_NIVEAU.get(nivo, []):
            if dc not in seen:
                seen.add(dc)
                doel_codes.append(dc)

    for dc in doel_codes:
        dm = DOELSTELLING_MAP.get(dc)
        if not dm:
            continue

        maatregelen = []
        seen_m = set()

        for nivo in niveaus_actief:
            if nivo == 'Basis':
                cats_to_check = active_cats
            else:
                cats_to_check = active_cats | {nivo}
            
            for cat in cats_to_check:
                for m in dm.get('maatregelen', {}).get(cat, []):
                    if m not in seen_m:
                        seen_m.add(m)
                        maatregelen.append(m)

        if not maatregelen:
            continue

        compliance.append({
            'code': dc,
            'titel': dm['titel'],
            'omschrijving': dm['omschrijving'],
            'scenario': dm.get('scenario', []),
            'maatregelen': maatregelen,
        })

    return compliance


def build_wat_als_analyse(strengste_niveau, opslag_type='binnen',
                             adr_klasse='', vg='', is_cmr=False, cmr_type='',
                             is_acute_tox=False, acute_tox_cat='',
                             is_brandbaar_k8=False, clp_classificatie=''):
    """Voor elk opslagtype: bereken aantal maatregelen en verschil versus gekozen type."""
    types = [
        ('binnen', 'Binnenopslag'),
        ('buiten', 'Buitenopslag'),
        ('container', '(Tank)containers'),
        ('tijdelijk', 'Tijdelijke opslag'),
    ]
    resultaten = {}
    all_compliance = {}

    for type_key, type_label in types:
        compliance = build_compliance(strengste_niveau, opslag_type=type_key,
                    adr_klasse=adr_klasse, vg=vg,
                    is_cmr=is_cmr, cmr_type=cmr_type,
                    is_acute_tox=is_acute_tox, acute_tox_cat=acute_tox_cat,
                    is_brandbaar_k8=is_brandbaar_k8, clp_classificatie=clp_classificatie)
        all_m = set()
        doel_codes = set()
        for dobj in compliance:
            for m in dobj['maatregelen']:
                all_m.add(m)
            doel_codes.add(dobj['code'])
        all_compliance[type_key] = {
            'compliance': compliance,
            'aantal_maatregelen': len(all_m),
            'doelstellingen': sorted(doel_codes),
            'maatregelen': sorted(all_m),
        }

    # Referentie = binnen
    ref = all_compliance.get('binnen', {})
    ref_m = set(ref.get('maatregelen', []))
    ref_d = set(ref.get('doelstellingen', []))

    for type_key, type_label in types:
        d = all_compliance[type_key]
        d_m = set(d['maatregelen'])
        d_d = set(d['doelstellingen'])
        verschil = len(d_m) - len(ref_m)
        extra_m = sorted(d_m - ref_m)
        minder_m = sorted(ref_m - d_m)
        extra_d = sorted(d_d - ref_d)
        minder_d = sorted(ref_d - d_d)

        if verschil > 0:
            impact = 'strenger'
        elif verschil < 0:
            impact = 'milder'
        else:
            impact = 'zelfde'

        resultaten[type_key] = {
            'label': type_label,
            'aantal_maatregelen': d['aantal_maatregelen'],
            'verschil': verschil,
            'impact': impact,
            'extra_maatregelen': extra_m,
            'minder_maatregelen': minder_m,
            'extra_doelstellingen': extra_d,
            'minder_doelstellingen': minder_d,
        }

    return resultaten, all_compliance



def get_measures_per_niveau(opslag_type='binnen', adr_klasse='', extra_routes=None):
    """Bereken welke maatregelen verplicht zijn per PGS 15 niveau.
    
    Returns dict: {'Basis': [codes], 'C': [codes], 'B': [codes], 'A': [codes]}
    where each list contains the ADDITIONAL measures for that level on top of previous.
    """
    if extra_routes is None:
        extra_routes = []
    
    # Map frontend opslag_type to DOELSTELLING_MAP category keys
    type_map = {'binnen': 'Binnen', 'buiten': 'Buiten', 'container': 'Container', 'tijdelijk': 'Tijdelijk'}
    type_key = type_map.get(opslag_type, 'Binnen')
    
    # ADR-specific category keys
    adr_cat_map = {
        '2': 'ADR_2_gasflessen',
        '3': 'ADR_3',
        '4.1': 'ADR_4_1',
        '4.2': 'ADR_4_2',
        '4.3': 'ADR_4_3',
        '5.2': 'ADR_5_2',
        '6.1': 'ADR_6_1',
        '6.2': 'ADR_6_2',
    }
    
    # Collect measures per niveau from DOELSTELLING_MAP
    # Basis: all measures from DOELSTELLING_MAP['Dx']['maatregelen']['Basis']
    # + opslagtype-specific measures
    # + ADR-specific measures
    # C: extra measures tagged 'C' in doelstellingen
    # B: extra measures tagged 'B'
    # A: extra measures tagged 'A'
    
    basis_measures = set()
    c_extra = set()
    b_extra = set()
    a_extra = set()
    
    # Categories to check for Basis measures
    basis_cats = ['Basis', type_key]
    if adr_klasse:
        adr_cat = adr_cat_map.get(adr_klasse)
        if adr_cat:
            basis_cats.append(adr_cat)
    
    # Also add ADR-specific cats for extra routes (CMR, acute tox)
    for route in extra_routes:
        route_adr = route.get('adr_klasse', '')
        if route_adr:
            route_cat = adr_cat_map.get(route_adr)
            if route_cat and route_cat not in basis_cats:
                basis_cats.append(route_cat)
    
    for dc in DOELSTELLING_PER_NIVEAU.get('Basis', []):
        dm = DOELSTELLING_MAP.get(dc)
        if not dm:
            continue
        ma = dm.get('maatregelen', {})
        for cat in basis_cats:
            for m in ma.get(cat, []):
                basis_measures.add(m)
    
    # Niveau C extra
    for dc in DOELSTELLING_PER_NIVEAU.get('C', []):
        dm = DOELSTELLING_MAP.get(dc)
        if not dm:
            continue
        for m in dm.get('maatregelen', {}).get('C', []):
            c_extra.add(m)
    
    # Niveau B extra
    for dc in DOELSTELLING_PER_NIVEAU.get('B', []):
        dm = DOELSTELLING_MAP.get(dc)
        if not dm:
            continue
        for m in dm.get('maatregelen', {}).get('B', []):
            b_extra.add(m)
    
    # Niveau A extra
    for dc in DOELSTELLING_PER_NIVEAU.get('A', []):
        dm = DOELSTELLING_MAP.get(dc)
        if not dm:
            continue
        for m in dm.get('maatregelen', {}).get('A', []):
            a_extra.add(m)
    
    return {
        'Basis': sorted(basis_measures),
        'C': sorted(c_extra - basis_measures),
        'B': sorted(b_extra - basis_measures - c_extra),
        'A': sorted(a_extra - basis_measures - c_extra - b_extra),
    }

def build_flat_measures(compliance_list):
    """Bouw een flat lijst van unieke maatregelen.
    
    Input: compliance_list (output van build_compliance)
    Output: list of dicts met:
      - code: M-code
      - titel: naam
      - toelichting: korte beschrijving (eerste 100 tekens)
      - doelstellingen: lijst van D-codes die deze maatregel koppelen
      - categorie: 'Basis', 'C', 'B', of 'A'
    """
    seen = {}
    for dobj in compliance_list:
        dc = dobj['code']
        for m_code in dobj['maatregelen']:
            if m_code not in seen:
                info = MAATREGELEN_ALLE.get(m_code, (m_code, ''))
                seen[m_code] = {
                    'code': m_code,
                    'titel': info[0],
                    'toelichting': info[1][:100] + '...' if len(info[1]) > 100 else info[1],
                    'doelstellingen': [],
                    'categorie': 'Basis',
                }
            seen[m_code]['doelstellingen'].append(dc)
    
    result = list(seen.values())
    
    def sort_key(m):
        cat_order = {'Basis': 0, 'C': 1, 'B': 2, 'A': 3}
        return (cat_order.get(m['categorie'], 0), m['code'])
    
    result.sort(key=sort_key)
    return result


def build_drempelwaarden_tabel(stof):
    """Bouw drempelwaarden-tabel per route (ADR + CLP) voor een stof.

    Retourneert dict met routes, elk met niveaus, huidige positie en extra kg nodig.
    """
    hoev = stof.get('hoeveelheid', 0)
    adr = stof.get('adr_klasse', '')
    vg = stof.get('vg', '')
    routes = []

    # ADR route
    if adr and vg:
        nv = NIVEAUS.get((adr, vg))
        if nv:
            c_start, b_thresh, a_thresh = nv[1], nv[2], nv[3]
            # Bepaal huidig niveau via determine_niveau
            huidig_niveau = determine_niveau(adr, vg, hoev, stof.get('is_brandbaar_k8', False), stof.get('flashpoint'))
            niveaus = [
                {'niveau': 'Basis', 'drempel': 0, 'label': f'< {c_start} kg'},
            ]
            if c_start is not None:
                bereikt_c = hoev >= c_start
                niveaus.append({
                    'niveau': 'C', 'drempel': c_start,
                    'label': f'≥ {c_start} kg',
                    'bereikt': bereikt_c,
                    'extra_needed': 0 if bereikt_c else round(c_start - hoev, 1),
                })
            else:
                niveaus.append({'niveau': 'C', 'drempel': None, 'label': 'N.v.t.'})
            if b_thresh is not None:
                bereikt_b = hoev >= b_thresh
                niveaus.append({
                    'niveau': 'B', 'drempel': b_thresh,
                    'label': f'≥ {b_thresh} kg',
                    'bereikt': bereikt_b,
                    'extra_needed': 0 if bereikt_b else round(b_thresh - hoev, 1),
                })
            else:
                niveaus.append({'niveau': 'B', 'drempel': None, 'label': 'N.v.t.'})
            if a_thresh is not None:
                bereikt_a = hoev >= a_thresh
                niveaus.append({
                    'niveau': 'A', 'drempel': a_thresh,
                    'label': f'≥ {a_thresh} kg',
                    'bereikt': bereikt_a,
                    'extra_needed': 0 if bereikt_a else round(a_thresh - hoev, 1),
                })
            else:
                niveaus.append({'niveau': 'A', 'drempel': None, 'label': 'N.v.t.'})
            routes.append({
                'route_naam': f'ADR klasse {adr} VG {vg}',
                'huidige': hoev,
                'huidige_niveau': huidig_niveau,
                'niveaus': niveaus,
            })

    # CLP routes
    clp_text = stof.get('clp_classificatie', '')
    clp_routes_parsed = parse_clp_routes(
        clp_text,
        stof.get('is_cmr', 0), stof.get('cmr_type', ''),
        stof.get('is_acute_tox', 0), stof.get('acute_tox_cat', ''),
        stof.get('vorm', 'vast')
    )
    for label, r in clp_routes_parsed:
        if r['ondergrens'] == float('inf'):
            continue
        # Bepaal CLP huidig niveau
        n = 'Buiten scope'
        if hoev >= (r['C'] or float('inf')):
            n = 'C'
        if r['B'] is not None and hoev >= r['B']:
            n = 'B'
        if r['A'] is not None and hoev >= r['A']:
            n = 'A'

        niveaus = [{'niveau': 'Basis', 'drempel': 0, 'label': f'< {r["ondergrens"]} kg'}]
        for nivo_key in ['C', 'B', 'A']:
            thresh = r.get(nivo_key)
            if thresh is not None:
                bereikt = hoev >= thresh
                niveaus.append({
                    'niveau': nivo_key, 'drempel': thresh,
                    'label': f'≥ {thresh} kg',
                    'bereikt': bereikt,
                    'extra_needed': 0 if bereikt else round(thresh - hoev, 1),
                })
            else:
                niveaus.append({'niveau': nivo_key, 'drempel': None, 'label': 'N.v.t.'})
        routes.append({
            'route_naam': label,
            'huidige': hoev,
            'huidige_niveau': n if n != 'Buiten scope' else 'Basis',
            'niveaus': niveaus,
        })

    return {'routes': routes}



# ═══════════════════════════════════════════════════════════════════
# DATABASE
# ═══════════════════════════════════════════════════════════════════

def get_db():
    db = sqlite3.connect(DB_PATH)
    db.row_factory = sqlite3.Row
    return db

def init_db():
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    db = get_db()
    db.execute('''CREATE TABLE IF NOT EXISTS stoffen (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        session_id TEXT NOT NULL,
        cas TEXT, naam TEXT, hoeveelheid REAL, eenheid TEXT DEFAULT 'kg',
        vorm TEXT DEFAULT 'vloeistof', locatie TEXT DEFAULT '',
        adr_klasse TEXT DEFAULT '', vg TEXT DEFAULT '',
        clp_classificatie TEXT DEFAULT '', h_zinnen TEXT DEFAULT '',
        is_cmr INTEGER DEFAULT 0, cmr_type TEXT DEFAULT '',
        is_acute_tox INTEGER DEFAULT 0, acute_tox_cat TEXT DEFAULT '',
        is_brandbaar_k8 INTEGER DEFAULT 0,
        timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
    )''')
    db.execute('''CREATE TABLE IF NOT EXISTS scans (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        session_id TEXT NOT NULL,
        scan_data TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
    )''')
    db.commit()
    db.close()


# ═══════════════════════════════════════════════════════════════════
# MULTI-SOURCE LOOKUP ENGINE
# ═══════════════════════════════════════════════════════════════════

def lookup_pubchem(query):
    """Lookup substance via PubChem API. Returns CID, name, CAS, formula, MW, IUPAC."""
    try:
        is_cas = bool(re.match(r'^\d{2,7}-\d{2}-\d$', query))
        url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{query}/cids/JSON"
        r = requests.get(url, timeout=10)
        if r.status_code != 200:
            return None
        cid = r.json()['IdentifierList']['CID'][0]

        props = requests.get(
            f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{cid}/property/MolecularFormula,MolecularWeight,IUPACName,Title/JSON",
            timeout=10).json()

        cas = ''
        try:
            # Try synonyms endpoint (more reliable for CAS)
            syn_r = requests.get(
                f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{cid}/synonyms/TXT",
                timeout=10)
            if syn_r.status_code == 200:
                for line in syn_r.text.split('\n'):
                    if re.match(r'^\d{2,7}-\d{2}-\d$', line.strip()):
                        cas = line.strip()
                        break
            # Fallback: RegistryID
            if not cas:
                cas_r = requests.get(
                    f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{cid}/xref/RegistryID/JSON",
                    timeout=10)
                if cas_r.status_code == 200:
                    cas_r = cas_r.json()
                    for info in cas_r.get('InformationList', {}).get('Information', [{}]):
                        for rid in info.get('RegistryID', []):
                            if re.match(r'^\d{2,7}-\d{2}-\d$', rid):
                                cas = rid
                                break
                        if cas:
                            break
        except:
            pass

        if is_cas and not cas:
            cas = query

        p = props['PropertyTable']['Properties'][0]
        return {
            'cid': cid,
            'name': p.get('Title', p.get('IUPACName', '')),
            'formula': p.get('MolecularFormula', ''),
            'mw': str(p.get('MolecularWeight', 0)),
            'iupac': p.get('IUPACName', ''),
            'cas': cas,
        }
    except Exception as e:
        print(f"PubChem lookup error: {e}")
        return None


def lookup_echa(cas):
    """Lookup CLP classification from ECHA infocard by CAS number."""
    try:
        if not cas:
            return None
        # ECHA infocard API
        url = f"https://echa.europa.eu/information-on-chemicals/cl-inventory-database"
        # Use the public search endpoint
        search_url = f"https://echa.europa.eu/zh-hans/information-on-chemicals/cl-inventory-database/-/discli/details/{cas}"
        r = requests.get(search_url, timeout=10, headers={
            'User-Agent': 'Mozilla/5.0 (compatible; PGS15-Scanner/2.1)',
            'Accept': 'text/html,application/xhtml+xml',
        })
        if r.status_code != 200:
            return None

        html = r.text
        result = {'clp': '', 'h_zinnen': '', 'adr_klasse': '', 'vg': '', 'is_cmr': False, 'cmr_type': '', 'is_acute_tox': False, 'acute_tox_cat': ''}

        # Extract CLP classification
        clp_match = re.search(r'Classification\s*(?:&|and)?\s*Labelling[^<]*<[^>]*>([^<]+)', html, re.IGNORECASE)
        if not clp_match:
            clp_match = re.search(r'CLP\s*Classificatie[^<]*</[^>]+>\s*<[^>]*>([^<]+)', html, re.IGNORECASE)
        if clp_match:
            result['clp'] = clp_match.group(1).strip()

        # Extract H-zinnen
        h_matches = re.findall(r'H\d{3}[a-z]?(?:\s*:\s*[^<,<]+)?', html)
        if h_matches:
            result['h_zinnen'] = ', '.join(sorted(set(h_matches)))

        # Detect CMR
        cmr_patterns = [
            (r'Carc\.\s*1[AB]', 'Carc. 1A'),
            (r'Carc\.\s*2', 'Carc. 2'),
            (r'Mut\.\s*1[AB]', 'Mut. 1A'),
            (r'Repr\.\s*1[AB]', 'Repr. 1A'),
            (r'Repr\.\s*2', 'Repr. 2'),
        ]
        clp_text = html + ' ' + result['clp']
        for pattern, cmr_type in cmr_patterns:
            if re.search(pattern, clp_text, re.IGNORECASE):
                result['is_cmr'] = True
                result['cmr_type'] = cmr_type
                break

        # Detect Acute Tox
        tox_match = re.search(r'Acute\s*Tox\.\s*(\d)', html, re.IGNORECASE)
        if tox_match:
            result['is_acute_tox'] = True
            result['acute_tox_cat'] = tox_match.group(1)

        return result if any(result.values()) else None
    except Exception as e:
        print(f"ECHA lookup error: {e}")
        return None


def lookup_pubchem_transport(cid):
    """Get ADR class, packaging group, UN number from PubChem Transport section."""
    try:
        r = requests.get(
            f"https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/compound/{cid}/JSON",
            timeout=15
        )
        if r.status_code != 200:
            return None

        data = r.json()
        result = {'adr_klasse': '', 'vg': '', 'un_nummer': ''}

        def find_section(sections, target_heading):
            for s in sections:
                if s.get('TOCHeading', '') == target_heading:
                    return s
                found = find_section(s.get('Section', []), target_heading)
                if found:
                    return found
            return None

        all_sections = data.get('Record', {}).get('Section', [])
        transport = find_section(all_sections, 'Transport Information') or find_section(all_sections, 'Transport')
        if not transport:
            return None

        # Helper: extract strings from Value
        def get_strings(info_item):
            val = info_item.get('Value', info_item.get('String', ''))
            if isinstance(val, dict):
                swm = val.get('StringWithMarkup', [])
                if isinstance(swm, list):
                    return [v.get('String', '') for v in swm if isinstance(v, dict)]
            elif isinstance(val, list):
                return [v.get('String', str(v)) for v in val if isinstance(v, dict)]
            elif isinstance(val, str) and val:
                return [val]
            return []

        # Search all sub-sections within Transport
        def search_subsections(section):
            heading = section.get('TOCHeading', '')
            for i in section.get('Information', []):
                strings = get_strings(i)
                for s in strings:
                    # UN Classification
                    if 'UN Class' in heading or 'UN Class' in (i.get('Name', '')):
                        m = re.match(r'UN\s*Hazard\s*Class[:\s]*(\d+(?:\.\d)?)', s, re.IGNORECASE)
                        if m:
                            result['adr_klasse'] = m.group(1)
                        m = re.search(r'UN\s*Pack\s*Group[:\s]*(I{1,3})', s, re.IGNORECASE)
                        if m:
                            result['vg'] = m.group(1)

                    # Shipping Name — extract UN number
                    if 'Shipping' in heading or 'Shipping' in (i.get('Name', '')):
                        m = re.match(r'UN\s*(\d{4})', s)
                        if m and not result['un_nummer']:
                            result['un_nummer'] = f"UN {m.group(1)}"

                    # DOT Label — fallback for ADR class
                    if 'DOT Label' in heading or 'DOT Label' in (i.get('Name', '')):
                        if 'Corrosive' in s and not result['adr_klasse']:
                            result['adr_klasse'] = '8'
                        elif 'Flammable' in s and not result['adr_klasse']:
                            result['adr_klasse'] = '3'
                        elif 'Toxic' in s and not result['adr_klasse']:
                            result['adr_klasse'] = '6.1'

            for ss in section.get('Section', []):
                search_subsections(ss)

        search_subsections(transport)

        return result if result['adr_klasse'] else None
    except Exception as e:
        print(f"PubChem transport lookup error: {e}")
        return None


def lookup_pubchem_ghs(cid):
    """Get GHS/CLP classification from PubChem by CID."""
    try:
        r = requests.get(
            f"https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/compound/{cid}/JSON",
            timeout=15
        )
        if r.status_code != 200:
            return None

        data = r.json()
        result = {'clp': '', 'h_zinnen': '', 'signal': '', 'pictograms': [],
                  'is_cmr': False, 'cmr_type': '', 'is_acute_tox': False, 'acute_tox_cat': ''}

        # Find GHS Classification section
        def find_section(sections, target_heading):
            for s in sections:
                if s.get('TOCHeading', '') == target_heading:
                    return s
                found = find_section(s.get('Section', []), target_heading)
                if found:
                    return found
            return None

        all_sections = data.get('Record', {}).get('Section', [])
        ghs = find_section(all_sections, 'GHS Classification')
        if not ghs:
            # Try UN GHS Classification
            ghs = find_section(all_sections, 'UN GHS Classification')
        if not ghs:
            return None

        def extract_info(section):
            for i in section.get('Information', []):
                name = i.get('Name', '')
                val = i.get('Value', i.get('String', ''))

                # Normalize: Value can be dict{StringWithMarkup: [...]} or list or string
                items = []
                if isinstance(val, dict):
                    swm = val.get('StringWithMarkup', [])
                    if isinstance(swm, list):
                        items = swm
                elif isinstance(val, list):
                    items = val
                elif isinstance(val, str):
                    items = [{'String': val}]

                for v in items:
                    if not isinstance(v, dict):
                        continue
                    s = v.get('String', '')
                    markup = v.get('Markup', [])

                    if 'Hazard Statements' in name and s.startswith('H'):
                        h_match = re.match(r'(H\d{3}[a-z]?)', s)
                        if h_match and h_match.group(1) not in result['h_zinnen']:
                            if result['h_zinnen']:
                                result['h_zinnen'] += ', ' + h_match.group(1)
                            else:
                                result['h_zinnen'] = h_match.group(1)

                    if 'Signal' in name and s in ('Danger', 'Warning'):
                        result['signal'] = s

                    if 'Pictogram' in name:
                        for m in markup:
                            if m.get('Type') == 'Icon' and m.get('Extra'):
                                pic = m['Extra']
                                if pic not in result['pictograms']:
                                    result['pictograms'].append(pic)

            for ss in section.get('Section', []):
                extract_info(ss)

        extract_info(ghs)

        # Build CLP string
        parts = []
        if result['pictograms']:
            parts.append(' | '.join(result['pictograms']))
        if result['signal']:
            parts.append(result['signal'])
        if result['h_zinnen']:
            parts.append(result['h_zinnen'])
        result['clp'] = ' — '.join(parts)

        # Detect CMR from H-zinnen
        cmr_map = {
            'H350': 'Carc. 1A', 'H350i': 'Carc. 1B', 'H351': 'Carc. 2',
            'H340': 'Mut. 1A', 'H341': 'Mut. 1B',
            'H360F': 'Repr. 1A', 'H360D': 'Repr. 1B', 'H360': 'Repr. 1A',
            'H361f': 'Repr. 2', 'H361d': 'Repr. 2', 'H361': 'Repr. 2',
        }
        for h_code, cmr_type in cmr_map.items():
            if h_code in result['h_zinnen']:
                result['is_cmr'] = True
                result['cmr_type'] = cmr_type
                break

        # Detect Acute Tox from H-zinnen
        tox_map = {'H310': '1', 'H330': '1', 'H300': '1',
                   'H311': '2', 'H331': '2', 'H301': '2',
                   'H302': '3', 'H312': '3', 'H332': '3',
                   'H303': '4', 'H313': '4', 'H333': '4'}
        for h_code, cat in tox_map.items():
            if h_code in result['h_zinnen']:
                result['is_acute_tox'] = True
                result['acute_tox_cat'] = cat
                break

        return result if result['h_zinnen'] else None
    except Exception as e:
        print(f"PubChem GHS lookup error: {e}")
        return None


def lookup_rivm_rvs(cas=None, naam=None):
    """Lookup substance data from RIVM RVS Zoeksysteem. Scrapes search results."""
    try:
        import lxml.html as lh
        session = requests.Session()
        session.headers.update({'User-Agent': 'Mozilla/5.0 (compatible; PGS15-Scanner/2.1)'})

        # Get form page + token
        r = session.get('https://rvszoeksysteem.rivm.nl/Stoffen', timeout=15)
        if r.status_code != 200:
            return None

        tree = lh.fromstring(r.text)
        token = tree.xpath('//input[@name="__RequestVerificationToken"]/@value')
        token = token[0] if token else ''

        # Search
        form_data = {
            '__RequestVerificationToken': token,
            'Stofnaam': naam or '',
            'CasEgNummers': cas or '',
            'UnNummer': '',
            'ChemischeStoffgroep': '',
            'FunctioneleStoffgroep': '',
        }
        r = session.post('https://rvszoeksysteem.rivm.nl/Stoffen', data=form_data, timeout=15)
        if r.status_code != 200:
            return None

        tree = lh.fromstring(r.text)
        result = {'clp': '', 'h_zinnen': '', 'adr_klasse': '', 'vg': '', 'is_cmr': False, 'cmr_type': '', 'is_acute_tox': False, 'acute_tox_cat': '', 'rivm_url': ''}

        # Find first result link
        links = tree.xpath('//a[contains(@href, "/stof/detail/")]')
        if links:
            href = links[0].get('href', '')
            result['rivm_url'] = f'https://rvszoeksysteem.rivm.nl{href}'

            # Go to detail page
            detail_url = href if href.startswith('http') else f'https://rvszoeksysteem.rivm.nl{href}'
            r2 = session.get(detail_url, timeout=15)
            if r2.status_code == 200:
                detail_html = r2.text
                result['rivm_url'] = detail_url

                # Extract data from detail page
                # CLP classification
                for label in ['CLP-indeling', 'CLP classificatie', 'Classificatie en etikettering']:
                    elem = tree.xpath(f"//*[contains(text(),'{label}')]/following-sibling::*[1]/text()")
                    if elem:
                        result['clp'] = elem[0].strip()
                        break

                detail_tree = lh.fromstring(detail_html)
                # Try broader patterns on detail page
                all_text = detail_tree.text_content()

                # H-zinnen
                h_matches = re.findall(r'H\d{3}[a-z]?(?:\s*:\s*[^,<;\n]+)?', all_text)
                if h_matches:
                    result['h_zinnen'] = ', '.join(sorted(set(h_matches)))

                # CMR detection
                cmr_patterns = [
                    (r'Carc\.?\s*1[AB]', 'Carc. 1A'),
                    (r'Carc\.?\s*2', 'Carc. 2'),
                    (r'Mut\.?\s*1[AB]', 'Mut. 1A'),
                    (r'Repr\.?\s*1[AB]', 'Repr. 1A'),
                    (r'Repr\.?\s*2', 'Repr. 2'),
                ]
                for pattern, cmr_type in cmr_patterns:
                    if re.search(pattern, all_text, re.IGNORECASE):
                        result['is_cmr'] = True
                        result['cmr_type'] = cmr_type
                        break

                # Acute Tox detection
                tox_match = re.search(r'Acute\s*Tox\.?\s*(\d)', all_text, re.IGNORECASE)
                if tox_match:
                    result['is_acute_tox'] = True
                    result['acute_tox_cat'] = tox_match.group(1)

                # Extract physical state hint from text
                if re.search(r'poedervorm|poeder', all_text, re.IGNORECASE):
                    result['vorm_suggestie'] = 'poeder'
                elif re.search(r'gas', all_text, re.IGNORECASE) and not re.search(r'gasoline|benzine', all_text, re.IGNORECASE):
                    result['vorm_suggestie'] = 'gas'
                elif re.search(r'vloeistof|liquid', all_text, re.IGNORECASE):
                    result['vorm_suggestie'] = 'vloeistof'
                else:
                    result['vorm_suggestie'] = 'vast'

        return result
    except ImportError:
        print("lxml not installed, skipping RIVM lookup")
        return None
    except Exception as e:
        print(f"RIVM RVS lookup error: {e}")
        return None


def lookup_rivm_gesture(cas):
    """Lookup ADR classification from RIVM Gevarenclassificering database."""
    try:
        url = f"https://www.rivm.nl/stoffen/gevarenclassificering/{cas}"
        r = requests.get(url, timeout=10, headers={
            'User-Agent': 'Mozilla/5.0 (compatible; PGS15-Scanner/2.1)',
        })
        if r.status_code != 200:
            return None

        html = r.text
        result = {'adr_klasse': '', 'vg': '', 'un_nummer': ''}

        # ADR class extraction
        adr_match = re.search(r'ADR[- ]?(?:klasse|class)[^\d]*(\d+(?:\.\d)?)', html, re.IGNORECASE)
        if adr_match:
            result['adr_klasse'] = adr_match.group(1)

        # Verpakkingsgroep
        vg_match = re.search(r'(?:Verpakkingsgroep|Packaging group|VG)\s*(I{1,3})', html, re.IGNORECASE)
        if vg_match:
            result['vg'] = vg_match.group(1)

        # UN number
        un_match = re.search(r'UN\s*(\d{4})', html, re.IGNORECASE)
        if un_match:
            result['un_nummer'] = f"UN {un_match.group(1)}"

        return result if any(result.values()) else None
    except Exception as e:
        print(f"RIVM gesture lookup error: {e}")
        return None


def unified_lookup(cas='', naam=''):
    """Master lookup: combines PubChem + ECHA + RIVM + ADR fallback for maximum auto-fill."""
    query = cas or naam
    if not query or len(query) < 2:
        return None

    # 0) ADR fallback table (for isotopes & substances missing in PubChem Transport)
    ADR_FALLBACK = {
        '32488-44-1': ('3', 'II', 'UN 1114'),   # Benzene-13C6 → same as benzene
        '1076-43-3': ('3', 'II', 'UN 1114'),   # Benzene-d6 → same as benzene
        '7664-93-9': ('8', 'II', 'UN 1830'),   # Sulfuric acid
        '7664-39-3': ('8', 'I', 'UN 1052'),    # Hydrogen fluoride (anhydrous)
        '1310-73-2': ('8', 'II', 'UN 1824'),   # Sodium hydroxide solution
        '71-43-2':   ('3', 'II', 'UN 1114'),   # Benzene
        '10026-13-8': ('8', 'I', 'UN 1052'),   # HF gas
    }

    result = {
        'cas': '', 'name': '', 'iupac': '', 'formula': '', 'mw': '',
        'clp': '', 'h_zinnen': '',
        'adr_klasse': '', 'vg': '', 'un_nummer': '',
        'is_cmr': False, 'cmr_type': '',
        'is_acute_tox': False, 'acute_tox_cat': '',
        'vorm_suggestie': '',
        'flashpoint': None,
        'sources': [],
        'rivm_url': '',
    }

    # 1) PubChem — base data (name, CAS, formula, MW)
    pubchem = lookup_pubchem(query)
    cid = None
    if pubchem:
        cid = pubchem.get('cid')
        result['cas'] = pubchem.get('cas', '')
        result['name'] = pubchem.get('name', '')
        result['iupac'] = pubchem.get('iupac', '')
        result['formula'] = pubchem.get('formula', '')
        result['mw'] = pubchem.get('mw', '')
        result['sources'].append('PubChem')
        if pubchem.get('cas') and not cas:
            cas = pubchem['cas']

    # 2) PubChem GHS — CLP classification, H-zinnen, CMR, Acute Tox
    if cid:
        ghs = lookup_pubchem_ghs(cid)
        if ghs:
            if ghs.get('clp'):
                result['clp'] = ghs['clp']
            if ghs.get('h_zinnen'):
                result['h_zinnen'] = ghs['h_zinnen']
            if ghs.get('signal'):
                result['signal'] = ghs['signal']
            if ghs.get('pictograms'):
                result['pictograms'] = ghs['pictograms']
            if ghs.get('is_cmr'):
                result['is_cmr'] = True
                result['cmr_type'] = ghs['cmr_type']
            if ghs.get('is_acute_tox'):
                result['is_acute_tox'] = True
                result['acute_tox_cat'] = ghs['acute_tox_cat']
            result['sources'].append('PubChem GHS')

    # 3) RIVM RVS — additional data, RIVM link
    rivm = lookup_rivm_rvs(cas=cas, naam=naam if not cas else '')
    if rivm and rivm.get('rivm_url'):
        if rivm.get('clp'):
            result['clp'] = rivm['clp']
        if rivm.get('h_zinnen'):
            result['h_zinnen'] = rivm['h_zinnen']
        if rivm.get('is_cmr'):
            result['is_cmr'] = True
            result['cmr_type'] = rivm['cmr_type']
        if rivm.get('is_acute_tox'):
            result['is_acute_tox'] = True
            result['acute_tox_cat'] = rivm['acute_tox_cat']
        if rivm.get('vorm_suggestie'):
            result['vorm_suggestie'] = rivm['vorm_suggestie']
        result['rivm_url'] = rivm.get('rivm_url', '')
        result['sources'].append('RIVM RVS')

    # 3) PubChem Transport — ADR class, VG, UN number (most reliable)
    if cid and not result['adr_klasse']:
        transport = lookup_pubchem_transport(cid)
        if transport:
            if transport.get('adr_klasse'):
                result['adr_klasse'] = transport['adr_klasse']
            if transport.get('vg'):
                result['vg'] = transport['vg']
            if transport.get('un_nummer'):
                result['un_nummer'] = transport['un_nummer']
            result['sources'].append('PubChem Transport')

    # 3b) ADR fallback table — for isotopes & substances missing in PubChem
    if cas and not result['adr_klasse'] and cas in ADR_FALLBACK:
        fb = ADR_FALLBACK[cas]
        result['adr_klasse'] = fb[0]
        result['vg'] = fb[1]
        result['un_nummer'] = fb[2]
        result['sources'].append('ADR Fallback')

    # 3c) Flashpoint lookup table (PubChem has no FlashPoint API)
    FLASHPOINT_TABLE = {
        # ADR 8 VG I stoffen (meest relevant)
        '7697-37-2': None,       # Salpeterzuur — geen vlampunt (oxideerend)
        '7664-93-9': None,       # Zwavelzuur — geen vlampunt
        '7664-39-3': None,       # Waterstoffluoride — geen vlampunt
        '7647-01-0': None,       # Zoutzuur — geen vlampunt
        '7681-52-9': None,       # Natriumhypochloriet — geen vlampunt
        '1310-73-2': None,       # Natriumhydroxide — geen vlampunt
        # ADR 3 stoffen
        '67-64-1': -18,          # Aceton
        '108-88-3': 4,           # Tolueen
        '71-43-2': -11,          # Benzeen
        '68476-34-6': 55,        # Diesel
        '67-56-1': 11,           # Methanol
        '78-93-3': -17,          # Butanon (MEK)
        '100-41-4': 26,          # Ethylbenzeen
        '110-82-7': -6,          # Cyclohexaan
        '1330-20-7': -20,        # Xyleen (gemengd)
        '123-91-1': 25,          # 1,4-Dioxaan
        '75-09-2': None,         # Dichloormethaan — geen vlampunt
        '56-23-5': None,         # Trichloormethaan — geen vlampunt
        '107-21-1': 13,          # Ethyleenglycol
        '100-42-5': 31,          # Styreen
        '95-47-6': -1,           # o-Xyleen
        '108-38-3': 27,          # m-Xyleen
        '106-42-3': 27,          # p-Xyleen
        # ADR 5.1
        '7722-84-1': None,       # Waterstofperoxide — geen vlampunt
        # ADR 6.1
        '50-00-0': 53,           # Formaldehyde (oplossing)
        '151-50-8': None,        # Kaliumcyanide — geen vlampunt
        # ADR 8 VG II/III
        '25821-48-7': None,      # Fosforzuur
        '7681-57-4': None,      # Zwavelzuur verd.
    }
    if cas and cas in FLASHPOINT_TABLE and result['flashpoint'] is None:
        result['flashpoint'] = FLASHPOINT_TABLE[cas]
        result['sources'].append('Flashpoint DB')

    # 4) RIVM Gevarenclassificering — ADR class, VG, UN number (backup)
    if cas and not result['adr_klasse']:
        gesture = lookup_rivm_gesture(cas)
        if gesture:
            if gesture.get('adr_klasse'):
                result['adr_klasse'] = gesture['adr_klasse']
            if gesture.get('vg'):
                result['vg'] = gesture['vg']
            if gesture.get('un_nummer'):
                result['un_nummer'] = gesture['un_nummer']
            result['sources'].append('RIVM Classificatie')

    # 4) ECHA — CLP backup (if RIVM didn't provide CLP data)
    if cas and not result['clp']:
        echa = lookup_echa(cas)
        if echa:
            if echa.get('clp'):
                result['clp'] = echa['clp']
            if echa.get('h_zinnen') and not result['h_zinnen']:
                result['h_zinnen'] = echa['h_zinnen']
            if echa.get('is_cmr') and not result['is_cmr']:
                result['is_cmr'] = True
                result['cmr_type'] = echa['cmr_type']
            if echa.get('is_acute_tox') and not result['is_acute_tox']:
                result['is_acute_tox'] = True
                result['acute_tox_cat'] = echa['acute_tox_cat']
            result['sources'].append('ECHA')

    # If user provided CAS and we didn't find one, use input
    if not result['cas'] and cas:
        result['cas'] = cas
    if not result['name'] and naam:
        result['name'] = naam

    return result if result['cas'] or result['name'] else None


# ═══════════════════════════════════════════════════════════════════
# COMPLIANCE ENGINE
# ═══════════════════════════════════════════════════════════════════

def determine_niveau(adr_klasse, vg, hoeveelheid, is_brandbaar_k8=False, flashpoint=None):
    """Bepaal veiligheidsniveau op basis van PGS 15:2025 Tabel 6.

    Args:
        adr_klasse: ADR klasse (str)
        vg: Verpakkingsgroep (I/II/III)
        hoeveelheid: Hoeveelheid in kg
        is_brandbaar_k8: Of ADR 8+9 VG II+III brandbaar is (default False = onbrandbaar)
        flashpoint: Flashpoint in °C (optioneel, voor ADR 8 VG I onderscheid)
    """
    # ═══ ADR 8 VG I: flashpoint-afhankelijk ═══
    if adr_klasse == '8' and vg == 'I':
        if hoeveelheid < 250:
            return 'Buiten scope'
        fp = flashpoint if flashpoint is not None else 0
        if fp <= 100:  # FP 60-100°C: max A
            if hoeveelheid >= 10000:
                return 'A'
            return 'C'
        else:  # FP >100°C: max B (geen A!)
            if hoeveelheid >= 10000:
                return 'B'
            return 'C'

    # ═══ ADR 8+9 VG II+III: brandbaar vs onbrandbaar ═══
    if adr_klasse in ('8', '9') and vg in ('II', 'III'):
        if hoeveelheid < 250:
            return 'Buiten scope'
        if not is_brandbaar_k8:
            return 'Basis'  # onbrandbaar: ALLEEN Basis (geen C/B/A)
        # brandbaar: C<10ton, B>10ton, geen A
        if hoeveelheid >= 10000:
            return 'B'
        return 'C'

    # ═══ Standaard lookup ═══
    key = (adr_klasse, vg)
    if key not in NIVEAUS:
        return 'Geen ADR-klasse'
    ondergrens, c_start, b_thresh, a_thresh = NIVEAUS[key]
    if hoeveelheid < ondergrens:
        return 'Buiten scope'
    if c_start is None:
        # Geen C-niveau → alleen Basis (of B/A als thresholds bestaan)
        if b_thresh is not None and hoeveelheid >= b_thresh and a_thresh is not None and hoeveelheid >= a_thresh:
            return 'A'
        if b_thresh is not None and hoeveelheid >= b_thresh:
            return 'B'
        return 'Basis'
    # C-niveau bestaat
    if b_thresh is None and a_thresh is None:
        # Alleen C (bijv. ADR 5.2)
        return 'C'
    if b_thresh is None:
        # Geen B, wel A (bijv. ADR 3, 6.1, Acute Tox)
        if a_thresh is not None and hoeveelheid >= a_thresh:
            return 'A'
        return 'C'
    if a_thresh is None:
        # Geen A, wel B (bijv. ADR 5.1, 8+9 brandbaar)
        if hoeveelheid >= b_thresh:
            return 'B'
        return 'C'
    # Alle niveaus
    if hoeveelheid >= a_thresh:
        return 'A'
    if hoeveelheid >= b_thresh:
        return 'B'
    return 'C'


def parse_clp_routes(clp_text, is_cmr, cmr_type, is_acute_tox, acute_tox_cat, vorm):
    """Bepaal welke CLP-only routes van toepassing zijn (PGS 15:2025)."""
    routes = []
    clp_lower = clp_text.lower() if clp_text else ''

    # Acute Tox
    if is_acute_tox:
        cat = (acute_tox_cat or '').strip()
        # Normaliseer: "Acute Tox. 1" → "1", "1" → "1"
        if cat in ('1', 'Acute Tox. 1', 'Acute Tox.1'):
            routes.append(('CLP Acute Tox. 1', CLP_ROUTES['acute_tox_1']))
        elif cat in ('2', '3', '2/3', 'Acute Tox. 2', 'Acute Tox.2', 'Acute Tox. 3', 'Acute Tox.3', 'Acute Tox. 2/3'):
            routes.append(('CLP Acute Tox. 2/3', CLP_ROUTES['acute_tox_2_3']))

    # CMR (2025: vaste vorm = poeder/metaal → ondergrens 25kg alleen Basis; vloeibaar → 1kg alleen Basis)
    if is_cmr:
        if vorm in ('vloeibaar', 'liquid'):
            routes.append(('CLP CMR vloeibaar', CLP_ROUTES['cmr_vloeibaar']))
        else:
            # poeder, vast, metaal, onbekend → vaste vorm
            routes.append(('CLP CMR vaste vorm', CLP_ROUTES['cmr_vast']))

    # Also check clp text for hints
    if not is_acute_tox and 'acute tox' in clp_lower:
        # Match "Acute Tox. 1" or "Acute Tox. Cat. 1" or "Acute Tox.1"
        import re
        at_m = re.search(r'acute\s+tox\.?\s*(?:cat\.?\s*)?(\d)', clp_lower)
        if at_m:
            cat_num = at_m.group(1)
            if cat_num == '1':
                routes.append(('CLP Acute Tox. 1', CLP_ROUTES['acute_tox_1']))
            elif cat_num in ('2', '3'):
                routes.append(('CLP Acute Tox. 2/3', CLP_ROUTES['acute_tox_2_3']))

    if not is_cmr and any(x in clp_lower for x in ['h350', 'h340', 'h341', 'h350i', 'h351', 'h361']):
        if vorm in ('vloeibaar', 'liquid'):
            routes.append(('CLP CMR vloeibaar', CLP_ROUTES['cmr_vloeibaar']))
        else:
            routes.append(('CLP CMR vaste vorm', CLP_ROUTES['cmr_vast']))

    return routes


def check_seveso(stoffen):
    total = sum(s['hoeveelheid'] for s in stoffen)
    if total >= 20000: return 'Bovenste drempelwaarde (≥20.000 kg)', 'danger'
    if total >= 5000: return 'Onderste drempelwaarde (≥5.000 kg)', 'warning'
    return 'Niet van toepassing (<5.000 kg)', 'ok'


def check_bal(stoffen):
    total = sum(s['hoeveelheid'] for s in stoffen)
    if total >= 1000: return 'Vergunningplichtig (≥1.000 kg)', 'danger'
    return 'Geen vergunningplicht (<1.000 kg)', 'ok'


def build_aggregatie_samenvatting(result, opslag_type='binnen'):
    """Genereer samenvatting van alle verplichte maatregelen voor het samenstel.
    
    Context-aware: bouwt compliance op basis van geaggregeerd niveau + alle stof-eigenschappen.
    """
    stoffen = result.get('per_stof', [])
    agg = result.get('aggregatie', {})
    agg_niveau = agg.get('geaggregeerd_niveau', 'Buiten scope')
    niveau_order = {'A': 4, 'B': 3, 'C': 2, 'Buiten scope': 1}
    agg_is_relevant = niveau_order.get(agg_niveau, 0) >= niveau_order.get('C', 2)
    n_scope = len(stoffen)
    if n_scope == 0 or not agg_is_relevant:
        return {'totaal_maatregelen': 0, 'per_categorie': {}, 'maatregelen': []}

    # Combineer alle stof-eigenschappen
    has_acute_tox = any(bool(s.get('is_acute_tox', 0)) for s in stoffen)
    has_cmr = any(bool(s.get('is_cmr', 0)) for s in stoffen)
    has_brandbaar = any(bool(s.get('is_brandbaar_k8', 0)) for s in stoffen)
    clp_text = ' | '.join(s.get('clp_classificatie', '') for s in stoffen if s.get('clp_classificatie'))
    zwaarste = max(stoffen, key=lambda x: niveau_order.get(x.get('strengste_niveau', 'Buiten scope'), 0))

    # Bouw compliance voor het hele samenstel
    samenstel_compliance = build_compliance(
        agg_niveau, opslag_type=opslag_type,
        adr_klasse=zwaarste.get('adr_klasse', ''), vg=zwaarste.get('vg', ''),
        is_cmr=has_cmr, cmr_type='',
        is_acute_tox=has_acute_tox, acute_tox_cat='',
        is_brandbaar_k8=has_brandbaar,
        clp_classificatie=clp_text
    )

    # Verzamel unieke maatregelen
    all_codes = set()
    for dobj in samenstel_compliance:
        all_codes.update(dobj['maatregelen'])

    # Tel per maatregel op hoeveel stoffen
    code_count = {}
    for s in stoffen:
        s_m = {f['code'] for f in s.get('flat_measures', [])}
        for m in all_codes:
            if m in s_m:
                code_count[m] = code_count.get(m, 0) + 1

    # Bouw resultaat
    cat_order = {'Basis': 0, 'C': 1, 'B': 2, 'A': 3}
    maatregelen = []
    per_categorie = {}
    for m_code in sorted(all_codes):
        info = MAATREGELEN_ALLE.get(m_code)
        if not info:
            continue
        title, omsch = info
        cat = 'Basis'
        for nivo in ['A', 'B', 'C']:
            for dc in DOELSTELLING_PER_NIVEAU.get(nivo, []):
                dm = DOELSTELLING_MAP.get(dc)
                if dm and m_code in dm.get('maatregelen', {}).get(nivo, []):
                    cat = nivo
                    break
        if cat not in per_categorie:
            per_categorie[cat] = 0
        per_categorie[cat] += 1
        maatregelen.append({
            'code': m_code,
            'title': title,
            'category': cat,
            'van_toepassing_op': code_count.get(m_code, 0),
            'omschrijving': omsch,
        })

    return {
        'totaal_maatregelen': len(maatregelen),
        'per_categorie': per_categorie,
        'maatregelen': maatregelen,
    }

def build_opslagvoorstel(result):
    """Genereer concreet opslagvoorstel op basis van PGS15:2025 regels."""
    stoffen = result.get('per_stof', [])
    agg = result.get('aggregatie', {})
    agg_niveau = agg.get('geaggregeerd_niveau', 'Buiten scope')
    combi = result.get('combinatie', {})

    agg_niveau_order = {'A': 4, 'B': 3, 'C': 2, 'Buiten scope': 1}
    agg_is_relevant = agg_niveau_order.get(agg_niveau, 0) >= agg_niveau_order.get('C', 2)

    scope_stoffen = [s for s in stoffen
                     if s.get('pgs15') or agg_is_relevant]

    if not scope_stoffen:
        return {'advies': 'Geen PGS 15-stoffen in scope', 'niveau': '-', 'vakken': [],
                'scheiding_waarschuwingen': [], 'constructieve_eisen': []}

    # ── Groepeer per ADR-klasse voor vakindeling ──
    klassen = {}
    for s in scope_stoffen:
        k = s.get('adr_klasse', 'overig')
        if not k:
            k = 'overig'
        if k not in klassen:
            klassen[k] = []
        klassen[k].append(s)

    # ── Scheidingswaarschuwingen ──
    scheiding_waarschuwingen = []
    scheiding_data = combi.get('scheiding', {})
    conflicten = scheiding_data.get('conflicten', [])
    checked_pairs = set()
    for cf in conflicten:
        pair = tuple(sorted([cf.get('stof1',''), cf.get('stof2','')]))
        if pair not in checked_pairs:
            checked_pairs.add(pair)
            scheiding_waarschuwingen.append(cf['reden'])

    # ── Vakken op basis van ADR-klasse ──
    KLASSE_NAAM = {
        '1': 'Ontploffingsgevaarlijke stoffen', '2': 'Gassen',
        '3': 'Brandbare vloeistoffen', '4.1': 'Brandbare vaste stoffen',
        '4.2': 'Zelfontbrandende stoffen', '4.3': 'Stoffen die in contact met water brandbare gassen vormen',
        '5.1': 'Brandbevorderende stoffen', '5.2': 'Organische peroxiden',
        '6.1': 'Giftige stoffen', '6.2': 'Besmettelijke stoffen',
        '8': 'Corrosieve/bijtende stoffen', '9': 'Overige gevaarlijke stoffen',
        'overig': 'Overige stoffen (geen ADR-klasse)',
    }

    # Groepeer scheidingsgroepen die gescheiden moeten worden
    vakken = []
    vak_idx = 1
    for k, k_stoffen in sorted(klassen.items()):
        totaal = sum(s['hoeveelheid'] for s in k_stoffen)
        naam = KLASSE_NAAM.get(k, f'ADR klasse {k}')
        label = f'Vak {vak_idx} — {naam} (ADR klasse {k})'

        # Eisen per vak op basis van niveau
        eisen = []
        eisen.append('Opvangvoorziening 110%/10%')
        eisen.append('VIB beschikbaar')
        eisen.append('Etikettering conform CLP')
        if agg_niveau in ('C', 'B', 'A'):
            eisen.append('WBDBO 60/90 min')
            eisen.append('Noodverlichting bij stroomuitval')
            eisen.append('Minimaal 2 vluchtroutes')
            eisen.append('Bluswatervoorziening')
            eisen.append('Nooddouche/oogspoeling (binnen 10s)')
            eisen.append('Plattegrond beschikbaar voor hulpdiensten')
        if agg_niveau in ('B', 'A'):
            eisen.append('Vak max 300 m²')
            eisen.append('Brandoverslagpreventie')
            eisen.append('Rook- en warmteafvoer')
        if agg_niveau == 'A':
            eisen.append('Automatische branddetectie')
            eisen.append('Brandwerende constructie 120 min')

        # Specifieke eisen per klasse
        if k == '3':
            eisen.append('Blusmiddel: poeder/schuim (geen water bij bepaalde stoffen)')
            eisen.append('Ontstekingsbronnen zoveel mogelijk voorkomen')
        if k == '8':
            eisen.append('Nooddouche verplicht (binnen 10 seconden)')
            eisen.append('Zuur-/base-resistente opvangvoorziening')
        if k == '5.1' or k == '5.2':
            eisen.append('Scheiden van brandbare stoffen (verplicht)')
            eisen.append('Temperatuurbeheersing')

        # CMR check
        cmr_stoffen = [s for s in k_stoffen if s.get('is_cmr')]
        if cmr_stoffen:
            eisen.append('CMR-stoffen: apart opslaan in gesloten systeem of afgescheiden opslag')

        vakken.append({
            'label': label,
            'adr_klasse': k,
            'stoffen': [s['naam'] for s in k_stoffen],
            'totaal_kg': totaal,
            'eisen': list(dict.fromkeys(eisen)),  # dedup behoud volgorde
        })
        vak_idx += 1

    # ── Constructieve eisen (geheel) ──
    constructieve_eisen = [
        'Opvangvoorziening: minimaal 110% grootste eenheid of 10% totaal',
        'Draagbaar blustoestel (type passend bij stoffen)',
        'Minimaal 2 onafhankelijke vluchtroutes',
        'Paniekuitrusting op deuren in vluchtroutes',
        'Noodverlichting bij stroomuitval',
        'VIB/SDS beschikbaar voor alle opgeslagen stoffen',
        'Rook-/vuurverbod aangegeven met verbodsborden',
    ]
    if agg_niveau in ('C', 'B', 'A'):
        constructieve_eisen += [
            'WBDBO constructie: 60 min dragend, 90 min brandscheiding',
            'Bluswatervoorziening met voldoende capaciteit',
            'Nooddouche/oogspoeling bereikbaar binnen 10 seconden',
            'Plattegrond beschikbaar voor hulpdiensten',
            'Calamiteiteninstructie aanwezig',
            'Manoeuvreerruimte tussen opslageenheden',
            'Aanrijdbeveiliging stellingen',
        ]
    if agg_niveau in ('B', 'A'):
        constructieve_eisen += [
            'Vakindeling: max 300 m² per vak',
            'Brandoverslagpreventie tussen vakken',
            'Rook- en warmteafvoerinstallatie',
            'Constructieve brandbeveiliging (scheidingswanden, -deuren)',
            'Brandweer toegang van alle kanten',
        ]
    if agg_niveau == 'A':
        constructieve_eisen += [
            'Brandwerende constructie: 120 min dragend',
            'Automatisch branddetectiesysteem (alarmeert brandweer)',
            'VBB-systeem (brandbeheersing) of bedrijfsbrandweer',
            'Uitgangspuntendocument (UPD) vereist',
        ]
    if agg.get('catchall'):
        constructieve_eisen.append('Catch-all (≥1.000 kg): basismaatregelen verplicht voor alle stoffen')

    # ── Advies ──
    if len(vakken) > 1:
        advies = f'Opslag in {len(vakken)} aparte vakken per ADR-klasse vereist (niveau {agg_niveau})'
    else:
        advies = f'Opslag in 1 opslagvoorziening (niveau {agg_niveau})'
    if scheiding_waarschuwingen:
        advies += ' — scheidingsplicht van toepassing'

    return {
        'advies': advies,
        'niveau': agg_niveau,
        'vakken': vakken,
        'scheiding_waarschuwingen': scheiding_waarschuwingen,
        'constructieve_eisen': constructieve_eisen,
    }


def run_scan(stoffen, opslag_type='binnen'):
    results = []
    for s in stoffen:
        adr = s.get('adr_klasse', '')
        vg = s.get('vg', 'I')
        hoev = s.get('hoeveelheid', 0)

        # ADR ondergrens
        ondergrens = ONDERGRENS.get(adr, {}).get(vg)
        adr_boven = ondergrens is not None and hoev >= ondergrens

        # ADR niveau
        adr_niveau = determine_niveau(adr, vg, hoev, s.get('is_brandbaar_k8', False), s.get('flashpoint'))

        # CLP routes
        clp_routes = parse_clp_routes(
            s.get('clp_classificatie', ''),
            s.get('is_cmr', 0), s.get('cmr_type', ''),
            s.get('is_acute_tox', 0), s.get('acute_tox_cat', ''),
            s.get('vorm', 'vast')
        )
        clp_boven = [(label, r) for label, r in clp_routes
                     if r['ondergrens'] != float('inf') and hoev >= r['ondergrens']]

        # Collect all applicable levels
        all_niveaus = []
        if adr_boven and adr_niveau not in ('Buiten scope', 'Geen ADR-klasse'):
            all_niveaus.append(('ADR', adr_niveau))
        for label, r in clp_boven:
            # Boven ondergrens → default Niveau C (als A/B niet bereikt)
            n = 'C'
            if r['B'] is not None and hoev >= r['B']:
                n = 'B'
            if r['A'] is not None and hoev >= r['A']:
                n = 'A'
            all_niveaus.append((label, n))

        # Strengste niveau
        niveau_order = {'A': 4, 'B': 3, 'C': 2, 'Buiten scope': 1, 'Geen ADR-klasse': 0}
        strengste = max(all_niveaus, key=lambda x: niveau_order.get(x[1], 0)) if all_niveaus else ('—', 'Buiten scope')

        # PGS 15 applicable?
        total_all = sum(x['hoeveelheid'] for x in stoffen)
        pgs15 = (adr_boven or len(clp_boven) > 0 or total_all >= 1000)

        # Required measures (from MAATREGELEN_ALLE dict)
        all_m = list(MAATREGELEN_ALLE.keys())
        req_measures = {'Basis': all_m}
        if strengste[1] in ('C', 'B', 'A'):
            req_measures['C'] = all_m
        if strengste[1] in ('B', 'A'):
            req_measures['B'] = all_m
        if strengste[1] == 'A':
            req_measures['A'] = all_m

        # Build versietabel for this substance
        versies = []
        # ADR versie
        if adr and vg:
            nv = NIVEAUS.get((adr, vg))
            if nv:
                versies.append({
                    'route': f'ADR klasse {adr}, VG {vg}',
                    'ondergrens': nv[0],
                    'C': nv[1] if nv[1] else 'N.v.t.',
                    'B': nv[2] if nv[2] else 'N.v.t.',
                    'A': nv[3] if nv[3] else 'N.v.t.',
                    'actueel': adr_niveau if adr_boven else '—',
                    'is_actueel': adr_boven,
                })
        # CLP versies
        for label, r in clp_routes:
            if r['ondergrens'] == float('inf'):
                continue
            versies.append({
                'route': label,
                'ondergrens': r['ondergrens'],
                'C': r['C'] if r['C'] else 'N.v.t.',
                'B': r['B'] if r['B'] else 'N.v.t.',
                'A': r['A'] if r['A'] else 'N.v.t.',
                'actueel': None,
                'is_actueel': False,
            })
            # Mark the actual CLP level
            for an, aniveau in all_niveaus:
                if an == label:
                    versies[-1]['actueel'] = aniveau
                    versies[-1]['is_actueel'] = True

        results.append({
            'id': s.get('id'), 'naam': s.get('naam', 'Onbekend'),
            'cas': s.get('cas', ''), 'hoeveelheid': hoev,
            'vorm': s.get('vorm', ''), 'adr_klasse': adr, 'vg': vg,
            'ondergrens': ondergrens, 'adr_boven': adr_boven,
            'adr_niveau': adr_niveau,
            'clp_routes': [{'label': l, 'ondergrens': r['ondergrens'] if r['ondergrens'] != float('inf') else 'N.v.t.'} for l, r in clp_routes],
            'clp_boven': [{'label': l, 'ondergrens': r['ondergrens']} for l, r in clp_boven],
            'all_niveaus': all_niveaus,
            'strengste_route': strengste[0],
            'strengste_niveau': strengste[1],
            'pgs15': pgs15,
            'status': 'PGS 15 van toepassing' if pgs15 else 'Buiten PGS 15 scope',
            'maatregelen': req_measures,
            'versies': versies,
            # Volledige PGS 15 context
            'doelstellingen': DOELEN.get(strengste[1], DOELEN.get('Basis', {})),
            'compliance': build_compliance(
                    strengste[1], opslag_type=opslag_type,
                    adr_klasse=adr, vg=vg,
                    is_cmr=bool(s.get('is_cmr', 0)), cmr_type=s.get('cmr_type', ''),
                    is_acute_tox=bool(s.get('is_acute_tox', 0)), acute_tox_cat=s.get('acute_tox_cat', ''),
                    is_brandbaar_k8=bool(s.get('is_brandbaar_k8', 0)),
                    clp_classificatie=s.get('clp_classificatie', '')
                ),
            'drempelwaarden': build_drempelwaarden_tabel(s),
            'h_zinnen': s.get('h_zinnen', ''),
            'clp_classificatie': s.get('clp_classificatie', ''),
            'is_cmr': bool(s.get('is_cmr', 0)),
            'cmr_type': s.get('cmr_type', ''),
            'is_acute_tox': bool(s.get('is_acute_tox', 0)),
            'acute_tox_cat': s.get('acute_tox_cat', ''),
        })

        # Post-process: add flat_measures per stof
        compliance_data = results[-1].get('compliance', [])
        s['flat_measures'] = build_flat_measures(compliance_data)
        results[-1]['flat_measures'] = s['flat_measures']

    # Aggregatie
    total = sum(s['hoeveelheid'] for s in stoffen)
    per_klasse = {}
    for s in stoffen:
        k = s.get('adr_klasse', '')
        if k:
            if k not in per_klasse:
                per_klasse[k] = {'aantal': 0, 'totaal': 0}
            per_klasse[k]['aantal'] += 1
            per_klasse[k]['totaal'] += s['hoeveelheid']

    # Aggregated niveau (heaviest class)
    agg_niveau = 'Buiten scope'
    for k, info in per_klasse.items():
        kg = info['totaal']
        for vg in ('I', 'II', 'III'):
            n = determine_niveau(k, vg, kg)
            if niveau_order.get(n, 0) > niveau_order.get(agg_niveau, 0):
                agg_niveau = n

    # BUG 3 FIX: Also consider CLP-only routes for aggregatie
    for s in stoffen:
        if s.get('adr_klasse', ''):
            continue  # already handled via ADR above
        clp_routes_s = parse_clp_routes(
            s.get('clp_classificatie', ''),
            s.get('is_cmr', 0), s.get('cmr_type', ''),
            s.get('is_acute_tox', 0), s.get('acute_tox_cat', ''),
            s.get('vorm', 'vast')
        )
        hoev = s.get('hoeveelheid', 0)
        for label, r in clp_routes_s:
            if r['ondergrens'] == float('inf') or hoev < r['ondergrens']:
                continue
            n = 'Buiten scope'
            if r['C'] is not None and hoev >= r['C']:
                n = 'C'
            if r['B'] is not None and hoev >= r['B']:
                n = 'B'
            if r['A'] is not None and hoev >= r['A']:
                n = 'A'
            if niveau_order.get(n, 0) > niveau_order.get(agg_niveau, 0):
                agg_niveau = n

    aggregatie = {
        'totaal_kg': total,
        'catchall': total >= 1000,
        'per_klasse': per_klasse,
        'geaggregeerd_niveau': agg_niveau,
    }

    seveso_text, seveso_status = check_seveso(stoffen)
    bal_text, bal_status = check_bal(stoffen)

    scan_result = {
        'per_stof': results,
        'aggregatie': aggregatie,
        'seveso': seveso_text, 'seveso_status': seveso_status,
        'bal': bal_text, 'bal_status': bal_status,
        'measure_names': {k: v[0] for k, v in MAATREGELEN_ALLE.items()},
    }

    # v2.3: aggregatie samenvatting + opslagvoorstel
    scan_result['aggregatie_samenvatting'] = build_aggregatie_samenvatting(scan_result, opslag_type=opslag_type)
    scan_result['opslagvoorstel'] = build_opslagvoorstel(scan_result)

    # Wat-als analyse: vergelijk opslagtypes
    wat_als_per_stof = []
    for s in results:
        if s.get('strengste_niveau') not in ('Buiten scope', '', None):
            wa, _ = build_wat_als_analyse(
                    s['strengste_niveau'], opslag_type=opslag_type,
                    adr_klasse=s.get('adr_klasse', ''), vg=s.get('vg', ''),
                    is_cmr=bool(s.get('is_cmr', 0)), cmr_type=s.get('cmr_type', ''),
                    is_acute_tox=bool(s.get('is_acute_tox', 0)), acute_tox_cat=s.get('acute_tox_cat', ''),
                    is_brandbaar_k8=bool(s.get('is_brandbaar_k8', 0)),
                    clp_classificatie=s.get('clp_classificatie', '')
                )
            wat_als_per_stof.append({'naam': s['naam'], 'cas': s.get('cas', ''), 'wat_als': wa})
    scan_result['wat_als'] = wat_als_per_stof
    scan_result['opslag_type'] = opslag_type

    # Flat measures voor samenstel
    # Flat measures voor samenstel — context-aware op basis van geaggregeerd niveau + alle stof-eigenschappen
    agg_nivo = scan_result.get('aggregatie', {}).get('geaggregeerd_niveau', 'Buiten scope')
    if agg_nivo not in ('Buiten scope', '', None):
        has_acute_tox = any(bool(s.get('is_acute_tox', 0)) for s in results)
        has_cmr = any(bool(s.get('is_cmr', 0)) for s in results)
        has_brandbaar = any(bool(s.get('is_brandbaar_k8', 0)) for s in results)
        clp_text = ' | '.join(s.get('clp_classificatie', '') for s in results if s.get('clp_classificatie'))
        zwaarste = max(results, key=lambda x: niveau_order.get(x.get('strengste_niveau', 'Buiten scope'), 0))
        samenstel_compliance = build_compliance(
            agg_nivo, opslag_type=opslag_type,
            adr_klasse=zwaarste.get('adr_klasse', ''), vg=zwaarste.get('vg', ''),
            is_cmr=has_cmr, cmr_type='',
            is_acute_tox=has_acute_tox, acute_tox_cat='',
            is_brandbaar_k8=has_brandbaar,
            clp_classificatie=clp_text
        )
        samenstel_flat = build_flat_measures(samenstel_compliance)
        for sf in samenstel_flat:
            count = 0
            for s in results:
                s_m = {f['code'] for f in s.get('flat_measures', [])}
                if sf['code'] in s_m:
                    count += 1
            sf['van_toepassing_op'] = count
        scan_result['flat_measures_samenstel'] = samenstel_flat
    else:
        scan_result['flat_measures_samenstel'] = []

    return scan_result


# ═══════════════════════════════════════════════════════════════════
# TOELICHTINGEN
# ═══════════════════════════════════════════════════════════════════

TOELICHTINGEN = {
    'ondergrens': {
        'titel': 'Ondergrens',
        'tekst': 'De minimumhoeveelheid in kg vanaf welke PGS 15 van toepassing is op deze stof. Wordt bepaald door de ADR-klasse en verpakkingsgroep (VG), of door CLP-classificatie (CMR, Acute Toxiciteit). Staat in PGS 15:2025 Tabel 5 (ADR) of Tabel 6 (CLP).',
        'icon': '📏',
    },
    'adr_niveau': {
        'titel': 'ADR-versie (C/B/A)',
        'tekst': 'Op basis van de ADR-klasse en verpakkingsgroep wordt een veiligheidsniveau bepaald (C, B of A). Niveau C = basisvoorschriften, B = verhoogde eisen (brandveiligheid, noodplan), A = hoogste niveau (automatische branddetectie, VBB-systeem). Zie PGS 15:2025 Tabel 6.',
        'icon': '🚛',
    },
    'clp_routes': {
        'titel': 'CLP-routes',
        'tekst': 'Naast ADR kan PGS 15 ook van toepassing zijn via CLP-classificatie: CMR-stoffen (carcinogeen, mutageen, reprotoxisch) en Acute Tox. categorie 1-3. Elke route heeft eigen drempelwaarden. CMR vloeibaar ≥1 kg, CMR poedervorm ≥25 kg, CMR vaste metaalvorm = buiten scope, Acute Tox. 1 ≥1 kg, Acute Tox. 2/3 ≥1 kg.',
        'icon': '☠️',
    },
    'strengste_niveau': {
        'titel': 'Strengste niveau',
        'tekst': 'Wanneer meerdere routes van toepassing zijn (bijv. ADR én CLP), geldt het strengste niveau. Volgorde: A (strengst) > B > C > Buiten scope. Alle maatregelen van het strengste niveau én lagere niveaus zijn verplicht.',
        'icon': '⚡',
    },
    'maatregelen': {
        'titel': 'Verplichte maatregelen',
        'tekst': 'PGS 15:2025 kent 4 niveaus maatregelen:\n• Basis: Altijd verplicht bij stoffen binnen scope (o.a. opvangvoorziening, VIB, vluchtroutes, draagbaar blustoestel)\n• C: Extra bij niveau C (o.a. WBDBO 60/90 min, noodplan, nooddouche)\n• B: Extra bij niveau B (o.a. brandoverslagpreventie, rookafvoer, vakindeling)\n• A: Extra bij niveau A (o.a. brandwerende constructie, automatische branddetectie, VBB)',
        'icon': '🛡️',
    },
    'seveso': {
        'titel': 'Seveso III',
        'tekst': 'De Seveso-richtlijn (BRZO in Nederland) is van toepassing op inrichtingen met gevaarlijke stoffen boven bepaalde drempels. Ondergrens: 5.000 kg (gevaarlijke stoffen) of 10.000 kg (brandbare vloeistoffen/gassen). Boven de bovengrens geldt de volledige vergunningplicht.',
        'icon': '🏭',
    },
    'bal': {
        'titel': 'Besluit externe veiligheid inrichtingen (Bvi)',
        'tekst': 'Het Bvi (voorheen Besluit externe veiligheid) verplicht tot het nemen van maatregelen om externe veiligheid te waarborgen bij opslag van gevaarlijke stoffen ≥1.000 kg. Omvat risicobeoordeling en maatregelen tegen aanrijding, lekkage en brand.',
        'icon': '📋',
    },
    'aggregatie': {
        'titel': 'Aggregatie',
        'tekst': 'Stoffen worden per ADR-klasse geaggregeerd. Bij klasse 8 brandbare stoffen tellen alle klasse 8 stoffen samen mee. Bij ≥1.000 kg totaal (catchall) gelden altijd de basisvoorschriften van PGS 15.',
        'icon': '📊',
    },
}

# Maatregel-toelichtingen
MAATREGELEN_TOELICHTING = {k: {'label': v[0], 'omschrijving': v[1]} for k, v in MAATREGELEN_ALLE.items()}


# ═══════════════════════════════════════════════════════════════════

# Scheidingsgroepen volgens PGS 15:2025
SCHEIDINGSGROEPEN = {
    'SG1': {
        'naam': 'Ontvlambare vloeistoffen',
        'omschrijving': 'ADR klasse 3 vloeistoffen, klasse 4.1 vloeibaar, klasse 6.1 ontvlambaar vloeibaar',
        'adr_klassen': ['3', '4.1', '6.1'],
        'brandbaar': True,
    },
    'SG2': {
        'naam': 'Brandbevorderende stoffen',
        'omschrijving': 'ADR klasse 5.1, klasse 5.2, klasse 4.1 vast (niet SG1)',
        'adr_klassen': ['5.1', '5.2'],
        'brandbaar': True,
    },
    'SG3': {
        'naam': 'Ontploffingsgevaarlijke stoffen',
        'omschrijving': 'ADR klasse 1, ontploffingsgevaarlijke stoffen',
        'adr_klassen': ['1'],
        'brandbaar': True,
    },
    'SG4': {
        'naam': 'Giftige stoffen',
        'omschrijving': 'ADR klasse 6.1 (niet ontvlambaar), klasse 6.2',
        'adr_klassen': ['6.1', '6.2'],
        'brandbaar': False,
    },
    'SG5': {
        'naam': 'Corrosieve stoffen',
        'omschrijving': 'ADR klasse 8',
        'adr_klassen': ['8'],
        'brandbaar': False,
    },
    'SG6': {
        'naam': 'Overige gevaarlijke stoffen',
        'omschrijving': 'ADR klasse 9, overige PGS 15-stoffen',
        'adr_klassen': ['9'],
        'brandbaar': False,
    },
}

# Scheidingsmatrix: welke groepen MOETEN gescheiden worden
# True = scheiden verplicht, False = scheiden niet verplicht (mag samen)
SCHEIDINGSMATRIX = {
    ('SG1','SG1'): False,  # Ontvlambare vloeistoffen mogen bij elkaar (wel per subgroep)
    ('SG1','SG2'): True,   # Ontvlambare + brandbevorderend = scheiden
    ('SG1','SG3'): True,   # Ontvlambare + ontploffingsgevaarlijk = scheiden
    ('SG1','SG4'): False,  # Ontvlambare + giftig = mag samen (wel letten op toxiciteit bij brand)
    ('SG1','SG5'): False,  # Ontvlambare + corrosief = mag samen
    ('SG1','SG6'): False,  # Ontvlambare + overig = mag samen
    ('SG2','SG2'): False,  # Brandbevorderend mag bij elkaar
    ('SG2','SG3'): True,   # Brandbevorderend + ontploffingsgevaarlijk = scheiden
    ('SG2','SG4'): True,   # Brandbevorderend + giftig = scheiden (giftige gassen bij reactie)
    ('SG2','SG5'): True,   # Brandbevorderend + corrosief = scheiden (bijv. zuur + oxidator)
    ('SG2','SG6'): False,  # Brandbevorderend + overig = mag samen
    ('SG3','SG3'): True,   # Ontploffingsgevaarlijk = altijd scheiden
    ('SG3','SG4'): True,   # Ontploffingsgevaarlijk + giftig = scheiden
    ('SG3','SG5'): True,   # Ontploffingsgevaarlijk + corrosief = scheiden
    ('SG3','SG6'): True,   # Ontploffingsgevaarlijk + overig = scheiden
    ('SG4','SG4'): False,  # Giftig mag bij elkaar
    ('SG4','SG5'): True,   # Giftig + corrosief = scheiden (bijv. zuur + cyaanide → giftig gas)
    ('SG4','SG6'): False,  # Giftig + overig = mag samen
    ('SG5','SG5'): False,  # Corrosief mag bij elkaar
    ('SG5','SG6'): False,  # Corrosief + overig = mag samen
    ('SG6','SG6'): False,  # Overig mag bij elkaar
}

def get_scheidingsgroep(adr_klasse, is_cmr=False, is_acute_tox=False):
    """Bepaal de scheidingsgroep op basis van ADR-klasse en CLP-eigenschappen."""
    if not adr_klasse:
        return None
    for sg_id, sg in SCHEIDINGSGROEPEN.items():
        if adr_klasse in sg['adr_klassen']:
            return sg_id
    return 'SG6'  # fallback: overige


def check_scheiding(stoffen):
    """Check of stoffen gescheiden moeten worden volgens PGS 15 Tabel 8."""
    result = {
        'groepen': {},           # sg_id -> {naam, stoffen: [...]}
        'conflicten': [],        # [{stof1, stof2, groep1, groep2, reden}]
        'toelichting': [],       # algemene scheidingsadviezen
    }

    # Groepeer stoffen per scheidingsgroep
    for s in stoffen:
        sg = get_scheidingsgroep(s.get('adr_klasse', ''), s.get('is_cmr', 0), s.get('is_acute_tox', 0))
        if sg:
            if sg not in result['groepen']:
                result['groepen'][sg] = {
                    'id': sg,
                    'naam': SCHEIDINGSGROEPEN[sg]['naam'],
                    'omschrijving': SCHEIDINGSGROEPEN[sg]['omschrijving'],
                    'stoffen': [],
                }
            result['groepen'][sg]['stoffen'].append(s.get('naam', 'Onbekend'))

    # Check conflicten tussen alle paren
    checked = set()
    for s1 in stoffen:
        sg1 = get_scheidingsgroep(s1.get('adr_klasse', ''))
        if not sg1:
            continue
        for s2 in stoffen:
            if s1.get('id') == s2.get('id'):
                continue
            pair = tuple(sorted([s1.get('id'), s2.get('id')]))
            if pair in checked:
                continue
            checked.add(pair)

            sg2 = get_scheidingsgroep(s2.get('adr_klasse', ''))
            if not sg2:
                continue

            key = tuple(sorted([sg1, sg2]))
            if SCHEIDINGSMATRIX.get(key, False):
                reden = f"{SCHEIDINGSGROEPEN[sg1]['naam']} + {SCHEIDINGSGROEPEN[sg2]['naam']} vormen een onverenigbare combinatie"
                result['conflicten'].append({
                    'stof1': s1.get('naam'),
                    'stof2': s2.get('naam'),
                    'groep1': sg1,
                    'groep2': sg2,
                    'reden': reden,
                })

    # Algemene toelichting
    if result['conflicten']:
        result['toelichting'].append(
            'Volgens PGS 15:2025 Tabel 8 moeten onverenigbare stoffen gescheiden worden opgeslagen. '
            'Dit kan door middel van afstand, scheidingswanden of afzonderlijke opslagruimten.'
        )
    if not result['conflicten'] and len(stoffen) > 1:
        result['toelichting'].append(
            'De opgegeven stoffen vormen geen onverenigbare combinatie volgens PGS 15 Tabel 8. '
            'Let op: dit is een basiscontrole. Raadpleeg altijd de volledige PGS 15 scheidingstabel voor specifieke uitzonderingen.'
        )

    return result


def check_aggregatie(stoffen):
    """Analyseer aggregatie-effecten: stoffen per ADR-klasse die samen niveau opschuiven."""
    result = {
        'per_klasse': {},
        'opschuivingen': [],  # [{klasse, totaal, huidig_niveau, drempel_volgende, verschil}]
        'advies': [],
    }

    # Groepeer per ADR-klasse
    klassen = {}
    for s in stoffen:
        k = s.get('adr_klasse', '')
        if k:
            if k not in klassen:
                klassen[k] = {'totaal': 0, 'stoffen': []}
            klassen[k]['totaal'] += s.get('hoeveelheid', 0)
            klassen[k]['stoffen'].append(s.get('naam'))

    # Bepaal niveau per klasse
    niveau_order = {'A': 4, 'B': 3, 'C': 2, 'Buiten scope': 1}
    for k, info in klassen.items():
        # Bepaal zwaarste VG in deze klasse
        vg_order = {'I': 1, 'II': 2, 'III': 3}
        zwaarste_vg = 'III'
        for s in stoffen:
            if s.get('adr_klasse') == k:
                v = s.get('vg', 'III')
                if vg_order.get(v, 99) < vg_order.get(zwaarste_vg, 99):
                    zwaarste_vg = v

        nivo = determine_niveau(k, zwaarste_vg, info['totaal'])
        result['per_klasse'][k] = {
            'totaal': info['totaal'],
            'aantal': len(info['stoffen']),
            'stoffen': info['stoffen'],
            'zwaarste_vg': zwaarste_vg,
            'niveau': nivo,
        }

        # Check of toevoegen van deze stof het niveau opschuift
        for s in stoffen:
            if s.get('adr_klasse') != k:
                continue
            zonder = info['totaal'] - s.get('hoeveelheid', 0)
            if zonder > 0:
                nivo_zonder = determine_niveau(k, zwaarste_vg, zonder)
                if niveau_order.get(nivo, 0) > niveau_order.get(nivo_zonder, 0):
                    result['opschuivingen'].append({
                        'klasse': k,
                        'stof': s.get('naam'),
                        'hoeveelheid': s.get('hoeveelheid'),
                        'totaal_met': info['totaal'],
                        'totaal_zonder': zonder,
                        'niveau_met': nivo,
                        'niveau_zonder': nivo_zonder,
                    })

    # Catch-all check
    totaal = sum(s.get('hoeveelheid', 0) for s in stoffen)
    if totaal >= 1000:
        result['advies'].append(
            f"Totaal opslag ({totaal:.0f} kg) overschrijdt de catch-all drempel van 1.000 kg. "
            f"PGS 15 is van toepassing op alle gevaarlijke stoffen, ongeacht de ADR-klasse."
        )

    # Specifieke aggregatie-adviezen
    for k, info in result['per_klasse'].items():
        if info['aantal'] > 1:
            result['advies'].append(
                f"ADR klasse {k}: {info['aantal']} stoffen ({info['totaal']:.0f} kg totaal) "
                f"bepalen gezamenlijk niveau {info['niveau']}. "
                f"Individuele stoffen: {', '.join(info['stoffen'])}."
            )

    return result


def check_synergie(stoffen):
    """Analyseer synergie-effecten: CMR + Acute Tox, brandbaar + CMR, etc."""
    result = {
        'vondsten': [],
        'extra_eisen': [],
        'advies': [],
    }

    has_cmr = [s for s in stoffen if s.get('is_cmr')]
    has_acute_tox = [s for s in stoffen if s.get('is_acute_tox')]
    has_brandbaar = [s for s in stoffen if s.get('adr_klasse') in ('3', '4.1', '4.2', '4.3', '5.1', '5.2')]
    has_corrosief = [s for s in stoffen if s.get('adr_klasse') == '8']

    # CMR + brandbaar = extra voorzichtig bij brand
    if has_cmr and has_brandbaar:
        cmr_names = [s.get('naam') for s in has_cmr]
        brand_names = [s.get('naam') for s in has_brandbaar]
        result['vondsten'].append({
            'type': 'CMR + Brandbaar',
            'icon': '☢️',
            'stoffen': cmr_names + brand_names,
            'beschrijving': (
                f"CMR-stoffen ({', '.join(cmr_names)}) in combinatie met brandbare stoffen "
                f"({', '.join(brand_names)}) vormen een verhoogd risico. Bij brand kunnen CMR-stoffen "
                f"vrijkomen als giftige rookgassen. Extra aandacht voor: rook- en warmteafvoer, "
                f"branddetectie, en evacuatieprocedure rekening houdend met giftige rook."
            ),
            'impact': 'high',
        })

    # CMR + corrosief = giftige gassen bij reactie
    if has_cmr and has_corrosief:
        cmr_names = [s.get('naam') for s in has_cmr]
        corr_names = [s.get('naam') for s in has_corrosief]
        result['vondsten'].append({
            'type': 'CMR + Corrosief',
            'icon': '☠️',
            'stoffen': cmr_names + corr_names,
            'beschrijving': (
                f"CMR-stoffen ({', '.join(cmr_names)}) in combinatie met corrosieve stoffen "
                f"({', '.join(corr_names)}) kunnen bij menging giftige gassen vormen. "
                f"Zorg voor strikte scheiding en adequate ventilatie. "
                f"Denk aan bijv. zuur + cyaanide → waterstofcyaanidegas."
            ),
            'impact': 'high',
        })

    # Acute Tox + brandbaar = giftige rook bij brand
    if has_acute_tox and has_brandbaar:
        tox_names = [s.get('naam') for s in has_acute_tox]
        brand_names = [s.get('naam') for s in has_brandbaar]
        result['vondsten'].append({
            'type': 'Acute Tox + Brandbaar',
            'icon': '💨',
            'stoffen': tox_names + brand_names,
            'beschrijving': (
                f"Stoffen met acute toxiciteit ({', '.join(tox_names)}) in combinatie met brandbare "
                f"stoffen ({', '.join(brand_names)}) kunnen bij brand giftige rookgassen verspreiden. "
                f"Extra eisen: uitgebreider noodplan, hulpdiensten informeren over giftige rook, "
                f"en evacuatieroute buiten de rookverspreiding."
            ),
            'impact': 'medium',
        })

    # Meerdere Acute Tox stoffen = cumulatief effect
    if len(has_acute_tox) > 1:
        names = [s.get('naam') for s in has_acute_tox]
        cats = [f"{s.get('naam')} (Cat. {s.get('acute_tox_cat','?')})" for s in has_acute_tox]
        result['vondsten'].append({
            'type': 'Cumulatieve Acute Toxiciteit',
            'icon': '⚠️',
            'stoffen': names,
            'beschrijving': (
                f"Meerdere acuut toxische stoffen: {', '.join(cats)}. "
                f"PGS 15 hanteert een cumulatiebeginsel: stoffen met Acute Tox. Cat. 1-3 "
                f"worden gezamenlijk opgeteld bij de CLP-route. De gecombineerde hoeveelheid "
                f"bepaalt het vereiste veiligheidsniveau."
            ),
            'impact': 'medium',
        })

    # Veel stoffen = hoger complexiteitsniveau
    if len(stoffen) >= 5:
        result['advies'].append(
            f"Met {len(stoffen)} stoffen neemt de complexiteit van de opslag toe. "
            f"Overweeg een indeling in opslagzones per risicocategorie en zorg voor "
            f"een gedetailleerd intern noodplan."
        )

    # Extra eisen afleiden
    if has_cmr:
        result['extra_eisen'].append('CMR-stoffen vereisen extra labelvoorschriften en registratie (ARBO-catalogus)')
    if has_acute_tox:
        result['extra_eisen'].append('Acuut toxische stoffen vereisen nooddouche/oogspoeling binnen 10 seconden (M115)')
    if has_brandbaar and has_corrosief:
        result['extra_eisen'].append('Brandbare + corrosieve stoffen: specifieke blusmiddelen vereist (geen water bij bepaalde reacties)')

    if not result['vondsten']:
        result['advies'].append('Geen specifieke synergie-effecten gevonden tussen de opgegeven stoffen.')

    return result

# ROUTES
# ═══════════════════════════════════════════════════════════════════

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/info/<aspect>')
def api_info(aspect):
    """Return explanation for a scan aspect."""
    if aspect in TOELICHTINGEN:
        return jsonify(TOELICHTINGEN[aspect])
    if aspect.startswith('M') and aspect in MAATREGELEN_TOELICHTING:
        info = MAATREGELEN_TOELICHTING[aspect]
        return jsonify({'titel': info['label'], 'tekst': info['omschrijving'], 'icon': '🔧'})
    return jsonify({'error': 'Onbekend aspect'}), 404

@app.route('/api/info/maatregelen/<code>')
def api_info_maatregel(code):
    """Return explanation for a specific measure."""
    if code in MAATREGELEN_TOELICHTING:
        info = MAATREGELEN_TOELICHTING[code]
        return jsonify({'titel': info['label'], 'tekst': info['omschrijving'], 'icon': '🔧'})
    return jsonify({'error': 'Onbekende maatregel'}), 404

@app.route('/api/lookup', methods=['POST'])
def api_lookup():
    data = request.get_json()
    cas = data.get('cas', '').strip()
    naam = data.get('naam', '').strip()
    query = data.get('query', '').strip() or cas or naam
    if not query or len(query) < 2:
        return jsonify({'error': 'Geen query opgegeven'}), 400

    result = unified_lookup(cas=cas, naam=naam if not cas else query)
    if not result:
        # Fallback: try PubChem only
        pc = lookup_pubchem(query)
        if pc:
            pc['sources'] = ['PubChem']
            return jsonify(pc)
        return jsonify({'error': 'Stof niet gevonden'}), 404

    return jsonify(result)

@app.route('/api/stoffen', methods=['GET'])
def api_get_stoffen():
    sid = session.get('session_id', 'default')
    db = get_db()
    rows = db.execute('SELECT * FROM stoffen WHERE session_id=? ORDER BY id', (sid,)).fetchall()
    db.close()
    return jsonify([dict(r) for r in rows])

@app.route('/api/stoffen', methods=['POST'])
def api_add_stof():
    data = request.get_json()
    sid = session.get('session_id')
    if not sid:
        sid = session['session_id'] = os.urandom(8).hex()
    db = get_db()
    db.execute(
        'INSERT INTO stoffen (session_id,cas,naam,hoeveelheid,eenheid,vorm,locatie,adr_klasse,vg,clp_classificatie,h_zinnen,is_cmr,cmr_type,is_acute_tox,acute_tox_cat,is_brandbaar_k8,flashpoint) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
        (sid, data.get('cas',''), data.get('naam',''), data.get('hoeveelheid',0),
         'kg', data.get('vorm','vloeistof'), data.get('locatie',''),
         data.get('adr_klasse',''), data.get('vg',''),
         data.get('clp_classificatie',''), data.get('h_zinnen',''),
         1 if data.get('is_cmr') else 0, data.get('cmr_type',''),
         1 if data.get('is_acute_tox') else 0, data.get('acute_tox_cat',''),
         1 if data.get('is_brandbaar_k8') else 0, data.get('flashpoint')))
    db.commit()
    stof_id = db.execute('SELECT last_insert_rowid()').fetchone()[0]
    db.close()
    return jsonify({'id': stof_id, 'status': 'ok'})

@app.route('/api/stoffen/<int:stof_id>', methods=['DELETE'])
def api_delete_stof(stof_id):
    db = get_db()
    db.execute('DELETE FROM stoffen WHERE id=?', (stof_id,))
    db.commit()
    db.close()
    return jsonify({'status': 'ok'})

@app.route('/api/scan', methods=['POST'])
def api_scan():
    sid = session.get('session_id', 'default')
    db = get_db()
    rows = db.execute('SELECT * FROM stoffen WHERE session_id=? ORDER BY id', (sid,)).fetchall()
    db.close()
    stoffen = [dict(r) for r in rows]
    if not stoffen:
        return jsonify({'error': 'Voeg eerst stoffen toe'}), 400
    result = run_scan(stoffen, opslag_type=request.json.get('opslag_type', 'binnen') if request.is_json else 'binnen')
    # Safety: ensure no float('inf') or NaN in JSON
    def sanitize(obj):
        if isinstance(obj, float):
            if obj == float('inf') or obj == float('-inf') or obj != obj:
                return None
            return round(obj, 4)
        if isinstance(obj, dict):
            return {k: sanitize(v) for k, v in obj.items()}
        if isinstance(obj, list):
            return [sanitize(v) for v in obj]
        return obj
    out = sanitize(result)
    # Voeg toelichtingen mee voor frontend
    out['toelichtingen'] = TOELICHTINGEN

    # Stoffencombinatieanalyse
    out['combinatie'] = {
        'scheiding': check_scheiding(stoffen),
        'aggregatie': check_aggregatie(stoffen),
        'synergie': check_synergie(stoffen),
    }

    return jsonify(out)

@app.route('/api/rapport/docx', methods=['POST'])
def api_rapport_docx():
    from docx import Document
    from docx.shared import Pt, Cm, RGBColor, Inches, Emu
    from docx.enum.text import WD_ALIGN_PARAGRAPH
    from docx.enum.table import WD_TABLE_ALIGNMENT
    from docx.enum.section import WD_ORIENT
    from docx.oxml.ns import nsdecls, qn
    from docx.oxml import parse_xml
    from datetime import datetime

    data = request.get_json()
    result = data.get('result', {})
    if not result:
        return jsonify({'error': 'Geen scan data'}), 400

    bedrijfsnaam = result.get('bedrijfsnaam', 'Generiek')
    scan_datum = datetime.now().strftime('%d-%m-%Y')
    per_stof = result.get('per_stof', [])
    agg = result.get('aggregatie', {})
    combi = result.get('combinatie', {})
    samenvatting = result.get('aggregatie_samenvatting', {})
    voorstel = result.get('opslagvoorstel', {})
    wat_als_data = result.get('wat_als', [])
    gekozen_opslag = result.get('opslag_type', 'binnen')
    nivo_agg = agg.get('geaggregeerd_niveau', '-')

    # ── Brand kleuren ──
    CLR_PRIMARY = RGBColor(0x00, 0x33, 0x66)
    CLR_DARK = RGBColor(0x1F, 0x29, 0x37)
    CLR_SECONDARY = RGBColor(0x37, 0x41, 0x51)
    CLR_WHITE = RGBColor(0xFF, 0xFF, 0xFF)
    CLR_SUCCESS = RGBColor(0x00, 0xA8, 0x59)
    CLR_WARNING = RGBColor(0xF5, 0x9E, 0x0B)
    CLR_ERROR = RGBColor(0xEF, 0x44, 0x44)
    CLR_INFO = RGBColor(0x3B, 0x82, 0xF6)
    CLR_LIGHT = RGBColor(0xF0, 0xF4, 0xF8)
    CLR_CAT = {'Basis': CLR_SUCCESS, 'C': CLR_INFO, 'B': CLR_WARNING, 'A': CLR_ERROR}

    def nivo_color(n):
        return {'A': CLR_ERROR, 'B': CLR_WARNING, 'C': CLR_INFO}.get(n, CLR_SECONDARY)

    doc = Document()

    # ── Stijlen ──
    style = doc.styles['Normal']
    style.font.name = 'Calibri'
    style.font.size = Pt(11)
    style.paragraph_format.space_after = Pt(6)
    style.paragraph_format.line_spacing = 1.15

    for level, (sz, clr) in enumerate([(18, CLR_PRIMARY), (15, CLR_PRIMARY), (13, CLR_DARK), (11, CLR_DARK)], 1):
        hs = doc.styles[f'Heading {level}']
        hs.font.name = 'Calibri'
        hs.font.size = Pt(sz)
        hs.font.bold = True
        hs.font.color.rgb = clr
        hs.paragraph_format.space_before = Pt(12)
        hs.paragraph_format.space_after = Pt(6)

    # ── Marges ──
    for section in doc.sections:
        section.top_margin = Cm(2.5)
        section.bottom_margin = Cm(2)
        section.left_margin = Cm(2.5)
        section.right_margin = Cm(2.5)

    # ── Helpers ──
    def add_para(text, bold=False, italic=False, size=11, color=None, align=None, space_after=6):
        p = doc.add_paragraph()
        r = p.add_run(text)
        r.font.name = 'Calibri'
        r.font.size = Pt(size)
        r.bold = bold
        r.italic = italic
        if color:
            r.font.color.rgb = color
        if align:
            p.alignment = align
        p.paragraph_format.space_after = Pt(space_after)
        return p

    def set_cell_shading(cell, color_hex):
        shading = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color_hex}"/>')
        cell._tc.get_or_add_tcPr().append(shading)

    def set_no_wrap(cell, no_wrap=False):
        tc = cell._tc
        tcPr = tc.get_or_add_tcPr()
        tcW = tcPr.find(qn('w:tcW'))
        if tcW is None:
            tcW = parse_xml(f'<w:tcW {nsdecls("w")} w:w="0" w:type="auto"/>')
            tcPr.append(tcW)
        v = 'true' if no_wrap else 'false'
        noWrap = tcPr.find(qn('w:noWrap'))
        if noWrap is None:
            noWrap = parse_xml(f'<w:noWrap {nsdecls("w")} w:val="{v}"/>')
            tcPr.append(noWrap)
        else:
            noWrap.set(qn('w:val'), v)

    def style_table(table, col_widths=None):
        table.alignment = WD_TABLE_ALIGNMENT.CENTER
        table.autofit = True
        # Allow layout across pages for long tables
        tbl = table._tbl
        tblPr = tbl.tblPr
        if tblPr is None:
            tblPr = parse_xml(f'<w:tblPr {nsdecls("w")}/>')
            tbl.insert(0, tblPr)
        # Set autofit
        existing = tblPr.find(qn('w:tblLayout'))
        if existing is None:
            tblPr.append(parse_xml(f'<w:tblLayout {nsdecls("w")} w:type="autofit"/>'))
        # noWrap OFF on all cells
        for row in table.rows:
            for cell in row.cells:
                set_no_wrap(cell, False)

    def make_header_row(table, headers, widths=None):
        hdr = table.rows[0]
        for i, txt in enumerate(headers):
            c = hdr.cells[i]
            c.text = ''
            p = c.paragraphs[0]
            r = p.add_run(str(txt))
            r.font.name = 'Calibri'
            r.font.size = Pt(9)
            r.font.bold = True
            r.font.color.rgb = CLR_WHITE
            set_cell_shading(c, '003366')
            set_no_wrap(c, False)
        style_table(table, widths)
        return table

    def add_data_row(table, values, highlight=None):
        row = table.add_row()
        for i, val in enumerate(values):
            c = row.cells[i]
            c.text = ''
            p = c.paragraphs[0]
            r = p.add_run(str(val) if val is not None else '-')
            r.font.name = 'Calibri'
            r.font.size = Pt(9)
            set_no_wrap(c, False)
        if highlight:
            for c in row.cells:
                set_cell_shading(c, highlight)
        return row

    def fmt_nivo(n):
        return str(n) if n and n not in ('Buiten scope', '', None) else '-'

    # ══════════════════════════════════════════════════════════════
    # VOORBLAD
    # ══════════════════════════════════════════════════════════════
    for _ in range(3):
        doc.add_paragraph()
    try:
        doc.add_picture('/root/projects/jg/assets/branding/jvg-logo-white-large.png', width=Cm(4))
        doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.LEFT
    except Exception:
        add_para('JvG Consultancy', bold=True, size=24, color=CLR_PRIMARY, align=WD_ALIGN_PARAGRAPH.LEFT)
    doc.add_paragraph()
    add_para('PGS 15:2025', bold=True, size=28, color=CLR_PRIMARY)
    add_para('Compliance Toetsing', bold=True, size=22, color=CLR_PRIMARY)
    doc.add_paragraph()
    if bedrijfsnaam != 'Generiek':
        add_para(bedrijfsnaam, bold=True, size=16, color=CLR_DARK)
        doc.add_paragraph()
    add_para(f'Datum van analyse: {scan_datum}', size=12, color=CLR_SECONDARY)
    add_para(f'Aantal stoffen in scope: {len(per_stof)}', size=12, color=CLR_SECONDARY)
    add_para(f'Totaal opslag: {agg.get("totaal_kg", 0):.1f} kg', size=12, color=CLR_SECONDARY)
    add_para(f'Geaggregeerd veiligheidsniveau: {nivo_agg}', size=12, color=nivo_color(nivo_agg))
    doc.add_paragraph()
    doc.add_paragraph()
    add_para('Vertrouwelijk — JvG Consultancy', italic=True, size=10, color=CLR_SECONDARY)
    doc.add_page_break()

    # ══════════════════════════════════════════════════════════════
    # HEADER / FOOTER
    # ══════════════════════════════════════════════════════════════
    section = doc.sections[0]
    section.header.is_linked_to_previous = False
    hp = section.header.paragraphs[0]
    hp.alignment = WD_ALIGN_PARAGRAPH.LEFT
    try:
        run = hp.add_run()
        run.add_picture('/root/projects/jg/assets/branding/jvg-logo-white-medium.png', width=Cm(1.8), height=Cm(1.6))
    except Exception:
        r = hp.add_run('JvG Consultancy')
        r.font.size = Pt(8)
        r.font.color.rgb = CLR_SECONDARY
    # Right side header text
    hp2 = section.header.add_paragraph()
    hp2.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    r2 = hp2.add_run(f'PGS 15:2025 Compliance Toetsing — {bedrijfsnaam}')
    r2.font.size = Pt(8)
    r2.font.color.rgb = CLR_SECONDARY
    r2.font.name = 'Calibri'

    section.footer.is_linked_to_previous = False
    fp = section.footer.paragraphs[0]
    fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = fp.add_run(f'PGS 15:2025 Publicatie 565 — {scan_datum} — Vertrouwelijk')
    r.font.size = Pt(8)
    r.font.color.rgb = CLR_SECONDARY
    r.font.name = 'Calibri'
    fp2 = section.footer.add_paragraph()
    fp2.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r2 = fp2.add_run('Pagina ')
    r2.font.size = Pt(8)
    r2.font.color.rgb = CLR_SECONDARY
    fldChar1 = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="begin"/>')
    instrText = parse_xml(f'<w:instrText {nsdecls("w")} xml:space="preserve"> PAGE </w:instrText>')
    fldChar2 = parse_xml(f'<w:fldChar {nsdecls("w")} w:fldCharType="end"/>')
    fp2.add_run()._r.append(fldChar1)
    fp2.add_run()._r.append(instrText)
    fp2.add_run()._r.append(fldChar2)

    # ══════════════════════════════════════════════════════════════
    # 1. INLEIDING & SCOPE
    # ══════════════════════════════════════════════════════════════
    doc.add_heading('1. Inleiding & Scope', level=1)
    add_para('Doel van de toetsing', bold=True, size=12, color=CLR_PRIMARY)
    add_para(f'Dit rapport bevat de PGS 15:2025 compliance toetsing voor {bedrijfsnaam}. De toetsing bepaalt '
             'per stof het vereiste veiligheidsniveau op basis van ADR- en CLP-classificering, deelt '
             'verplichte maatregelen toe volgens doelstellingen en scenario\'s, en analyseert stoffencombinatie-effecten '
             'inclusief aggregatie, scheiding en synergie. Het rapport sluit af met een voorstel voor gezamenlijke opslag.')
    doc.add_paragraph()
    add_para('Referentiekader', bold=True, size=12, color=CLR_PRIMARY)
    add_para('Publicatiegroep Stoffen (PGS) 15:2025, Publicatie 565 — "Opslag van pakketten vloeistoffen". '
             'Deze publicatie stelt veiligheidsniveaus (Basis, C, B, A) vast op basis van de hoeveelheid en '
             'gevaarsklasse van opgeslagen stoffen, en koppelt daar verplichte maatregelen aan.')
    doc.add_paragraph()
    add_para('Scope', bold=True, size=12, color=CLR_PRIMARY)
    in_scope_stoffen = [s for s in per_stof if s.get('strengste_niveau') not in ('Buiten scope', '', None)]
    out_scope_stoffen = [s for s in per_stof if s.get('strengste_niveau') in ('Buiten scope', '', None)]
    add_para(f'{len(per_stof)} stoffen zijn meegenomen in de analyse. Hiervan vallen {len(in_scope_stoffen)} binnen '
             f'de werkingssfeer van PGS 15:2025 en {len(out_scope_stoffen)} buiten scope.')
    if in_scope_stoffen:
        add_para('Stoffen in scope: ' + ', '.join([s['naam'] for s in in_scope_stoffen]), size=10, color=CLR_SECONDARY)
    if out_scope_stoffen:
        add_para('Stoffen buiten scope: ' + ', '.join([s['naam'] for s in out_scope_stoffen]), size=10, color=CLR_SECONDARY)
    doc.add_paragraph()
    add_para(f'Datum van analyse: {scan_datum}', size=10, color=CLR_SECONDARY)

    # ══════════════════════════════════════════════════════════════
    # 2. STOFFENOVERZICHT
    # ══════════════════════════════════════════════════════════════
    doc.add_heading('2. Stoffenoverzicht', level=1)
    add_para('Tabel 1: Overzicht alle stoffen', bold=True, size=11, color=CLR_DARK)
    doc.add_paragraph()
    t = doc.add_table(rows=1, cols=10)
    make_header_row(t, ['#', 'Stof', 'CAS', 'ADR', 'VG', 'Hoeveelheid', 'Eenheid', 'Vorm', 'Niveau', 'Status'])
    for idx, s in enumerate(per_stof, 1):
        nivo = s.get('strengste_niveau', '-')
        status = 'In scope' if nivo not in ('Buiten scope', '', None) else 'Buiten scope'
        add_data_row(t, [
            idx, s['naam'], s.get('cas', '-'),
            s.get('adr_klasse', '-'), s.get('vg', '-'),
            f"{s['hoeveelheid']:.1f}", s.get('eenheid', 'kg'),
            s.get('vorm', '-'), fmt_nivo(nivo), status
        ], highlight='FFF3CD' if status == 'Buiten scope' else None)
    doc.add_paragraph()

    # Aggregatie per ADR-klasse sub-tabel
    add_para('Tabel 2: Aggregatie per ADR-klasse', bold=True, size=11, color=CLR_DARK)
    doc.add_paragraph()
    per_klasse = agg.get('per_klasse', {})
    if per_klasse:
        t2 = doc.add_table(rows=1, cols=5)
        make_header_row(t2, ['ADR-klasse', 'Aantal stoffen', 'Totaal (kg)', 'Ondergrens (kg)', 'Bereikt niveau'])
        for k, info in per_klasse.items():
            add_data_row(t2, [
                k, info.get('aantal', '-'), f"{info.get('totaal', 0):.1f}",
                info.get('ondergrens', '-'), info.get('niveau', '-')
            ])
    else:
        add_para('Geen aggregatiegegevens beschikbaar.', italic=True, color=CLR_SECONDARY, size=10)
    doc.add_paragraph()

    # Totaal + catch-all
    add_para(f'Totaal gewicht: {agg.get("totaal_kg", 0):.1f} kg', bold=True, size=11)
    catchall = agg.get('catchall', False)
    add_para(f'Catch-all (totaal >= 1.000 kg): {"JA — Basismaatregelen verplicht" if catchall else "Nee — niet van toepassing"}',
             bold=True, color=CLR_ERROR if catchall else CLR_SUCCESS, size=11)

    doc.add_page_break()

    # ══════════════════════════════════════════════════════════════
    # 3. NIVEAUBEPALING PER STOF
    # ══════════════════════════════════════════════════════════════
    doc.add_heading('3. Niveaubepaling per Stof', level=1)
    add_para('Per stof wordt zowel de ADR-route als de CLP-route(n) bepaald. Het strengste niveau uit alle routes '
             'is het verplichte veiligheidsniveau.', size=10, color=CLR_SECONDARY)

    for idx, s in enumerate(per_stof, 1):
        nivo = s.get('strengste_niveau', '-')
        in_scope = nivo not in ('Buiten scope', '', None)

        doc.add_heading(f'3.{idx} {s["naam"]} ({s.get("cas", "-")})', level=2)

        # ADR-route info
        add_para('ADR-route', bold=True, size=11, color=CLR_DARK)
        adr_info = f'Klasse {s.get("adr_klasse", "-")} / VG {s.get("vg", "-")} — Ondergrens: {s.get("ondergrens", "-")} kg — '
        adr_info += f'Hoeveelheid: {s["hoeveelheid"]:.1f} kg' if s.get('hoeveelheid') else 'Hoeveelheid: -'
        adr_info += f' — Niveau: {fmt_nivo(s.get("adr_niveau"))}'
        add_para(adr_info, size=10)

        # CLP-versietabel (VERPLICHT per BEORDELINGSAANPAK §2.2)
        versies = s.get('versies', [])
        if versies:
            doc.add_paragraph()
            add_para('CLP/ADR Versietabel', bold=True, size=10, color=CLR_DARK)
            vt = doc.add_table(rows=1, cols=6)
            make_header_row(vt, ['Route', 'Ondergrens', 'Niveau C', 'Niveau B', 'Niveau A', 'Actueel'])
            for v in versies:
                def _fmt_v(x):
                    return 'N.v.t.' if x in ('N.v.t.', None, '-') else str(x)
                act = v.get('actueel')
                act_str = f'Niveau {act}' if act and act not in ('—', '', None) else 'Buiten scope'
                vals = [
                    str(v.get('route', '')), f"{v.get('ondergrens', '')} kg",
                    _fmt_v(v.get('C')), _fmt_v(v.get('B')), _fmt_v(v.get('A')), act_str
                ]
                hl = 'E8F5E9' if v.get('is_actueel') else None
                add_data_row(vt, vals, highlight=hl)
        else:
            add_para('Geen CLP-versietabel beschikbaar voor deze stof.', italic=True, size=9, color=CLR_SECONDARY)

        # Strengste route
        doc.add_paragraph()
        add_para(f'Strengste route: {s.get("strengste_route", "-")}', bold=True, size=11, color=CLR_PRIMARY)
        add_para(f'Vereist veiligheidsniveau: {fmt_nivo(nivo)}', bold=True, size=12, color=nivo_color(nivo))

        # CMR / Acute Tox
        if s.get('is_cmr'):
            add_para(f'CMR: {s.get("cmr_type", "")} — Vorm: {s.get("vorm", "-")} — CLP-ondergrens: {s.get("ondergrens", "-")} kg',
                     size=10, color=CLR_ERROR)
        if s.get('is_acute_tox'):
            add_para(f'Acute Toxiciteit Cat. {s.get("acute_tox_cat", "?")} — CLP-ondergrens: {s.get("ondergrens", "-")} kg',
                     size=10, color=CLR_WARNING)

        # Drempelwaarden-tabel
        dw_data = s.get('drempelwaarden', {})
        dw_routes = dw_data.get('routes', [])
        if dw_routes:
            add_para('Drempelwaarden', bold=True, size=11, color=CLR_DARK)
            add_para(f'Huidige hoeveelheid: {s.get("hoeveelheid", 0)} kg', size=10, color=CLR_SECONDARY)
            for route in dw_routes:
                doc.add_paragraph()
                dp = doc.add_paragraph()
                r = dp.add_run(f'{route["route_naam"]} — Huidig: {route["huidige_niveau"]}')
                r.font.bold = True
                r.font.size = Pt(10)
                r.font.color.rgb = CLR_PRIMARY
                dt = doc.add_table(rows=1, cols=4)
                make_header_row(dt, ['Niveau', 'Drempel', 'Status', 'Maatregelen'])
                for nivo in route['niveaus']:
                    is_huidig = nivo['niveau'] == route['huidige_niveau'] or (nivo['niveau'] == 'Basis' and route['huidige_niveau'] == 'Buiten scope')
                    is_bereikt = nivo.get('bereikt', False)
                    if nivo.get('drempel') is not None:
                        label = f'≥ {nivo["drempel"]} kg'
                    else:
                        label = 'N.v.t.'
                    if is_huidig:
                        status = '✅ HUIDIG'
                    elif is_bereikt:
                        status = 'Bereikt'
                    elif nivo.get('drempel') is not None:
                        extra = nivo['extra_needed']
                        status = f'+{extra:,} kg nodig' if extra >= 1000 else f'+{extra} kg nodig'
                    else:
                        status = 'N.v.t.'
                    m_codes = nivo.get('maatregelen', [])
                    m_text = ', '.join(m_codes[:12]) + (f' (+{len(m_codes)-12})' if len(m_codes) > 12 else '')
                    hl = 'E8F5E9' if is_huidig else None
                    add_data_row(dt, [nivo['niveau'], label, status, m_text], highlight=hl)

        if not in_scope:
            add_para(f'{s["naam"]} valt buiten de werkingssfeer van PGS 15:2025.', italic=True, color=CLR_SECONDARY)

        doc.add_paragraph()

    doc.add_page_break()

    # ══════════════════════════════════════════════════════════════
    # 3. SAMENSTEL MAATREGELEN (alle maatregelen, één keer)
    # ══════════════════════════════════════════════════════════════
    doc.add_heading('3. Samenstel Maatregelen', level=1)
    flat_samenstel = result.get('flat_measures_samenstel', [])

    if len(per_stof) > 1 and flat_samenstel:
        add_para(f'Alle unieke verplichte maatregelen voor het gehele samenstel ({len(per_stof)} stoffen).',
                 size=10, color=CLR_SECONDARY)
        mt = doc.add_table(rows=1, cols=4)
        make_header_row(mt, ['Code', 'Maatregel', 'Omschrijving', 'Van toepassing op'])
        for m in flat_samenstel:
            van_toep = f"{m.get('van_toepassing_op', 1)} van {len(per_stof)} stoffen"
            doel_text = ', '.join(m.get('doelstellingen', []))
            add_data_row(mt, [m['code'], m['titel'], m['toelichting'], van_toep])
        doc.add_paragraph()
    else:
        # Single stof: use flat_measures from first in-scope stof
        in_scope_stoffen = [s for s in per_stof if s.get('strengste_niveau','') not in ('Buiten scope', '', None)]
        if in_scope_stoffen:
            single_flat = in_scope_stoffen[0].get('flat_measures', [])
            add_para(f'Alle verplichte maatregelen voor {in_scope_stoffen[0]["naam"]}.',
                     size=10, color=CLR_SECONDARY)
            mt = doc.add_table(rows=1, cols=3)
            make_header_row(mt, ['Code', 'Maatregel', 'Omschrijving'])
            for m in single_flat:
                add_data_row(mt, [m['code'], m['titel'], m['toelichting']])
            doc.add_paragraph()
        else:
            add_para('Geen verplichte maatregelen.', italic=True, color=CLR_SECONDARY)

    doc.add_page_break()

    # ══════════════════════════════════════════════════════════════
    # BIJLAGE A: DOELSTELLINGEN D1-D13
    # ══════════════════════════════════════════════════════════════
    doc.add_heading('Bijlage A: Doelstellingen D1-D13', level=1)
    add_para('Referentie: per stof de van toepassing zijnde doelstellingen met gekoppelde scenario\'s en verplichte maatregelen.',
             size=10, color=CLR_SECONDARY)

    for idx, s in enumerate(per_stof, 1):
        nivo = s.get('strengste_niveau', '-')
        in_scope = nivo not in ('Buiten scope', '', None)
        compliance = s.get('compliance', [])

        doc.add_heading(f'A.{idx} {s["naam"]}', level=2)

        if not in_scope or not compliance:
            add_para('Niet van toepassing.', italic=True, color=CLR_SECONDARY)
            doc.add_paragraph()
            continue

        for dobj in compliance:
            dp = doc.add_paragraph()
            r = dp.add_run(f'{dobj["code"]} — {dobj["titel"]}')
            r.font.bold = True
            r.font.size = Pt(11)
            r.font.color.rgb = CLR_PRIMARY
            add_para(dobj.get('omschrijving', ''), size=10, color=CLR_SECONDARY)
            sc_text = ', '.join(dobj.get('scenario', []))
            if sc_text:
                add_para(f"Scenario's: {sc_text}", size=9, color=CLR_SECONDARY)
            ml = dobj.get('maatregelen', [])
            if ml:
                mt = doc.add_table(rows=1, cols=3)
                make_header_row(mt, ['Code', 'Maatregel', 'Toelichting'])
                for m in ml:
                    info = MAATREGELEN_TOELICHTING.get(m, {})
                    label = info.get('label', m)
                    omsch = info.get('omschrijving', '')
                    add_data_row(mt, [m, label, omsch])
                doc.add_paragraph()

    # ── Bijlage: Samenstel ──
    agg = result.get('aggregatie', {})
    agg_nivo = agg.get('geaggregeerd_niveau', '-')
    niveau_order = {'A': 4, 'B': 3, 'C': 2, 'Buiten scope': 1, 'Geen ADR-klasse': 0}
    agg_compliance = []
    if agg_nivo not in ('Buiten scope', '', None):
        from app import build_compliance
        # Bouw compliance voor het samenstel op geaggregeerd niveau
        # Gebruik de unie van ADR-klassen van alle stoffen
        adr_set = {}
        for s in per_stof:
            ak = s.get('adr_klasse', '')
            if ak:
                adr_set[ak] = s
        if adr_set:
            # Pak de zwaarste stof als representatief
            zwaarste = max(per_stof, key=lambda x: niveau_order.get(x.get('strengste_niveau', 'Buiten scope'), 0))
            agg_compliance = build_compliance(
                agg_nivo, opslag_type=gekozen_opslag,
                adr_klasse=zwaarste.get('adr_klasse', ''), vg=zwaarste.get('vg', ''),
                is_cmr=bool(zwaarste.get('is_cmr', 0)), cmr_type=zwaarste.get('cmr_type', ''),
                is_acute_tox=bool(zwaarste.get('is_acute_tox', 0)), acute_tox_cat=zwaarste.get('acute_tox_cat', ''),
                is_brandbaar_k8=bool(zwaarste.get('is_brandbaar_k8', 0)),
                clp_classificatie=zwaarste.get('clp_classificatie', '')
            )

    doc.add_heading('Bijlage A.2: Samenstel — Scenario\'s, Doelstellingen & Maatregelen', level=2)
    add_para(f'Geaggregeerd niveau: {agg_nivo}. Scenario\'s, doelstellingen en maatregelen voor het gehele samenstel.',
             size=10, color=CLR_SECONDARY)

    if agg_compliance:
        for dobj in agg_compliance:
            dp = doc.add_paragraph()
            r = dp.add_run(f'{dobj["code"]} — {dobj["titel"]}')
            r.font.bold = True
            r.font.size = Pt(11)
            r.font.color.rgb = CLR_PRIMARY
            add_para(dobj.get('omschrijving', ''), size=10, color=CLR_SECONDARY)
            sc_text = ', '.join(dobj.get('scenario', []))
            if sc_text:
                add_para(f"Scenario's: {sc_text}", size=9, color=CLR_SECONDARY)
            ml = dobj.get('maatregelen', [])
            if ml:
                mt = doc.add_table(rows=1, cols=3)
                make_header_row(mt, ['Code', 'Maatregel', 'Toelichting'])
                for m in ml:
                    info = MAATREGELEN_TOELICHTING.get(m, {})
                    add_data_row(mt, [m, info.get('label', m), info.get('omschrijving', '')])
                doc.add_paragraph()
    else:
        add_para('Geen samenstel-niveau van toepassing.', italic=True, color=CLR_SECONDARY)

    doc.add_page_break()

    # ══════════════════════════════════════════════════════════════
    # 4A. VERGELIJKING OPSLAGTYPES ("WAT ALS"-ANALYSE)
    # ══════════════════════════════════════════════════════════════
    if wat_als_data:
        type_labels = {'binnen': 'Binnenopslag', 'buiten': 'Buitenopslag', 'container': '(Tank)containers', 'tijdelijk': 'Tijdelijke opslag'}
        doc.add_heading('4A. Vergelijking Opslagtypes ("Wat Als"-analyse)', level=1)
        add_para(f'Gekozen opslagtype: {type_labels.get(gekozen_opslag, gekozen_opslag)}. Per stof wordt vergeleken hoeveel maatregelen per opslagtype gelden.',
                 size=10, color=CLR_SECONDARY)

        for ws in wat_als_data:
            doc.add_heading(f'{ws["naam"]}', level=2)
            wa = ws.get('wat_als', {})
            if not wa:
                continue

            vt = doc.add_table(rows=1, cols=4)
            make_header_row(vt, ['Opslagtype', 'Aantal maatregelen', 'Verschil vs. Binnenopslag', 'Impact'])
            for t in ['binnen', 'buiten', 'container', 'tijdelijk']:
                d = wa.get(t)
                if not d:
                    continue
                is_gekozen = t == gekozen_opslag
                label = f'{"✅ " if is_gekozen else ""}{d["label"]}{" (gekozen)" if is_gekozen else ""}'
                verschil = str(d['verschil']) if d['verschil'] != 0 else '—'
                impact = d['impact'].capitalize()
                add_data_row(vt, [label, str(d['aantal_maatregelen']), verschil, impact])

            # Detail per alternatief type
            for t in ['binnen', 'buiten', 'container', 'tijdelijk']:
                d = wa.get(t)
                if not d or t == gekozen_opslag:
                    continue
                parts = []
                if d.get('extra_maatregelen'):
                    parts.append(f'Extra maatregelen: {", ".join(d["extra_maatregelen"])}')
                if d.get('minder_maatregelen'):
                    parts.append(f'Minder maatregelen: {", ".join(d["minder_maatregelen"])}')
                if d.get('extra_doelstellingen'):
                    parts.append(f'Extra doelstellingen: {", ".join(d["extra_doelstellingen"])}')
                if parts:
                    add_para(f'{d["label"]}: {" | ".join(parts)}', size=9, color=CLR_SECONDARY)

            doc.add_paragraph()

        # Advies
        doc.add_paragraph()
        add_para('Advies', bold=True, size=11, color=CLR_DARK)
        add_para('Het opslagtype met het minste aantal maatregelen biedt de minste administratieve last, maar kan inhoudelijk niet altijd haalbaar zijn. Overweeg elk type op basis van de specifieke situatie en vergunningsvereisten.',
                 size=10, color=CLR_SECONDARY)
        doc.add_page_break()

    # ══════════════════════════════════════════════════════════════
    # 4. AGGREGATIE-ANALYSE
    # ══════════════════════════════════════════════════════════════
    doc.add_heading('4. Aggregatie-analyse', level=1)

    # Per ADR-klasse
    add_para('Tabel: Aggregatie per ADR-klasse', bold=True, size=11, color=CLR_DARK)
    doc.add_paragraph()
    if per_klasse:
        at = doc.add_table(rows=1, cols=6)
        make_header_row(at, ['ADR-klasse', 'Stoffen', 'Totaal (kg)', 'Ondergrens (kg)', 'Bereikt niveau', 'Opschuiving'])
        for k, info in per_klasse.items():
            opschuiving = info.get('opschuiving', '')
            add_data_row(at, [
                k, ', '.join(info.get('stoffen', [])), f"{info.get('totaal', 0):.1f}",
                info.get('ondergrens', '-'), info.get('niveau', '-'),
                opschuiving if opschuiving else '—'
            ])
    else:
        add_para('Geen aggregatiegegevens beschikbaar.', italic=True, color=CLR_SECONDARY, size=10)

    doc.add_paragraph()
    add_para(f'Geaggregeerd niveau: {fmt_nivo(nivo_agg)}', bold=True, size=12, color=nivo_color(nivo_agg))

    # Seveso/BRZO
    add_para(f'Seveso III / BRZO status: {result.get("seveso_status", result.get("seveso", "-"))}', bold=True, size=11,
             color=CLR_ERROR if 'Bovenste' in result.get('seveso', '') else CLR_SUCCESS)
    add_para(f'BAL Art. 3.27 status: {result.get("bal_status", result.get("bal", "-"))}', bold=True, size=11,
             color=CLR_ERROR if 'Vergunning' in result.get('bal', '') else CLR_SUCCESS)

    # Catch-all
    add_para(f'Catch-all (totaal >= 1.000 kg): {"JA" if catchall else "NEE"}', bold=True, size=11,
             color=CLR_ERROR if catchall else CLR_SUCCESS)

    # Niveau-opschuivingen
    agg_data = combi.get('aggregatie', {})
    opschuivingen = agg_data.get('opschuivingen', [])
    if opschuivingen:
        doc.add_paragraph()
        add_para('Niveau-opschuivingen door aggregatie:', bold=True, color=CLR_WARNING)
        for o in opschuivingen:
            add_para(f'  {o["stof"]} ({o["hoeveelheid"]:.0f} kg): individueel Niveau {o["niveau_zonder"]} '
                     f'→ geaggregeerd Niveau {o["niveau_met"]}', size=10)

    doc.add_page_break()

    # ══════════════════════════════════════════════════════════════
    # 7. VOORSTEL GEZAMENLIJKE OPSLAG
    # ══════════════════════════════════════════════════════════════
    doc.add_heading('5. Voorstel Gezamenlijke Opslag', level=1)

    if voorstel and voorstel.get('vakken'):
        add_para(voorstel.get('advies', 'Geen advies beschikbaar.'), bold=True, size=11)
        doc.add_paragraph()

        # Vakindeling
        for v in voorstel['vakken']:
            add_para(v.get('label', 'Vak'), bold=True, size=12, color=CLR_PRIMARY)
            add_para(f'Stoffen: {", ".join(v.get("stoffen", []))} — {v.get("totaal_kg", 0):.1f} kg', size=10)
            if v.get('eisen'):
                for e in v['eisen']:
                    add_para(f'  • {e}', size=9, color=CLR_SECONDARY)
            # CMR-stoffen apart vak
            if v.get('cmr'):
                add_para(f'  CMR-stoffen: {", ".join(v["cmr"])} — verplicht afgescheiden vak met aanvullende eisen',
                         size=9, color=CLR_ERROR)
            doc.add_paragraph()

        # Stoffen buiten scope
        if out_scope_stoffen:
            add_para('Stoffen buiten scope (niet opgenomen in opslagvoorstel):', bold=True, size=10, color=CLR_DARK)
            for s in out_scope_stoffen:
                add_para(f'  • {s["naam"]} — geen ADR-classificatie of hoeveelheid onder PGS 15-ondergrens', size=9, color=CLR_SECONDARY)
            doc.add_paragraph()

        # Scheidingsadvies
        scheiding = combi.get('scheiding', {})
        conflicten = scheiding.get('conflicten', [])
        if conflicten:
            add_para('Scheidingsadvies', bold=True, size=12, color=CLR_ERROR)
            add_para('De volgende stoffen MOETEN gescheiden worden opgeslagen:', size=10)
            for cf in conflicten:
                add_para(f'  {cf["stof1"]} + {cf["stof2"]}: {cf["reden"]}', size=9, color=CLR_ERROR)
            doc.add_paragraph()
        else:
            add_para('Scheidingsadvies: geen onverenigbare combinaties gevonden volgens PGS 15 Tabel 8.', size=10, color=CLR_SUCCESS)

        # Synergie-effecten
        synergie = combi.get('synergie', {})
        vondsten = synergie.get('vondsten', [])
        if vondsten:
            add_para('Synergie-effecten', bold=True, size=12, color=CLR_WARNING)
            for v in vondsten:
                clr = CLR_ERROR if v.get('impact') == 'high' else CLR_WARNING
                add_para(f'  {v.get("icon", "!")} {v.get("type", "")}: {v.get("beschrijving", "")}', size=9, color=clr)
            doc.add_paragraph()

        # Constructieve eisen
        if voorstel.get('constructieve_eisen'):
            add_para('Constructieve Eisen (niveau-afhankelijk)', bold=True, size=12, color=CLR_PRIMARY)
            for e in voorstel['constructieve_eisen']:
                add_para(f'  {"\u2610"} {e}', size=9, color=CLR_SECONDARY)
    else:
        add_para('Geen opslagvoorstel beschikbaar.', italic=True, color=CLR_SECONDARY)

    doc.add_page_break()

    # ══════════════════════════════════════════════════════════════
    # 8. CONCLUSIE & AANBEVELINGEN
    # ══════════════════════════════════════════════════════════════
    doc.add_heading('6. Conclusie & Aanbevelingen', level=1)

    # Samenvatting findings
    add_para('Samenvatting findings', bold=True, size=12, color=CLR_PRIMARY)
    if nivo_agg in ('A', 'B'):
        add_para(f'Met een geaggregeerd veiligheidsniveau {nivo_agg} gelden de meest strenge eisen uit PGS 15:2025. '
                 f'Alle maatregelen van niveau Basis, C, B en {nivo_agg} zijn verplicht.')
    elif nivo_agg == 'C':
        add_para(f'Met een geaggregeerd veiligheidsniveau C gelden de maatregelen van niveau Basis en C.')
    else:
        add_para('De opgeslagen stoffen vallen binnen of buiten de werkingssfeer van PGS 15:2025.')

    if catchall:
        add_para('Door de catch-all (totaal >= 1.000 kg) zijn aanvullend de Basismaatregelen verplicht.', color=CLR_ERROR)

    doc.add_paragraph()

    # Prioriteerde actielijst
    add_para('Prioriteerde actielijst', bold=True, size=12, color=CLR_PRIMARY)
    actions = []
    if nivo_agg in ('A', 'B'):
        actions.append(('1', 'Implementeer alle verplichte maatregelen van niveau A/B (zie Hoofdstuk 3).', 'Directie'))
    if nivo_agg in ('A', 'B', 'C'):
        actions.append(('2', 'Implementeer alle verplichte maatregelen van niveau C.', 'HSEQ Manager'))
    actions.append(('3', 'Implementeer Basismaatregelen voor alle in-scope stoffen.', 'Facilitair'))
    if conflicten:
        actions.append(('4', 'Realiseer fysieke scheiding van onverenigbare stoffen (zie Hoofdstuk 5).', 'Facilitair'))
    actions.append(('5', 'Update RI&E en veiligheidsinstructies met bevindingen uit dit rapport.', 'HSEQ Manager'))
    if catchall:
        actions.append(('6', 'Documenteer catch-all toepassing en basismaatregelen in beheerplan.', 'HSEQ Manager'))

    if actions:
        at = doc.add_table(rows=1, cols=3)
        make_header_row(at, ['#', 'Actie', 'Verantwoordelijke'])
        for prio, actie, verantwoordelijke in actions:
            add_data_row(at, [prio, actie, verantwoordelijke])

    doc.add_paragraph()
    add_para('Dit rapport is automatisch gegenereerd door de JvG Consultancy PGS 15:2025 Compliance Scanner. '
             'Raadpleeg altijd de volledige PGS 15:2025 Publicatie 565 voor de definitieve tekst van de voorschriften.',
             italic=True, size=9, color=CLR_SECONDARY)

    # ══════════════════════════════════════════════════════════════
    # BIJLAGE A — SCENARIO'S
    # ══════════════════════════════════════════════════════════════
    doc.add_page_break()
    doc.add_heading('Bijlage A — Scenario\'s', level=1)
    add_para('Overzicht van alle oorzaak- en gevolgscenario\'s uit PGS 15:2025 Hoofdstuk 4. Elk scenario beschrijft een concreet incidentpad.', size=10, color=CLR_SECONDARY)
    doc.add_paragraph()

    # Build reverse mappings
    s_to_d = {}
    s_to_m = {}
    for dc, dv in DOELSTELLING_MAP.items():
        for sc in dv.get('scenario', []):
            s_to_d.setdefault(sc, []).append(dc)
            for cat, mcodes in dv.get('maatregelen', {}).items():
                s_to_m.setdefault(sc, set()).update(mcodes)

    # Scenario uitleg in begrijpelijke taal
    s_uitleg = {
        'S1': 'Een voertuig (vorkheftruck, vrachtwagen) rijdt tegen verpakkingen of stellingen aan. Dit kan leiden tot lekkage of val van stoffen.',
        'S2': 'Een verpakking valt van een stelling, pallet of tijdens het verplaatsen. De impact kan de verpakking beschadigen waardoor de stof vrijkomt.',
        'S3': 'Een stelling of deel van de opslaghal bezwijgt onder de belasting. Alle verpakkingen die erop staan vallen en stoffen komen vrij.',
        'S4': 'Verpakkingen worden verkeerd gestapeld (te hoog, zware op lichte). De onderste verpakking faalt door overbelasting en de stof komt vrij.',
        'S5': 'Veroudering, UV-straling, corrosie of een reeds beschadigde verpakking leidt tot lekkage zonder externe oorzaak.',
        'S6': 'Een verpakking die niet bestand is tegen drukverschillen (bijv. door temperatuurwisseling) faalt door over- of onderdruk.',
        'S7': 'Bij het openen van een verpakking voor monstername wordt een fout gemaakt (verkeerd gereedschap, te ruw), waardoor de stof onbedoeld vrijkomt.',
        'S8': 'Een brand ontstaat in de opslagvoorziening zelf (bijv. door ontsteking van brandbare stoffen) en betrekt de opgeslagen gevaarlijke stoffen.',
        'S9': 'Een brand buiten de opslagvoorziening, maar binnen het terrein van de locatie, bereikt de opslag en betrekt de gevaarlijke stoffen.',
        'S10': 'Een brand van buiten de locatie (bijv. aangrenzend bedrijf, natuurbrand) bereikt de opslagvoorziening en betrekt de gevaarlijke stoffen.',
        'S11': 'De primaire gevolg: na een incident komt een gevaarlijke stof daadwerkelijk vrij uit de verpakking. Dit is het startpunt voor verdere gevolgen.',
        'S12': 'De vrijgekomen stof verspreidt zich via damp/gas in de lucht of als vloeistof over de grond, buiten de directe opslaglocatie.',
        'S13': 'De vrijgekomen stoffen of omgevingsbrand leidt tot een brand in de opslagvoorziening met alle gevolgen van dien.',
        'S14': 'De brand in de opslag escaleert naar andere gebouwen, opslageenheden of activiteiten binnen de locatiebegrenzing.',
        'S15': 'De brand escaleert naar de omgeving buiten de locatiebegrenzing, met mogelijke gevolgen voor omwonenden en derden.',
        'S16': 'Stofspecifieke scenario\'s die niet onder S1-S15 vallen, zoals zelfontleding, polymerisatie, of specifieke reacties van de opgeslagen stof.',
    }

    at = doc.add_table(rows=1, cols=4)
    make_header_row(at, ['Code', 'Omschrijving', 'Uitleg', 'Doelstellingen + maatregelen'])
    for s_code in sorted(SCENARIOS.keys(), key=lambda x: int(x[1:])):
        gerelateerd = ', '.join(sorted(s_to_d.get(s_code, [])))
        maatregelen = ', '.join(sorted(s_to_m.get(s_code, set())))[:200]
        add_data_row(at, [s_code, SCENARIOS[s_code], s_uitleg.get(s_code, ''), f'{gerelateerd}\nMaatregelen: {maatregelen}'])
    style_table(at)

    # ══════════════════════════════════════════════════════════════
    # BIJLAGE B — DOELSTELLINGEN
    # ══════════════════════════════════════════════════════════════
    doc.add_page_break()
    doc.add_heading('Bijlage B — Doelstellingen', level=1)
    add_para('Overzicht van alle doelstellingen (D1-D13) met bijbehorende maatregelen per categorie.', size=10, color=CLR_SECONDARY)
    doc.add_paragraph()

    for dc in sorted(DOELSTELLING_MAP.keys(), key=lambda x: int(x[1:])):
        dv = DOELSTELLING_MAP[dc]
        doc.add_heading(f'{dc} — {dv["titel"]}', level=2)
        add_para(dv['omschrijving'], size=10, color=CLR_SECONDARY)

        # Scenario's
        sc_list = ', '.join(sorted(dv.get('scenario', [])))
        add_para(f'Relevante scenario\'s: {sc_list}', size=10, italic=True)

        # Maatregelen per categorie
        cat_labels = {
            'Basis': 'Basismaatregelen',
            'Binnen': 'Binnenopslag',
            'Buiten': 'Buitenopslag',
            'Container': '(Tank)container',
            'Tijdelijk': 'Tijdelijke opslag',
            'C': 'Niveau C', 'B': 'Niveau B', 'A': 'Niveau A',
            'BVK': 'Brandveiligheidsopslagkast',
            'ADR_2_gasflessen': 'ADR kl. 2 gasflessen',
            'ADR_2_spuitbussen': 'ADR kl. 2 spuitbussen/gaspatronen',
            'ADR_3_VG_I_II': 'ADR kl. 3 VG I/II',
            'ADR_4_1_VG_II_III': 'ADR kl. 4.1 VG II/III',
            'ADR_4_2_4_3_VG_III': 'ADR kl. 4.2/4.3 VG III',
            'ADR_4_3_VG_II_III': 'ADR kl. 4.3 VG II/III',
            'ADR_5_2': 'ADR kl. 5.2',
            'ADR_6_1_VG_I_II': 'ADR kl. 6.1 VG I/II',
            'ADR_6_2': 'ADR kl. 6.2',
            'acuut_toxisch': 'Acuut toxisch',
        }
        maatregelen = dv.get('maatregelen', {})
        if maatregelen:
            for cat, m_codes in maatregelen.items():
                if not m_codes:
                    continue
                label = cat_labels.get(cat, cat)
                add_para(f'{label}: {', '.join(sorted(m_codes))}', size=9)
        doc.add_paragraph()

    # ══════════════════════════════════════════════════════════════
    # BIJLAGE C — MAATREGELEN
    # ══════════════════════════════════════════════════════════════
    doc.add_page_break()
    doc.add_heading('Bijlage C — Maatregelen', level=1)
    add_para('Volledig overzicht van alle maatregelen uit PGS 15:2025 Hoofdstuk 7 met niveautoewijzing en doelstellingskoppeling.', size=10, color=CLR_SECONDARY)
    doc.add_paragraph()

    # Build reverse mapping: maatregel -> doelstellingen
    m_to_d = {}
    for dc, dv in DOELSTELLING_MAP.items():
        for cat, mcodes in dv.get('maatregelen', {}).items():
            for m in mcodes:
                m_to_d.setdefault(m, set()).add(dc)

    # Determine per M-code in welke niveaus het voorkomt
    def m_niveaus(code):
        nivs = set()
        for dc, dv in DOELSTELLING_MAP.items():
            for cat, mcodes in dv.get('maatregelen', {}).items():
                if code in mcodes:
                    if cat == 'Basis':
                        nivs.add('Basis')
                    elif cat in ('C', 'B', 'A'):
                        nivs.add(f'{cat}+')
                    else:
                        nivs.add('Context')
        return ', '.join(sorted(nivs)) if nivs else 'Basis'

    def m_sort_key(code):
        if code.startswith('MW'):
            return (0, int(code[2:]))
        return (1, int(code[1:]))

    sorted_measures = sorted(MAATREGELEN_ALLE.items(), key=lambda x: m_sort_key(x[0]))

    mt = doc.add_table(rows=1, cols=5)
    make_header_row(mt, ['Code', 'Titel', 'Toelichting', 'Niveau', 'Doelstellingen'])
    for m_code, (m_titel, m_oms) in sorted_measures:
        doelen = ', '.join(sorted(m_to_d.get(m_code, set())))
        add_data_row(mt, [m_code, m_titel, m_oms or '—', m_niveaus(m_code), doelen])
    style_table(mt)

    # ══════════════════════════════════════════════════════════════
    # SAVE
    # ══════════════════════════════════════════════════════════════
    versie = 'v2.0'
    fname = f'PGS15_Compliance_Toetsing_{bedrijfsnaam.replace(" ", "_")}_{versie}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.docx'

    deliverables_dir = '/root/projects/jg/2026-pgs15-scanner/deliverables/docx/'
    os.makedirs(deliverables_dir, exist_ok=True)
    doc.save(os.path.join(deliverables_dir, fname))

    data_dir = os.path.join(os.path.dirname(__file__), 'data')
    os.makedirs(data_dir, exist_ok=True)
    doc.save(os.path.join(data_dir, fname))

    log_dir = '/root/projects/jg/2026-pgs15-scanner/logs/'
    os.makedirs(log_dir, exist_ok=True)
    log_path = os.path.join(log_dir, 'changelog.md')
    with open(log_path, 'a') as lf:
        lf.write(f'\n| {versie} | {scan_datum} | DOCX rapport professioneel geüpgraded: {len(per_stof)} stoffen, niveau {nivo_agg}, bedrijfsnaam {bedrijfsnaam} |\n')

    return jsonify({'file': fname})


@app.route('/api/download/<filename>')
def api_download_file(filename):
    """Serve generated report files from the data directory."""
    data_dir = os.path.join(os.path.dirname(__file__), 'data')
    filepath = os.path.join(data_dir, filename)
    if not os.path.abspath(filepath).startswith(os.path.abspath(data_dir)):
        return jsonify({'error': 'Invalid filename'}), 400
    if not os.path.exists(filepath):
        return jsonify({'error': 'File not found'}), 404
    return send_file(filepath, as_attachment=True)



@app.route('/api/rapport/html', methods=['POST'])
def api_rapport_html():
    data = request.get_json()
    result = data.get('result', {})
    if not result:
        return jsonify({'error': 'Geen scan data'}), 400

    agg = result.get('aggregatie', {})
    stoffen = result.get('per_stof', [])
    combi = result.get('combinatie', {})
    now = datetime.now().strftime('%d-%m-%Y')

    # Bouw samenstel compliance voor bijlage
    na = agg.get('geaggregeerd_niveau', '-')
    agg_compliance = []
    niveau_order = {'A': 4, 'B': 3, 'C': 2, 'Buiten scope': 1, 'Geen ADR-klasse': 0}
    if na not in ('Buiten scope', '', None):
        zwaarste = max(stoffen, key=lambda x: niveau_order.get(x.get('strengste_niveau', 'Buiten scope'), 0))
        agg_compliance = build_compliance(
            na, opslag_type=data.get('opslag_type', data.get('gekozen_opslag_type', 'binnen')),
            adr_klasse=zwaarste.get('adr_klasse', ''), vg=zwaarste.get('vg', ''),
            is_cmr=bool(zwaarste.get('is_cmr', 0)), cmr_type=zwaarste.get('cmr_type', ''),
            is_acute_tox=bool(zwaarste.get('is_acute_tox', 0)), acute_tox_cat=zwaarste.get('acute_tox_cat', ''),
            is_brandbaar_k8=bool(zwaarste.get('is_brandbaar_k8', 0)),
            clp_classificatie=zwaarste.get('clp_classificatie', '')
        )

    logo_b64 = ''
    try:
        with open('/root/projects/jg/assets/branding/logo-base64.txt', 'r') as lf:
            logo_b64 = lf.read().strip()
    except Exception:
        pass

    def nbg(n): return {'A':'#FEF2F2','B':'#FFFBEB','C':'#EFF6FF'}.get(n,'#F8F9FA')
    def nfg(n): return {'A':'#991B1B','B':'#92400E','C':'#1E40AF'}.get(n,'#374151')
    def nbrd(n): return {'A':'#EF4444','B':'#F59E0B','C':'#3B82F6'}.get(n,'#E5E7EB')

    def mrows(mlist):
        out = ''
        for m in mlist:
            lbl = MAATREGELEN_TOELICHTING.get(m,{}).get('label', m)
            oms = MAATREGELEN_TOELICHTING.get(m,{}).get('omschrijving','')
            out += '<tr><td style="padding:8px 12px;border-bottom:1px solid #E5E7EB;font-weight:600;color:#003366;white-space:nowrap;">' + m + '</td>'
            out += '<td style="padding:8px 12px;border-bottom:1px solid #E5E7EB;">' + lbl + '</td>'
            out += '<td style="padding:8px 12px;border-bottom:1px solid #E5E7EB;font-size:12px;color:#374151;">' + oms + '</td></tr>'
        return out

    sh = ''
    for idx, s in enumerate(stoffen, 1):
        nivo = s.get('strengste_niveau', '')
        inscope = s.get('pgs15') and nivo not in ('Buiten scope', '')
        d = s.get('doelstellingen', {})
        ma = s.get('maatregelen', {})

        sh += '<div style="page-break-inside:avoid;margin-bottom:24px;">'
        sh += '<h2 style="color:#003366;font-size:18px;margin-bottom:12px;border-bottom:2px solid #003366;padding-bottom:6px;">' + str(idx) + '. ' + s['naam'] + '</h2>'
        sh += '<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:13px;"><thead><tr style="background:#003366;color:#FFF;"><th style="padding:8px 12px;text-align:left;">Eigenschap</th><th style="padding:8px 12px;text-align:left;">Waarde</th></tr></thead><tbody>'

        props = [
            ('CAS', s.get('cas','-')),
            ('Hoeveelheid', str(round(s['hoeveelheid'],1)) + ' kg'),
            ('Vorm', s.get('vorm','-')),
            ('ADR/VG', str(s.get('adr_klasse','-')) + ' / ' + str(s.get('vg','-'))),
            ('Ondergrens', str(s['ondergrens']) + ' kg' if s.get('ondergrens') else 'N.v.t.'),
            ('H-zinnen', s.get('h_zinnen','-')),
        ]
        for i, (l, v) in enumerate(props):
            bg = '#F8F9FA' if i % 2 == 0 else '#FFF'
            sh += '<tr style="background:' + bg + ';"><td style="padding:6px 12px;font-weight:600;">' + l + '</td><td style="padding:6px 12px;">' + str(v) + '</td></tr>'
        sh += '</tbody></table>'

        if not inscope:
            sh += '<p style="color:#374151;font-style:italic;">Buiten PGS 15 werkingssfeer.</p></div>'
            continue

        routes = s.get('all_niveaus', [])
        rt = ', '.join([r[0] + ' -&gt; Niveau ' + r[1] for r in routes])
        sh += '<div style="padding:10px 14px;background:' + nbg(nivo) + ';border-left:4px solid ' + nbrd(nivo) + ';margin-bottom:16px;">'
        sh += '<strong style="color:' + nfg(nivo) + ';">PGS 15:2025 - Niveau ' + nivo + '</strong> (strengste: ' + str(s.get('strengste_route','-')) + ')<br>'
        sh += '<span style="color:#374151;">' + rt + '</span></div>'

        # CLP versietabel
        versies = s.get('versies', [])
        if len(versies) > 1:
            sh += '<h3 style="color:#003366;font-size:14px;margin-bottom:8px;">Versietabel (ADR + CLP)</h3>'
            sh += '<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:12px;"><thead><tr style="background:#003366;color:#FFF;"><th style="padding:8px 10px;text-align:left;">Route</th><th style="padding:8px 10px;text-align:right;">Ondergrens</th><th style="padding:8px 10px;text-align:right;">C</th><th style="padding:8px 10px;text-align:right;">B</th><th style="padding:8px 10px;text-align:right;">A</th><th style="padding:8px 10px;text-align:center;">Huidig</th></tr></thead><tbody>'
            for vi, v in enumerate(versies):
                vbg = '#F0FDF4' if v.get('is_actueel') else ('#F8F9FA' if vi % 2 == 0 else '#FFF')
                vc = nfg(v.get('actueel','')) if v.get('is_actueel') else '#9CA3AF'
                a_val = v.get('A')
                a_str = 'N.v.t.' if a_val is None else str(a_val)
                b_val = v.get('B')
                b_str = 'N.v.t.' if b_val == 'N.v.t.' or b_val is None else str(b_val)
                c_val = v.get('C')
                c_str = 'N.v.t.' if c_val == 'N.v.t.' or c_val is None else str(c_val)
                act = v.get('actueel')
                act_str = '<strong style="color:' + vc + ';">Niveau ' + str(act) + '</strong>' if act and act != '—' else 'Buiten scope'
                sh += '<tr style="background:' + vbg + ';"><td style="padding:6px 10px;font-weight:500;">' + str(v.get('route','')) + '</td><td style="padding:6px 10px;text-align:right;">' + str(v.get('ondergrens','')) + ' kg</td><td style="padding:6px 10px;text-align:right;">' + c_str + '</td><td style="padding:6px 10px;text-align:right;">' + b_str + '</td><td style="padding:6px 10px;text-align:right;">' + a_str + '</td><td style="padding:6px 10px;text-align:center;">' + act_str + '</td></tr>'
            sh += '</tbody></table>'

        # Drempelwaarden
        dw_html = s.get('drempelwaarden', {})
        dw_routes_html = dw_html.get('routes', [])
        if dw_routes_html:
            sh += '<h3 style="color:#003366;font-size:14px;margin:12px 0 8px;">Drempelwaarden</h3>'
            sh += '<p style="font-size:12px;color:#374151;margin-bottom:8px;">Huidige hoeveelheid: <strong>' + str(round(s['hoeveelheid'],1)) + ' kg</strong></p>'
            for route in dw_routes_html:
                sh += '<div style="margin-bottom:10px;">'
                sh += '<strong style="color:#003366;font-size:12px;">' + route['route_naam'] + ' — Huidig: ' + route['huidige_niveau'] + '</strong>'
                sh += '<table style="width:100%;border-collapse:collapse;margin-top:4px;font-size:12px;"><thead><tr style="background:#003366;color:#FFF;"><th style="padding:6px 10px;text-align:left;">Niveau</th><th style="padding:6px 10px;text-align:right;">Drempel</th><th style="padding:6px 10px;text-align:right;">Status</th></tr></thead><tbody>'
                for nivo in route['niveaus']:
                    is_h = nivo['niveau'] == route['huidige_niveau'] or (nivo['niveau'] == 'Basis' and route['huidige_niveau'] == 'Buiten scope')
                    is_b = nivo.get('bereikt', False)
                    bg = '#E8F5E9' if is_h else ('#F8F9FA' if route['niveaus'].index(nivo) % 2 == 0 else '#FFF')
                    label = str(nivo.get('label', 'N.v.t.'))
                    if is_h:
                        status = '<strong style="color:#00A859;">\u2705 HUIDIG</strong>'
                    elif is_b:
                        status = '<span style="color:#3B82F6;">Bereikt</span>'
                    elif nivo.get('drempel') is not None:
                        extra = nivo['extra_needed']
                        status = '<span style="color:#F59E0B;">\u25B8 +' + str(extra) + ' kg</span>'
                    else:
                        status = '<span style="color:#9CA3AF;">N.v.t.</span>'
                    sh += '<tr style="background:' + bg + ';"><td style="padding:5px 10px;">' + nivo['niveau'] + '</td><td style="padding:5px 10px;text-align:right;">' + label + '</td><td style="padding:5px 10px;text-align:right;">' + status + '</td></tr>'
                sh += '</tbody></table></div>'

        sh += '</div>'

    # Wat Als Analyse
    wa_html = ''
    wat_als_html = result.get('wat_als', [])
    gekozen_html = result.get('opslag_type', 'binnen')
    type_labels_html = {'binnen': 'Binnenopslag', 'buiten': 'Buitenopslag', 'container': '(Tank)containers', 'tijdelijk': 'Tijdelijke opslag'}
    if wat_als_html:
        wa_html = '<div style="page-break-before:always;"><h2 style="color:#003366;font-size:18px;margin-bottom:12px;border-bottom:2px solid #003366;">2A. Vergelijking Opslagtypes ("Wat Als"-analyse)</h2>'
        wa_html += '<p style="margin-bottom:12px;color:#374151;font-size:12px;">Gekozen opslagtype: <strong>' + type_labels_html.get(gekozen_html, gekozen_html) + '</strong>. Vergelijking van verplichte maatregelen per opslagtype.</p>'
        for ws in wat_als_html:
            wa = ws.get('wat_als', {})
            if not wa:
                continue
            wa_html += '<h3 style="color:#003366;font-size:14px;margin:12px 0 8px;">' + ws['naam'] + '</h3>'
            wa_html += '<table style="width:100%;border-collapse:collapse;margin-bottom:12px;font-size:12px;"><thead><tr style="background:#003366;color:#FFF;"><th style="padding:8px 10px;text-align:left;">Opslagtype</th><th style="padding:8px 10px;text-align:right;">Maatregelen</th><th style="padding:8px 10px;text-align:right;">Verschil</th><th style="padding:8px 10px;text-align:left;">Impact</th></tr></thead><tbody>'
            for t in ['binnen', 'buiten', 'container', 'tijdelijk']:
                d = wa.get(t)
                if not d:
                    continue
                is_g = t == gekozen_html
                bg = '#F0FDF4' if is_g else ('#F8F9FA' if ['binnen','buiten','container','tijdelijk'].index(t) % 2 == 0 else '#FFF')
                label = ('\u2705 ' if is_g else '') + d['label'] + (' (gekozen)' if is_g else '')
                verschil = str(d['verschil']) if d['verschil'] != 0 else '—'
                ic = '#EF4444' if d['impact'] == 'strenger' else ('#22C55E' if d['impact'] == 'milder' else '#6B7280')
                wa_html += '<tr style="background:' + bg + ';"><td style="padding:6px 10px;font-weight:' + ('600' if is_g else '400') + ';">' + label + '</td><td style="padding:6px 10px;text-align:right;font-weight:600;">' + str(d['aantal_maatregelen']) + '</td><td style="padding:6px 10px;text-align:right;color:' + ic + ';font-weight:600;">' + verschil + '</td><td style="padding:6px 10px;color:' + ic + ';">' + d['impact'].capitalize() + '</td></tr>'
            wa_html += '</tbody></table>'
            for t in ['binnen', 'buiten', 'container', 'tijdelijk']:
                d = wa.get(t)
                if not d or t == gekozen_html:
                    continue
                parts = []
                if d.get('extra_maatregelen'):
                    parts.append('Extra: ' + ', '.join(d['extra_maatregelen']))
                if d.get('minder_maatregelen'):
                    parts.append('Minder: ' + ', '.join(d['minder_maatregelen']))
                if d.get('extra_doelstellingen'):
                    parts.append('Extra doelen: ' + ', '.join(d['extra_doelstellingen']))
                if parts:
                    wa_html += '<p style="font-size:11px;color:#374151;margin:4px 0;">' + d['label'] + ': ' + ' | '.join(parts) + '</p>'
        wa_html += '</div>'

    # Combinatieanalyse
    ch = ''
    if len(stoffen) > 1:
        ch = '<div style="page-break-before:always;"><h2 style="color:#003366;font-size:18px;margin-bottom:12px;border-bottom:2px solid #003366;">3. Stoffencombinatieanalyse</h2>'
        sch = combi.get('scheiding', {})
        cf = sch.get('conflicten', [])
        ch += '<h3 style="color:#003366;font-size:14px;">3.1 Onverenigbare Combinaties</h3>'
        if cf:
            for c in cf:
                ch += '<div style="padding:10px;background:#FEF2F2;border-left:4px solid #EF4444;margin-bottom:8px;"><strong style="color:#991B1B;">' + c['stof1'] + ' + ' + c['stof2'] + '</strong><br><span style="color:#374151;font-size:13px;">' + c['reden'] + '</span></div>'
        else:
            ch += '<div style="padding:10px;background:#F0FDF4;border-left:4px solid #00A859;margin-bottom:8px;"><strong style="color:#166534;">Geen onverenigbare combinaties</strong></div>'
        for v in combi.get('synergie', {}).get('vondsten', []):
            ib = '#FEF2F2' if v['impact'] == 'high' else '#FFFBEB'
            ch += '<div style="padding:10px;background:' + ib + ';margin-bottom:8px;"><strong>' + v['type'] + '</strong><br><span style="font-size:13px;color:#374151;">' + v['beschrijving'] + '</span></div>'
        ch += '</div>'

    na = agg.get('geaggregeerd_niveau', '-')

    CSS = '@page{size:A4;margin:2cm}*{margin:0;padding:0;box-sizing:border-box}body{font-family:Calibri,Arial,sans-serif;color:#1F2937;background:#FFF;font-size:13px;line-height:1.5}.container{max-width:900px;margin:0 auto;padding:20px}table{border-collapse:collapse}@media print{.no-print{display:none}}'

    html = '<!DOCTYPE html><html lang="nl"><head><meta charset="UTF-8"><title>PGS 15 Compliance Rapport</title><style>' + CSS + '</style></head><body><div class="container">'

    # Voorblad
    html += '<div style="text-align:center;padding:60px 0 40px;">'
    if logo_b64:
        html += '<img src="' + logo_b64 + '" alt="JvG Consultancy" style="height:80px;width:auto;margin-bottom:20px;">'
    html += '<h1 style="color:#003366;font-size:28px;">PGS 15:2025</h1>'
    html += '<h1 style="color:#003366;font-size:22px;margin-bottom:24px;">Compliance Rapport</h1>'
    html += '<p style="color:#374151;">Datum: ' + now + ' | Stoffen: ' + str(len(stoffen)) + ' | Totaal: ' + str(round(agg.get('totaal_kg', 0), 1)) + ' kg | Niveau: ' + str(na) + '</p>'
    html += '<p style="margin-top:40px;color:#374151;font-size:11px;font-style:italic;">Vertrouwelijk - Alleen voor intern gebruik</p></div>'

    html += '<div style="page-break-before:always;"></div>'
    html += '<h2 style="color:#003366;font-size:18px;margin-bottom:12px;border-bottom:2px solid #003366;">1. Management Samenvatting</h2>'
    html += '<p style="margin-bottom:16px;">Dit rapport presenteert de PGS 15:2025 compliance scan resultaten.</p>'

    html += '<div style="page-break-before:always;"></div>'
    html += '<h2 style="color:#003366;font-size:18px;margin-bottom:12px;border-bottom:2px solid #003366;">2. Per-product Analyse</h2>'
    html += sh + wa_html + ch

    # Samenstel: alle maatregelen in één tabel
    flat_samenstel_html = result.get('flat_measures_samenstel', [])
    if flat_samenstel_html:
        html += '<div style="page-break-before:always;"><h2 style="color:#003366;font-size:18px;margin-bottom:12px;border-bottom:2px solid #003366;">3. Samenstel Maatregelen</h2>'
        html += '<p>Totaal <strong>' + str(len(flat_samenstel_html)) + '</strong> unieke maatregelen voor het samenstel (' + str(len(stoffen)) + ' stoffen).</p>'
        html += '<table style="width:100%;border-collapse:collapse;margin-top:12px;"><thead><tr style="background:#003366;color:#FFF;"><th style="padding:8px 12px;">Code</th><th style="padding:8px 12px;">Maatregel</th><th style="padding:8px 12px;">Omschrijving</th><th style="padding:8px 12px;">Van toepassing op</th></tr></thead><tbody>'
        for mi2, fm2 in enumerate(flat_samenstel_html):
            bg2 = '#F8F9FA' if mi2 % 2 == 0 else '#FFF'
            van_toep2 = str(fm2.get('van_toepassing_op', 1)) + ' van ' + str(len(stoffen))
            html += '<tr style="background:' + bg2 + ';"><td style="padding:6px 12px;font-weight:600;color:#003366;">' + fm2['code'] + '</td><td style="padding:6px 12px;">' + fm2['titel'] + '</td><td style="padding:6px 12px;font-size:12px;color:#374151;">' + fm2.get('toelichting', '')[:120] + '</td><td style="padding:6px 12px;font-size:12px;color:#374151;">' + van_toep2 + '</td></tr>'
        html += '</tbody></table></div>'
    else:
        # Single stof fallback
        in_scope_h = [s for s in stoffen if s.get('pgs15') and s.get('strengste_niveau','') not in ('Buiten scope', '')]
        if in_scope_h:
            sf2 = in_scope_h[0].get('flat_measures', [])
            if sf2:
                html += '<div style="page-break-before:always;"><h2 style="color:#003366;font-size:18px;margin-bottom:12px;border-bottom:2px solid #003366;">3. Verplichte Maatregelen</h2>'
                html += '<p>Totaal <strong>' + str(len(sf2)) + '</strong> maatregelen voor ' + in_scope_h[0]['naam'] + '.</p>'
                html += '<table style="width:100%;border-collapse:collapse;margin-top:12px;"><thead><tr style="background:#003366;color:#FFF;"><th style="padding:8px 12px;">Code</th><th style="padding:8px 12px;">Maatregel</th><th style="padding:8px 12px;">Omschrijving</th></tr></thead><tbody>'
                for mi3, fm3 in enumerate(sf2):
                    bg3 = '#F8F9FA' if mi3 % 2 == 0 else '#FFF'
                    html += '<tr style="background:' + bg3 + ';"><td style="padding:6px 12px;font-weight:600;color:#003366;">' + fm3['code'] + '</td><td style="padding:6px 12px;">' + fm3['titel'] + '</td><td style="padding:6px 12px;font-size:12px;color:#374151;">' + fm3.get('toelichting', '')[:120] + '</td></tr>'
                html += '</tbody></table></div>'

    voor_h = result.get('opslagvoorstel', {})
    if voor_h and voor_h.get('vakken'):
        html += '<div style="page-break-before:always;"><h2 style="color:#003366;font-size:18px;margin-bottom:12px;border-bottom:2px solid #003366;">4. Voorstel Gezamenlijke Opslag</h2>'
        html += '<div style="padding:10px 14px;background:#EFF6FF;border-left:4px solid #3B82F6;margin-bottom:16px;"><strong>' + voor_h.get('advies','') + '</strong></div>'
        for v in voor_h['vakken']:
            html += '<div style="border:1px solid #E5E7EB;border-radius:8px;padding:12px;margin-bottom:10px;"><strong>' + v['label'] + '</strong><br><span style="font-size:12px;color:#374151;">' + ', '.join(v['stoffen']) + ' — ' + str(round(v['totaal_kg'],1)) + ' kg</span><ul style="margin-top:6px;font-size:12px;">'
            for e in v.get('eisen', []):
                html += '<li>' + e + '</li>'
            html += '</ul></div>'
        if voor_h.get('constructieve_eisen'):
            html += '<h3 style="color:#003366;font-size:14px;margin-top:16px;">Constructieve Eisen</h3><ul style="font-size:12px;">'
            for e in voor_h['constructieve_eisen']:
                html += '<li>' + e + '</li>'
            html += '</ul>'
        html += '</div>'

    html += '<div style="page-break-before:always;"></div>'
    html += '<h2 style="color:#003366;font-size:18px;margin-bottom:12px;border-bottom:2px solid #003366;">5. Conclusie</h2>'
    html += '<p>Geaggregeerd veiligheidsniveau: <strong>' + str(na) + '</strong>.</p>'
    html += '<p style="font-size:11px;color:#374151;font-style:italic;margin-top:24px;">JvG Consultancy PGS 15 Compliance Scanner. Raadpleeg altijd de volledige PGS 15:2025 publicatie.</p>'
    html += '<div style="margin-top:40px;padding-top:12px;border-top:2px solid #E5E7EB;text-align:center;font-size:10px;color:#374151;">JvG Consultancy - Vertrouwelijk</div></div>'

    # ══════════════════════════════════════════════════════════════
    # BIJLAGEN HTML
    # ══════════════════════════════════════════════════════════════
    html += '<div style="page-break-before:always;"></div>'
    html += '<hr style="margin:24px 0;border:2px solid #003366;">'
    html += '<h2 style="color:#003366;font-size:20px;margin-bottom:16px;">Bijlagen</h2>'

    # Bijlage A — Scenario's
    html += '<h3 style="color:#003366;font-size:16px;margin-bottom:8px;border-bottom:1px solid #003366;padding-bottom:4px;">Bijlage A — Scenario\'s</h3>'
    html += '<p style="margin-bottom:12px;color:#374151;font-size:12px;">Overzicht van alle oorzaak- en gevolgscenario\'s uit PGS 15:2025 Hoofdstuk 4.</p>'

    s_uitleg_h = {
        'S1': 'Een voertuig rijdt tegen verpakkingen of stellingen aan, wat leidt tot lekkage of val van stoffen.',
        'S2': 'Een verpakking valt van een stelling of tijdens verplaatsing. De impact beschadigt de verpakking.',
        'S3': 'Een stelling of deel van de opslaghal bezwijkt onder belasting. Alle verpakkingen vallen.',
        'S4': 'Verkeerde stapeling (te hoog, zware op lichte) laat de onderste verpakking falen.',
        'S5': 'Veroudering, corrosie of een reeds beschadigde verpakking leidt tot lekkage zonder externe oorzaak.',
        'S6': 'Drukverschillen (bijv. temperatuur) laten een verpakking falen door over- of onderdruk.',
        'S7': 'Fout bij het openen van een verpakking voor monstername waardoor de stof onbedoeld vrijkomt.',
        'S8': 'Brand ontstaat in de opslagvoorziening zelf en betrekt de opgeslagen gevaarlijke stoffen.',
        'S9': 'Brand buiten de opslag maar binnen het terrein bereikt de opslag.',
        'S10': 'Brand van buiten de locatie bereikt de opslagvoorziening.',
        'S11': 'Gevaarlijke stof komt daadwerkelijk vrij uit de verpakking (primair gevolg).',
        'S12': 'Vrijgekomen stof verspreidt via damp/gas of vloeistof buiten de opslaglocatie.',
        'S13': 'Vrijgekomen stoffen of omgevingsbrand leidt tot brand in de opslagvoorziening.',
        'S14': 'Brand escaleert naar andere gebouwen/opslageenheden binnen de locatiebegrenzing.',
        'S15': 'Brand escaleert naar de omgeving buiten de locatiebegrenzing.',
        'S16': 'Stofspecifieke scenario\'s zoals zelfontleding, polymerisatie of specifieke reacties.',
    }

    s_to_d_h = {}
    s_to_m_h = {}
    for _dc, _dv in DOELSTELLING_MAP.items():
        for _sc in _dv.get('scenario', []):
            s_to_d_h.setdefault(_sc, []).append(_dc)
            for _cat, _mc in _dv.get('maatregelen', {}).items():
                s_to_m_h.setdefault(_sc, set()).update(_mc)

    html += '<table style="width:100%;border-collapse:collapse;margin-bottom:24px;font-size:12px;"><thead><tr style="background:#003366;color:#FFF;"><th style="padding:8px 12px;text-align:left;">Code</th><th style="padding:8px 12px;text-align:left;">Omschrijving</th><th style="padding:8px 12px;text-align:left;">Uitleg</th><th style="padding:8px 12px;text-align:left;">Doelstellingen</th></tr></thead><tbody>'
    for s_code in sorted(SCENARIOS.keys(), key=lambda x: int(x[1:])):
        gerelateerd = ', '.join(sorted(s_to_d_h.get(s_code, [])))
        bg = '#F8F9FA' if int(s_code[1:]) % 2 == 0 else '#FFF'
        html += '<tr style="background:' + bg + ';"><td style="padding:6px 12px;font-weight:600;color:#003366;white-space:nowrap;">' + s_code + '</td><td style="padding:6px 12px;">' + SCENARIOS[s_code] + '</td><td style="padding:6px 12px;color:#374151;font-size:11px;">' + s_uitleg_h.get(s_code, '') + '</td><td style="padding:6px 12px;">' + gerelateerd + '</td></tr>'
    html += '</tbody></table>'

    # Bijlage B — Doelstellingen
    html += '<h3 style="color:#003366;font-size:16px;margin-bottom:8px;border-bottom:1px solid #003366;padding-bottom:4px;">Bijlage B — Doelstellingen</h3>'
    html += '<p style="margin-bottom:12px;color:#374151;font-size:12px;">Overzicht van alle doelstellingen (D1-D13) met bijbehorende maatregelen per categorie.</p>'
    for dc in sorted(DOELSTELLING_MAP.keys(), key=lambda x: int(x[1:])):
        dv = DOELSTELLING_MAP[dc]
        html += '<div style="margin-bottom:16px;">'
        html += '<h4 style="color:#003366;font-size:14px;">' + dc + ' — ' + dv['titel'] + '</h4>'
        html += '<p style="font-size:12px;color:#374151;margin-bottom:4px;">' + dv['omschrijving'] + '</p>'
        html += '<p style="font-size:11px;color:#6B7280;margin-bottom:4px;"><em>Scenario\'s: ' + ', '.join(sorted(dv.get('scenario', []))) + '</em></p>'
        cat_labels_html = {
            'Basis': 'Basis', 'Binnen': 'Binnenopslag', 'Buiten': 'Buitenopslag',
            'Container': '(Tank)container', 'Tijdelijk': 'Tijdelijke opslag',
            'C': 'Niveau C', 'B': 'Niveau B', 'A': 'Niveau A', 'BVK': 'BVK',
            'ADR_2_gasflessen': 'ADR kl.2 gasflessen', 'ADR_2_spuitbussen': 'ADR kl.2 spuitbussen',
            'ADR_3_VG_I_II': 'ADR kl.3 VG I/II', 'ADR_4_1_VG_II_III': 'ADR kl.4.1 VG II/III',
            'ADR_4_2_4_3_VG_III': 'ADR kl.4.2/4.3 VG III', 'ADR_4_3_VG_II_III': 'ADR kl.4.3 VG II/III',
            'ADR_5_2': 'ADR kl.5.2', 'ADR_6_1_VG_I_II': 'ADR kl.6.1 VG I/II', 'ADR_6_2': 'ADR kl.6.2',
            'acuut_toxisch': 'Acuut toxisch',
        }
        maatregelen = dv.get('maatregelen', {})
        if maatregelen:
            html += '<table style="width:100%;border-collapse:collapse;font-size:11px;margin-top:4px;"><thead><tr style="background:#F0F4F8;"><th style="padding:4px 8px;text-align:left;">Categorie</th><th style="padding:4px 8px;text-align:left;">Maatregelen</th></tr></thead><tbody>'
            for cat, m_codes in sorted(maatregelen.items()):
                if not m_codes: continue
                label = cat_labels_html.get(cat, cat)
                html += '<tr><td style="padding:4px 8px;font-weight:600;color:#003366;">' + label + '</td><td style="padding:4px 8px;">' + ', '.join(sorted(m_codes)) + '</td></tr>'
            html += '</tbody></table>'
        html += '</div>'

    # Bijlage B.2 — Samenstel: Scenario's, Doelstellingen & Maatregelen
    html += '<h3 style="color:#003366;font-size:16px;margin-bottom:8px;border-bottom:1px solid #003366;padding-bottom:4px;">Bijlage B.2 — Samenstel: Scenario\'s, Doelstellingen & Maatregelen</h3>'
    html += '<p style="margin-bottom:12px;color:#374151;font-size:12px;">Geaggregeerd niveau: <strong>' + str(na) + '</strong>. Van toepassing zijnde scenario\'s, doelstellingen en maatregelen voor het gehele samenstel.</p>'
    if agg_compliance:
        for dobj in agg_compliance:
            html += '<div style="margin-bottom:12px;">'
            html += '<h4 style="color:#003366;font-size:13px;">' + dobj["code"] + ' — ' + dobj["titel"] + '</h4>'
            html += '<p style="font-size:12px;color:#374151;">' + dobj.get("omschrijving", "") + '</p>'
            sc_text = ', '.join(dobj.get("scenario", []))
            if sc_text:
                html += '<p style="font-size:11px;color:#6B7280;"><em>Scenario\'s: ' + sc_text + '</em></p>'
            ml = dobj.get("maatregelen", [])
            if ml:
                html += '<table style="width:100%;border-collapse:collapse;font-size:11px;margin-top:4px;"><thead><tr style="background:#F0F4F8;"><th style="padding:4px 8px;text-align:left;">Code</th><th style="padding:4px 8px;text-align:left;">Maatregel</th><th style="padding:4px 8px;text-align:left;">Toelichting</th></tr></thead><tbody>'
                for m in ml:
                    info = MAATREGELEN_TOELICHTING.get(m, {})
                    html += '<tr><td style="padding:4px 8px;font-weight:600;color:#003366;">' + m + '</td><td style="padding:4px 8px;">' + info.get('label', m) + '</td><td style="padding:4px 8px;color:#374151;">' + (info.get('omschrijving', '')[:80] + '...' if len(info.get('omschrijving', '')) > 80 else info.get('omschrijving', '')) + '</td></tr>'
                html += '</tbody></table>'
            html += '</div>'
    else:
        html += '<p style="color:#374151;font-size:12px;">Geen samenstel-niveau van toepassing.</p>'

    # Bijlage C — Maatregelen
    html += '<h3 style="color:#003366;font-size:16px;margin-bottom:8px;border-bottom:1px solid #003366;padding-bottom:4px;">Bijlage C — Maatregelen</h3>'
    html += '<p style="margin-bottom:12px;color:#374151;font-size:12px;">Volledig overzicht van alle maatregelen uit PGS 15:2025 Hoofdstuk 7.</p>'

    # Reverse mapping: M-code -> doelstellingen
    m_to_d_h = {}
    for _dc, _dv in DOELSTELLING_MAP.items():
        for _cat, _mc in _dv.get('maatregelen', {}).items():
            for _m in _mc:
                m_to_d_h.setdefault(_m, set()).add(_dc)

    def m_niveaus_h(code):
        nivs = set()
        for _dc2, _dv2 in DOELSTELLING_MAP.items():
            for _cat2, _mc2 in _dv2.get('maatregelen', {}).items():
                if code in _mc2:
                    if _cat2 == 'Basis': nivs.add('Basis')
                    elif _cat2 in ('C','B','A'): nivs.add(_cat2 + '+')
                    else: nivs.add('Context')
        return ', '.join(sorted(nivs)) if nivs else 'Basis'

    html += '<table style="width:100%;border-collapse:collapse;margin-bottom:24px;font-size:11px;"><thead><tr style="background:#003366;color:#FFF;"><th style="padding:6px 10px;text-align:left;">Code</th><th style="padding:6px 10px;text-align:left;">Titel</th><th style="padding:6px 10px;text-align:left;">Toelichting</th><th style="padding:6px 10px;text-align:left;">Niveau</th><th style="padding:6px 10px;text-align:left;">Doelstellingen</th></tr></thead><tbody>'
    def m_sort_key_h(code):
        if code.startswith('MW'): return (0, int(code[2:]))
        return (1, int(code[1:]))
    for m_code, (m_titel, m_oms) in sorted(MAATREGELEN_ALLE.items(), key=lambda x: m_sort_key_h(x[0])):
        bg = '#F8F9FA' if m_sort_key_h(m_code)[1] % 2 == 0 else '#FFF'
        doelen = ', '.join(sorted(m_to_d_h.get(m_code, set())))
        html += '<tr style="background:' + bg + ';"><td style="padding:5px 10px;font-weight:600;color:#003366;white-space:nowrap;">' + m_code + '</td><td style="padding:5px 10px;">' + m_titel + '</td><td style="padding:5px 10px;color:#374151;">' + (m_oms if m_oms else '-') + '</td><td style="padding:5px 10px;font-size:10px;">' + m_niveaus_h(m_code) + '</td><td style="padding:5px 10px;font-size:10px;">' + doelen + '</td></tr>'
    html += '</tbody></table>'
    html += '<div class="no-print" style="position:fixed;bottom:20px;right:20px;"><button onclick="window.print()" style="background:#003366;color:#FFF;border:none;padding:10px 20px;border-radius:6px;cursor:pointer;font-size:14px;">Afdrukken / PDF</button></div>'
    html += '</body></html>'

    fname = 'PGS15_Compliance_Rapport_v1.0_' + datetime.now().strftime('%Y%m%d_%H%M%S') + '.html'
    ddir = '/root/projects/jg/2026-pgs15-scanner/deliverables/html/'
    os.makedirs(ddir, exist_ok=True)
    with open(os.path.join(ddir, fname), 'w') as f:
        f.write(html)
    datadir = os.path.join(os.path.dirname(__file__), 'data')
    os.makedirs(datadir, exist_ok=True)
    with open(os.path.join(datadir, fname), 'w') as f:
        f.write(html)
    return jsonify({'file': fname})



if __name__ == '__main__':
    init_db()
    app.run(host='127.0.0.1', port=5060, debug=False)

# ═══════════════════════════════════════════════════════════════════
# SCHEIDINGSTABEL (PGS 15:2025 Tabel 8)
# ═══════════════════════════════════════════════════════════════════
