#!/usr/bin/env python3
"""REACH Compliance Scanner — Engine v3.0 (PGS 15 Diepgang)

Volledige REACH-verordening toetsing per stof met:
1. Compliance doelstellingen per REACH-artikel (Art. 5-6, 7, 14, 31-32, 57-59, 67-73, Titel IV, Titel V)
2. Maatregelenhiërarchie (Eliminatie → Substitutie → Technisch → Organisatorisch → PBM)
3. Aggregatie & Synergie analyse voor multi-stof scenario's
4. Compliance score per stof (0-100)
5. Tonnage bandbreedte ladder met Annex VII-X testvereisten, CSR secties, kosten, doorlooptijd, transition roadmap
6. Wettelijke bronverwijzingen (REACH artikel, Annex, CLP, Arbowet/Arbobesluit)

Zelfstandig werkend — geen externe API calls.
"""

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

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)    # Volledig compliant
SCORE_GROOTTEELS = (70, 89)   # Grotendeels compliant
SCORE_DEELS = (50, 69)        # Deels compliant
SCORE_NIET = (0, 49)          # Niet-compliant

# ── Aggregatie risico scores ──
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

# ── Tonnage banden (5-band ladder) ──
TONNAGE_BANDS = {
    '<1': {
        'label': '< 1 ton/jaar',
        'min': 0, 'max': 1,
        'registratie': False,
        'annex': 'Geen',
        'tests': [],
        'csr_verplicht': False,
        'csr_secties': [],
        'kosten_indicatie': '€0 — geen registratie',
        'doorlooptijd': 'Geen',
        'transition_roadmap': [
            'Bij opschaling naar 1-10 ton: REACH-registratie verplicht (Art. 6)',
            'Annex VII dataset voorbereiden (fysisch-chemisch + basis toxicologie)',
            'SIEF-deelname starten bij eerste productie/import',
        ],
        'artikel': 'REACH Art. 2(1) — geen registratieplicht onder 1 ton/jaar',
    },
    '1-10': {
        'label': '1 – 10 ton/jaar',
        'min': 1, 'max': 10,
        'registratie': True,
        'annex': 'Annex VII',
        'tests': [
            'Smeltpunt / kookpunt',
            'Dichtheid',
            'Dampdruk',
            'Wateroplosbaarheid',
            'Verdelingscoëfficiënt (log Kow)',
            'Ontvlambaarheid',
            'Corrosie/irritatie (in vitro)',
            'Oogirritatie (in vitro)',
            'Huidgevoeligheid (in vitro)',
            'Mutageniteit (Ames test)',
            'Acute toxiciteit (oraal)',
            'Korte-termijn toxiciteit waterorganismen (Daphnia)',
            'Groeiremming algen',
            'Afbreekbaarheid (ready biodegradability)',
        ],
        'csr_verplicht': False,
        'csr_secties': ['Sectie A (indien CMR cat. 1A/1B of PBT/vPvB)'],
        'kosten_indicatie': '€15.000 – €50.000 (LoA consortium lager)',
        'doorlooptijd': '3 – 6 maanden (met LoA korter)',
        'transition_roadmap': [
            'Bij opschaling naar 10-100 ton: Annex VIII tests toevoegen',
            'CSR wordt VERPLICHT (Art. 14)',
            'Blootstellingsscenario\'s opstellen',
            'Uitgebreidere toxicologische dataset',
        ],
        'artikel': 'REACH Art. 5, 6, 10 — registratieplicht Annex VII',
    },
    '10-100': {
        'label': '10 – 100 ton/jaar',
        'min': 10, 'max': 100,
        'registratie': True,
        'annex': 'Annex VII + VIII',
        'tests': [
            'Alle Annex VII tests',
            'Acute toxiciteit (inhalatie, dermaal)',
            'Herhaalde blootstelling toxiciteit (28-dag)',
            'Reproductietoxiciteit (screening)',
            'Ontwikkelingstoxiciteit (screening)',
            'Subacute toxiciteit',
            'Langere-termijn toxiciteit waterorganismen',
            'Actief slib respiratie remming',
        ],
        'csr_verplicht': True,
        'csr_secties': [
            'A. Stofidentiteit + fysisch-chemische eigenschappen',
            'B. Gevaarsbeoordeling (humaan + milieu)',
            'C. PBT/vPvB-beoordeling',
            'D. Blootstellingsscenario\'s',
            'E. Risicokarakterisering',
        ],
        'kosten_indicatie': '€50.000 – €150.000 (LoA consortium lager)',
        'doorlooptijd': '6 – 12 maanden',
        'transition_roadmap': [
            'Bij opschaling naar 100-1000 ton: Annex IX tests toevoegen',
            'Uitgebreid CSR met alle blootstellingsscenario\'s',
            'Extra ecotoxicologische testen',
            'ECHA compliance check verwacht',
        ],
        'artikel': 'REACH Art. 5, 6, 10, 14 — registratieplicht Annex VII+VIII, CSR verplicht',
    },
    '100-1000': {
        'label': '100 – 1.000 ton/jaar',
        'min': 100, 'max': 1000,
        'registratie': True,
        'annex': 'Annex VII + VIII + IX',
        'tests': [
            'Alle Annex VII+VIII tests',
            'Subchronische toxiciteit (90-dag)',
            'Ontwikkelingstoxiciteit (pre-natale)',
            'Twee-generatie reproductiestudie',
            'Kinetiek / metabolisme',
            'Bioaccumulatie in vis',
            'Langere-termijn toxiciteit Daphnia',
            'Langere-termijn toxiciteit vis',
            'Bodemorganismen toxiciteit',
            'Planten toxiciteit',
        ],
        'csr_verplicht': True,
        'csr_secties': [
            'A. Stofidentiteit + fysisch-chemische eigenschappen',
            'B. Uitgebreide gevarenbeoordeling',
            'C. PBT/vPvB-beoordeling',
            'D. Alle blootstellingsscenario\'s',
            'E. Volledige risicokarakterisering',
        ],
        'kosten_indicatie': '€150.000 – €500.000',
        'doorlooptijd': '12 – 24 maanden',
        'transition_roadmap': [
            'Bij opschaling naar >1000 ton: Annex X tests toevoegen',
            'Volledige dataset + chronische studies',
            'Substance Evaluation mogelijk (CoRAP)',
            'DU-meldingen verplicht',
        ],
        'artikel': 'REACH Art. 5, 6, 10, 14, 77 — registratieplicht Annex VII-IX, CSR verplicht, ECHA evaluatie',
    },
    '>1000': {
        'label': '> 1.000 ton/jaar',
        'min': 1000, 'max': float('inf'),
        'registratie': True,
        'annex': 'Annex VII + VIII + IX + X',
        'tests': [
            'Alle Annex VII-IX tests',
            'Chronische toxiciteit (langdurig)',
            'Carcinogeniteitsstudie (indien nodig)',
            'Volledige reproductiestudie (2-gen)',
            'Volledige ecotoxicologische dataset',
            'Endocriene disruptie screening (indien nodig)',
            'PBT/vPvB assessment',
        ],
        'csr_verplicht': True,
        'csr_secties': [
            'A. Volledige stofidentiteit',
            'B. Volledige gevarenbeoordeling',
            'C. Uitgebreide PBT/vPvB-beoordeling',
            'D. Alle blootstellingsscenario\'s + alle gebruiksvoorwaarden',
            'E. Volledige risicokarakterisering',
        ],
        'kosten_indicatie': '€500.000 – €2.000.000+',
        'doorlooptijd': '24 – 48 maanden',
        'transition_roadmap': [
            'Volledig geregistreerd — continue monitoring',
            'ECHA Substance Evaluation (CoRAP) verwacht',
            'Periodieke dossier-update verplicht',
            'Autorisatie mogelijk bij SVHC-designatie',
        ],
        'artikel': 'REACH Art. 5, 6, 10, 14, 77 — volledige registratie Annex VII-X, CSR verplicht',
    },
}

