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

V2.0 Fixes (PGS 15:2025 Publicatie 565 compliance):
1. Voetnoot c: ADR 8+9 VG II/III onbrandbaar → Basis (geen C/B/A)
2. CLP-route ALLEEN voor stoffen ZONDER ADR-classificatie
3. CMR → arbo_flag, NIET niveau-bepalend voor ADR-geclassificeerde stoffen
4. Aggregatie per (ADR-klasse, VG) combinatie, strengste wint
5. Volledige PGS 15:2025 referenties in docstrings

Backwards compatible: v1 API (get_strictste_niveau, determine_niveau, etc.) blijft werken.
"""

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

log = logging.getLogger(__name__)

# ═══════════════════════════════════════════════════════════════════
# SECTIE 1: CONSTANTEN & DATA (ongewijzigd t.o.v. v1, behalve waar aangegeven)
# ═══════════════════════════════════════════════════════════════════

STATUS_COMPLIANT = 'COMPLIANT'
STATUS_NIET_COMPLIANT = 'NIET-COMPLIANT'
STATUS_NVT = 'NIET-VAN-TOEPASSING'
STATUS_ACTIE = 'ACTIE-VEREIST'

SCORE_VOLLEDIG = (90, 100)
SCORE_GROOTTEELS = (70, 89)
SCORE_DEELS = (50, 69)
SCORE_NIET = (0, 49)

RISICO_LAAG = 'LAAG'
RISICO_MEDIUM = 'MEDIUM'
RISICO_HOOG = 'HOOG'
RISICO_KRITIEK = 'KRITIEK'

HIËRARCHIE_ELIMINATIE = 1
HIËRARCHIE_SUBSTITUTIE = 2
HIËRARCHIE_TECHNISCH = 3
HIËRARCHIE_ORGANISATORISCH = 4
HIËRARCHIE_PBM = 5

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)
# PGS 15:2025 Tabel 2 ondergrensen + Tabel 6 niveaubepaling
NIVEAUS = {
    # PGS 15:2025 Tabel 6: ADR 3 — Brandbare vloeistoffen
    ('3', 'I'):    (1, 1, None, 10000),       # <10 ton → C, >10 ton → A
    ('3', 'II'):   (25, 25, None, 10000),      # <10 ton → C, >10 ton → A
    ('3', 'III'):  (50, 50, None, 10000),      # <10 ton → C, >10 ton → A
    # PGS 15:2025 Tabel 6: ADR 4.1
    ('4.1', 'I'):  (1, None, 10000, 20000),    # Maatwerk / B >10 ton, A >20 ton
    ('4.1', 'II'): (50, None, 50, 20000),      # B vanaf ondergrens, A >20 ton
    ('4.1', 'III'):(50, None, 50, None),       # B vanaf ondergrens
    # PGS 15:2025 Tabel 6: ADR 4.2
    ('4.2', 'I'):  (50, None, 50, 20000),
    ('4.2', 'II'): (50, None, 50, 20000),
    ('4.2', 'III'):(50, None, 50, None),
    # PGS 15:2025 Tabel 6: ADR 4.3
    ('4.3', 'I'):  (50, None, 50, 20000),
    ('4.3', 'II'): (50, None, 50, 20000),
    ('4.3', 'III'):(50, None, 50, None),
    # PGS 15:2025 Tabel 6: ADR 5.1 — <2,5 ton → C (60 min), >2,5 ton → B
    ('5.1', 'II'): (50, 50, 2500, None),
    ('5.1', 'III'):(50, 50, 2500, None),
    # PGS 15:2025 Tabel 6: ADR 5.2 — <1 ton → C
    ('5.2', 'I'):  (30, 30, None, None),
    # PGS 15:2025 Tabel 6: ADR 6.1
    ('6.1', 'I'):  (1, 1, None, 1000),         # <1 ton → C, >1 ton → A
    ('6.1', 'II'): (50, 50, None, 10000),      # <10 ton → C, >10 ton → A
    ('6.1', 'III'):(50, 50, None, 10000),      # <10 ton → C, >10 ton → A
    # PGS 15:2025 Tabel 6: ADR 8 VG I — afhankelijk van vlampunt
    ('8', 'I'):    (250, 250, 10000, 10000),
    # PGS 15:2025 Tabel 6: ADR 8 VG II+III — 250 kg ondergrens
    # Let op: C/B alleen bij brandbaar of acute tox cat 1/2/3 (zie voetnoot c)
    ('8', 'II'):   (250, 250, 10000, None),
    ('8', 'III'):  (250, 250, 10000, None),
    # PGS 15:2025 Tabel 6: ADR 9 VG II+III — 250 kg ondergrens
    ('9', 'II'):   (250, 250, 10000, None),
    ('9', 'III'):  (250, 250, 10000, None),
}

# ═══ Ondergrenzen per ADR-klasse en VG — PGS 15:2025 Tabel 2 ═══
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 — PGS 15:2025 Tabel 6 (stoffen ZONDER ADR) ═══
CLP_ROUTES = {
    'acute_tox_1': {'label': 'Acute Tox. Cat. 1 (CLP)', 'ondergrens': 1, 'C': 1000, 'B': None, 'A': 1000},
    'acute_tox_2_3': {'label': 'Acute Tox. Cat. 2/3 (CLP)', '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, Doelstellingen, Maatregelen, etc. (identiek aan v1) ──
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 = [
    ('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'),
]

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 = [
    {
        'type': 'ZUUR_BASE',
        'label': 'Zuren + Basen',
        'risico': 'Exotherme reactie, gasvorming (H2, Cl2), spatten',
        'adr_groep_a': ['8'],
        'adr_groep_b': ['8'],
        '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,
    },
]

H_PBM_MAP = {
    'H300': ['handschoenen', 'gelaatsmasker', 'ademlucht', 'beschermlaag'],
    'H301': ['handschoenen', 'gelaatsmasker', 'beschermlaag'],
    'H302': ['handscoenen'],
    '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': ['handscoenen', '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_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 (H340, H341, H350, H351, H360, H361)."""
    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).

    PGS 15:2025 categorisatie:
    - Cat 1: H300, H310, H330 (Acute Tox. 1/2 in CLP)
    - Cat 2: H301, H311, H331 (Acute Tox. 3 in CLP)
    - Cat 3: H302, H312, H332 (Acute Tox. 4 in CLP)
    """
    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 _is_brandbaar(h_zinnen: list, flashpoint: float = None) -> bool:
    """Bepaal of stof brandbaar is op basis van H-zinnen of vlampunt.

    PGS 15:2025 Tabel 6 voetnoot c: onbrandbaar = geen brandbare H-zinnen
    EN geen vlampunt < 100°C.
    """
    codes = parse_h_codes(h_zinnen)
    brandbaar_codes = {220, 221, 222, 224, 225, 226, 228, 240, 241, 242, 250, 251, 252, 260, 261, 270, 271, 272, 280, 281}
    if codes & brandbaar_codes:
        return True
    if flashpoint is not None and flashpoint < 100:
        return True
    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.

    PGS 15:2025 Tabel 6 + Tabel 2.
    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

    # ── PGS 15:2025 Voetnoot c: ADR 8+9 VG II/III onbrandbaar → altijd Basis ──
    # Tabel 6 heeft "-" (geen niveau) voor onbrandbare ADR 8+9 → altijd basisveiligheidsniveau
    # Dit geldt OOK onder de Tabel 2 ondergrens, omdat er geen C/B/A niveau is om te activeren
    if adr_klasse in ('8', '9') and vg in ('II', 'III') and not is_brandbaar_k8:
        return 'Basis'

    # PGS 15:2025 Tabel 2 — ondergrens check
    if hoeveelheid < ondergrens:
        return 'Buiten scope'

    # ── PGS 15:2025 Tabel 6: 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'
        if hoeveelheid >= 10000:
            return 'A'
        if hoeveelheid >= 250:
            return 'C'
        return 'Basis'

    # ── PGS 15:2025 Tabel 6: ADR 8+9 VG II/III brandbaar ──
    # (Onbrandbaar is hierboven al afgevangen via voetnoot c)
    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


# ═══════════════════════════════════════════════════════════════════
# SECTIE 2B: V2 NIVEAU BEPALING (NIEUW)
# ═══════════════════════════════════════════════════════════════════

NIVEAU_PRIORITEIT = {'A': 4, 'B': 3, 'C': 2, 'Basis': 1, 'Buiten scope': 0}


def get_strictste_niveau_v2(adr_klasse: str, vg: str, hoeveelheid: float,
                             h_zinnen: list = [], vorm: str = 'vloeistof',
                             flashpoint: float = None) -> Tuple[str, str, dict]:
    """V2: Bepaal strengste niveau — PGS 15:2025 compliant.

    BELANGRIJKSTE WIJZIGINGEN t.o.v. v1:
    - CLP-route ALLEEN voor stoffen ZONDER ADR-classificatie
    - CMR → arbo_flag, niet niveau-bepalend bij ADR-classificatie
    - Voetnoot c implementatie voor ADR 8+9 onbrandbaar

    Returns: (strengste_niveau, route_beschrijving, arbo_flags)
    """
    arbo_flags = {
        'cmr': False,
        'acute_tox': False,
        'acute_tox_cat': '',
        'clp_route_toegepast': False,
    }

    # ── Check CMR en acute toxiciteit ──
    cmr_flag = is_cmr(h_zinnen)
    acute_flag, acute_cat = is_acute_tox(h_zinnen)
    arbo_flags['cmr'] = cmr_flag
    arbo_flags['acute_tox'] = acute_flag
    arbo_flags['acute_tox_cat'] = acute_cat

    has_adr = bool(adr_klasse and vg)

    # ── ADR-route (indien ADR-geclassificeerd) ──
    if has_adr:
        # PGS 15:2025 Tabel 6: ADR-route is LEIDEND
        # Brandbaarheid check voor ADR 8/9
        is_brandbaar = _is_brandbaar(h_zinnen, flashpoint)
        n = determine_niveau(adr_klasse, vg, hoeveelheid,
                            is_brandbaar_k8=is_brandbaar,
                            flashpoint=flashpoint)
        route = f'ADR {adr_klasse} VG {vg}'

        # PGS 15:2025 voetnoot c: ADR 8+9 onbrandbaar → Basis (ook onder ondergrens)
        if adr_klasse in ('8', '9') and vg in ('II', 'III') and not is_brandbaar:
            route = f'ADR {adr_klasse} VG {vg} — Voetnoot c (onbrandbaar)'
            return 'Basis', route, arbo_flags

        # PGS 15:2025 voetnoot c: ADR 8+9 onbrandbaar → Basis
        if n == 'Basis' and adr_klasse in ('8', '9') and vg in ('II', 'III') and not is_brandbaar:
            route = f'ADR {adr_klasse} VG {vg} — Voetnoot c (onbrandbaar)'

        # ADR-geclassificeerd → GEEN CLP-route voor niveau
        # CMR en acute toxiciteit WEL in arbo_flags voor maatregelen-advies
        return n, route, arbo_flags

    # ── CLP-route (ALLEEN voor stoffen ZONDER ADR-classificatie) ──
    # PGS 15:2025 Tabel 6: CLP-stoffen rij
    niveaus = []
    labels = []

    if acute_flag:
        arbo_flags['clp_route_toegepast'] = True
        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:
        arbo_flags['clp_route_toegepast'] = True
        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', arbo_flags

    strengste_idx = max(range(len(niveaus)), key=lambda i: NIVEAU_PRIORITEIT.get(niveaus[i], 0))
    return niveaus[strengste_idx], labels[strengste_idx], arbo_flags


def get_strictste_niveau(adr_klasse: str, vg: str, hoeveelheid: float,
                          h_zinnen: list = [], vorm: str = 'vloeistof',
                          flashpoint: float = None) -> Tuple[str, str]:
    """V1 backwards compatible wrapper. Returns (niveau, route)."""
    n, route, _ = get_strictste_niveau_v2(
        adr_klasse, vg, hoeveelheid, h_zinnen, vorm, flashpoint
    )
    return n, route


def bereken_strictste_niveau_van_alle_routes(stof: dict) -> Tuple[str, str]:
    """V1 backwards compatible wrapper."""
    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: AGGREGATIE V2 (NIEUW)
# ═══════════════════════════════════════════════════════════════════

def bereken_geaggregeerd_niveau(stoffen: list) -> dict:
    """Bereken geaggregeerd niveau voor een lijst stoffen.

    PGS 15:2025 Tabel 6 voetnoot a: "Zowel hoofdgevaar als bijkomend gevaar
    wordt meegenomen. Bij meerdere ADR-klassen: totaal + zwaarste categorie."

    Args:
        stoffen: Lijst van stof-dicts met adr_klasse, vg, hoeveelheid, h_zinnen

    Returns:
        Dict met: eindniveau, per_groep resultaten, catch_all
    """
    # ── Stap 1: Aggregeren per (ADR-klasse, VG) combinatie ──
    groepen = {}
    for stof in stoffen:
        adr = stof.get('adr_klasse', '')
        vg = stof.get('vg', '')
        if not adr:
            continue
        key = (adr, vg)
        if key not in groepen:
            groepen[key] = {
                'adr_klasse': adr,
                'vg': vg,
                'totale_hoeveelheid': 0,
                'stoffen': [],
                'h_zinnen_union': set(),
            }
        hoev = float(stof.get('hoeveelheid', 0) or 0)
        groepen[key]['totale_hoeveelheid'] += hoev
        groepen[key]['stoffen'].append(stof.get('naam', ''))
        groepen[key]['h_zinnen_union'].update(stof.get('h_zinnen', []))

    # ── Stap 2: Niveau per groep bepalen ──
    per_groep = {}
    for key, groep in groepen.items():
        adr, vg = key
        h_union = list(groep['h_zinnen_union'])
        is_brandbaar = _is_brandbaar(h_union)

        niveau = determine_niveau(
            adr, vg, groep['totale_hoeveelheid'],
            is_brandbaar_k8=is_brandbaar,
        )
        route = f'ADR {adr} VG {vg} (aggregatie: {", ".join(groep["stoffen"][:3])})'

        # Voetnoot c check
        if adr in ('8', '9') and vg in ('II', 'III') and not is_brandbaar:
            niveau = 'Basis'
            route = f'ADR {adr} VG {vg} — Voetnoot c (aggregatie, onbrandbaar)'

        per_groep[key] = {
            'niveau': niveau,
            'route': route,
            'totale_hoeveelheid': groep['totale_hoeveelheid'],
            'aantal_stoffen': len(groep['stoffen']),
            'stoffen': groep['stoffen'],
        }

    # ── Stap 3: Strengste niveau = eindniveau ──
    if not per_groep:
        return {
            'eindniveau': 'Buiten scope',
            'per_groep': {},
            'catch_all': False,
        }

    strengste_niveau = 'Buiten scope'
    strengste_route = ''
    for key, result in per_groep.items():
        if NIVEAU_PRIORITEIT.get(result['niveau'], 0) > NIVEAU_PRIORITEIT.get(strengste_niveau, 0):
            strengste_niveau = result['niveau']
            strengste_route = result['route']

    # ── Stap 4: Catch-all check (PGS 15:2025 §2 / Tabel 1) ──
    totaal_alle_stoffen = sum(g['totale_hoeveelheid'] for g in groepen.values())
    catch_all = totaal_alle_stoffen > 1000

    if catch_all and strengste_niveau == 'Buiten scope':
        strengste_niveau = 'Basis'
        strengste_route = 'Catch-all (>1000 kg totaal)'

    return {
        'eindniveau': strengste_niveau,
        'eindroute': strengste_route,
        'per_groep': per_groep,
        'totaal_alle_stoffen_kg': totaal_alle_stoffen,
        'catch_all': catch_all,
    }


# ═══════════════════════════════════════════════════════════════════
# SECTIE 4: COMPLIANCE TOETSING (identiek aan v1, met niveau uit v2)
# ═══════════════════════════════════════════════════════════════════

def toets_hoofdstuk_3(stof: dict, context: dict) -> dict:
    """PGS 15:2025 Hoofdstuk 3: Algemene veiligheidseisen."""
    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_min = 60
    if niveau == 'A':
        wbdbo_min = 90
    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

    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

    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:
    """PGS 15:2025 Hoofdstuk 4: Opslagsystemen en -methoden."""
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')
    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

    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

    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:
                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

    if niveau in ('A', 'B'):
        oppervlak = context.get('vloeroppervlak', 0)
        if oppervlak > 300:
            acties.append(f'M40/M42: Vakindeling vereist — vakken max 300 m²')
            score -= 10
            status = STATUS_ACTIE

    if not context.get('verpakking_goedgekeurd', True):
        acties.append('M48: Verpakking moet voldoen')
        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.',
        'maatregelen': acties, 'score_impact': score,
    }


def toets_hoofdstuk_5(stof: dict, context: dict) -> dict:
    """PGS 15:2025 Hoofdstuk 5: Brandveiligheid."""
    naam = stof.get('naam', '')
    niveau = context.get('strengste_niveau', 'Basis')

    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

    if niveau == 'A':
        if not context.get('vbb_systeem', False):
            acties.append('M96: VBB-systeem VERPLICHT bij niveau A')
            score -= 25
            status = STATUS_NIET_COMPLIANT
        elif not context.get('upd_aanwezig', False):
            acties.append('M99: UPD opstellen')
            score -= 15
            status = STATUS_ACTIE

    oppervlak = context.get('vloeroppervlak', 0)
    benodigd = max(1, int(oppervlak / 200))
    aanwezig = context.get('blustoestellen', 0)
    if aanwezig < benodigd:
        acties.append(f'M95: Minimaal {benodigd} draagbaar blustoestel(len) nodig')
        score -= 10
        status = STATUS_ACTIE

    if niveau in ('A', 'B', 'C') and not context.get('bluswatervoorziening', True):
        acties.append('M103: Bluswatervoorziening aanleggen')
        score -= 15
        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.',
        'maatregelen': acties, 'score_impact': score,
    }


def toets_hoofdstuk_6(stof: dict, context: dict) -> dict:
    """PGS 15:2025 Hoofdstuk 6: Milieumaatregelen."""
    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',
            '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})

    if vorm in ('vloeistof', 'vloeibaar') and not context.get('vloer_vloeistofdicht', True):
        acties.append('M112: Vloer moet vloeistofdicht zijn')
        score -= 15
        status = STATUS_ACTIE

    if not context.get('geen_rioolverbinding', True):
        acties.append('M29: Vloer mag geen openingen naar riolering hebben')
        score -= 15
        status = STATUS_ACTIE

    if niveau == 'A' and not context.get('bluswateropvang', False):
        acties.append('M32: Bluswateropvang aanleggen')
        score -= 20
        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.',
        'maatregelen': acties, 'score_impact': score,
    }


def toets_hoofdstuk_7(stof: dict, context: dict) -> dict:
    """PGS 15:2025 Hoofdstuk 7: Interne transportveiligheid."""
    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.', 'maatregelen': [], 'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    if not context.get('aanrijdbeveiliging', True):
        acties.append('M77: Aanrijdbeveiliging stellingen')
        score -= 10
        status = STATUS_ACTIE
    if not context.get('stelling_inspectie', True):
        acties.append('M82: Jaarlijkse stellinginspectie')
        score -= 10
        status = STATUS_ACTIE
    if not context.get('transport_beperkt', True):
        acties.append('M46: Transportmiddelen beperken')
        score -= 5
        status = STATUS_ACTIE

    if not acties:
        acties.append('M46: Transportmiddelen beperkt')
        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.',
        'maatregelen': acties, 'score_impact': score,
    }


def toets_hoofdstuk_8(stof: dict, context: dict) -> dict:
    """PGS 15:2025 Hoofdstuk 8: Organisatorische maatregelen."""
    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.', 'maatregelen': [], 'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    if hoeveelheid > 2500 and not context.get('deskundig_personeel', False):
        acties.append('M85: Deskundig personeel aanstellen')
        score -= 15
        status = STATUS_ACTIE
    if not context.get('instructie_medewerkers', True):
        acties.append('M86: Instructie medewerkers')
        score -= 10
        status = STATUS_ACTIE
    if not context.get('noodplan', True):
        acties.append('M113: Intern noodplan opstellen')
        score -= 15
        status = STATUS_ACTIE
    if not context.get('vib Beschikbaar', True):
        acties.append('M137: VIB/SDS beschikbaar stellen')
        score -= 10
        status = STATUS_ACTIE

    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 + oogspoelvoorziening')
        score -= 10
        status = STATUS_ACTIE

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

    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.',
        'maatregelen': acties, 'score_impact': score,
    }


def toets_hoofdstuk_9(stof: dict, context: dict) -> dict:
    """PGS 15:2025 Hoofdstuk 9: Externe veiligheid."""
    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',
            'uitleg': f'{naam}: hoeveelheid onder drempel.', 'maatregelen': [], 'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    if niveau in ('A', 'B') and not context.get('prvg_vergunning', True):
        acties.append('PRVG: Vergunningcheck uitvoeren')
        score -= 15
        status = STATUS_ACTIE
    if not context.get('bereikbaarheid_terrein', True):
        acties.append('M110: Terrein toegankelijk voor hulpdiensten')
        score -= 10
        status = STATUS_ACTIE

    if not acties:
        acties.append('M110: Bereikbaarheid terrein gewaarborgd')

    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.',
        'maatregelen': acties, 'score_impact': score,
    }


def toets_bijlage_a(stof: dict, context: dict) -> dict:
    """PGS 15:2025 Bijlage A: Stofspecifieke eisen."""
    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', 'arbobesluit': 'Arbobesluit Art. 4.1',
            'bouwbesluit': None, 'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel.', 'maatregelen': [], 'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

    acute_flag, _ = is_acute_tox(h_zinnen)
    if acute_flag and vg in ('I', 'II') and not context.get('un_verpakking', True):
        acties.append('M49: UN-gekeurde verpakkingen vereist')
        score -= 15
        status = STATUS_ACTIE

    if adr_klasse == '5.2' and not context.get('peroxid_tempbeheersing', True):
        acties.append('M143: Temperatuurbeheersing organische peroxiden')
        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}.',
        'maatregelen': acties, 'score_impact': score,
    }


def toets_bijlage_b(stof: dict, context: dict) -> dict:
    """PGS 15:2025 Bijlage B: Mengselregels."""
    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', 'arbobesluit': 'Arbobesluit Art. 3.17',
            'bouwbesluit': None, 'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel.', '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')

    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}.',
        'maatregelen': acties, 'score_impact': score,
    }


def toets_bijlage_c(stof: dict, context: dict) -> dict:
    """PGS 15:2025 Bijlage C: Maximale hoeveelheden."""
    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', 'arbobesluit': None,
            'bouwbesluit': 'Bouwbesluit 2012', 'wet_milieubeer': None,
            'uitleg': f'{naam}: hoeveelheid onder drempel.', 'maatregelen': [], 'score_impact': 0,
        }

    acties = []
    score = 0
    status = STATUS_COMPLIANT

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

    if not acties:
        acties.append('M15: Binnen max vloeroppervlak')

    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}.',
        'maatregelen': acties, 'score_impact': score,
    }


def _check_onverenigbaar(adr_a: str, adr_b: str) -> Optional[dict]:
    """Check onverenigbaarheid 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 5: MAATREGELENHIËRARCHIE
