#!/usr/bin/env python3
"""PGS 15:2025 Compliance Engine v1.0

Volledige PGS 15:2025 compliance toetsing per opslagsituatie met:
1. Compliance toetsing per PGS 15 hoofdstuk (Hfd. 3-9 + Bijlagen A-C)
2. Maatregelenhiërarchie (Eliminatie → Substitutie → Technisch → Organisatorisch → PBM)
3. Aggregatie & Synergie analyse voor multi-stof opslag
4. Compliance score per stof/opslagsituatie (0-100)
5. Wettelijke bronverwijzingen (PGS 15:2025, Arbobesluit, Bouwbesluit 2012, Wet milieubeheer)

Zelfstandig werkend — geen externe API calls.
Architectuur gebaseerd op REACH Compliance Scanner Engine v3.0.
Data gebaseerd op bestaande PGS 15 scanner (app.py).
"""

import json, logging, re
from typing import Dict, List, Any, Optional, Tuple
from datetime import datetime

log = logging.getLogger(__name__)

# ═══════════════════════════════════════════════════════════════════
# SECTIE 1: CONSTANTEN & DATA
# ═══════════════════════════════════════════════════════════════════

# ── Compliance statussen ──
STATUS_COMPLIANT = 'COMPLIANT'
STATUS_NIET_COMPLIANT = 'NIET-COMPLIANT'
STATUS_NVT = 'NIET-VAN-TOEPASSING'
STATUS_ACTIE = 'ACTIE-VEREIST'

# ── Compliance score drempels ──
SCORE_VOLLEDIG = (90, 100)
SCORE_GROOTTEELS = (70, 89)
SCORE_DEELS = (50, 69)
SCORE_NIET = (0, 49)

# ── Risico classificatie ──
RISICO_LAAG = 'LAAG'
RISICO_MEDIUM = 'MEDIUM'
RISICO_HOOG = 'HOOG'
RISICO_KRITIEK = 'KRITIEK'

# ── Maatregelenhiërarchie niveaus ──
HIËRARCHIE_ELIMINATIE = 1
HIËRARCHIE_SUBSTITUTIE = 2
HIËRARCHIE_TECHNISCH = 3
HIËRARCHIE_ORGANISATORISCH = 4
HIËRARCHIE_PBM = 5

# ── PGS 15 Veiligheidsniveaus ──
NIVEAU_BASIS = 'Basis'
NIVEAU_C = 'C'
NIVEAU_B = 'B'
NIVEAU_A = 'A'

# ═══ PGS 15:2025 Tabel 6 — Niveaus (Publicatie 565) ═══
# Format: (ondergrens, C_start, B_threshold, A_threshold)
NIVEAUS = {
    ('3', 'I'):    (1, 1, None, 10000),
    ('3', 'II'):   (25, 25, None, 10000),
    ('3', 'III'):  (50, 50, None, 10000),
    ('4.1', 'I'):  (1, None, 10000, 20000),
    ('4.1', 'II'): (50, None, 50, 20000),
    ('4.1', 'III'):(50, None, 50, None),
    ('4.2', 'I'):  (50, None, 50, 20000),
    ('4.2', 'II'): (50, None, 50, 20000),
    ('4.2', 'III'):(50, None, 50, None),
    ('4.3', 'I'):  (50, None, 50, 20000),
    ('4.3', 'II'): (50, None, 50, 20000),
    ('4.3', 'III'):(50, None, 50, None),
    ('5.1', 'II'): (50, 50, 2500, None),
    ('5.1', 'III'):(50, 50, 2500, None),
    ('5.2', 'I'):  (30, 30, None, None),
    ('6.1', 'I'):  (1, 1, None, 1000),
    ('6.1', 'II'): (50, 50, None, 10000),
    ('6.1', 'III'):(50, 50, None, 10000),
    ('8', 'I'):    (250, 250, 10000, 10000),
    ('8', 'II'):   (250, 250, 10000, None),
    ('8', 'III'):  (250, 250, 10000, None),
    ('9', 'II'):   (250, 250, 10000, None),
    ('9', 'III'):  (250, 250, 10000, None),
}

# ═══ Ondergrenzen per ADR-klasse en VG ═══
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-only routes ═══
CLP_ROUTES = {
    '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},
    'cmr_vloeibaar': {'label': 'CMR (vloeibaar)', 'ondergrens': 1, 'C': None, 'B': None, 'A': None},
}

# ═══ Scenario's (PGS 15:2025 Hoofdstuk 4) ═══
SCENARIOS = {
    'S1': 'Verpakking wordt aangereden waardoor de gevaarlijke stof vrijkomt',
    'S2': 'Verpakking valt en faalt waardoor de stof vrijkomt',
    'S3': 'Stelling/opslaghal bezwijkt, verpakkingen vallen',
    'S4': 'Overbelasting/verkeerde stapeling, verpakking faalt',
    'S5': 'Veroudering/degradatie verpakking, stof komt vrij',
    'S6': 'Over-/onderdruk, verpakking faalt',
    'S7': 'Fouten bij openen voor monstername',
    'S8': 'Verpakte stoffen betrokken bij brand IN opslagvoorziening',
    'S9': 'Verpakte stoffen betrokken bij brand VANUIT locatie',
    'S10': 'Verpakte stoffen betrokken bij brand VANUIT buiten locatie',
    'S11': 'Vrijkomen gevaarlijke stof uit verpakking',
    'S12': 'Verspreiding damp/gas of vloeistof',
    'S13': 'Brand in opslagvoorziening',
    'S14': 'Escalatie brand binnen locatiebegrenzing',
    'S15': 'Escalatie brand naar omgeving',
    'S16': 'Overige stofspecifieke scenario\'s',
}

# ═══ Doelstellingen per niveau ═══
DOELSTELLINGEN = [
    ('D1', 'Voorkomen dat een aanrijding plaatsvindt'),
    ('D2', 'Zeker stellen fysieke bescherming opslaglocaties'),
    ('D3', 'Juiste opslag in juiste opslagvoorziening'),
    ('D4', 'Voorkomen gevaarlijke situatie na lekkage'),
    ('D5', 'Adequaat reageren bij noodsituaties'),
    ('D6', 'Hulpdiensten adequaat kunnen optreden'),
    ('D7', 'Voorkomen verspreiding'),
    ('D8', 'Voorkomen van ontsteking'),
    ('D9', 'Voorkomen van brand in opslagvoorziening'),
    ('D10', 'Voorkomen escalatie (passief) - compartimenteren'),
    ('D11', 'Voorkomen escalatie (actief) - beheersen'),
    ('D12', 'Brand (semi)automatisch blussen'),
    ('D13', 'Stofspecifieke risico\'s beheersen'),
]

# ═══ Maatregelenhiërarchie templates ═══
MAATREGELEN_HIERARCHIE = {
    HIËRARCHIE_ELIMINATIE: {
        'niveau': 'Eliminatie',
        'prioriteit': 1,
        'beschrijving': 'Kan de stof volledig verwijderd worden uit de opslag?',
        'voorbeelden': [
            'Proces herontwerpen zonder deze stof',
            'Alternatief productontwerp',
            'Gebruik volledig stoppen',
        ],
    },
    HIËRARCHIE_SUBSTITUTIE: {
        'niveau': 'Substitutie',
        'prioriteit': 2,
        'beschrijving': 'Kan een minder gevaarlijke stof (mildere VG/ADR-klasse) worden gebruikt?',
        'voorbeelden': [
            'Vervangen door stof met mildere H-classificatie',
            'Overstappen op watergedragen systeem',
            'Biobased alternatief overwegen',
        ],
    },
    HIËRARCHIE_TECHNISCH: {
        'niveau': 'Technische maatregelen',
        'prioriteit': 3,
        'beschrijving': 'Technische maatregelen om risico bij opslag te minimaliseren',
        'voorbeelden': [
            'Brandveiligheidsopslagkast (NEN-EN 14470-1)',
            'Vloeistofdichte lekbak / opvangvoorziening',
            'Gasdetectiesysteem',
            'Brandwerende scheiding (30/60/90 min WBDBO)',
            'VBB-systeem (vast opgesteld)',
            'Aanrijdbeveiliging',
            'Ventilatie (min. 5 luchtverversingen/uur)',
        ],
    },
    HIËRARCHIE_ORGANISATORISCH: {
        'niveau': 'Organisatorische maatregelen',
        'prioriteit': 4,
        'beschrijving': 'Procedures en organisatie om opslagrisico te beheersen',
        'voorbeelden': [
            'Interne noodplan / calamiteitenprocedure',
            'Instructie en training medewerkers (M86)',
            'Periodieke inspectie stellingen (M82)',
            'Etiket- en SDS-controle (M45, M136)',
            'Deskundig personeel aanstellen (M85)',
            'Calamiteitenploeg (M122)',
            'Lekkage-instructie (M120)',
        ],
    },
    HIËRARCHIE_PBM: {
        'niveau': 'Persoonlijke beschermingsmiddelen',
        'prioriteit': 5,
        'beschrijving': 'PBM als laatste redmiddel bij werkzaamheden in opslagvoorziening',
        'voorbeelden': [
            'Chemiebestendige handschoenen (EN 374)',
            'Veiligheidsbril / spatscherm (EN 166)',
            'Gelaatsmasker / halfmasker (EN 140/143)',
            'Ademluchtsysteem (EN 14594)',
            'Beschermende kleding (EN 14605)',
        ],
    },
}