# ── ROLLEN (wie registreert) ──
ROLLEN = {
    'fabrikant': {
        'label': 'Fabrikant (produceert stoffen in de EU)',
        'art': 'Art. 5, 6 REACH',
        'registreert': True,
        'verplichtingen': [
            'Registratie bij ≥1 ton/jaar per stof',
            'Indienen registratiedossier bij ECHA via REACH-IT',
            'SDS opstellen en bijwerken',
            'CLP-classificatie & etikettering',
            'Risicobeheersmaatregelen implementeren',
            'CSR bij ≥10 ton/jaar',
            'Blootstellingsscenario\'s bijwerken bij ≥10 ton/jaar',
        ],
    },
    'importeur': {
        'label': 'Importeur (importeert stoffen/mengsels van buiten EU)',
        'art': 'Art. 5, 6 REACH',
        'registreert': True,
        'verplichtingen': [
            'Registratie bij ≥1 ton/jaar per stof',
            'Indienen registratiedossier bij ECHA via REACH-IT',
            'SDS opstellen en bijwerken',
            'CLP-classificatie & etikettering',
            'CSR bij ≥10 ton/jaar',
            'SIEF-deelname verplicht',
        ],
    },
    'downstream_gebruiker': {
        'label': 'Downstream Gebruiker',
        'art': 'Art. 37-38 REACH',
        'registreert': False,
        'verplichtingen': [
            'SDS raadplegen en opvolgen',
            'Blootstellingsscenario\'s implementeren',
            'Risicobeheersmaatregelen toepassen',
            'ECHA-melding bij niet-geautoriseerd gebruik (Art. 38)',
            'Alternatieven beoordelen bij SVHC (Art. 37(4))',
        ],
    },
    'distributeur': {
        'label': 'Distributeur',
        'art': 'Art. 31, 34 REACH',
        'registreert': False,
        'verplichtingen': [
            'SDS doorgeven aan klanten',
            'Informatie doorgeven over risico\'s',
            'Etikettering en verpakking controleren (CLP)',
            'SVHC-informatie doorgeven (Art. 33)',
        ],
    },
    'enkel_importeur': {
        'label': 'Enige Importeur (Only Representative)',
        'art': 'Art. 8 REACH',
        'registreert': True,
        'verplichtingen': [
            'Registratie voor niet-EU fabrikant',
            'Volledige REACH-registratieverplichtingen',
            'Informatieverschaffing aan EU-importeurs',
            'SIEF-deelname',
        ],
    },
}

# ── REACH Compliance Doelstellingen (per artikel) ──
REACH_DOELSTELLINGEN = {
    'art_5_6': {
        'label': 'Registratieverplichting',
        'artikel': 'REACH Art. 5, 6',
        'annex': 'Annex VII-X',
        'clp_koppeling': 'CLP Art. 4 (classificatie verplicht)',
        'arbowet': 'Arbowet Art. 5 (RI&E)',
        'beschrijving': 'Stoffen ≥1 ton/jaar moeten geregistreerd zijn bij ECHA. "No data, no market" beginsel.',
        'drempel': 1.0,  # ton/jaar
    },
    'art_7': {
        'label': 'Stoffen in artikelen',
        'artikel': 'REACH Art. 7',
        'annex': None,
        'clp_koppeling': None,
        'arbowet': None,
        'beschrijving': 'Registratieplicht voor stoffen die bedoeld zijn om vrij te komen uit artikelen >1 ton/jaar. SVHC-melding bij >0,1% gewicht.',
        'drempel': 1.0,
    },
    'art_14': {
        'label': 'Chemical Safety Report (CSR)',
        'artikel': 'REACH Art. 14',
        'annex': 'Annex I',
        'clp_koppeling': 'CLP Art. 4 (gevaarsidentificatie)',
        'arbowet': 'Arbobesluit Art. 4.1 (CMR blootstellingslimieten)',
        'beschrijving': 'CSR verplicht bij ≥10 ton/jaar. Bevat gevarenbeoordeling, blootstellingsscenario\'s en risicokarakterisering.',
        'drempel': 10.0,
    },
    'art_31_32': {
        'label': 'SDS en communicatie in de keten',
        'artikel': 'REACH Art. 31, 32',
        'annex': 'Annex II',
        'clp_koppeling': 'CLP Art. 4, 13, Annex I-VII (etikettering)',
        'arbowet': 'Arbobesluit Art. 3.17 (informatieplicht werknemer)',
        'beschrijving': 'SDS verplicht bij gevaarlijke stoffen/mengsels. Doorgeven aan afnemers. Blootstellingsscenario\'s bij ≥10 ton/jaar.',
        'drempel': 0,  # altijd bij gevaarlijke stoffen
    },
    'art_57_59': {
        'label': 'SVHC autorisatie (kandidaatlijst / Annex XIV)',
        'artikel': 'REACH Art. 57, 58, 59',
        'annex': 'Annex XIV, XV',
        'clp_koppeling': 'CLP Art. 4 (CMR classificatie)',
        'arbowet': 'Arbobesluit Art. 4.1 (CMR blootstellingslimieten)',
        'beschrijving': 'SVHC-identificatie: CMR 1A/1B, PBT, vPvB, equivalent concern. Autorisatie verplicht na sunset date.',
        'drempel': 0.001,  # >0,1% gewicht
    },
    'art_67_73': {
        'label': 'Restricties (Annex XVII)',
        'artikel': 'REACH Art. 67-73',
        'annex': 'Annex XVII',
        'clp_koppeling': 'CLP Art. 4 (gevaarsclassificatie)',
        'arbowet': 'Arbobesluit Art. 4.1-4.4 (stoffenlimieten)',
        'beschrijving': 'Stoffen op Annex XVII hebben specifieke gebruiksbeperkingen. Vervaardiging, plaatsing op de markt en gebruik zijn aan voorwaarden gebonden.',
        'drempel': 0,
    },
    'titel_iv': {
        'label': 'Gevaarlijke mengsels (CLP-koppeling)',
        'artikel': 'REACH Titel IV (Art. 19-21)',
        'annex': None,
        'clp_koppeling': 'CLP Verordening (EU) 1272/2008 volledig',
        'arbowet': 'Arbobesluit Art. 3.17 (informatie werkgever)',
        'beschrijving': 'CLP-verordening: classificatie, etikettering en verpakking van stoffen en mengsels. PCN-melding voor mengsels (CLP Annex VIII).',
        'drempel': 0,
    },
    'titel_v': {
        'label': 'Douane- en importregels',
        'artikel': 'REACH Titel V (Art. 50)',
        'annex': None,
        'clp_koppeling': None,
        'arbowet': None,
        'beschrijving': 'Stoffen in douanetoezicht zijn vrijgesteld van registratie (Art. 2(1)(b)). PIC-melding bij export buiten EU (Verordening 649/2012).',
        'drempel': 0,
    },
}