# ═══════════════════════════════════════════════════════════════════

def genereer_maatregelen_hierarchie(stof: dict, context: dict) -> list:
    """Genereer maatregelen per hiërarchieniveau."""
    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)
    hoeveelheid = float(stof.get('hoeveelheid', 0) or 0)

    hiërarchie = []

    # Niveau 1: Eliminatie
    elim_acties = []
    if 'carcinogeen' in effects:
        elim_acties.append(f'{naam} verwijderen — Carc. 1A/1B')
    if 'reprotoxisch' in effects:
        elim_acties.append(f'{naam} verwijderen — Repr.')
    if niveau == 'A':
        elim_acties.append(f'{naam}: niveau A — overweeg eliminatie')

    hiërarchie.append({
        'niveau': HIËRARCHIE_ELIMINATIE,
        **MAATREGELEN_HIERARCHIE[HIËRARCHIE_ELIMINATIE],
        'acties': elim_acties if elim_acties else ['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('Minder gevaarlijk alternatief zoeken — CMR-verplichting')
    if vg == 'I':
        subst_acties.append('Overstappen naar VG II of III equivalent')

    hiërarchie.append({
        'niveau': HIËRARCHIE_SUBSTITUTIE,
        **MAATREGELEN_HIERARCHIE[HIËRARCHIE_SUBSTITUTIE],
        'acties': subst_acties if subst_acties else ['Substitutie overwegen'],
        '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 + gasdetectie')
    if 'corrosief' in effects:
        tech_acties.append('Nooddouche + oogwasstation (M115)')
        tech_acties.append('Vloeistofdichte lekbak (M30)')
    if niveau == 'A':
        tech_acties.append('VBB-systeem installeren (M96)')
    tech_acties.append('Aanrijdbeveiliging stellingen (M77)')

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

    # Niveau 4: Organisatorisch
    org_acties = [
        'Intern noodplan (M113)',
        'Instructie medewerkers (M86)',
        'Periodieke stellinginspectie (M82)',
    ]
    if hoeveelheid > 2500:
        org_acties.append('Deskundig personeel aanstellen (M85)')
    if niveau == 'A':
        org_acties.append('Calamiteitenploeg vormen (M122)')

    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 6: AGGREGATIE & SYNERGIE ANALYSE
# ═══════════════════════════════════════════════════════════════════

def analyseer_aggregatie(stoffen_result: list) -> dict:
    """Analyseer cumulatieve risico's bij multi-stof opslag."""
    if len(stoffen_result) < 2:
        return {
            'van_toepassing': False,
            'score': RISICO_LAAG,
            'onverenigbare_combinaties': [],
            'cumulatief_per_klasse': {},
            'brandlading_aggregatie': {},
            'aanbevelingen': [],
        }

    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 = {}
    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', ''))

    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,
    }

    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 — compartimenteren')
    if totale_brandlading > 10000:
        aanbevelingen.append('Totale brandlading >10.000 kg — niveau A maatregelen')

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


# ═══════════════════════════════════════════════════════════════════
# SECTIE 7: COMPLIANCE SCORE
# ═══════════════════════════════════════════════════════════════════

def bereken_compliance_score(toetsingen: dict) -> dict:
    """Bereken compliance score 0-100."""
    base_score = 100
    total_impact = sum(t.get('score_impact', 0) for t in toetsingen.values())
    score = max(0, min(100, base_score + total_impact))

    if score >= 90:
        label, kleur = 'Volledig compliant', '#22C55E'
    elif score >= 70:
        label, kleur = 'Grotendeels compliant', '#3B82F6'
    elif score >= 50:
        label, kleur = 'Deels compliant', '#F59E0B'
    else:
        label, kleur = 'Niet-compliant', '#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 8: ADAPTER CLASS
# ═══════════════════════════════════════════════════════════════════

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

    def __init__(self, context: dict = None):
        self.context = context or {}

    def analyseer(self, stoffen: list, context: dict = None) -> dict:
        """Volledige PGS 15 compliance analyse."""
        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)

        # V2: Geaggregeerd niveau
        geaggregeerd = bereken_geaggregeerd_niveau(stoffen)

        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),
            'geaggregeerd_niveau': geaggregeerd['eindniveau'],
            'geaggregeerd_route': geaggregeerd.get('eindroute', ''),
            '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,
            'geaggregeerd_niveau': geaggregeerd,
            'samenvatting': samenvatting,
        }

    def _analyseer_stof(self, stof: dict, context: dict) -> dict:
        """Interne: volledige analyse per stof (v2)."""
        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')

        # V2: gebruik get_strictste_niveau_v2
        niveau, route, arbo_flags = get_strictste_niveau_v2(
            adr_klasse, vg, hoeveelheid, h_zinnen, vorm,
            stof.get('flashpoint'),
        )

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

        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 = bereken_compliance_score(toetsingen)
        hiërarchie = genereer_maatregelen_hierarchie(stof, stof_context)
        effects = get_gezondheidseffecten(h_zinnen)

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