# ═══ Onverenigbare combinaties (Bijlage B — Mengselregels) ═══
# Sterke reducties/verboden bij gezamenlijke opslag
ONVERENIGBARE_COMBINATIES = [
    {
        'type': 'ZUUR_BASE',
        'label': 'Zuren + Basen',
        'risico': 'Exotherme reactie, gasvorming (H2, Cl2), spatten',
        'adr_groep_a': ['8'],  # Corrosieve stoffen (zuren)
        'adr_groep_b': ['8'],  # Corrosieve stoffen (basen)
        'detail': 'Sterke zuren (H314) niet opslaan met sterke basen. Compartimenteren of ≥3m scheiding.',
        'score_impact': 25,
    },
    {
        'type': 'OXIDEREND_BRANDBAAR',
        'label': 'Oxiderend + Brandbaar',
        'risico': 'Verhoogd brand/explosierisico, zelfversnelligende reactie',
        'adr_groep_a': ['5.1', '5.2'],
        'adr_groep_b': ['3', '4.1', '4.2', '4.3'],
        'detail': 'ADR 5.1/5.2 gescheiden opslaan van ADR 3, 4.1, 4.2, 4.3. Scheiding via compartimentering of afstand.',
        'score_impact': 30,
    },
    {
        'type': 'TOXISCH_BRANDBAAR',
        'label': 'Giftig + Brandbaar',
        'risico': 'Giftige rook/gassen bij brand, verhoogd gezondheidsrisico hulpdiensten',
        'adr_groep_a': ['6.1'],
        'adr_groep_b': ['3', '4.1'],
        'detail': 'ADR 6.1 VG I gescheiden van brandbare vloeistoffen. Brandblussing speciaal (geen water bij waterreactieve).',
        'score_impact': 20,
    },
    {
        'type': 'GAS_VLOEISTOF',
        'label': 'Gasflessen + Brandbare vloeistoffen',
        'risico': 'Gasverspreiding + ontstekingsbron, explosiegevaar',
        'adr_groep_a': ['2'],
        'adr_groep_b': ['3'],
        'detail': 'Gasflessen op ≥5m afstand van brandbare vloeistoffen, of brandwerend gescheiden (60 min).',
        'score_impact': 25,
    },
    {
        'type': 'CMR_COMBINATIE',
        'label': 'CMR-stoffen cumulatie',
        'risico': 'Cumulatief carcinogeen/reprotoxisch risico',
        'adr_groep_a': ['6.1'],
        'adr_groep_b': ['6.1'],
        'detail': 'CMR-stoffen bij voorkeur apart opslaan. Bij gezamenlijke opslag: extra PBM en blootstellingsbeheer.',
        'score_impact': 20,
    },
    {
        'type': 'WATERREACTIEF',
        'label': 'Waterreactieve stoffen + Vocht',
        'risico': 'Waterstofgasvorming, brand/explosie, corrosieve dampen',
        'adr_groep_a': ['4.2', '4.3'],
        'adr_groep_b': [],
        'detail': 'ADR 4.2/4.3: droge opslag, bescherming tegen inregenen (M51), geen waterblussing.',
        'score_impact': 25,
    },
    {
        'type': 'EXPLOSIEF_BRANDBAAR',
        'label': 'Organische peroxiden + Brandbaar',
        'risico': 'Zelfversnelligende ontleding, detonatie',
        'adr_groep_a': ['5.2'],
        'adr_groep_b': ['3', '4.1'],
        'detail': 'ADR 5.2: temperatuurbeheersing, aparte opslagvoorziening (M143), max 1000 kg LQ.',
        'score_impact': 35,
    },
]

# ═══ PBM mapping op H-zinnen ═══
H_PBM_MAP = {
    'H300': ['handschoenen', 'gelaatsmasker', 'ademlucht', 'beschermlaag'],
    'H301': ['handschoenen', 'gelaatsmasker', 'beschermlaag'],
    'H302': ['handschoenen'],
    'H304': ['handschoenen', 'beschermlaag'],
    'H310': ['handschoenen', 'gelaatsmasker', 'beschermlaag'],
    'H311': ['handschoenen', 'gelaatsmasker'],
    'H312': ['handschoenen'],
    'H314': ['handschoenen', 'veiligheidsbril', 'beschermlaag', 'nooddouche'],
    'H315': ['handschoenen'],
    'H317': ['handschoenen', 'beschermlaag'],
    'H318': ['veiligheidsbril'],
    'H319': ['veiligheidsbril'],
    'H330': ['handschoenen', 'gelaatsmasker', 'ademlucht', 'beschermlaag'],
    'H331': ['handschoenen', 'gelaatsmasker', 'beschermlaag'],
    'H332': ['handschoenen', 'gelaatsmasker'],
    'H335': ['gelaatsmasker'],
    'H336': ['gelaatsmasker'],
    'H340': ['handschoenen', 'gelaatsmasker', 'ademlucht', 'beschermlaag'],
    'H341': ['handschoenen', 'gelaatsmasker', 'beschermlaag'],
    'H350': ['handschoenen', 'gelaatsmasker', 'ademlucht', 'beschermlaag'],
    'H351': ['handschoenen', 'gelaatsmasker', 'beschermlaap'],
    'H360': ['handschoenen', 'gelaatsmasker', 'ademlucht', 'beschermlaap'],
    'H361': ['handschoenen', 'gelaatsmasker', 'beschermlaap'],
    'H370': ['handschoenen', 'gelaatsmasker', 'ademlucht', 'beschermlaap'],
    'H371': ['handschoenen', 'gelaatsmasker', 'beschermlaap'],
    'H372': ['handschoenen', 'gelaatsmasker'],
    'H373': ['handschoenen', 'gelaatsmasker'],
    'H400': ['handschoenen', 'lekopvang'],
    'H410': ['handschoenen', 'lekopvang'],
    'H411': ['handschoenen', 'lekopvang'],
    'H412': ['lekopvang'],
}

PBM_BESCHRIJVINGEN = {
    'handschoenen': 'Chemiebestendige handschoenen (EN 374)',
    'veiligheidsbril': 'Veiligheidsbril / spatscherm (EN 166)',
    'gelaatsmasker': 'Gelaatsmasker / halfmasker (EN 140/143)',
    'ademlucht': 'Ademluchtsysteem (EN 14594)',
    'beschermlaap': 'Beschermende kleding (EN 14605)',
    'beschermlaag': 'Beschermende kleding (EN 14605)',
    'nooddouche': 'Nooddouche + oogwasstation (EN 15154)',
    'lekopvang': 'Vloeistofdichte lekbak / opvangbak',
    'laarzen': 'Chemiebestendige laarzen (EN 13832)',
}

# ═══ Wettelijke bronverwijzingen ═══
WETTELIJKE_BRONNEN = {
    'pgs15': {
        'label': 'PGS 15:2025',
        'uitgever': 'Publicatiereeks Gevaarlijke Stoffen',
        'verwijzing': 'PGS 15:2025 — Opslag van verpakte gevaarlijke stoffen',
    },
    'arbobesluit': {
        'label': 'Arbobesluit',
        'uitgever': 'Ministerie van SZW',
        'verwijzing': 'Arbobesluit Art. 3.17 (informatieplicht), Art. 4.1 (CMR), Art. 18 (gezondheidstoezicht)',
    },
    'bouwbesluit': {
        'label': 'Bouwbesluit 2012',
        'uitgever': 'Ministerie van BZK',
        'verwijzing': 'Bouwbesluit 2012 — brandwerendheid, WBDBO, NEN 6068/6069, NEN-EN 13501-1',
    },
    'wet_milieubeheer': {
        'label': 'Wet milieubeheer / Omgevingswet',
        'uitgever': 'Ministerie van I&W',
        'verwijzing': 'Besluit kwaliteit leefomgeving (Bkl) — vergunningplicht, PRVG, VRG',
    },
    'clp': {
        'label': 'CLP Verordening (EU) 1272/2008',
        'uitgever': 'EU',
        'verwijzing': 'Classificatie, etikettering en verpakking van stoffen en mengsels',
    },
    'adr': {
        'label': 'ADR 2025',
        'uitgever': 'UNECE',
        'verwijzing': 'Europees Akoord betreffende het internationaal vervoer van gevaarlijke goederen over de weg',
    },
}


# ═══════════════════════════════════════════════════════════════════
# SECTIE 2: HELPER FUNCTIES
# ═══════════════════════════════════════════════════════════════════

def parse_h_codes(h_zinnen: list) -> set:
    """Extraheer H-codes uit lijst van H-zinnen."""
    codes = set()
    for h in h_zinnen:
        m = re.match(r'H(\d{3})', h)
        if m:
            codes.add(int(m.group(1)))
    return codes


def get_gezondheidseffecten(h_zinnen: list) -> list:
    """Bepaal gezondheidseffect categorieën op basis van H-zinnen."""
    codes = parse_h_codes(h_zinnen)
    effects = []
    if codes & {300, 310, 330, 301, 311, 331}:
        effects.append('acuut_toxisch')
    if codes & {314}:
        effects.append('corrosief')
    if codes & {317, 334}:
        effects.append('sensibiliserend')
    if codes & {340, 350}:
        effects.append('carcinogeen')
    if codes & {360, 361}:
        effects.append('reprotoxisch')
    if codes & {372, 373}:
        effects.append('orgaantoxisch')
    if codes & {400, 410, 411}:
        effects.append('milieugevaarlijk')
    if codes & {220, 221, 222, 224, 225, 226, 240, 241, 242}:
        effects.append('brandbaar')
    if codes & {260, 261}:
        effects.append('waterreactief')
    if codes & {271, 272}:
        effects.append('oxiderend')
    return effects