# ── Maatregelenhiërarchie templates ──
MAATREGELEN_HIERARCHIE = {
    HIËRARCHIE_ELIMINATIE: {
        'niveau': 'Eliminatie',
        'prioriteit': 1,
        'beschrijving': 'Kan het risico volledig weggenomen worden door het gebruik te stoppen?',
        'voorbeelden': [
            'Proces herontwerpen zonder deze stof',
            'Alternatief productontwerp',
            'Gebruik volledig stoppen',
        ],
    },
    HIËRARCHIE_SUBSTITUTIE: {
        'niveau': 'Substitutie',
        'prioriteit': 2,
        'beschrijving': 'Kan een minder gevaarlijke stof 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 blootstelling te minimaliseren',
        'voorbeelden': [
            'Lokale afzuiging (LEV)',
            'Gesloten systeem',
            'ATEX-zonering',
            'Nooddouche + oogwasstation',
            'Vloeistofdichte lekbak',
            'Ventilatie (min. 5 luchtverversingen/uur)',
            'Gasdetectiesysteem',
        ],
    },
    HIËRARCHIE_ORGANISATORISCH: {
        'niveau': 'Organisatorische maatregelen',
        'prioriteit': 4,
        'beschrijving': 'Procedures en organisatie om risico te beheersen',
        'voorbeelden': [
            'Werkplekinstructiekaart (WIK)',
            'Specifieke training en instructie',
            'Blootstellingsregister',
            'Periodiek gezondheidstoezicht (PGO)',
            'Schriftelijke werkprocedure',
            'Noodplan / calamiteitenprocedure',
            'Periodieke etiket- en SDS-controle',
        ],
    },
    HIËRARCHIE_PBM: {
        'niveau': 'Persoonlijke beschermingsmiddelen',
        'prioriteit': 5,
        'beschrijving': 'PBM als laatste redmiddel wanneer hogere hiërarchie niet toereikend',
        'voorbeelden': [
            'Chemiebestendige handschoenen (EN 374)',
            'Veiligheidsbril / spatscherm (EN 166)',
            'Gelaatsmasker / halfmasker (EN 140/143)',
            'Ademluchtsysteem (EN 14594)',
            'Beschermende kleding (EN 14605)',
            'Chemiebestendige laarzen (EN 13832)',
        ],
    },
}

# ── 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', 'beschermlaag'],
    'H360': ['handschoenen', 'gelaatsmasker', 'ademlucht', 'beschermlaag'],
    'H361': ['handschoenen', 'gelaatsmasker', 'beschermlaag'],
    'H370': ['handschoenen', 'gelaatsmasker', 'ademlucht', 'beschermlaag'],
    'H371': ['handschoenen', 'gelaatsmasker', 'beschermlaag'],
    'H372': ['handschoenen', 'gelaatsmasker'],
    'H373': ['handschoenen', 'gelaatsmasker'],
    'H400': ['handschoenen', 'lekopvang'],
    'H410': ['handschoenen', 'lekopvang'],
    'H411': ['handschoenen', 'lekopvang'],
    'H412': ['lekopvang'],
}

# ── Technische maatregelen mapping op H-zinnen ──
H_TECH_MAP = {
    'corrosief': ['nooddouche', 'lekopvang'],
    'toxisch': ['afzuiging'],
    'brandbaar': ['atex', 'gasdetectie', 'branddetectie'],
    'milieu': ['lekopvang'],
}

# ── Synergie matrix (gevaarlijke combinaties) ──
SYNERGIE_COMBINATIES = [
    {
        'type': 'ZUUR_BASE_REACTIE',
        'label': 'Zuur + Base reactie',
        'risico': 'Exotherme reactie, gasvorming, spatten',
        'score_impact': 20,
        'h_patterns': [['H314'], ['H314']],  # corrosief + corrosief
    },
    {
        'type': 'CMR_CUMULATIEF',
        'label': 'Cumulatieve CMR blootstelling',
        'risico': 'Verhoogd kanker/reproductierisico door cumulatie',
        'score_impact': 25,
        'h_patterns': [['H340', 'H350', 'H350i'], ['H340', 'H350', 'H350i']],
    },
    {
        'type': 'TOXISCHE_CUMULATIE',
        'label': 'Cumulatieve acute toxiciteit',
        'risico': 'Synergistische versterking van toxische effecten',
        'score_impact': 20,
        'h_patterns': [['H300', 'H310', 'H330'], ['H300', 'H310', 'H330']],
    },
    {
        'type': 'OXIDEREND_BRANDBAAR',
        'label': 'Oxiderend + brandbaar',
        'risico': 'Verhoogd brand/ontploffingsrisico',
        'score_impact': 30,
        'h_patterns': [['H271', 'H272'], ['H220', 'H221', 'H222', 'H223', 'H224', 'H225', 'H226', 'H240', 'H241', 'H242']],
    },
    {
        'type': 'WATERREACTIEF',
        'label': 'Waterreactieve + vocht',
        'risico': 'Waterstofgasvorming, brand/explosie',
        'score_impact': 25,
        'h_patterns': [['H260', 'H261'], []],
    },
    {
        'type': 'MILIEU_CUMULATIEF',
        'label': 'Cumulatief milieugevaar',
        'risico': 'Synergistische schade aan aquatische ecosystemen',
        'score_impact': 15,
        'h_patterns': [['H400', 'H410', 'H411'], ['H400', 'H410', 'H411']],
    },
]

# ── Vrijstellingen ──
VRIJSTELLINGEN = {
    'radioactief': {'label': 'Radioactieve stoffen', 'art': 'REACH Art. 2(1)(a)'},
    'douane': {'label': 'Stoffen onder douanetoezicht', 'art': 'REACH Art. 2(1)(b)'},
    'tussentijds': {'label': 'Tussentijdse opslag', 'art': 'REACH Art. 2(1)(c)'},
    'vervoer': {'label': 'Vervoer van gevaarlijke stoffen', 'art': 'REACH Art. 2(1)(d)'},
    'afval': {'label': 'Afval', 'art': 'REACH Art. 2(2)'},
    'voeding': {'label': 'Levensmiddelen/voedermiddelen', 'art': 'REACH Art. 2(5)(b)'},
    'geneesmiddelen': {'label': 'Geneesmiddelen', 'art': 'REACH Art. 2(5)(c)'},
    'cosmetica': {'label': 'Cosmetica (eindproduct)', 'art': 'REACH Art. 2(6)'},
    'polymeren': {'label': 'Polymeren (niet monomeren)', 'art': 'REACH Art. 2(9)'},
    'onderzoek': {'label': 'PPORD', 'art': 'REACH Art. 9'},
}

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

def get_tonnage_band(volume: float) -> tuple:
    """Bepaal tonnage-band op basis van volume (ton/jaar)."""
    for key, band in TONNAGE_BANDS.items():
        if band['min'] <= volume < band['max']:
            return key, band
    return '>1000', TONNAGE_BANDS['>1000']


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, 224, 240, 241, 250, 260, 261}:
        effects.append('brandbaar')
    return effects


def derive_pictograms(h_zinnen: list) -> list:
    """Bepaal GHS-pictogrammen op basis van H-zinnen."""
    codes = parse_h_codes(h_zinnen)
    pics = []
    if codes & {200, 201, 202, 203, 204, 240, 241, 242, 260, 261}:
        pics.append({'code': 'GHS02', 'label': 'Brandbaar', 'icon': '🔥'})
    if codes & {300, 310, 311, 330, 331}:
        pics.append({'code': 'GHS06', 'label': 'Acuut toxisch', 'icon': '💀'})
    if codes & {314}:
        pics.append({'code': 'GHS05', 'label': 'Corrosief', 'icon': '☢️'})
    if codes & {318, 319}:
        pics.append({'code': 'GHS07', 'label': 'Irriterend', 'icon': '⚠️'})
    if codes & {400, 410, 411}:
        pics.append({'code': 'GHS09', 'label': 'Milieugevaarlijk', 'icon': '🌿'})
    if codes & {340, 350, 360, 370}:
        pics.append({'code': 'GHS08', 'label': 'Gezondheidsgevaar', 'icon': '🛡️'})
    return pics


