#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
generate_qc_audit.py — QC Audit voor alle deliverables (MASTER_SOP §5.2 + §5.4)
Project : 2026-voorjaarsconventie-110an (District 110AN, Lions Clubs)

Checks per deliverable:
  Technical : opent zonder errors, versietag in naam, in /deliverables/, changelog
  Gate 0    : Visual & Tone (geen placeholders, fonts, logo/afbeeldingen, tone-of-voice)
  Tier 1    : JvG Stylegate (branding, template-naamgeving, geen dummy-data)
  Tier 2    : Wetgeving (governance-referenties, classificaties, CAS/H-zin-formaten)
  Tier 3    : Feiten (sources.md-controleset), §3.5-matrix compleet, cross-consistentie

Output : <root>/deliverables/audit/QC_Audit_Rapport_v1.0.{docx,json}
Gebruik: python3 generate_qc_audit.py [--root <projectmap>]
Deps   : python-docx, openpyxl, python-pptx (missende lib => FAIL-check, geen crash)
"""

import argparse
import datetime
import json
import re
import zipfile
from pathlib import Path

try:
    import docx  # python-docx
    HAS_DOCX = True
except ImportError:
    HAS_DOCX = False
try:
    import openpyxl
    HAS_OPENPYXL = True
except ImportError:
    HAS_OPENPYXL = False
try:
    from pptx import Presentation
    HAS_PPTX = True
except ImportError:
    HAS_PPTX = False

DEFAULT_ROOT = Path("/root/projects/jg/2026-voorjaarsconventie-110an")
REPORT_BASENAME = "QC_Audit_Rapport_v1.0"

DELIVERABLES = [
    "Projectplan_PVA_v1.0.docx",
    "Procedure_Voorjaarsconventie_2027_v1.0.docx",
    "Werkinstructie_Voorjaarsconventie_2027_v1.0.docx",
    "Checklist_Voorjaarsconventie_2027_v1.0.xlsx",
    "Governance_Compliance_Matrix_v1.0.xlsx",
    "Beleidsdocument_Voorjaarsconventie_2027_v1.0.docx",
    "Presentatie_Voorjaarsconventie_2027_v1.0.pptx",
]

# ---- Controle-set feiten (bron: sources.md) --------------------------------
FACT_SET = {
    "district_110AN": {
        "label": "District: 110AN",
        "patterns": [r"\b110\s?AN\b", r"\bDistrict\s+110\s?AN\b"],
        "must_appear_in": DELIVERABLES,
    },
    "datum_april_2027": {
        "label": "Datum: april 2027",
        "patterns": [r"april\s*2027", r"\b04-2027\b", r"2027\s*\(april\)"],
        "must_appear_in": DELIVERABLES,
    },
    "stemrechten_1_op_10": {
        "label": "Stemrechten: 1 per 10 leden",
        "patterns": [r"1\s*(?:stem|stemrecht)[a-z\s]{0,30}per\s*10", r"1\s*/\s*10",
                     r"1\s*:\s*10", r"per\s+tien\s+leden"],
        "must_appear_in": [
            "Procedure_Voorjaarsconventie_2027_v1.0.docx",
            "Beleidsdocument_Voorjaarsconventie_2027_v1.0.docx",
            "Governance_Compliance_Matrix_v1.0.xlsx",
            "Presentatie_Voorjaarsconventie_2027_v1.0.pptx",
        ],
    },
    "stemrechten_min_max": {
        "label": "Stemrechten: min 1 / max 5",
        "patterns": [r"min[a-z]*\s*1[\s\S]{0,80}?max[a-z]*\s*5",
                     r"maximum\s*(?:van\s*)?5\s*stem", r"ten\s+minste\s+1[\s\S]{0,60}?hoogstens\s+5"],
        "must_appear_in": [
            "Procedure_Voorjaarsconventie_2027_v1.0.docx",
            "Beleidsdocument_Voorjaarsconventie_2027_v1.0.docx",
        ],
    },
    "quorum_50pct": {
        "label": "Quorum: >50% (meer dan de helft)",
        "patterns": [r"quorum[\s\S]{0,60}?>\s*50", r"quorum[\s\S]{0,60}?meer\s+dan\s+de\s+helft",
                     r"quorum[\s\S]{0,60}?50\s*%"],
        "must_appear_in": [
            "Procedure_Voorjaarsconventie_2027_v1.0.docx",
            "Beleidsdocument_Voorjaarsconventie_2027_v1.0.docx",
            "Governance_Compliance_Matrix_v1.0.xlsx",
        ],
    },
    "voorzitter_dg": {
        "label": "Voorzitter: District Governor 2026-2027",
        "patterns": [r"(?:District\s+Governor|DG)\s*2026[\s\-–]20?27", r"Governor\s*2026[\s\-–]20?27"],
        "must_appear_in": [
            "Procedure_Voorjaarsconventie_2027_v1.0.docx",
            "Werkinstructie_Voorjaarsconventie_2027_v1.0.docx",
            "Beleidsdocument_Voorjaarsconventie_2027_v1.0.docx",
        ],
    },
    "governance_hr_md110": {
        "label": "Governance: Huishoudelijk Reglement MD 110 mei 2021",
        "patterns": [r"Huishoudelijk\s+Reglement[\s\S]{0,120}?mei\s*2021",
                     r"Huishoudelijk\s+Reglement[\s\S]{0,120}?(?:MD\s*110|110\s?AN)"],
        "must_appear_in": [
            "Procedure_Voorjaarsconventie_2027_v1.0.docx",
            "Governance_Compliance_Matrix_v1.0.xlsx",
            "Beleidsdocument_Voorjaarsconventie_2027_v1.0.docx",
        ],
    },
    "budget_2526": {
        "label": "Budgetreferentie: Districtbegroting 25-26",
        "patterns": [r"Districtbegroting\s*25[\s\-–]?26", r"Districtbegroting\s*2025[\s\-–]?2026"],
        "must_appear_in": [
            "Projectplan_PVA_v1.0.docx",
            "Beleidsdocument_Voorjaarsconventie_2027_v1.0.docx",
        ],
    },
}

# ---- Gate 0 ----------------------------------------------------------------
PLACEHOLDER_PATTERNS = [
    (r"\bTODO\b", "TODO-marker"),
    (r"\bTBD\b", "TBD-marker"),
    (r"\bFIXME\b", "FIXME-marker"),
    (r"\bX{3,}\b", "XXX-placeholder"),
    (r"\{\{[^}]{1,60}\}\}", "niet-ingevulde template-variabele {{...}}"),
    (r"Lorem\s+ipsum", "Lorem-ipsum tekst"),
    (r"\[(?:voornaam|naam|datum|invullen|organisatie|functie)\]", "invul-placeholder tussen haakjes"),
    (r"<voornaam>|<naam>|<datum>", "chevron-placeholder"),
]
DUMMY_PATTERNS = [
    (r"\bdummy\b", "woord 'dummy'"),
    (r"\btestdata\b", "testdata"),
    (r"\bvoorbeeld@voorbeeld\.(?:nl|com)\b", "voorbeeld-e-mailadres"),
    (r"\bJan\s+Jansen\b|\bPiet\s+de\s+Vries\b", "generieke voorbeeldnaam"),
]
TONE_VIOLATION_PATTERNS = [
    (r"\btof\b|\bcool\b|\bvet\s+belangrijk\b", "informeel/straattaal-woord"),
    (r"\blekker\s+\w+", "informeel woord 'lekker'"),
    (r"\bgaat\s+zijn\s+zin\s+doen\b", "informeel idioom"),
    (r"\bwtf\b|\bomg\b", "onprofessionele expressie"),
    (r"[!?]{3,}", "overmatig gebruik van !/?"),
]

# ---- Tier 1 / Tier 2 -------------------------------------------------------
BRANDING_REQUIRED_TERMS = ["Lions", "110AN"]
TEMPLATE_PREFIXES = ("Projectplan", "Procedure", "Werkinstructie", "Checklist",
                     "Governance_Compliance_Matrix", "Beleidsdocument", "Presentatie")
VERSION_TAG_RE = re.compile(r"_v\d+\.\d+$", re.IGNORECASE)
HR_LABEL = "Huishoudelijk Reglement"
CAS_RE = re.compile(r"\b\d{2,7}-\d{2}-\d\b")
HZIN_RE = re.compile(r"\bH\d{3}\b")
CLASSIFICATION_TERMS = [r"\bopenbaar\b", r"\bintern\b", r"\bvertrouwelijk\b"]
MATRIX_EXPECTED_TERMS = ["Huishoudelijk Reglement", "Districtbegroting", "quorum", "stemrecht"]
CHANGELOG_TERMS = [r"wijzigingsgeschiedenis", r"changelog", r"versiegeschiedenis",
                   r"documenthistorie", r"versiebeheer", r"\bv1\.0\b"]

# ---- Cross-deliverable consistentie ---------------------------------------
CONSISTENCY_CONFLICTS = [
    (r"\bDistrict\s+110\s?(?!AN)[A-Z]{1,2}\b", "Verkeerd district-suffix (moet 110AN)"),
    (r"\bvoorjaarsconventie\s+2026\b", "Verkeerd conventiejaar (moet 2027)"),
    (r"quorum[\s\S]{0,60}?(?:2/3|twee\s?derde|100\s*%|unaniem)", "Afwijkende quorum-regel"),
    (r"stemrecht[a-z\s]{0,40}?(?:per\s+5\b|per\s+20\b|per\s+25\b)", "Afwijkende stemrecht-verhouding"),
]

TIER_LABELS = ["Technical", "Gate 0", "Tier 1", "Tier 2", "Tier 3"]
SEVERITY_RANK = {"INFO": 0, "WARN": 1, "FAIL": 2}


def find_file(root: Path, fname: str):
    """Zoek bestand recursief binnen /deliverables/ (incl. submappen docx/, pptx/)."""
    base = root / "deliverables"
    hits = sorted(p for p in base.rglob(fname) if p.is_file())
    return hits[0] if hits else None


def rx_any(patterns, text):
    for pat in patterns:
        m = re.search(pat, text, re.IGNORECASE)
        if m:
            return pat, m
    return None, None


# ---- Extractie per formaat -------------------------------------------------
def _docx_extract(path: Path):
    d = docx.Document(str(path))
    parts = [p.text for p in d.paragraphs]
    for table in d.tables:
        for row in table.rows:
            for cell in row.cells:
                parts.append(cell.text)
    try:
        for sec in d.sections:
            for hf in (sec.header, sec.footer):
                for p in hf.paragraphs:
                    parts.append(p.text)
    except Exception:
        pass
    fonts = set()
    for p in d.paragraphs:
        for run in p.runs:
            if run.font.name:
                fonts.add(run.font.name)
    try:
        for st in d.styles:
            if getattr(st, "font", None) and st.font.name:
                fonts.add(st.font.name)
    except Exception:
        pass
    n_img = 0
    try:
        n_img = max(len(d.inline_shapes),
                    sum(1 for rel in d.part.rels.values() if "image" in rel.reltype))
    except Exception:
        pass
    return "\n".join(parts), sorted(fonts), n_img


def _xlsx_extract(path: Path):
    wb = openpyxl.load_workbook(str(path), data_only=False)
    parts, fonts = [], set()
    for ws in wb.worksheets:
        parts.append(f"### sheet: {ws.title}")
        for row in ws.iter_rows():
            for cell in row:
                if cell.value is not None:
                    parts.append(str(cell.value))
                if cell.font and cell.font.name:
                    fonts.add(cell.font.name)
    return "\n".join(parts), sorted(fonts), 0


def _pptx_extract(path: Path):
    prs = Presentation(str(path))
    parts, fonts, n_img = [], set(), 0
    for slide in prs.slides:
        for shape in slide.shapes:
            if getattr(shape, "has_text_frame", False):
                for para in shape.text_frame.paragraphs:
                    for run in para.runs:
                        parts.append(run.text)
                        if run.font.name:
                            fonts.add(run.font.name)
            try:
                if shape.shape_type == 13:  # PICTURE
                    n_img += 1
            except Exception:
                pass
        try:
            if slide.has_notes_slide and slide.notes_slide.notes_text_frame is not None:
                parts.append(slide.notes_slide.notes_text_frame.text)
        except Exception:
            pass
    return "\n".join(parts), sorted(fonts), n_img


def extract(path: Path):
    suf = path.suffix.lower()
    if suf == ".docx":
        if not HAS_DOCX:
            raise RuntimeError("python-docx niet geinstalleerd")
        return _docx_extract(path)
    if suf == ".xlsx":
        if not HAS_OPENPYXL:
            raise RuntimeError("openpyxl niet geinstalleerd")
        return _xlsx_extract(path)
    if suf == ".pptx":
        if not HAS_PPTX:
            raise RuntimeError("python-pptx niet geinstalleerd")
        return _pptx_extract(path)
    raise RuntimeError(f"Onbekend formaat: {suf}")


def zip_valid(path: Path) -> bool:
    try:
        with zipfile.ZipFile(path) as zf:
            return zf.testzip() is None
    except Exception:
        return False


def add(findings, tier, severity, check, detail):
    findings.append({"tier": tier, "severity": severity, "check": check, "detail": detail})


# ----------------------------------------------------------------------------
# Check-engines
# ----------------------------------------------------------------------------
def check_technical(fname, path, text, findings):
    tier = "Technical"
    if "deliverables" in path.parts:
        add(findings, tier, "INFO", "Locatie", "Bestand staat in /deliverables/ (submap toegestaan)")
    else:
        add(findings, tier, "FAIL", "Locatie", f"Bestand staat NIET in /deliverables/: {path.parent}")
    if VERSION_TAG_RE.search(path.stem):
        add(findings, tier, "INFO", "Versietag", f"Versietag in bestandsnaam: {path.stem}")
    else:
        add(findings, tier, "FAIL", "Versietag", "Geen versietag (_vX.Y) in bestandsnaam")
    if zip_valid(path):
        add(findings, tier, "INFO", "Integriteit", "OOXML-zip opent en valideert zonder errors")
    else:
        add(findings, tier, "FAIL", "Integriteit", "Bestand is geen geldige OOXML-zip")
    if rx_any(CHANGELOG_TERMS, text)[0]:
        add(findings, tier, "INFO", "Changelog", "Versie-/wijzigingshistorie aangetroffen in document")
    else:
        add(findings, tier, "WARN", "Changelog", "Geen changelog/versiehistorie teruggevonden in documentinhoud")


def check_gate0(fname, path, text, fonts, n_img, findings):
    tier = "Gate 0"
    for pat, label in PLACEHOLDER_PATTERNS:
        m = re.search(pat, text, re.IGNORECASE)
        if m:
            add(findings, tier, "FAIL", "Placeholders", f"{label} gevonden: '{m.group(0)}'")
    if not any(f["check"] == "Placeholders" for f in findings if f["tier"] == tier):
        add(findings, tier, "INFO", "Placeholders", "Geen placeholders aangetroffen")
    if n_img > 0:
        add(findings, tier, "INFO", "Logo/visuals", f"{n_img} afbeelding(en) aanwezig (logo-vermoeden)")
    else:
        add(findings, tier, "WARN", "Logo/visuals", "Geen afbeeldingen/logo gedetecteerd")
    if len(fonts) == 0:
        add(findings, tier, "WARN", "Lettertypes", "Geen expliciete fonts gedetecteerd (styling via thema)")
    elif len(fonts) <= 3:
        add(findings, tier, "INFO", "Lettertypes", f"Consistent fontgebruik ({len(fonts)}): {', '.join(fonts)}")
    else:
        add(findings, tier, "WARN", "Lettertypes", f"{len(fonts)} verschillende fonts: {', '.join(fonts[:8])}")
    tone_hits = [lbl for pat, lbl in TONE_VIOLATION_PATTERNS if re.search(pat, text, re.IGNORECASE)]
    if tone_hits:
        add(findings, tier, "WARN", "Tone-of-voice", "Mogelijke tone-schendingen: " + "; ".join(tone_hits))
    else:
        add(findings, tier, "INFO", "Tone-of-voice", "Geen informele taal of tone-schendingen gedetecteerd")


def check_tier1(fname, path, text, findings):
    tier = "Tier 1"
    missing = [t for t in BRANDING_REQUIRED_TERMS if not re.search(t, text, re.IGNORECASE)]
    if missing:
        add(findings, tier, "FAIL", "Branding", f"Verplichte branding-termen ontbreken: {', '.join(missing)}")
    else:
        add(findings, tier, "INFO", "Branding", "Lions/110AN-branding aanwezig")
    if fname.startswith(TEMPLATE_PREFIXES) and VERSION_TAG_RE.search(path.stem):
        add(findings, tier, "INFO", "Template-conformiteit", "Bestandsnaam volgt templatenorm <Type>_<Naam>_vX.Y")
    else:
        add(findings, tier, "WARN", "Template-conformiteit", "Bestandsnaam wijkt af van templatenorm")
    dummy_hits = [lbl for pat, lbl in DUMMY_PATTERNS if re.search(pat, text, re.IGNORECASE)]
    if dummy_hits:
        add(findings, tier, "FAIL", "Geen dummy", "Dummy-data gevonden: " + "; ".join(dummy_hits))
    else:
        add(findings, tier, "INFO", "Geen dummy", "Geen dummy-/voorbeelddata gedetecteerd")


def check_tier2(fname, path, text, findings):
    tier = "Tier 2"
    if HR_LABEL.lower() in text.lower():
        if re.search(r"mei\s*2021", text, re.IGNORECASE) or re.search(r"(?:MD\s*110|110\s?AN)", text, re.IGNORECASE):
            add(findings, tier, "INFO", "Referentie HR", "Huishoudelijk Reglement correct gerefereerd incl. MD 110 / mei 2021")
        else:
            add(findings, tier, "WARN", "Referentie HR", "HR genoemd zonder jaartal 'mei 2021' of MD 110-vermelding")
    cas = CAS_RE.findall(text)
    if cas:
        add(findings, tier, "INFO", "CAS-nummers", f"CAS-nummerformaat correct ({len(cas)}x): {', '.join(cas[:5])}")
    hz = HZIN_RE.findall(text)
    if hz:
        add(findings, tier, "INFO", "H-zinnen", f"H-zinncodes in geldig formaat: {', '.join(sorted(set(hz)))}")
    if any(re.search(p, text, re.IGNORECASE) for p in CLASSIFICATION_TERMS):
        add(findings, tier, "INFO", "Classificatie", "Informatieclassificatie (openbaar/intern/vertrouwelijk) aanwezig")
    elif not fname.startswith(("Checklist", "Presentatie")):
        add(findings, tier, "WARN", "Classificatie", "Geen informatieclassificatie gevonden (voor zover verplicht)")


def check_tier3(fname, path, text, findings):
    tier = "Tier 3"
    for key, spec in FACT_SET.items():
        if fname not in spec["must_appear_in"]:
            continue
        pat, m = rx_any(spec["patterns"], text)
        if m:
            add(findings, tier, "INFO", f"Feit: {spec['label']}", f"Aangetroffen als: '{m.group(0)}'")
        else:
            add(findings, tier, "FAIL", f"Feit: {spec['label']}", "Verplichte feit niet teruggevonden in dit document")
    conflicts = [lbl for pat, lbl in CONSISTENCY_CONFLICTS if re.search(pat, text, re.IGNORECASE)]
    if conflicts:
        for c in conflicts:
            add(findings, tier, "FAIL", "Cross-consistentie", c)
    else:
        add(findings, tier, "INFO", "Cross-consistentie", "Geen conflicterende waarden met controleteset")
    if fname.startswith("Governance_Compliance_Matrix"):
        missing = [t for t in MATRIX_EXPECTED_TERMS if t.lower() not in text.lower()]
        if missing:
            add(findings, tier, "WARN", "Matrix compleetheid (§3.5)", f"Verwachte termen ontbreken in matrix: {', '.join(missing)}")
        else:
            add(findings, tier, "INFO", "Matrix compleetheid (§3.5)", "Alle verwachte governance-termen in matrix aanwezig")


# ----------------------------------------------------------------------------
# Audit per deliverable
# ----------------------------------------------------------------------------
def audit_deliverable(root, fname):
    result = {
        "bestand": fname,
        "pad": None,
        "bestaat": False,
        "geextrudeerd": False,
        "fonts": [],
        "afbeeldingen": 0,
        "tekstlengte": 0,
        "findings": [],
        "tier_status": {t: "NVT" for t in TIER_LABELS},
        "oordeel": "FAIL",
    }
    path = find_file(root, fname)
    if path is None:
        add(result["findings"], "Technical", "FAIL", "Aanwezigheid",
            f"{fname} niet gevonden in /deliverables/ (recursief gezocht)")
        result["tier_status"]["Technical"] = "FAIL"
        return result
    result["pad"] = str(path)
    result["bestaat"] = True
    try:
        text, fonts, n_img = extract(path)
        result.update(geextrudeerd=True, fonts=fonts, afbeeldingen=n_img, tekstlengte=len(text))
        add(result["findings"], "Technical", "INFO", "Openen", "Bestand opent zonder errors (parser OK)")
        check_technical(fname, path, text, result["findings"])
        check_gate0(fname, path, text, fonts, n_img, result["findings"])
        check_tier1(fname, path, text, result["findings"])
        check_tier2(fname, path, text, result["findings"])
        check_tier3(fname, path, text, result["findings"])
    except Exception as e:
        add(result["findings"], "Technical", "FAIL", "Openen", f"Bestand kon niet geopend/gelezen worden: {e}")
        result["tier_status"]["Technical"] = "FAIL"
        result["oordeel"] = "FAIL"
        return result
    for f in result["findings"]:
        t = f["tier"]
        cur = result["tier_status"].get(t, "INFO")
        if SEVERITY_RANK[f["severity"]] > SEVERITY_RANK.get(cur, 0):
            result["tier_status"][t] = f["severity"]
    worst = max((SEVERITY_RANK[f["severity"]] for f in result["findings"]), default=0)
    result["oordeel"] = "PASS" if worst == 0 else ("PASS_MET_WARNINGS" if worst == 1 else "FAIL")
    return result


# ----------------------------------------------------------------------------
# Rapportage (JSON + DOCX)
# ----------------------------------------------------------------------------
def build_json_report(root, results, started):
    total_fail = sum(1 for r in results if r["oordeel"] == "FAIL")
    total_warn = sum(1 for r in results if r["oordeel"] == "PASS_MET_WARNINGS")
    report = {
        "rapport": "QC Audit Rapport Voorjaarsconventie 2027 — District 110AN",
        "versie": "1.0",
        "standaard": "MASTER_SOP §5.2 + §5.4",
        "gegenereerd_op": started.isoformat(timespec="seconds"),
        "project_root": str(root),
        "controleteset_bron": "sources.md",
        "controleteset": {k: v["label"] for k, v in FACT_SET.items()},
        "deliverables_gecontroleerd": len(results),
        "samenvatting": {
            "PASS": len(results) - total_fail - total_warn,
            "PASS_MET_WARNINGS": total_warn,
            "FAIL": total_fail,
            "totaal": len(results),
            "eindoordeel": ("GO" if total_fail == 0 and total_warn == 0
                            else "CONDITIONEEL GO" if total_fail == 0
                            else "NO-GO"),
        },
        "resultaten": results,
    }
    return report


def write_docx_report(report, out_path):
    d = docx.Document()
    d.add_heading("QC Audit Rapport — Voorjaarsconventie 2027 (District 110AN)", level=0)
    d.add_paragraph(f"Standaard: {report['standaard']}  |  Versie: {report['versie']}")
    d.add_paragraph(f" Gegenereerd: {report['gegenereerd_op']}  |  Bron controleteset: sources.md")

    d.add_heading("1. Samenvatting", level=1)
    s = report["samenvatting"]
    d.add_paragraph(
        f"Deliverables gecontroleerd: {s['totaal']}. "
        f"PASS: {s['PASS']} | PASS met warnings: {s['PASS_MET_WARNINGS']} | FAIL: {s['FAIL']}. "
        f"Eindoordeel: {s['eindoordeel']}."
    )
    tbl = d.add_table(rows=1, cols=4)
    tbl.style = "Light Grid Accent 1"
    hdr = tbl.rows[0].cells
    for i, h in enumerate(["Deliverable", "Oordeel", "Kritiek (FAIL)", "Waarschuwingen"]):
        hdr[i].text = h
    for r in report["resultaten"]:
        row = tbl.add_row().cells
        row[0].text = r["bestand"]
        row[1].text = r["oordeel"]
        row[2].text = str(sum(1 for f in r["findings"] if f["severity"] == "FAIL"))
        row[3].text = str(sum(1 for f in r["findings"] if f["severity"] == "WARN"))

    d.add_heading("2. Controleteset (feiten uit sources.md)", level=1)
    for label in report["controleteset"].values():
        d.add_paragraph(label, style="List Bullet")

    d.add_heading("3. Bevindingen per deliverable", level=1)
    for r in report["resultaten"]:
        d.add_heading(f"{r['bestand']} — {r['oordeel']}", level=2)
        if not r["bestaat"]:
            d.add_paragraph("BESTAND NIET GEVONDEN in /deliverables/.")
            continue
        d.add_paragraph(f"Pad: {r['pad']}  |  Tekstlengte: {r['tekstlengte']} tekens  |  "
                        f"Fonts: {', '.join(r['fonts']) or '(thema-styling)'}  |  "
                        f"Afbeeldingen: {r['afbeeldingen']}")
        d.add_paragraph("Tier-status: " + ", ".join(f"{t}={st}" for t, st in r["tier_status"].items()))
        ftbl = d.add_table(rows=1, cols=4)
        ftbl.style = "Light List Accent 1"
        for i, h in enumerate(["Tier", "Severity", "Check", "Detail"]):
            ftbl.rows[0].cells[i].text = h
        for f in r["findings"]:
            row = ftbl.add_row().cells
            row[0].text = f["tier"]
            row[1].text = f["severity"]
            row[2].text = f["check"]
            row[3].text = f["detail"]

    d.add_heading("4. Eindoordeel", level=1)
    d.add_paragraph(
        f"Op basis van MASTER_SOP §5.2/§5.4 luidt het eindoordeel: {s['eindoordeel']}. "
        "Alle FAIL-bevindingen dienen vóór vrijgave (Gate-goedkeuring) hersteld te worden; "
        "WARN-bevindingen zijn ter beoordeling van de projectleider/QA."
    )
    out_path.parent.mkdir(parents=True, exist_ok=True)
    d.save(str(out_path))


# ----------------------------------------------------------------------------
# Main
# ----------------------------------------------------------------------------
def main():
    parser = argparse.ArgumentParser(description="QC Audit alle deliverables (MASTER_SOP §5.2/§5.4)")
    parser.add_argument("--root", type=Path, default=DEFAULT_ROOT, help="Projectroot")
    args = parser.parse_args()
    started = datetime.datetime.now()

    results = [audit_deliverable(args.root, fname) for fname in DELIVERABLES]
    report = build_json_report(args.root, results, started)

    audit_dir = args.root / "deliverables" / "audit"
    audit_dir.mkdir(parents=True, exist_ok=True)
    json_path = audit_dir / f"{REPORT_BASENAME}.json"
    docx_path = audit_dir / f"{REPORT_BASENAME}.docx"

    with open(json_path, "w", encoding="utf-8") as fh:
        json.dump(report, fh, ensure_ascii=False, indent=2)
    print(f"[OK] JSON geschreven: {json_path}")

    if not HAS_DOCX:
        print("[WAARSCHUWING] python-docx ontbreekt; DOCX-rapport niet gegenereerd")
    else:
        write_docx_report(report, docx_path)
        print(f"[OK] DOCX geschreven: {docx_path}")

    s = report["samenvatting"]
    print(f"Samenvatting: PASS={s['PASS']} WARN={s['PASS_MET_WARNINGS']} FAIL={s['FAIL']} "
          f"=> {s['eindoordeel']}")
    return 0 if s["FAIL"] == 0 else 1


if __name__ == "__main__":
    raise SystemExit(main())