def is_cmr(h_zinnen: list) -> bool:
    """Check of stof CMR-geclassificeerd is."""
    codes = parse_h_codes(h_zinnen)
    return bool(codes & {340, 341, 350, 351, 360, 361})


def is_acute_tox(h_zinnen: list) -> Tuple[bool, str]:
    """Check acute toxiciteit. Returns (is_acute, category)."""
    codes = parse_h_codes(h_zinnen)
    if codes & {300, 310, 330}:
        return True, '1'
    if codes & {301, 311, 331}:
        return True, '2'
    if codes & {302, 312, 332}:
        return True, '3'
    return False, ''


def determine_niveau(adr_klasse: str, vg: str, hoeveelheid: float,
                     is_brandbaar_k8: bool = False, flashpoint: float = None) -> str:
    """Bepaal PGS 15 veiligheidsniveau op basis van ADR-klasse, VG en hoeveelheid.

    Returns: 'Basis', 'C', 'B', 'A', of 'Buiten scope'
    """
    nv = NIVEAUS.get((adr_klasse, vg))
    if not nv:
        ondergrens = ONDERGRENS.get(adr_klasse, {}).get(vg, 0)
        if hoeveelheid < ondergrens:
            return 'Buiten scope'
        return 'Basis'

    ondergrens, c_start, b_thresh, a_thresh = nv

    if hoeveelheid < ondergrens:
        return 'Buiten scope'

    # Speciale logica ADR 8 VG I
    if adr_klasse == '8' and vg == 'I':
        if flashpoint is not None and flashpoint < 23:
            if hoeveelheid >= 10000:
                return 'A'
            if hoeveelheid >= 250:
                return 'C'
            return 'Basis'
        # Niet-brandbaar VG I
        if hoeveelheid >= 10000:
            return 'A'
        if hoeveelheid >= 250:
            return 'C'
        return 'Basis'

    # Speciale logica brandbaar ADR 8/9 VG II/III
    if adr_klasse in ('8', '9') and vg in ('II', 'III') and is_brandbaar_k8:
        if hoeveelheid >= 10000:
            return 'B'
        if hoeveelheid >= 250:
            return 'C'
        return 'Basis'

    # Standaard logica
    if a_thresh is not None and hoeveelheid >= a_thresh:
        return 'A'
    if b_thresh is not None and hoeveelheid >= b_thresh:
        return 'B'
    if c_start is not None and hoeveelheid >= c_start:
        return 'C'

    return 'Basis'


def _dedup(items: list) -> list:
    """Deduplicatie."""
    seen = set()
    out = []
    for item in items:
        k = item if isinstance(item, str) else item.get('label', str(item))
        if k not in seen:
            seen.add(k)
            out.append(item)
    return out


def get_strictste_niveau(adr_klasse: str, vg: str, hoeveelheid: float,
                          h_zinnen: list = [], vorm: str = 'vloeistof',
                          flashpoint: float = None) -> Tuple[str, str]:
    """Bepaal strengste niveau over alle routes (ADR + CLP).

    Returns: (strengste_niveau, route_beschrijving)
    """
    niveaus = []
    labels = []

    # ADR route
    if adr_klasse and vg:
        n = determine_niveau(adr_klasse, vg, hoeveelheid,
                            is_brandbaar_k8=False, flashpoint=flashpoint)
        niveaus.append(n)
        labels.append(f'ADR {adr_klasse} VG {vg}')

    # CLP routes
    cmr_flag = is_cmr(h_zinnen)
    acute_flag, acute_cat = is_acute_tox(h_zinnen)

    if acute_flag:
        route_key = 'acute_tox_1' if acute_cat == '1' else 'acute_tox_2_3'
        r = CLP_ROUTES.get(route_key)
        if r:
            n = 'Buiten scope'
            if hoeveelheid >= r['ondergrens']:
                if r['A'] is not None and hoeveelheid >= r['A']:
                    n = 'A'
                elif r['B'] is not None and hoeveelheid >= r['B']:
                    n = 'B'
                elif r['C'] is not None and hoeveelheid >= r['C']:
                    n = 'C'
                else:
                    n = 'Basis'
            niveaus.append(n)
            labels.append(r['label'])

    if cmr_flag:
        r_key = 'cmr_vloeibaar' if vorm == 'vloeistof' else 'cmr_vast'
        r = CLP_ROUTES.get(r_key)
        if r:
            n = 'Buiten scope' if hoeveelheid < r['ondergrens'] else 'Basis'
            niveaus.append(n)
            labels.append(r['label'])

    if not niveaus:
        return 'Buiten scope', 'Geen route'

    # Strengste niveau
    prio = {'A': 4, 'B': 3, 'C': 2, 'Basis': 1, 'Buiten scope': 0}
    strengste_idx = max(range(len(niveaus)), key=lambda i: prio.get(niveaus[i], 0))
    return niveaus[strengste_idx], labels[strengste_idx]


def bereken_strictste_niveau_van_alle_routes(stof: dict) -> Tuple[str, str]:
    """Publieke wrapper: bereken strengste niveau voor een stof dict."""
    return get_strictste_niveau(
        stof.get('adr_klasse', ''),
        stof.get('vg', ''),
        float(stof.get('hoeveelheid', 0) or 0),
        stof.get('h_zinnen', []),
        stof.get('vorm', 'vloeistof'),
        stof.get('flashpoint'),
    )


# ═══════════════════════════════════════════════════════════════════
# SECTIE 3: COMPLIANCE TOETSING PER PGS 15 HOOFDSTUK
# ═══════════════════════════════════════════════════════════════════