def signaalwoord(h_zinnen: list) -> str:
    """Bepaal GHS signaalwoord."""
    codes = parse_h_codes(h_zinnen)
    if codes & {300, 310, 330, 314, 318, 340, 350, 360, 370, 400, 410}:
        return 'Gevaar'
    if h_zinnen:
        return 'Waarschuwing'
    return ''


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


# ═══════════════════════════════════════════════════════════════════
# SECTIE 3: COMPLIANCE TOETSING PER REACH-ARTIKEL
# ═══════════════════════════════════════════════════════════════════

def toets_art_5_6(stof: dict, rol_info: dict) -> dict:
    """Art. 5-6: Registratieverplichting."""
    volume = float(stof.get('volume', 0) or 0)
    is_mengsel = stof.get('mengsel', False)
    vrijstelling = stof.get('vrijstelling')
    naam = stof.get('naam', '')

    if vrijstelling:
        return {
            'status': STATUS_NVT,
            'artikel': 'REACH Art. 5, 6',
            'annex': None,
            'clp': 'CLP Art. 4',
            'arbowet': 'Arbowet Art. 5',
            'uitleg': f'Vrijgesteld: {VRIJSTELLINGEN.get(vrijstelling, {}).get("label", vrijstelling)} — geen registratieplicht.',
            'maatregelen': [],
            'score_impact': 0,
        }

    if not rol_info.get('registreert', False):
        return {
            'status': STATUS_NVT,
            'artikel': 'REACH Art. 5, 6',
            'annex': None,
            'clp': 'CLP Art. 4',
            'arbowet': 'Arbowet Art. 5',
            'uitleg': f'Rol "{rol_info.get("label", "")}" registreert niet zelf. Registratieplicht ligt bij fabrikant/importeur.',
            'maatregelen': ['Controleer of leverancier geregistreerd heeft', 'Vraag registratienummer op'],
            'score_impact': 0,
        }

    band_key, band = get_tonnage_band(volume)

    if volume < 1:
        return {
            'status': STATUS_COMPLIANT,
            'artikel': 'REACH Art. 5, 6',
            'annex': None,
            'clp': 'CLP Art. 4',
            'arbowet': 'Arbowet Art. 5',
            'uitleg': f'{naam}: {volume} ton/jaar — onder registratiedrempel (1 ton/jaar). CLP- en Arbowet-verplichtingen blijven gelden.',
            'maatregelen': ['CLP-classificatie controleren', 'RI&E bijwerken'],
            'score_impact': 0,
        }

    if volume >= 1:
        return {
            'status': STATUS_ACTIE,
            'artikel': 'REACH Art. 5, 6',
            'annex': band['annex'],
            'clp': 'CLP Art. 4',
            'arbowet': 'Arbowet Art. 5',
            'uitleg': f'{naam}: {volume} ton/jaar — registratie VERPLICHT bij ECHA (Annex {band["annex"]}). Kostenindicatie: {band["kosten_indicatie"]}. Doorlooptijd: {band["doorlooptijd"]}.',
            'maatregelen': [
                f'REACH-registratie starten — {band["annex"]} dataset',
                f'SIEF/consortium deelnemen voor datadeling',
                f'LoA (Letter of Access) aanvragen indien beschikbaar',
                f'Registratiedossier indienen via REACH-IT',
            ],
            'score_impact': -30 if not stof.get('_geregistreerd') else 0,
        }

    return {'status': STATUS_NVT, 'artikel': 'REACH Art. 5, 6', 'uitleg': 'Onbekende situatie.', 'maatregelen': [], 'score_impact': 0}