# ═══════════════════════════════════════════════════════════════════
# CLI TEST
# ═══════════════════════════════════════════════════════════════════

if __name__ == '__main__':
    test_stoffen = [
        {
            'naam': 'H₂SO₄ 25%',
            'cas': '7664-93-9',
            'adr_klasse': '8',
            'vg': 'II',
            'hoeveelheid': 150,
            'vorm': 'vloeistof',
            'h_zinnen': ['H314'],
        },
        {
            'naam': 'H₂O₂ 30%',
            'cas': '7722-84-1',
            'adr_klasse': '5.1',
            'vg': 'II',
            'hoeveelheid': 150,
            'vorm': 'vloeistof',
            'h_zinnen': ['H271', 'H302', 'H314', 'H332'],
        },
        {
            'naam': 'Na-persulfate',
            'cas': '7775-27-1',
            'adr_klasse': '5.1',
            'vg': 'III',
            'hoeveelheid': 150,
            'vorm': 'vast',
            'h_zinnen': ['H272', 'H302', 'H315', 'H317', 'H319', 'H335', 'H412'],
        },
    ]

    analyzer = PGS15ComplianceAnalyzer()
    result = analyzer.analyseer(test_stoffen)
    print(json.dumps(result['samenvatting'], indent=2, ensure_ascii=False))