def toets_hoofdstuk_3(stof: dict, context: dict) -> dict:
    """Hoofdstuk 3: Algemene veiligheidseisen (gebouwen, constructies, brandwerendheid).

    Toetst: M21 (WBDBO), M24 (draagconstructie), M25 (brandwerendheidscriteria),
    M26 (afdekking), M27 (dakconstructie), M15 (max vloeroppervlak),
    M17 (opslag op verdieping).
    """
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')
    opslag_type = context.get('opslag_type', 'binnen')
    hoeveelheid = float(stof.get('hoeveelheid', 0) or 0)

    if niveau == 'Buiten scope':
        return {
            'status': STATUS_NVT,
            'hoofdstuk': 'PGS 15:2025 Hoofdstuk 3',
            'paragraaf': '§3.1-3.4',
            'arbobesluit': None,
            'bouwbesluit': 'Bouwbesluit 2012 — NEN 6068, NEN 6069',
            'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel — Hoofdstuk 3 niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    # WBDBO-eis
    wbdbo_min = 60  # standaard 60 min
    if niveau == 'A':
        wbdbo_min = 90  # 90 min bij niveau A
    wbdbo_aanwezig = context.get('wbdbo_minuten', 0)

    if wbdbo_aanwezig < wbdbo_min:
        acties.append(f'M21: WBDBO verhogen naar minimaal {wbdbo_min} min (huidig: {wbdbo_aanwezig} min) — NEN 6068')
        score -= 15
        status = STATUS_ACTIE

    # Max vloeroppervlak
    if niveau in ('A', 'B') and hoeveelheid > 20000:
        acties.append('M15: Max vloeroppervlak 2.500 m² (met VBB) of 1.000 m² (zonder VBB, ≤20.000 kg)')
        score -= 10
        status = STATUS_ACTIE

    # Dakconstructie
    if opslag_type == 'binnen':
        dak_materiaal = context.get('dak_materiaal', '')
        if dak_materiaal and dak_materiaal.lower() not in ('a1', 'a2'):
            acties.append('M27: Dak moet brandklasse A1 of A2 hebben — NEN 6063')
            score -= 10
            status = STATUS_ACTIE

    if not acties:
        acties.append(f'WBDBO ≥{wbdbo_min} min bevestigd (M21)')
        acties.append('Draagconstructie brandwerendheid conform WBDBO (M24)')

    return {
        'status': status,
        'hoofdstuk': 'PGS 15:2025 Hoofdstuk 3',
        'paragraaf': '§3.1-3.4',
        'arbobesluit': None,
        'bouwbesluit': 'Bouwbesluit 2012 — NEN 6068, NEN 6069, NEN-EN 13501-1',
        'wet_milieubeer': None,
        'uitleg': f'{naam}: niveau {niveau} — WBDBO ≥{wbdbo_min} min vereist. {"Afwijkingen geconstateerd." if score < 0 else "Voldoet aan eisen."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_hoofdstuk_4(stof: dict, context: dict) -> dict:
    """Hoofdstuk 4: Opslagsystemen en -methoden (stapeling, ventilatie, scheiding).

    Toetst: M30 (opvangvoorziening), M35 (onverenigbare combinaties), M36 (gescheiden opslag),
    M40-42 (vakindeling), M45 (controle), M48 (verpakking), M52-53 (stapelen).
    """
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')
    opslag_type = context.get('opslag_type', 'binnen')
    h_zinnen = stof.get('h_zinnen', [])

    if niveau == 'Buiten scope':
        return {
            'status': STATUS_NVT,
            'hoofdstuk': 'PGS 15:2025 Hoofdstuk 4',
            'paragraaf': '§4.1-4.5',
            'arbobesluit': 'Arbobesluit Art. 3.17',
            'bouwbesluit': None,
            'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel — Hoofdstuk 4 niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    # Opvangvoorziening
    vorm = stof.get('vorm', 'vloeistof')
    if vorm in ('vloeistof', 'vloeibaar'):
        if not context.get('opvangvoorziening', False):
            acties.append('M30: Opvangvoorziening aanbrengen (≥110% grootste verpakking of ≥10% totale inhoud)')
            score -= 15
            status = STATUS_ACTIE

    # Scheiding (M35)
    overige_stoffen = context.get('overige_stoffen_in_opslag', [])
    if overige_stoffen:
        adr_klasse = stof.get('adr_klasse', '')
        for andere in overige_stoffen:
            andere_adr = andere.get('adr_klasse', '')
            if adr_klasse and andere_adr and adr_klasse != andere_adr:
                # Check onverenigbaarheid
                combo_risico = _check_onverenigbaar(adr_klasse, andere_adr)
                if combo_risico:
                    acties.append(f'M35: Onverenigbare combinatie met {andere.get("naam", "")} — {combo_risico["detail"]}')
                    score -= combo_risico['score_impact']
                    status = STATUS_ACTIE

    # Vakindeling bij niveau A/B
    if niveau in ('A', 'B'):
        oppervlak = context.get('vloeroppervlak', 0)
        if oppervlak > 300:
            acties.append(f'M40/M42: Vakindeling vereist — vakken max 300 m², gescheiden door gangpad 3,5 m of 30 min brandwerend')
            score -= 10
            status = STATUS_ACTIE

    # Verpakking
    if not context.get('verpakking_goedgekeurd', True):
        acties.append('M48: Verpakking moet voldoen — niets ontsnapt, materiaal bestand, bestand tegen behandeling')
        score -= 10
        status = STATUS_ACTIE

    if not acties:
        acties.append('M35: Scheiding van onverenigbare stoffen gecontroleerd')
        acties.append('M48: Verpakking in orde')

    return {
        'status': status,
        'hoofdstuk': 'PGS 15:2025 Hoofdstuk 4',
        'paragraaf': '§4.1-4.5',
        'arbobesluit': 'Arbobesluit Art. 3.17',
        'bouwbesluit': None,
        'wet_milieubeer': None,
        'uitleg': f'{naam}: niveau {niveau} — opslagsystemen gecontroleerd. {"Afwijkingen geconstateerd." if score < 0 else "Voldoet."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_hoofdstuk_5(stof: dict, context: dict) -> dict:
    """Hoofdstuk 5: Brandveiligheid (blussystemen, detectie, compartimentering).

    Toetst: M68 (machines/installaties), M71-72 (pallets), M89 (failsafe),
    M95 (draagbaar blustoestel), M96 (VBB-systeem), M97 (bedrijfsbrandweer),
    M99-102 (UPD), M103-108 (bluswatervoorziening).
    """
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')
    adr_klasse = stof.get('adr_klasse', '')

    if niveau == 'Buiten scope':
        return {
            'status': STATUS_NVT,
            'hoofdstuk': 'PGS 15:2025 Hoofdstuk 5',
            'paragraaf': '§5.1-5.6',
            'arbobesluit': None,
            'bouwbesluit': 'Bouwbesluit 2012 — NEN 6068',
            'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel — Hoofdstuk 5 niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    # VBB-systeem bij niveau A
    if niveau == 'A':
        vbb_aanwezig = context.get('vbb_systeem', False)
        if not vbb_aanwezig:
            acties.append('M96: VBB-systeem (vast opgesteld brandbeheersings- en blussysteem) VERPLICHT bij niveau A')
            score -= 25
            status = STATUS_NIET_COMPLIANT
        else:
            # UPD aanwezig?
            if not context.get('upd_aanwezig', False):
                acties.append('M99: Uitgangspuntendocument (UPD) opstellen — goedgekeurd door bevoegd gezag')
                score -= 15
                status = STATUS_ACTIE

    # Draagbaar blustoestel
    oppervlak = context.get('vloeroppervlak', 0)
    benodigd_blustoestellen = max(1, int(oppervlak / 200))
    aanwezig_blustoestellen = context.get('blustoestellen', 0)
    if aanwezig_blustoestellen < benodigd_blustoestellen:
        acties.append(f'M95: Minimaal {benodigd_blustoestellen} draagbaar blustoestel(len) nodig (1 per 200 m²), huidig: {aanwezig_blustoestellen}')
        score -= 10
        status = STATUS_ACTIE

    # Bluswatervoorziening
    if niveau in ('A', 'B', 'C'):
        if not context.get('bluswatervoorziening', True):
            acties.append('M103: Bluswatervoorziening aanleggen — bereikbaar en afgestemd op brandweermaterieel')
            score -= 15
            status = STATUS_ACTIE

    # Losse pallets
    if not context.get('pallets_regel', True):
        acties.append('M71-72: Losse brandbare pallets niet aanwezig (max 24 stuks of 5% in apart vak)')
        score -= 5
        status = STATUS_ACTIE

    if not acties:
        acties.append('M95: Draagbaar blustoestel aanwezig')
        if niveau == 'A':
            acties.append('M96: VBB-systeem operationeel')

    return {
        'status': status,
        'hoofdstuk': 'PGS 15:2025 Hoofdstuk 5',
        'paragraaf': '§5.1-5.6',
        'arbobesluit': None,
        'bouwbesluit': 'Bouwbesluit 2012 — NEN 6068',
        'wet_milieubeer': None,
        'uitleg': f'{naam}: niveau {niveau} — brandveiligheid gecontroleerd. {"Afwijkingen geconstateerd." if score < 0 else "Voldoet."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_hoofdstuk_6(stof: dict, context: dict) -> dict:
    """Hoofdstuk 6: Milieumaatregelen (uitloging, bodembescherming, afwatering).

    Toetst: M29 (verspreiding voorkomen), M30 (opvangvoorziening),
    M32 (bluswateropvang), M112 (vloer eisen).
    """
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')
    h_zinnen = stof.get('h_zinnen', [])
    vorm = stof.get('vorm', 'vloeistof')

    if niveau == 'Buiten scope':
        return {
            'status': STATUS_NVT,
            'hoofdstuk': 'PGS 15:2025 Hoofdstuk 6',
            'paragraaf': '§6.1-6.3',
            'arbobesluit': None,
            'bouwbesluit': None,
            'wet_milieubeer': 'Wet milieubeheer / Omgevingswet — voorkomen bodemverontreiniging',
            'uitleg': f'{naam}: hoeveelheid onder drempel — Hoofdstuk 6 niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    codes = parse_h_codes(h_zinnen)
    is_milieugevaarlijk = bool(codes & {400, 410, 411, 412})

    # Vloeistofdichte vloer
    if vorm in ('vloeistof', 'vloeibaar') and not context.get('vloer_vloeistofdicht', True):
        acties.append('M112: Vloer moet vloeistofdicht en bestand zijn tegen opgesloten stoffen')
        score -= 15
        status = STATUS_ACTIE

    # Verspreiding voorkomen (geen rioolverbinding)
    if not context.get('geen_rioolverbinding', True):
        acties.append('M29: Vloer mag geen openingen met directe verbinding naar riolering/oppervlaktewater hebben')
        score -= 15
        status = STATUS_ACTIE

    # Bluswateropvang bij niveau A
    if niveau == 'A':
        if not context.get('bluswateropvang', False):
            acties.append('M32: Bluswateropvang aanleggen — capaciteit volgens PGS 14 (≥100% nominale capaciteit voor ADR 6.1)')
            score -= 20
            status = STATUS_ACTIE

    # Milieugevaarlijke stoffen
    if is_milieugevaarlijk and not context.get('bodembescherming', True):
        acties.append('Aanvullend: bodembescherming en afwateringssysteem controleren — Wet milieubeheer')
        score -= 10
        status = STATUS_ACTIE

    if not acties:
        acties.append('M29: Verspreiding via riool voorkomen')
        if vorm in ('vloeistof', 'vloeibaar'):
            acties.append('M112: Vloeistofdichte vloer bevestigd')

    return {
        'status': status,
        'hoofdstuk': 'PGS 15:2025 Hoofdstuk 6',
        'paragraaf': '§6.1-6.3',
        'arbobesluit': None,
        'bouwbesluit': None,
        'wet_milieubeer': 'Wet milieubeheer / Omgevingswet',
        'uitleg': f'{naam}: niveau {niveau} — milieumaatregelen gecontroleerd. {"Afwijkingen." if score < 0 else "Voldoet."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_hoofdstuk_7(stof: dict, context: dict) -> dict:
    """Hoofdstuk 7: Interne transportveiligheid (heftrucks, leidingen, pompen).

    Toetst: M46 (gemotoriseerde transportmiddelen), M74 (materieel containers),
    M76 (manoeuvreerruimte), M77-78 (aanrijdbeveiliging stellingen),
    M82 (periodieke inspectie stellingen).
    """
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')

    if niveau == 'Buiten scope':
        return {
            'status': STATUS_NVT,
            'hoofdstuk': 'PGS 15:2025 Hoofdstuk 7',
            'paragraaf': '§7.1-7.4',
            'arbobesluit': 'Arbobesluit Art. 3.17',
            'bouwbesluit': None,
            'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel — Hoofdstuk 7 niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    # Aanrijdbeveiliging
    if not context.get('aanrijdbeveiliging', True):
        acties.append('M77: Aanrijdbeveiliging stellingen aanbrengen op hoeken en onderdoorgangen')
        score -= 10
        status = STATUS_ACTIE

    # Stellinginspectie
    if not context.get('stelling_inspectie', True):
        acties.append('M82: Jaarlijkse visuele inspectie stellingconstructie — resultaten registreren en 5 jaar bewaren')
        score -= 10
        status = STATUS_ACTIE

    # Transportmiddelen beperken
    if not context.get('transport_beperkt', True):
        acties.append('M46: Bewegingen en verblijf gemotoriseerde transportmiddelen tot minimum beperken')
        score -= 5
        status = STATUS_ACTIE

    if not acties:
        acties.append('M46: Transportmiddelen beperkt tot noodzakelijke werkzaamheden')
        acties.append('M82: Stellinginspectie up-to-date')

    return {
        'status': status,
        'hoofdstuk': 'PGS 15:2025 Hoofdstuk 7',
        'paragraaf': '§7.1-7.4',
        'arbobesluit': 'Arbobesluit Art. 3.17',
        'bouwbesluit': None,
        'wet_milieubeer': None,
        'uitleg': f'{naam}: niveau {niveau} — transportveiligheid gecontroleerd. {"Afwijkingen." if score < 0 else "Voldoet."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_hoofdstuk_8(stof: dict, context: dict) -> dict:
    """Hoofdstuk 8: Organisatorische maatregelen (procedures, training, inspecties).

    Toetst: M85 (deskundig personeel), M86 (instructie), M87 (opleiding bestuurders),
    M88 (toegangsbeheer), M113-114 (noodplan), M115 (nooddouche),
    M116-118 (informatie hulpdiensten), M120-121 (lekkage-/calamiteiteninstructie),
    M122-123 (calamiteitenploeg, middelen), M130-133 (vluchtroutes),
    M134-137 (signalering, etiketten, VIB/SDS).
    """
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')
    hoeveelheid = float(stof.get('hoeveelheid', 0) or 0)

    if niveau == 'Buiten scope':
        return {
            'status': STATUS_NVT,
            'hoofdstuk': 'PGS 15:2025 Hoofdstuk 8',
            'paragraaf': '§8.1-8.6',
            'arbobesluit': 'Arbobesluit Art. 3.17, Art. 18',
            'bouwbesluit': None,
            'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel — Hoofdstuk 8 niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    # Deskundig personeel bij >2500 kg
    if hoeveelheid > 2500 and not context.get('deskundig_personeel', False):
        acties.append('M85: Deskundig personeel aanstellen — aantoonbare vakbekwaamheid (≥2500 kg)')
        score -= 15
        status = STATUS_ACTIE

    # Instructie medewerkers
    if not context.get('instructie_medewerkers', True):
        acties.append('M86: Instructie medewerkers — gevaren, maatregelen, intern noodplan')
        score -= 10
        status = STATUS_ACTIE

    # Noodplan
    if not context.get('noodplan', True):
        acties.append('M113: Intern noodplan opstellen met procedures voor detectie, waarschuwing, ontruiming en incidentbestrijding')
        score -= 15
        status = STATUS_ACTIE

    # VIB/SDS
    if not context.get('vib Beschikbaar', True):
        acties.append('M137: Veiligheidsinformatiebladen (VIB/SDS) beschikbaar stellen voor medewerkers en hulpdiensten')
        score -= 10
        status = STATUS_ACTIE

    # Informatie hulpdiensten
    if not context.get('info_hulpdiensten', True):
        acties.append('M116: Informatie aan hulpdiensten beschikbaar stellen — afgestemd met veiligheidsregio')
        score -= 10
        status = STATUS_ACTIE

    # Calamiteitenploeg
    if niveau == 'A' and not context.get('calamiteitenploeg', False):
        acties.append('M122: Calamiteitenploeg aanstellen of beschikbaar hebben — getraind in eerste maatregelen')
        score -= 15
        status = STATUS_ACTIE

    # Nooddouche
    h_zinnen = stof.get('h_zinnen', [])
    codes = parse_h_codes(h_zinnen)
    if codes & {314, 300, 310, 330} and not context.get('nooddouche', True):
        acties.append('M115: Nooddouche en oogspoelvoorziening aanbrengen — binnen 10 seconden bereikbaar')
        score -= 10
        status = STATUS_ACTIE

    # Vluchtroutes
    if not context.get('vluchtroutes', True):
        acties.append('M130-132: Voldoende vluchtroutes, duidelijk gemarkeerd, deuren van buitenas te openen')
        score -= 10
        status = STATUS_ACTIE

    if not acties:
        acties.append('M86: Instructie medewerkers up-to-date')
        acties.append('M113: Noodplan aanwezig en geëvalueerd')

    return {
        'status': status,
        'hoofdstuk': 'PGS 15:2025 Hoofdstuk 8',
        'paragraaf': '§8.1-8.6',
        'arbobesluit': 'Arbobesluit Art. 3.17, Art. 18',
        'bouwbesluit': None,
        'wet_milieubeer': None,
        'uitleg': f'{naam}: niveau {niveau} — organisatorische maatregelen gecontroleerd. {"Afwijkingen." if score < 0 else "Voldoet."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_hoofdstuk_9(stof: dict, context: dict) -> dict:
    """Hoofdstuk 9: Externe veiligheid (PRVG, VRG, veiligheidsafstanden).

    Toetst: M23 (escłatatie voorkomen), M90 (WBDBO via afstand),
    M93 (afstand ADR 7), M109-110 (bereikbaarheid terrein).
    """
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')

    if niveau == 'Buiten scope':
        return {
            'status': STATUS_NVT,
            'hoofdstuk': 'PGS 15:2025 Hoofdstuk 9',
            'paragraaf': '§9.1-9.3',
            'arbobesluit': None,
            'bouwbesluit': None,
            'wet_milieubeer': 'Omgevingswet — PRVG, VRG, vergunningplicht',
            'uitleg': f'{naam}: hoeveelheid onder drempel — Hoofdstuk 9 niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    # PRVG-check
    if niveau in ('A', 'B') and not context.get('prvg_vergunning', True):
        acties.append('PRVG: Plan voor Risicobeperking en Veiligheidsvoorzieningen — vergunningcheck uitvoeren')
        score -= 15
        status = STATUS_ACTIE

    # Bereikbaarheid terrein
    if not context.get('bereikbaarheid_terrein', True):
        acties.append('M110: Terrein via twee ingangen toegankelijk voor hulpdiensten — min. 3,5 m breedte')
        score -= 10
        status = STATUS_ACTIE

    # Escalatie
    if not context.get('escalatie_voorkomen', True):
        acties.append('M23: Incidenten mogen zich niet verplaatsen naar aangrenzende opslagvoorzieningen')
        score -= 15
        status = STATUS_ACTIE

    if not acties:
        acties.append('M110: Bereikbaarheid terrein gewaarborgd')
        acties.append('M23: Escalatie maatregelen aanwezig')

    return {
        'status': status,
        'hoofdstuk': 'PGS 15:2025 Hoofdstuk 9',
        'paragraaf': '§9.1-9.3',
        'arbobesluit': None,
        'bouwbesluit': None,
        'wet_milieubeer': 'Omgevingswet — PRVG, VRG',
        'uitleg': f'{naam}: niveau {niveau} — externe veiligheid gecontroleerd. {"Afwijkingen." if score < 0 else "Voldoet."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_bijlage_a(stof: dict, context: dict) -> dict:
    """Bijlage A: Stofspecifieke eisen per gevarenklasse.

    Toetst: M43 (ADR 4.1 VG II/III gescheiden opslag), M44 (ADR 4.2/4.3 + ADR 3),
    M49 (acuut toxische stoffen UN-verpakking), M54-56 (gasflessen),
    M143 (organische peroxiden).
    """
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')
    adr_klasse = stof.get('adr_klasse', '')
    vg = stof.get('vg', '')
    h_zinnen = stof.get('h_zinnen', [])

    if niveau == 'Buiten scope':
        return {
            'status': STATUS_NVT,
            'hoofdstuk': 'PGS 15:2025 Bijlage A',
            'paragraaf': 'Bijlage A — Stofspecifieke eisen',
            'arbobesluit': 'Arbobesluit Art. 4.1',
            'bouwbesluit': None,
            'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel — Bijlage A niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    acute_flag, acute_cat = is_acute_tox(h_zinnen)

    # Acuut toxische stoffen
    if acute_flag and vg in ('I', 'II'):
        if not context.get('un_verpakking', True):
            acties.append(f'M49: Acuut toxische stoffen uitsluitend in UN-gekeurde verpakkingen — opslaghoogte max 1,80 m (VG I)')
            score -= 15
            status = STATUS_ACTIE

    # ADR 4.1 VG II/III
    if adr_klasse == '4.1' and vg in ('II', 'III'):
        acties.append('M43: ADR 4.1 VG II/III — stoffen met gevaarsaspect D/DT alleen met elkaar opslaan, SR2 altijd apart')

    # ADR 4.2/4.3 + ADR 3
    if adr_klasse in ('4.2', '4.3') and vg == 'III':
        combi_hoeveelheid = context.get('combi_423_3_hoeveelheid', 0)
        if combi_hoeveelheid > 10000:
            acties.append('M44: Combinatie ADR 4.2/4.3 VG III + ADR 3 VG III >10.000 kg → niveau A vereist + 30 min brandwerende scheiding')
            score -= 15
            status = STATUS_ACTIE

    # Gasflessen
    if adr_klasse == '2':
        waterinhoud = context.get('waterinhoud_flessen', 0)
        if waterinhoud > 250:
            acties.append('M8: Gasflessen ≥250 l — strengere eisen: ventilatie, brandwerendheid, scheiding')
        if not context.get('gasflessen_vastgezet', True):
            acties.append('M56: Gasflessen tegen omvallen beveiligen — ketting, beugel of spanband')
            score -= 10
            status = STATUS_ACTIE

    # Organische peroxiden
    if adr_klasse == '5.2':
        if not context.get('peroxid_tempbeheersing', True):
            acties.append('M143: Organische peroxiden — temperatuurbeheersing, gescheiden opslag, koeling bij brand')
            score -= 15
            status = STATUS_ACTIE

    if not acties:
        acties.append('Bijlage A: Stofspecifieke eisen gecontroleerd')

    return {
        'status': status,
        'hoofdstuk': 'PGS 15:2025 Bijlage A',
        'paragraaf': 'Bijlage A',
        'arbobesluit': 'Arbobesluit Art. 4.1',
        'bouwbesluit': None,
        'wet_milieubeer': None,
        'uitleg': f'{naam}: niveau {niveau} — stofspecifieke eisen gecontroleerd. {"Afwijkingen." if score < 0 else "Voldoet."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_bijlage_b(stof: dict, context: dict) -> dict:
    """Bijlage B: Mengselregels (scheidingstabel).

    Toetst: M35 (onverenigbare combinaties), M36 (gescheiden opslag stellingen).
    Volledige onverenigbaarheidsmatrix gebaseerd op ADR-segregatietabel.
    """
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')
    adr_klasse = stof.get('adr_klasse', '')

    if niveau == 'Buiten scope':
        return {
            'status': STATUS_NVT,
            'hoofdstuk': 'PGS 15:2025 Bijlage B',
            'paragraaf': 'Bijlage B — Mengselregels',
            'arbobesluit': 'Arbobesluit Art. 3.17',
            'bouwbesluit': None,
            'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel — Bijlage B niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    overige_stoffen = context.get('overige_stoffen_in_opslag', [])
    if overige_stoffen and adr_klasse:
        for andere in overige_stoffen:
            andere_adr = andere.get('adr_klasse', '')
            if andere_adr and andere_adr != adr_klasse:
                combo = _check_onverenigbaar(adr_klasse, andere_adr)
                if combo:
                    acties.append(f'M35: {combo["label"]} — {andere.get("naam", "?")}: {combo["detail"]}')
                    score -= combo['score_impact']
                    status = STATUS_ACTIE

    if not acties:
        acties.append('M35: Geen onverenigbare combinaties geconstateerd in huidige opslag')
    else:
        acties.append('M36: In stellingen: onverenigbare stoffen NIET boven elkaar plaatsen')

    return {
        'status': status,
        'hoofdstuk': 'PGS 15:2025 Bijlage B',
        'paragraaf': 'Bijlage B',
        'arbobesluit': 'Arbobesluit Art. 3.17',
        'bouwbesluit': None,
        'wet_milieubeer': None,
        'uitleg': f'{naam}: niveau {niveau} — mengselregels gecontroleerd. {f"{len(acties)} onverenigbare combinaties." if score < 0 else "Geen conflicten."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_bijlage_c(stof: dict, context: dict) -> dict:
    """Bijlage C: Maximale hoeveelheden per gevarenklasse.

    Toetst: M15 (max vloeroppervlak), M16 (spuitbussen/gaspatronen),
    M17 (opslag verdieping), M18 (max cilinderpakket), M19 (max organische peroxiden).
    """
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')
    adr_klasse = stof.get('adr_klasse', '')
    hoeveelheid = float(stof.get('hoeveelheid', 0) or 0)

    if niveau == 'Buiten scope':
        return {
            'status': STATUS_NVT,
            'hoofdstuk': 'PGS 15:2025 Bijlage C',
            'paragraaf': 'Bijlage C — Maximale hoeveelheden',
            'arbobesluit': None,
            'bouwbesluit': 'Bouwbesluit 2012',
            'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel — Bijlage C niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    # Max opslagvoorziening
    max_vloer = 2500  # m² met VBB
    if not context.get('vbb_systeem', False):
        max_vloer = 1000 if hoeveelheid <= 20000 else 2500
    oppervlak = context.get('vloeroppervlak', 0)
    if oppervlak > max_vloer:
        acties.append(f'M15: Vloeroppervlak {oppervlak} m² overschrijdt max {max_vloer} m² — splitsen in meerdere opslagvoorzieningen')
        score -= 20
        status = STATUS_NIET_COMPLIANT

    # Opslag op verdieping
    verdieping = context.get('verdieping', 'begane grond')
    if verdieping in ('kelder', 'hoger') and hoeveelheid > 500:
        acties.append(f'M17: Max 500 kg op kelder/hogere verdieping (huidig: {hoeveelheid} kg) — tenzij onbrandbare stoffen')
        score -= 15
        status = STATUS_ACTIE
    elif verdieping == '1e' and hoeveelheid > 2500:
        acties.append(f'M17: Max 2.500 kg op 1e verdieping (huidig: {hoeveelheid} kg)')
        score -= 15
        status = STATUS_ACTIE

    # Organische peroxiden
    if adr_klasse == '5.2' and hoeveelheid > 1000:
        acties.append('M19: Max 1.000 kg organische peroxiden per opslagvoorziening (LQ verpakt)')
        score -= 20
        status = STATUS_NIET_COMPLIANT

    # Gasflessen cilinderpakket
    if adr_klasse == '2':
        max_ci = 1000 if context.get('giftig_gas', False) else 3000
        waterinhoud = context.get('waterinhoud_totaal', 0)
        if waterinhoud > max_ci:
            acties.append(f'M18: Totale waterinhoud cilinderpakket {waterinhoud} l > max {max_ci} l')
            score -= 15
            status = STATUS_ACTIE

    if not acties:
        acties.append(f'M15: Binnen max vloeroppervlak ({max_vloer} m²)')
        if verdieping == 'begane grond':
            acties.append('M17: Begane grond — geen verdiepingsbeperking')

    return {
        'status': status,
        'hoofdstuk': 'PGS 15:2025 Bijlage C',
        'paragraaf': 'Bijlage C',
        'arbobesluit': None,
        'bouwbesluit': 'Bouwbesluit 2012',
        'wet_milieubeer': None,
        'uitleg': f'{naam}: niveau {niveau} — maximale hoeveelheden gecontroleerd. {"Overschrijding." if score < 0 else "Binnen limieten."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def _check_onverenigbaar(adr_a: str, adr_b: str) -> Optional[dict]:
    """Check of twee ADR-klassen onverenigbaar zijn volgens Bijlage B."""
    for combo in ONVERENIGBARE_COMBINATIES:
        ga = combo['adr_groep_a']
        gb = combo['adr_groep_b']
        if adr_a in ga and (not gb or adr_b in gb):
            return combo
        if adr_b in ga and (not gb or adr_a in gb):
            return combo
    return None


# ═══════════════════════════════════════════════════════════════════
# SECTIE 4: MAATREGELENHIËRARCHIE
# ═══════════════════════════════════════════════════════════════════

def genereer_maatregelen_hierarchie(stof: dict, context: dict) -> list:
    """Genereer maatregelen per hiërarchieniveau voor een opslagsituatie."""
    naam = stof.get('naam', '')
    h_zinnen = stof.get('h_zinnen', [])
    adr_klasse = stof.get('adr_klasse', '')
    vg = stof.get('vg', '')
    niveau = context.get('strengste_niveau', 'Basis')
    effects = get_gezondheidseffecten(h_zinnen)
    codes = parse_h_codes(h_zinnen)

    hiërarchie = []

    # ── Niveau 1: Eliminatie ──
    elim_acties = []
    if 'carcinogeen' in effects:
        elim_acties.append(f'{naam} verwijderen uit opslag — Carc. 1A/1B, vervanging verplicht (Arbobesluit Art. 4.1)')
    if 'reprotoxisch' in effects:
        elim_acties.append(f'{naam} verwijderen — Repr. stof, vervanging overwegen')
    if 'waterreactief' in effects:
        elim_acties.append(f'{naam} verwijderen — waterreactieve stof, aanzienlijk risico bij lekkage')
    if niveau == 'A':
        elim_acties.append(f'{naam}: niveau A — overweeg eliminatie of substantiële hoeveelheidsreductie')

    hiërarchie.append({
        'niveau': HIËRARCHIE_ELIMINATIE,
        **MAATREGELEN_HIERARCHIE[HIËRARCHIE_ELIMINATIE],
        'acties': elim_acties if elim_acties else ['Opslag heroverwegen — eliminatie niet direct noodzakelijk'],
        'van_toepassing': bool(elim_acties),
    })

    # ── Niveau 2: Substitutie ──
    subst_acties = []
    if 'carcinogeen' in effects or 'reprotoxisch' in effects:
        subst_acties.append(f'Minder gevaarlijk alternatief zoeken — CMR-verplichting')
    if 'corrosief' in effects:
        subst_acties.append('Milder alternatief overwegen — minder corrosief equivalent')
    if 'brandbaar' in effects:
        subst_acties.append('Minder brandbaar alternatief overwegen — hogere VG of andere ADR-klasse')
    if vg == 'I':
        subst_acties.append(f'Overstappen naar VG II of III equivalent — lagere PGS 15 drempels')
    if niveau in ('A', 'B'):
        subst_acties.append(f'Substitutie kan niveau verlagen van {niveau} naar C of Basis')

    hiërarchie.append({
        'niveau': HIËRARCHIE_SUBSTITUTIE,
        **MAATREGELEN_HIERARCHIE[HIËRARCHIE_SUBSTITUTIE],
        'acties': subst_acties if subst_acties else ['Substitutie overwegen bij proceswijziging'],
        'van_toepassing': bool(subst_acties),
    })

    # ── Niveau 3: Technisch ──
    tech_acties = []
    if niveau in ('A', 'B', 'C'):
        tech_acties.append(f'Veiligheidsniveau {niveau} technische eisen implementeren')
    if 'brandbaar' in effects:
        tech_acties.append('ATEX-zonering implementeren — Richtlijn 2014/34/EU')
        tech_acties.append('Gasdetectiesysteem installeren')
    if 'corrosief' in effects:
        tech_acties.append('Nooddouche + oogwasstation binnen 10 seconden bereik — M115')
        tech_acties.append('Vloeistofdichte lekbak / opvangvoorziening — M30')
    if niveau == 'A':
        tech_acties.append('VBB-systeem installeren — M96')
        tech_acties.append('UPD (Uitgangspuntendocument) laten opstellen — M99')
    if stof.get('vorm', 'vloeistof') in ('vloeistof', 'vloeibaar'):
        tech_acties.append('Opvangvoorziening controleren — ≥110% grootste verpakking — M30')
    tech_acties.append('Aanrijdbeveiliging stellingen — M77')
    if not tech_acties:
        tech_acties.append('Algemene ventilatie — min. 5 luchtverversingen/uur — M111')

    hiërarchie.append({
        'niveau': HIËRARCHIE_TECHNISCH,
        **MAATREGELEN_HIERARCHIE[HIËRARCHIE_TECHNISCH],
        'acties': _dedup(tech_acties),
        'van_toepassing': True,
    })

    # ── Niveau 4: Organisatorisch ──
    org_acties = [
        'Interne noodplan / calamiteitenprocedure opstellen — M113',
        'Instructie en training medewerkers — M86',
        'Periodieke inspectie stellingconstructie — M82 (jaarlijks)',
    ]
    hoeveelheid = float(stof.get('hoeveelheid', 0) or 0)
    if hoeveelheid > 2500:
        org_acties.append('Deskundig personeel aanstellen — M85')
    if niveau == 'A':
        org_acties.append('Calamiteitenploeg vormen — M122')
    org_acties.extend([
        'Lekkage-instructie opstellen — M120',
        'Calamiteiteninstructie ophangen — M121',
        'Etiket- en SDS-controle procedure — M45, M136',
        'VIB/SDS beschikbaar stellen — M137',
        'Informatie hulpdiensten up-to-date — M116',
    ])

    hiërarchie.append({
        'niveau': HIËRARCHIE_ORGANISATORISCH,
        **MAATREGELEN_HIERARCHIE[HIËRARCHIE_ORGANISATORISCH],
        'acties': _dedup(org_acties),
        'van_toepassing': True,
    })

    # ── Niveau 5: PBM ──
    pbm_acties = []
    for h in h_zinnen:
        for pattern, items in H_PBM_MAP.items():
            if h.startswith(pattern):
                for item in items:
                    if item not in pbm_acties:
                        pbm_acties.append(item)
    pbm_beschrijvingen = [PBM_BESCHRIJVINGEN.get(p, p) for p in pbm_acties]
    if not pbm_beschrijvingen:
        pbm_beschrijvingen = ['Basis PBM: handschoenen en veiligheidsbril']

    hiërarchie.append({
        'niveau': HIËRARCHIE_PBM,
        **MAATREGELEN_HIERARCHIE[HIËRARCHIE_PBM],
        'acties': pbm_beschrijvingen,
        'van_toepassing': bool(h_zinnen),
    })

    return hiërarchie


# ═══════════════════════════════════════════════════════════════════
# SECTIE 5: AGGREGATIE & SYNERGIE ANALYSE
# ═══════════════════════════════════════════════════════════════════

def analyseer_aggregatie(stoffen_result: list) -> dict:
    """Analyseer cumulatieve risico's bij multi-stof opslag.

    Bevat:
    - Incompatibele combinaties (Bijlage B)
    - Cumulatieve risico's per gevarenklasse
    - Brandveiligheidsaggregatie (totale brandlading)
    """
    if len(stoffen_result) < 2:
        return {
            'van_toepassing': False,
            'score': RISICO_LAAG,
            'onverenigbare_combinaties': [],
            'cumulatief_per_klasse': {},
            'brandlading_aggregatie': {},
            'aanbevelingen': [],
        }

    # ── Onverenigbare combinaties ──
    onverenigbaar = []
    checked = set()
    for i, s1 in enumerate(stoffen_result):
        for j, s2 in enumerate(stoffen_result):
            if i >= j:
                continue
            key = tuple(sorted([s1.get('naam', ''), s2.get('naam', '')]))
            if key in checked:
                continue
            checked.add(key)
            adr1 = s1.get('adr_klasse', '')
            adr2 = s2.get('adr_klasse', '')
            combo = _check_onverenigbaar(adr1, adr2)
            if combo:
                onverenigbaar.append({
                    'stof_a': s1.get('naam', ''),
                    'stof_b': s2.get('naam', ''),
                    'type': combo['type'],
                    'label': combo['label'],
                    'risico': combo['risico'],
                    'detail': combo['detail'],
                    'score_impact': combo['score_impact'],
                })

    # ── Cumulatief per gevarenklasse ──
    cumulatief = {}
    for s in stoffen_result:
        adr = s.get('adr_klasse', '')
        hoev = float(s.get('hoeveelheid', 0) or 0)
        if adr:
            if adr not in cumulatief:
                cumulatief[adr] = {'aantal_stoffen': 0, 'totale_hoeveelheid': 0, 'stoffen': []}
            cumulatief[adr]['aantal_stoffen'] += 1
            cumulatief[adr]['totale_hoeveelheid'] += hoev
            cumulatief[adr]['stoffen'].append(s.get('naam', ''))

    # ── Brandlading aggregatie ──
    brandbare_klassen = ['3', '4.1', '4.2', '4.3']
    totale_brandlading = 0
    brandbare_stoffen = []
    for s in stoffen_result:
        adr = s.get('adr_klasse', '')
        if adr in brandbare_klassen:
            hoev = float(s.get('hoeveelheid', 0) or 0)
            totale_brandlading += hoev
            brandbare_stoffen.append(s.get('naam', ''))

    brandlading = {
        'totale_brandlading_kg': totale_brandlading,
        'aantal_brandbare_stoffen': len(brandbare_stoffen),
        'brandbare_stoffen': brandbare_stoffen,
        'niveau_impact': 'A' if totale_brandlading > 10000 else ('B' if totale_brandlading > 2000 else 'C' if totale_brandlading > 0 else 'Geen'),
    }

    # ── Score ──
    total_impact = sum(c['score_impact'] for c in onverenigbaar)
    if total_impact >= 50 or len(onverenigbaar) >= 3:
        score = RISICO_KRITIEK
    elif total_impact >= 30 or len(onverenigbaar) >= 2:
        score = RISICO_HOOG
    elif total_impact >= 15:
        score = RISICO_MEDIUM
    else:
        score = RISICO_LAAG

    aanbevelingen = []
    if onverenigbaar:
        aanbevelingen.append('Scheid onverenigbare stoffen onmiddellijk — compartimenteren of afstand creëren')
    if totale_brandlading > 10000:
        aanbevelingen.append('Totale brandlading >10.000 kg — niveau A maatregelen vereisen (VBB, UPD)')
    if any(c['aantal_stoffen'] > 3 for c in cumulatief.values()):
        aanbevelingen.append('Meerdere stoffen per ADR-klasse — cumulatief risico evalueren')
    if score in (RISICO_HOOG, RISICO_KRITIEK):
        aanbevelingen.append('Overweeg opslag in afzonderlijke opslagvoorzieningen')

    return {
        'van_toepassing': True,
        'aantal_stoffen': len(stoffen_result),
        'score': score,
        'onverenigbare_combinaties': onverenigbaar,
        'cumulatief_per_klasse': cumulatief,
        'brandlading_aggregatie': brandlading,
        'aanbevelingen': aanbevelingen,
    }


# ═══════════════════════════════════════════════════════════════════
# SECTIE 6: COMPLIANCE SCORE
# ═══════════════════════════════════════════════════════════════════

def bereken_compliance_score(toetsingen: dict) -> dict:
    """Bereken compliance score 0-100 op basis van alle toetsingen."""
    base_score = 100
    total_impact = 0

    for key, toets in toetsingen.items():
        impact = toets.get('score_impact', 0)
        total_impact += impact

    score = max(0, min(100, base_score + total_impact))

    if score >= 90:
        label = 'Volledig compliant'
        kleur = '#22C55E'
    elif score >= 70:
        label = 'Grotendeels compliant — kleine acties'
        kleur = '#3B82F6'
    elif score >= 50:
        label = 'Deels compliant — meerdere acties nodig'
        kleur = '#F59E0B'
    else:
        label = 'Niet-compliant — directe actie vereist'
        kleur = '#EF4444'

    return {
        'score': score,
        'label': label,
        'kleur': kleur,
        'base_score': base_score,
        'total_impact': total_impact,
        'toetsingen': {k: v.get('score_impact', 0) for k, v in toetsingen.items()},
    }


# ═══════════════════════════════════════════════════════════════════
# SECTIE 7: ADAPTER CLASS
# ═══════════════════════════════════════════════════════════════════

class PGS15ComplianceAnalyzer:
    """PGS 15:2025 Compliance Analyzer.

    Adapter class die de volledige compliance engine samenvoegt.
    Gebruik analyseer() voor een volledige analyse van één of meerdere stoffen.
    """

    def __init__(self, context: dict = None):
        """Initialiseer analyzer met optionele standaard context.

        Args:
            context: Dict met standaard opslagparameters (opslag_type, wbdbo, etc.)
        """
        self.context = context or {}

    def analyseer(self, stoffen: list, context: dict = None) -> dict:
        """Volledige PGS 15 compliance analyse.

        Args:
            stoffen: Lijst van stof-dicts met minimaal: naam, adr_klasse, vg, hoeveelheid, h_zinnen
            context: Optionele override van standaard context

        Returns:
            Dict met per_stof resultaten, aggregatie, samenvatting
        """
        ctx = {**self.context, **(context or {})}
        resultaten = []

        for stof in stoffen:
            try:
                resultaat = self._analyseer_stof(stof, ctx)
                resultaten.append(resultaat)
            except Exception as e:
                log.error(f'Fout bij analyse {stof.get("naam", "?")}: {e}')
                resultaten.append({
                    'naam': stof.get('naam', 'Onbekend'),
                    'cas': stof.get('cas', ''),
                    'error': str(e),
                    'compliance_score': {'score': 0, 'label': 'Analyse fout', 'kleur': '#EF4444'},
                })

        aggregatie = analyseer_aggregatie(resultaten)

        samenvatting = {
            'totaal_stoffen': len(resultaten),
            'niveau_a': sum(1 for r in resultaten if r.get('niveau') == 'A'),
            'niveau_b': sum(1 for r in resultaten if r.get('niveau') == 'B'),
            'niveau_c': sum(1 for r in resultaten if r.get('niveau') == 'C'),
            'niveau_basis': sum(1 for r in resultaten if r.get('niveau') == 'Basis'),
            'buiten_scope': sum(1 for r in resultaten if r.get('niveau') == 'Buiten scope'),
            'gemiddelde_score': sum(r.get('compliance_score', {}).get('score', 0) for r in resultaten) / max(len(resultaten), 1),
            'kritieke_stoffen': [r['naam'] for r in resultaten if r.get('compliance_score', {}).get('score', 100) < 50],
            'aggregatie_score': aggregatie.get('score', RISICO_LAAG),
            'onverenigbare_combinaties': len(aggregatie.get('onverenigbare_combinaties', [])),
        }

        return {
            'timestamp': datetime.utcnow().isoformat() + 'Z',
            'per_stof': resultaten,
            'aggregatie': aggregatie,
            'samenvatting': samenvatting,
        }

    def _analyseer_stof(self, stof: dict, context: dict) -> dict:
        """Interne: volledige analyse per stof."""
        naam = stof.get('naam', '')
        cas = stof.get('cas', '')
        adr_klasse = stof.get('adr_klasse', '')
        vg = stof.get('vg', '')
        hoeveelheid = float(stof.get('hoeveelheid', 0) or 0)
        h_zinnen = stof.get('h_zinnen', [])
        vorm = stof.get('vorm', 'vloeistof')

        # Bepaal strengste niveau
        niveau, route = get_strictste_niveau(
            adr_klasse, vg, hoeveelheid, h_zinnen, vorm,
            stof.get('flashpoint'),
        )

        # Context verrijken
        stof_context = {
            **context,
            'strengste_niveau': niveau,
            'overige_stoffen_in_opslag': context.get('overige_stoffen_in_opslag', []),
        }

        # ── Compliance toetsingen per hoofdstuk ──
        toetsingen = {
            'hoofdstuk_3': toets_hoofdstuk_3(stof, stof_context),
            'hoofdstuk_4': toets_hoofdstuk_4(stof, stof_context),
            'hoofdstuk_5': toets_hoofdstuk_5(stof, stof_context),
            'hoofdstuk_6': toets_hoofdstuk_6(stof, stof_context),
            'hoofdstuk_7': toets_hoofdstuk_7(stof, stof_context),
            'hoofdstuk_8': toets_hoofdstuk_8(stof, stof_context),
            'hoofdstuk_9': toets_hoofdstuk_9(stof, stof_context),
            'bijlage_a': toets_bijlage_a(stof, stof_context),
            'bijlage_b': toets_bijlage_b(stof, stof_context),
            'bijlage_c': toets_bijlage_c(stof, stof_context),
        }

        # ── Compliance score ──
        compliance_score = bereken_compliance_score(toetsingen)

        # ── Maatregelenhiërarchie ──
        hiërarchie = genereer_maatregelen_hierarchie(stof, stof_context)

        # ── CLP info ──
        effects = get_gezondheidseffecten(h_zinnen)

        # ── Actiepunten ──
        actiepunten = self._genereer_actiepunten(naam, niveau, toetsingen, compliance_score)

        # ── Wettelijke bronverwijzingen ──
        bronnen = self._genereer_bronnen(toetsingen)

        return {
            'naam': naam,
            'cas': cas,
            'adr_klasse': adr_klasse,
            'vg': vg,
            'hoeveelheid': hoeveelheid,
            'vorm': vorm,
            'niveau': niveau,
            'route': route,
            'h_zinnen': h_zinnen,
            'gezondheidseffecten': effects,
            'compliance_score': compliance_score,
            'toetsingen': toetsingen,
            'hiërarchie': hiërarchie,
            'actiepunten': actiepunten,
            'wettelijke_bronnen': bronnen,
        }

    def _genereer_actiepunten(self, naam: str, niveau: str,
                               toetsingen: dict, compliance_score: dict) -> list:
        """Genereer geprioriteerde actiepunten."""
        score = compliance_score.get('score', 100)
        acties = []

        if score < 50:
            acties.append({
                'prioriteit': 'KRITIEK',
                'actie': f'{naam}: Compliance score {score}/100 — directe actie vereist',
                'deadline': 'Onmiddellijk',
                'bron': 'PGS 15:2025',
            })

        for key, toets in toetsingen.items():
            if toets.get('status') == STATUS_NIET_COMPLIANT:
                for m in toets.get('maatregelen', []):
                    acties.append({
                        'prioriteit': 'KRITIEK',
                        'actie': f'{naam}: {m}',
                        'deadline': 'Direct',
                        'bron': toets.get('hoofdstuk', ''),
                    })
            elif toets.get('status') == STATUS_ACTIE:
                for m in toets.get('maatregelen', []):
                    acties.append({
                        'prioriteit': 'HOOG',
                        'actie': f'{naam}: {m}',
                        'deadline': 'Binnen 30 dagen',
                        'bron': toets.get('hoofdstuk', ''),
                    })

        if niveau == 'A':
            acties.append({
                'prioriteit': 'HOOG',
                'actie': f'{naam}: Niveau A — VBB-systeem + UPD vereisen, vergunningcheck uitvoeren',
                'deadline': 'Vóór ingebruikname',
                'bron': 'PGS 15:2025 Hoofdstuk 5',
            })

        return acties

    def _genereer_bronnen(self, toetsingen: dict) -> list:
        """Genereer wettelijke bronverwijzingen op basis van toetsingen."""
        bronnen = []
        seen = set()

        for key, toets in toetsingen.items():
            for field in ['hoofdstuk', 'arbobesluit', 'bouwbesluit', 'wet_milieubeer']:
                val = toets.get(field)
                if val and val not in seen:
                    seen.add(val)
                    bronnen.append(val)

        return bronnen


# ═══════════════════════════════════════════════════════════════════
# SECTIE 8: CLI INTERFACE (test)
# ═══════════════════════════════════════════════════════════════════

if __name__ == '__main__':
    # Testdata: 3 stoffen in dezelfde opslag
    test_stoffen = [
        {
            'naam': 'Aceton',
            'cas': '67-64-1',
            'adr_klasse': '3',
            'vg': 'II',
            'hoeveelheid': 5000,
            'vorm': 'vloeistof',
            'h_zinnen': ['H225', 'H319', 'H336'],
        },
        {
            'naam': 'Natriumhypochloriet (bleekloog)',
            'cas': '7681-52-9',
            'adr_klasse': '8',
            'vg': 'II',
            'hoeveelheid': 3000,
            'vorm': 'vloeistof',
            'h_zinnen': ['H314', 'H400'],
            'is_brandbaar_k8': False,
        },
        {
            'naam': 'Tolueen',
            'cas': '108-88-3',
            'adr_klasse': '3',
            'vg': 'II',
            'hoeveelheid': 8000,
            'vorm': 'vloeistof',
            'h_zinnen': ['H226', 'H305', 'H312', 'H319', 'H336'],
        },
    ]

    test_context = {
        'opslag_type': 'binnen',
        'wbdbo_minuten': 60,
        'vbb_systeem': False,
        'upd_aanwezig': False,
        'opvangvoorziening': True,
        'vloer_vloeistofdicht': True,
        'geen_rioolverbinding': True,
        'aanrijdbeveiliging': True,
        'stelling_inspectie': True,
        'transport_beperkt': True,
        'deskundig_personeel': True,
        'instructie_medewerkers': True,
        'noodplan': True,
        'vib Beschikbaar': True,
        'info_hulpdiensten': True,
        'calamiteitenploeg': False,
        'nooddouche': True,
        'vluchtroutes': True,
        'prvg_vergunning': True,
        'bereikbaarheid_terrein': True,
        'escalatie_voorkomen': True,
        'bluswatervoorziening': True,
        'bluswateropvang': False,
        'bodembescherming': True,
        'pallets_regel': True,
        'blustoestellen': 2,
        'vloeroppervlak': 400,
        'verpakking_goedgekeurd': True,
        'verdieping': 'begane grond',
        'dak_materiaal': 'a1',
    }

    analyzer = PGS15ComplianceAnalyzer(context=test_context)
    result = analyzer.analyseer(test_stoffen)

    print(json.dumps(result, indent=2, ensure_ascii=False, default=str))