def toets_art_7(stof: dict, rol_info: dict) -> dict:
    """Art. 7: Stoffen in artikelen."""
    is_artikel = stof.get('in_artikel', False)
    svhc = stof.get('_svhc', False)
    volume = float(stof.get('volume', 0) or 0)
    naam = stof.get('naam', '')
    percentage = float(stof.get('percentage', 100) or 100)

    if not is_artikel and not stof.get('mengsel', False):
        return {
            'status': STATUS_NVT,
            'artikel': 'REACH Art. 7',
            'annex': None,
            'clp': None,
            'arbowet': None,
            'uitleg': f'{naam}: geen stof in artikel — Art. 7 niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    status = STATUS_COMPLIANT
    score = 0

    if is_artikel and volume >= 1:
        acties.append(f'Registratie als stof in artikel verplicht (Art. 7(1)) — {volume} ton/jaar')
        status = STATUS_ACTIE
        score = -20

    if svhc and percentage > 0.1:
        acties.append('SVHC >0,1% in artikel — SCIP-melding verplicht (Art. 7(2))')
        acties.append('Kopers informeren over SVHC (Art. 33)')
        if status == STATUS_COMPLIANT:
            status = STATUS_ACTIE
        score -= 15

    return {
        'status': status,
        'artikel': 'REACH Art. 7',
        'annex': None,
        'clp': None,
        'arbowet': None,
        'uitleg': f'{naam}: {"in artikel" if is_artikel else "als mengsel"}. {"Registratie + " if is_artikel and volume >= 1 else ""}{"SVHC-melding" if svhc and percentage > 0.1 else "geen aanvullende verplichtingen"}.',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_art_14(stof: dict, rol_info: dict) -> dict:
    """Art. 14: Chemisch Veiligheidsrapport (CSR)."""
    volume = float(stof.get('volume', 0) or 0)
    is_mengsel = stof.get('mengsel', False)
    naam = stof.get('naam', '')
    h_zinnen = stof.get('_h_zinnen', [])
    svhc = stof.get('_svhc', False)
    h_codes = parse_h_codes(h_zinnen)

    # CMR 1A/1B of PBT/vPvB → CSR verplicht ook <10 ton
    cmr_codes = {340, 350, 360, 370}
    is_cmr = bool(h_codes & cmr_codes)
    csr_always = is_cmr or svhc

    if not rol_info.get('registreert', False):
        return {
            'status': STATUS_NVT,
            'artikel': 'REACH Art. 14',
            'annex': 'Annex I',
            'clp': 'CLP Art. 4',
            'arbowet': 'Arbobesluit Art. 4.1',
            'uitleg': f'{naam}: rol registreert niet — CSR-plicht ligt bij registrant.',
            'maatregelen': ['Vraag CSR aan bij leverancier'],
            'score_impact': 0,
        }

    band_key, band = get_tonnage_band(volume)

    if volume >= 10 or csr_always:
        csr_secties = band.get('csr_secties', [])
        return {
            'status': STATUS_ACTIE,
            'artikel': 'REACH Art. 14',
            'annex': 'Annex I',
            'clp': 'CLP Art. 4',
            'arbowet': 'Arbobesluit Art. 4.1',
            'uitleg': f'{naam}: CSR VERPLICHT — {"≥10 ton/jaar" if volume >= 10 else "CMR/PBT/vPvB <10 ton"}. Secties: {", ".join(csr_secties) if csr_secties else "Annex I volledig"}.',
            'maatregelen': [
                'CSR opstellen conform Annex I structuur',
                'Blootstellingsscenario\'s definiëren (Alle gebruiksvoorwaarden)',
                'Risicokarakterisering uitvoeren (humaan + milieu)',
                'CSR opnemen in SDS (extended SDS)',
            ],
            'score_impact': -25 if not stof.get('_heeft_csr') else 0,
        }

    return {
        'status': STATUS_COMPLIANT,
        'artikel': 'REACH Art. 14',
        'annex': 'Annex I',
        'clp': 'CLP Art. 4',
        'arbowet': 'Arbobesluit Art. 4.1',
        'uitleg': f'{naam}: {volume} ton/jaar — geen CSR verplicht (drempel 10 ton/jaar, geen CMR/PBT/vPvB).',
        'maatregelen': [],
        'score_impact': 0,
    }


def toets_art_31_32(stof: dict, rol_info: dict) -> dict:
    """Art. 31-32: SDS en communicatie in de keten."""
    h_zinnen = stof.get('_h_zinnen', [])
    naam = stof.get('naam', '')
    volume = float(stof.get('volume', 0) or 0)
    is_mengsel = stof.get('mengsel', False)

    if not h_zinnen:
        return {
            'status': STATUS_COMPLIANT,
            'artikel': 'REACH Art. 31, 32',
            'annex': 'Annex II',
            'clp': 'CLP Art. 4, 13',
            'arbowet': 'Arbobesluit Art. 3.17',
            'uitleg': f'{naam}: geen gevaarlijke classificatie gevonden — SDS niet verplicht maar aanbevolen.',
            'maatregelen': ['SDS aanvragen bij leverancier (vrijwillig)'],
            'score_impact': 0,
        }

    acties = [
        'SDS controleren op volledigheid (16 secties, Annex II)',
        'SDS doorgeven aan alle afnemers',
        'Etikettering verifiëren (CLP Art. 13)',
    ]
    if volume >= 10:
        acties.append('Extended SDS met blootstellingsscenario\'s')
    if is_mengsel:
        acties.append('UFI genereren + PCN-melding (CLP Annex VIII)')

    return {
        'status': STATUS_ACTIE,
        'artikel': 'REACH Art. 31, 32',
        'annex': 'Annex II',
        'clp': 'CLP Art. 4, 13',
        'arbowet': 'Arbobesluit Art. 3.17',
        'uitleg': f'{naam}: gevaarlijke stof (H-zinnen: {", ".join(h_zinnen[:5])}{"..." if len(h_zinnen)>5 else ""}) — SDS VERPLICHT.{" Extended SDS met ES bij ≥10 ton." if volume >= 10 else ""}',
        'maatregelen': acties,
        'score_impact': -15 if not stof.get('_heeft_sds') else 0,
    }


def toets_art_57_59(stof: dict, rol_info: dict) -> dict:
    """Art. 57-59: SVHC autorisatie."""
    svhc = stof.get('_svhc', False)
    autorisatie = stof.get('_autorisatie', False)
    svhc_reden = stof.get('_svhc_reden', '')
    naam = stof.get('naam', '')

    if not svhc and not autorisatie:
        return {
            'status': STATUS_COMPLIANT,
            'artikel': 'REACH Art. 57-59',
            'annex': None,
            'clp': 'CLP Art. 4',
            'arbowet': 'Arbobesluit Art. 4.1',
            'uitleg': f'{naam}: niet op SVHC Candidate List — geen autorisatieplicht.',
            'maatregelen': [],
            'score_impact': 0,
        }

    acties = []
    score = 0

    if svhc:
        acties.extend([
            'SVHC geïdentificeerd — kopers informeren (Art. 33)',
            'SCIP-melding indienen bij ECHA (indien in artikel >0,1%)',
            'Vervanging onderzoek starten (Art. 55)',
        ])
        score -= 15

    if autorisatie:
        acties.extend([
            'AUTORISATIE AANVRAGEN bij ECHA (Art. 56)',
            'Sunset date controleren',
            'Alternatief onderzoeken indien geen autorisatie',
        ])
        score -= 25

    return {
        'status': STATUS_NIET_COMPLIANT if autorisatie else STATUS_ACTIE,
        'artikel': 'REACH Art. 57-59',
        'annex': 'Annex XIV, XV' if autorisatie else 'Annex XV',
        'clp': 'CLP Art. 4',
        'arbowet': 'Arbobesluit Art. 4.1',
        'uitleg': f'{naam}: SVHC{f" — {svhc_reden}" if svhc_reden else ""}. {"AUTORISATIE VEREIST (Annex XIV)." if autorisatie else "Vervanging overwegen, autorisatie voorbereiden."}',
        'maatregelen': acties,
        'score_impact': score,
    }


def toets_art_67_73(stof: dict, rol_info: dict) -> dict:
    """Art. 67-73: Restricties (Annex XVII)."""
    restrictie = stof.get('_restrictie', False)
    naam = stof.get('naam', '')

    if not restrictie:
        return {
            'status': STATUS_COMPLIANT,
            'artikel': 'REACH Art. 67-73',
            'annex': None,
            'clp': None,
            'arbowet': 'Arbobesluit Art. 4.1-4.4',
            'uitleg': f'{naam}: niet op Annex XVII — geen restricties.',
            'maatregelen': [],
            'score_impact': 0,
        }

    return {
        'status': STATUS_ACTIE,
        'artikel': 'REACH Art. 67-73',
        'annex': 'Annex XVII',
        'clp': 'CLP Art. 4',
        'arbowet': 'Arbobesluit Art. 4.1-4.4',
        'uitleg': f'{naam}: op Annex XVII — specifieke gebruiksbeperkingen gelden. Controleer ECHA voor exacte voorwaarden.',
        'maatregelen': [
            'Annex XVII restrictievoorwaarden controleren op ECHA',
            'Gebruik verifiëren tegen restrictievoorwaarden',
            'Alternatief zoeken bij niet-naleving',
        ],
        'score_impact': -20,
    }


def toets_titel_iv(stof: dict, rol_info: dict) -> dict:
    """Titel IV: Gevaarlijke mengsels (CLP-koppeling)."""
    h_zinnen = stof.get('_h_zinnen', [])
    is_mengsel = stof.get('mengsel', False)
    naam = stof.get('naam', '')
    volume = float(stof.get('volume', 0) or 0)

    acties = ['CLP-classificatie en etikettering verifiëren (CLP Art. 4, 13)']

    if is_mengsel and h_zinnen:
        acties.extend([
            'UFI genereren voor mengsel',
            'PCN-melding indienen bij ECHA (CLP Annex VIII)',
            'Etiket controleren: pictogrammen, H/P-zinnen, signaalwoord',
        ])
        return {
            'status': STATUS_ACTIE,
            'artikel': 'REACH Titel IV + CLP',
            'annex': 'CLP Annex I-VII, VIII',
            'clp': 'CLP Verordening (EU) 1272/2008',
            'arbowet': 'Arbobesluit Art. 3.17',
            'uitleg': f'{naam}: gevaarlijk mengsel — CLP-classificatie, etikettering en PCN-melding verplicht.',
            'maatregelen': acties,
            'score_impact': -10 if not stof.get('_heeft_clp') else 0,
        }

    if h_zinnen:
        return {
            'status': STATUS_ACTIE,
            'artikel': 'REACH Titel IV + CLP',
            'annex': 'CLP Annex I-VII',
            'clp': 'CLP Verordening (EU) 1272/2008',
            'arbowet': 'Arbobesluit Art. 3.17',
            'uitleg': f'{naam}: gevaarlijke stof — CLP-classificatie en etikettering verplicht.',
            'maatregelen': acties,
            'score_impact': -10 if not stof.get('_heeft_clp') else 0,
        }

    return {
        'status': STATUS_COMPLIANT,
        'artikel': 'REACH Titel IV + CLP',
        'annex': 'CLP Annex I-VII',
        'clp': 'CLP Verordening (EU) 1272/2008',
        'arbowet': 'Arbobesluit Art. 3.17',
        'uitleg': f'{naam}: geen gevaarlijke classificatie gevonden — CLP niet van toepassing maar aanbevolen te verifiëren.',
        'maatregelen': ['Classificatie laten verifiëren bij leverancier'],
        'score_impact': 0,
    }


def toets_titel_v(stof: dict, rol_info: dict) -> dict:
    """Titel V: Douane- en importregels."""
    rol = stof.get('_rol', '')
    naam = stof.get('naam', '')
    is_import = rol in ('importeur', 'enkel_importeur')

    if not is_import:
        return {
            'status': STATUS_NVT,
            'artikel': 'REACH Titel V (Art. 50)',
            'annex': None,
            'clp': None,
            'arbowet': None,
            'uitleg': f'{naam}: geen importeur — douaneregels niet van toepassing.',
            'maatregelen': [],
            'score_impact': 0,
        }

    return {
        'status': STATUS_COMPLIANT,
        'artikel': 'REACH Titel V (Art. 50)',
        'annex': None,
        'clp': None,
        'arbowet': None,
        'uitleg': f'{naam}: importeur — stoffen onder douanetoezicht zijn vrijgesteld (Art. 2(1)(b)). PIC-melding bij export buiten EU (Verord. 649/2012).',
        'maatregelen': ['PIC-melding indienen bij export buiten EU (35 dagen vooruit)'],
        'score_impact': 0,
    }


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

def genereer_maatregelen_hierarchie(stof: dict) -> list:
    """Genereer maatregelen per hiërarchieniveau voor een stof."""
    h_zinnen = stof.get('_h_zinnen', [])
    svhc = stof.get('_svhc', False)
    autorisatie = stof.get('_autorisatie', False)
    restrictie = stof.get('_restrictie', False)
    naam = stof.get('naam', '')
    effects = get_gezondheidseffecten(h_zinnen)
    h_codes = parse_h_codes(h_zinnen)
    blootstelling = stof.get('_blootstelling_routes', [])

    hiërarchie = []

    # ── Niveau 1: Eliminatie ──
    elim_acties = []
    if svhc or autorisatie:
        elim_acties.append(f'{naam} vervangen — SVHC/autorisatie vereist vervanging (Art. 55)')
    if restrictie:
        elim_acties.append(f'{naam} elimineren — Annex XVII restricties gelden')
    if 'carcinogeen' in effects:
        elim_acties.append(f'{naam} elimineren — Carc. 1A/1B stof, vervanging verplicht (Arbobesluit Art. 4.1)')
    if 'reprotoxisch' in effects:
        elim_acties.append(f'{naam} elimineren — Repr. stof, vervanging overwegen')

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

    # ── Niveau 2: Substitutie ──
    subst_acties = []
    if svhc:
        subst_acties.append(f'Alternatief zoeken voor {naam} — SVHC vervangingsonderzoek (Art. 55)')
    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(f'Milder alternatief overwegen — minder corrosief equivalent')
    if 'brandbaar' in effects:
        subst_acties.append('Watergedragen alternatief overwegen')

    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 'Inhalatie (ademhaling)' in blootstelling or any(c in h_codes for c in [300, 310, 330, 331, 335]):
        tech_acties.append('Lokale afzuiging (LEV) installeren — EN 14175')
    if 'corrosief' in effects or 314 in h_codes:
        tech_acties.append('Nooddouche + oogwasstation binnen 10 seconden bereik — EN 15154')
        tech_acties.append('Vloeistofdichte lekbak / opvangbak — EN 14470-1')
    if 'brandbaar' in effects:
        tech_acties.append('ATEX-zonering implementeren — Richtlijn 2014/34/EU')
        tech_acties.append('Vonkvrije gereedschappen en aarding')
        tech_acties.append('Gasdetectiesysteem installeren')
    if 'milieugevaarlijk' in effects:
        tech_acties.append('Vloeistofdichte vloer en opvangsystemen')
        tech_acties.append('Bluswateropvang')
    if 'acuut_toxisch' in effects:
        tech_acties.append('Gesloten systeem overwegen')
    if not tech_acties:
        tech_acties.append('Algemene ventilatie — min. 5 luchtverversingen/uur')

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

    # ── Niveau 4: Organisatorisch ──
    org_acties = [
        'Werkplekinstructiekaart (WIK) opstellen',
        'Specifieke training en instructie aan medewerkers',
    ]
    if svhc or 'carcinogeen' in effects or 'reprotoxisch' in effects:
        org_acties.append('Blootstellingsregister bijhouden')
        org_acties.append('Periodiek gezondheidstoezicht (PGO) — Arbobesluit Art. 18')
    if 'sensibiliserend' in effects:
        org_acties.append('Allergeen-beheersplan opstellen')
    org_acties.extend([
        'Schriftelijke werkprocedure',
        'Noodplan / calamiteitenprocedure',
        'Periodieke etiket- en SDS-controle (jaarlijks)',
    ])

    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)

    # Map PBM keys naar beschrijvingen
    pbm_map = {
        'handschoenen': 'Chemiebestendige handschoenen (EN 374)',
        'veiligheidsbril': 'Veiligheidsbril / spatscherm (EN 166)',
        'gelaatsmasker': 'Gelaatsmasker / halfmasker (EN 140/143)',
        'ademlucht': 'Ademluchtsysteem (EN 14594)',
        'beschermlaag': 'Beschermende kleding (EN 14605)',
        'nooddouche': 'Nooddouche + oogwasstation (EN 15154)',
        'lekopvang': 'Vloeistofdichte lekbak / opvangbak',
        'laarzen': 'Chemiebestendige laarzen (EN 13832)',
    }
    pbm_beschrijvingen = [pbm_map.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 en synergieën bij multi-stof gebruik."""
    if len(stoffen_result) < 2:
        return {
            'van_toepassing': False,
            'score': RISICO_LAAG,
            'gemeenschappelijke_effecten': [],
            'synergie_risicos': [],
            'cumulatieve_blootstelling': {},
            'aanbevelingen': [],
        }

    # Identificeer gemeenschappelijke gezondheidseffecten
    all_effects = {}
    for s in stoffen_result:
        effects = s.get('gezondheidseffecten', [])
        for eff in effects:
            if eff not in all_effects:
                all_effects[eff] = []
            all_effects[eff].append(s['naam'])

    gemeenschappelijk = [
        {'effect': eff, 'stoffen': namen, 'aantal': len(namen)}
        for eff, namen in all_effects.items()
        if len(namen) >= 2
    ]

    # Synergie analyse
    synergie_risicos = []
    for combo in SYNERGIE_COMBINATIES:
        matched = _check_synergie(combo, stoffen_result)
        if matched:
            synergie_risicos.append({
                'type': combo['type'],
                'label': combo['label'],
                'risico': combo['risico'],
                'score_impact': combo['score_impact'],
                'betrokken_stoffen': matched,
            })

    # Cumulatieve blootstelling (op volume basis)
    totale_volume = sum(float(s.get('volume', 0) or 0) for s in stoffen_result)
    cumulatief_per_effect = {}
    for eff, namen in all_effects.items():
        vol = sum(
            float(s.get('volume', 0) or 0)
            for s in stoffen_result
            if eff in s.get('gezondheidseffecten', [])
        )
        cumulatief_per_effect[eff] = {
            'volume_totaal': vol,
            'aantal_stoffen': len(namen),
            'stoffen': namen,
        }

    # Totale score
    total_synergie_impact = sum(r['score_impact'] for r in synergie_risicos)
    if total_synergie_impact >= 50 or len(synergie_risicos) >= 3:
        score = RISICO_KRITIEK
    elif total_synergie_impact >= 30 or len(synergie_risicos) >= 2:
        score = RISICO_HOOG
    elif total_synergie_impact >= 15 or len(gemeenschappelijk) >= 2:
        score = RISICO_MEDIUM
    else:
        score = RISICO_LAAG

    aanbevelingen = []
    if synergie_risicos:
        aanbevelingen.append('Scheid onverenigbare stoffen (PGS 15:2025 compatibiliteitstabel)')
    if gemeenschappelijk:
        aanbevelingen.append('Cumulatieve blootstelling evalueren — verlaag volumes of verbeter afzuiging')
    if score in (RISICO_HOOG, RISICO_KRITIEK):
        aanbevelingen.append('Overweeg TRA (Task Risk Assessment) voor gecombineerde blootstelling')
        aanbevelingen.append('Raadpleeg Arbo-arts voor gezondheidstoezicht')

    return {
        'van_toepassing': True,
        'aantal_stoffen': len(stoffen_result),
        'score': score,
        'gemeenschappelijke_effecten': gemeenschappelijk,
        'synergie_risicos': synergie_risicos,
        'cumulatieve_blootstelling': cumulatief_per_effect,
        'aanbevelingen': aanbevelingen,
    }


def _check_synergie(combo: dict, stoffen: list) -> list:
    """Check of een synergie combinatie van toepassing is op de stoffen."""
    group_a = combo['h_patterns'][0]
    group_b = combo['h_patterns'][1]
    matched_a = []
    matched_b = []

    for s in stoffen:
        h_zinnen = s.get('_h_zinnen', [])
        for h in h_zinnen:
            h_code = re.match(r'H(\d{3})', h)
            if h_code:
                hc = int(h_code.group(1))
                if any(hc == int(p) for p in group_a):
                    if s['naam'] not in matched_a:
                        matched_a.append(s['naam'])
                if group_b and any(hc == int(p) for p in group_b):
                    if s['naam'] not in matched_b:
                        matched_b.append(s['naam'])

    if not group_b:
        # Waterreactief: hoeft geen tweede groep
        return matched_a if matched_a else []
    if matched_a and matched_b:
        return list(set(matched_a + matched_b))
    return []


# ═══════════════════════════════════════════════════════════════════
# 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))

    # Score classificatie
    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: HOOFD ANALYSE FUNCTIE
# ═══════════════════════════════════════════════════════════════════

def analyseer_stof(stof: dict, session_data: dict) -> dict:
    """Volledige REACH compliance analyse per stof."""
    cas = stof.get('cas', '').strip()
    naam = stof.get('naam', '').strip()
    volume = float(stof.get('volume', 0) or 0)
    h_zinnen = stof.get('_h_zinnen', stof.get('h_zinnen', []))
    svhc = stof.get('_svhc', stof.get('svhc', False))
    autorisatie = stof.get('_autorisatie', stof.get('autorisatie', False))
    restrictie = stof.get('_restrictie', stof.get('restrictie', False))
    svhc_reden = stof.get('_svhc_reden', '')
    blootstelling_routes = stof.get('_blootstelling_routes', session_data.get('blootstelling_routes', []))

    # Verrijk stof met interne data
    stof_rich = {
        **stof,
        '_h_zinnen': h_zinnen,
        '_svhc': svhc,
        '_autorisatie': autorisatie,
        '_restrictie': restrictie,
        '_svhc_reden': svhc_reden,
        '_rol': session_data.get('rol', 'downstream_gebruiker'),
        '_blootstelling_routes': blootstelling_routes,
    }

    rol = session_data.get('rol', 'downstream_gebruiker')
    rol_info = ROLLEN.get(rol, ROLLEN['downstream_gebruiker'])
    band_key, band = get_tonnage_band(volume)

    # ── Compliance toetsingen ──
    toetsingen = {
        'art_5_6': toets_art_5_6(stof_rich, rol_info),
        'art_7': toets_art_7(stof_rich, rol_info),
        'art_14': toets_art_14(stof_rich, rol_info),
        'art_31_32': toets_art_31_32(stof_rich, rol_info),
        'art_57_59': toets_art_57_59(stof_rich, rol_info),
        'art_67_73': toets_art_67_73(stof_rich, rol_info),
        'titel_iv': toets_titel_iv(stof_rich, rol_info),
        'titel_v': toets_titel_v(stof_rich, rol_info),
    }

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

    # ── Maatregelenhiërarchie ──
    hiërarchie = genereer_maatregelen_hierarchie(stof_rich)

    # ── Tonnage ladder ──
    band_ladder = _genereer_band_ladder(volume, band_key)

    # ── CLP info ──
    effects = get_gezondheidseffecten(h_zinnen)
    pictogrammen = derive_pictograms(h_zinnen)
    sw = signaalwoord(h_zinnen)

    # ── Actiepunten ──
    actiepunten = _genereer_actiepunten(naam, volume, band, rol, svhc, autorisatie, restrictie, h_zinnen, stof.get('mengsel', False), compliance_score)

    return {
        'naam': naam,
        'cas': cas,
        'volume': volume,
        'tonnage_band': band_key,
        'band_label': band['label'],
        'compliance_score': compliance_score,
        'toetsingen': toetsingen,
        'hiërarchie': hiërarchie,
        'band_ladder': band_ladder,
        'clp': {
            'h_zinnen': h_zinnen,
            'pictogrammen': pictogrammen,
            'signaalwoord': sw,
            'gezondheidseffecten': effects,
        },
        'svhc': svhc,
        'autorisatie': autorisatie,
        'restrictie': restrictie,
        'actiepunten': actiepunten,
    }


def analyseer_session(session_data: dict) -> dict:
    """Hoofdfunctie: analyseer alle stoffen in een session."""
    stoffen = session_data.get('stoffen', [])
    if not stoffen:
        return {'error': 'Geen stoffen opgegeven', 'resultaten': [], 'aggregatie': {'van_toepassing': False}}

    resultaten = []
    for stof in stoffen:
        try:
            resultaat = analyseer_stof(stof, session_data)
            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
    aggregatie = analyseer_aggregatie(resultaten)

    # Samenvatting
    samenvatting = {
        'totaal_stoffen': len(resultaten),
        'svhc_stoffen': sum(1 for r in resultaten if r.get('svhc')),
        'restrictie_stoffen': sum(1 for r in resultaten if r.get('restrictie')),
        'autorisatie_stoffen': sum(1 for r in resultaten if r.get('autorisatie')),
        '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),
    }

    return {
        'bedrijf': {
            'naam': session_data.get('bedrijfsnaam', 'Onbekend'),
            'rol': session_data.get('rol', 'downstream_gebruiker'),
            'rol_label': ROLLEN.get(session_data.get('rol', ''), {}).get('label', ''),
            'locatie': session_data.get('locatie', 'EU'),
            'sector': session_data.get('sector', 'Overig'),
        },
        'per_stof': resultaten,
        'aggregatie': aggregatie,
        'samenvatting': samenvatting,
    }


# ═══════════════════════════════════════════════════════════════════
# SECTIE 8: HELPERS (intern)
# ═══════════════════════════════════════════════════════════════════

def _genereer_band_ladder(volume: float, huidige_band: str) -> list:
    """Genereer tonnage bandbreedte ladder."""
    band_order = ['<1', '1-10', '10-100', '100-1000', '>1000']
    huidige_idx = band_order.index(huidige_band)
    colors = {'<1': '#22C55E', '1-10': '#3B82F6', '10-100': '#F59E0B', '100-1000': '#F97316', '>1000': '#EF4444'}
    labels = {'<1': '< 1 ton/jaar', '1-10': '1 – 10 ton/jaar', '10-100': '10 – 100 ton/jaar', '100-1000': '100 – 1.000 ton/jaar', '>1000': '> 1.000 ton/jaar'}

    ladder = []
    for idx, bk in enumerate(band_order):
        band = TONNAGE_BANDS[bk]
        is_huidig = (bk == huidige_band)
        status = 'huidig' if is_huidig else ('overschreden' if idx < huidige_idx else 'toekomst')

        # Extra vs huidig
        extra = []
        if idx > huidige_idx:
            prev = TONNAGE_BANDS[band_order[idx - 1]]
            if band['annex'] != prev['annex']:
                extra.append(f'+ {band["annex"]} testen')
            if band['csr_verplicht'] and not prev['csr_verplicht']:
                extra.append('+ CSR verplicht')
            if len(band['tests']) > len(prev['tests']):
                extra.append(f'+{len(band["tests"]) - len(prev["tests"])} extra testen')

        ladder.append({
            'band_key': bk,
            'label': labels[bk],
            'status': status,
            'is_huidig': is_huidig,
            'color': colors[bk],
            'registratie': band['registratie'],
            'annex': band['annex'],
            'tests': band['tests'],
            'aantal_tests': len(band['tests']),
            'csr_verplicht': band['csr_verplicht'],
            'csr_secties': band['csr_secties'],
            'kosten_indicatie': band['kosten_indicatie'],
            'doorlooptijd': band['doorlooptijd'],
            'transition_roadmap': band['transition_roadmap'],
            'extra_vs_huidig': extra,
        })

    return ladder


def _genereer_actiepunten(naam, volume, band, rol, svhc, autorisatie, restrictie, h_zinnen, is_mengsel, compliance_score):
    """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',
            'art': 'REACH Art. 5 ("no data, no market")',
        })

    if volume >= 1 and ROLLEN.get(rol, {}).get('registreert', False):
        acties.append({
            'prioriteit': 'KRITIEK',
            'actie': f'{naam}: REACH-registratie starten — {band["label"]}',
            'deadline': 'Voorafgaand aan marktintroductie',
            'art': 'REACH Art. 5, 6',
        })

    if svhc:
        acties.append({
            'prioriteit': 'KRITIEK',
            'actie': f'{naam}: SVHC-maatregelen — klanten informeren, SCIP-melding, alternatieven beoordelen',
            'deadline': 'Onmiddellijk',
            'art': 'REACH Art. 33, 55',
        })

    if autorisatie:
        acties.append({
            'prioriteit': 'KRITIEK',
            'actie': f'{naam}: AUTORISATIE AANVRAGEN (Annex XIV)',
            'deadline': 'Vóór sunset date',
            'art': 'REACH Art. 56',
        })

    if volume >= 10 and ROLLEN.get(rol, {}).get('registreert', False):
        acties.append({
            'prioriteit': 'HOOG',
            'actie': f'{naam}: CSR opstellen met blootstellingsscenario\'s',
            'deadline': 'Onderdeel van registratiedossier',
            'art': 'REACH Art. 14',
        })

    if h_zinnen:
        acties.append({
            'prioriteit': 'HOOG',
            'actie': f'{naam}: SDS controleren en bijwerken',
            'deadline': 'Bij inwerkingtreding',
            'art': 'REACH Art. 31',
        })

    if restrictie:
        acties.append({
            'prioriteit': 'HOOG',
            'actie': f'{naam}: Annex XVII restrictievoorwaarden controleren',
            'deadline': 'Onmiddellijk',
            'art': 'REACH Art. 67-73',
        })

    if is_mengsel and h_zinnen:
        acties.append({
            'prioriteit': 'MEDIUM',
            'actie': f'{naam}: PCN-melding + UFI genereren',
            'deadline': 'Bij marktintroductie',
            'art': 'CLP Annex VIII',
        })

    acties.append({
        'prioriteit': 'MEDIUM',
        'actie': f'{naam}: CLP-classificatie verifiëren',
        'deadline': 'Continue',
        'art': 'CLP Art. 4',
    })

    acties.append({
        'prioriteit': 'MEDIUM',
        'actie': f'{naam}: RI&E bijwerken met REACH/CLP aspecten',
        'deadline': 'Jaarlijks',
        'art': 'Arbowet Art. 5',
    })

    return acties


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

if __name__ == '__main__':
    # Phoenix Metals testdata (7 stoffen)
    test_data = {
        'bedrijfsnaam': 'Phoenix Metals B.V.',
        'rol': 'downstream_gebruiker',
        'locatie': 'Nederland (EU)',
        'sector': 'Metaal & Metaalbewerking',
        'blootstelling_routes': ['Inhalatie (ademharing)', 'Huidcontact', 'Oogcontact'],
        'stoffen': [
            {
                'naam': 'Zwavelzuur',
                'cas': '7664-93-9',
                'volume': 50,
                'stofvorm': 'vloeibaar',
                'mengsel': False,
                'percentage': 96,
                'toepassing': 'Beitsen en neutraliseren',
                '_h_zinnen': ['H314', 'H290'],
                '_svhc': False,
                '_autorisatie': False,
                '_restrictie': False,
            },
            {
                'naam': 'Waterstofperoxide',
                'cas': '7722-84-1',
                'volume': 5,
                'stofvorm': 'vloeibaar',
                'mengsel': False,
                'percentage': 35,
                'toepassing': 'Oxiderende behandeling',
                '_h_zinnen': ['H271', 'H302', 'H314', 'H332'],
                '_svhc': False,
                '_autorisatie': False,
                '_restrictie': False,
            },
            {
                'naam': 'Chroom(VI) oxide',
                'cas': '1333-82-0',
                'volume': 2,
                'stofvorm': 'vast',
                'mengsel': False,
                'percentage': 100,
                'toepassing': 'Passiveren',
                '_h_zinnen': ['H271', 'H301', 'H311', 'H314', 'H317', 'H331', 'H340', 'H350', 'H360F', 'H372', 'H410'],
                '_svhc': True,
                '_svhc_reden': 'Carcinogeen cat. 1B, Mutageen cat. 1B, Reprotoxisch cat. 1B',
                '_autorisatie': True,
                '_restrictie': True,
            },
            {
                'naam': 'Natriumhydroxide',
                'cas': '1310-73-2',
                'volume': 30,
                'stofvorm': 'vast',
                'mengsel': False,
                'percentage': 50,
                'toepassing': 'Neutralisatie',
                '_h_zinnen': ['H290', 'H314'],
                '_svhc': False,
                '_autorisatie': False,
                '_restrictie': False,
            },
            {
                'naam': 'Nitric acid',
                'cas': '7697-37-2',
                'volume': 25,
                'stofvorm': 'vloeibaar',
                'mengsel': False,
                'percentage': 65,
                'toepassing': 'Beitsen',
                '_h_zinnen': ['H270', 'H272', 'H280', 'H314', 'H335', 'H400'],
                '_svhc': False,
                '_autorisatie': False,
                '_restrictie': False,
            },
            {
                'naam': 'Chloride zouten mengsel',
                'cas': '',
                'volume': 10,
                'stofvorm': 'vast',
                'mengsel': True,
                'percentage': 100,
                'toepassing': 'Oppervlaktebehandeling',
                '_h_zinnen': ['H302', 'H315', 'H319', 'H400'],
                '_svhc': False,
                '_autorisatie': False,
                '_restrictie': False,
            },
            {
                'naam': 'Phosphoric acid',
                'cas': '7664-38-2',
                'volume': 8,
                'stofvorm': 'vloeibaar',
                'mengsel': False,
                'percentage': 85,
                'toepassing': 'Fosfateren',
                '_h_zinnen': ['H315', 'H319'],
                '_svhc': False,
                '_autorisatie': False,
                '_restrictie': False,
            },
        ],
    }

    result = analyseer_session(test_data)

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