#!/usr/bin/env python3
"""brands.py — JvG HSEQ & Wintershall brand-builders voor generate_training.py.

Basis: formats/JVG_HSEQ_MASTER_FORMAT.pptx en formats/WINTERSHALL_MASTER_FORMAT.pptx.
Patroon: master als visuele basis (graphics onaangetast), teksten vervangen,
overtollige slides verwijderd, extra slides gedupliceerd uit bestaande templates.

Regels (MASTER_STYLEGUIDE §3.5):
- interne links uitsluitend slide-rel + action="ppaction://hlinksldjump"
- geen 'slide://' hyperlinks
- WINTERSHALL-REGEL: 0x JvG (geen logo, footer of tekst)
- TYPE-ADAPTATIE: toolbox = compact (kernslides, geen quiz), awareness = middel
  (korte quiz), course = volledig (alle secties + quiz + antwoordmodel)
"""
from __future__ import annotations

import math
import re
from copy import deepcopy
from pathlib import Path

from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE
from pptx.enum.text import PP_ALIGN
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
from pptx.oxml.ns import qn
from pptx.util import Inches, Pt

PROJ = Path(__file__).resolve().parent.parent

JVG_DONKER = "00386B"
JVG_TEKST = "334155"
JVG_ACCENT = "5BA649"
WS_PRIMARY = "005493"
WS_PANEL = "F0F8FF"
WS_BODY = "333333"
WS_FONT = "Arial"

JUMP_ACTION = "ppaction://hlinksldjump"


# ---------------------------------------------------------------- helpers --

def _runs_set(runs, new_text):
    if len(runs) >= 2 and runs[0].text.strip() in ("•", "✓", "■"):
        runs[1].text = new_text
        for r in runs[2:]:
            r._r.getparent().remove(r._r)
    else:
        runs[0].text = new_text
        for r in runs[1:]:
            r._r.getparent().remove(r._r)


def set_box_text(shape, new_text):
    """Tekst vervangen, eerste run-formatering behouden (ook '• x' 2-run)."""
    tf = shape.text_frame
    paras = tf.paragraphs
    if not paras[0].runs:
        paras[0].add_run().text = new_text
        return
    _runs_set(paras[0].runs, new_text)
    for p in paras[1:]:
        p._p.getparent().remove(p._p)


def set_box_lines(shape, lines):
    """Meerdere regels in één box; formatering paragraaf-1 gekloond per regel."""
    lines = [str(l) for l in lines if l and str(l).strip()]
    if not lines:
        rm(shape)
        return
    tf = shape.text_frame
    set_box_text(shape, lines[0])
    p0 = tf.paragraphs[0]._p
    for extra in lines[1:]:
        tf._txBody.append(deepcopy(p0))
        _runs_set(tf.paragraphs[-1].runs, extra)


def rm(shape):
    el = shape._element
    parent = el.getparent()
    if parent is not None:
        parent.remove(el)


def delete_slide(prs, slide):
    sldIdLst = prs.slides._sldIdLst
    for sldId in list(sldIdLst):
        rId = sldId.get(qn("r:id"))
        if prs.part.rels[rId].target_part is slide.part:
            prs.part.drop_rel(rId)
            sldIdLst.remove(sldId)
            return


def dup_slide(prs, src):
    """Slide klonen binnen dezelfde layout, incl. afbeeldings-rels."""
    layout = src.slide_layout
    new = prs.slides.add_slide(layout)
    for sh in list(new.shapes):
        rm(sh)
    for child in src.shapes._spTree:
        if child.tag in (qn("p:nvGrpSpPr"), qn("p:grpSpPr")):
            continue
        new.shapes._spTree.append(deepcopy(child))
    for el in new.shapes._spTree.iter():
        embed = el.get(qn("r:embed"))
        if embed and embed in src.part.rels:
            img_part = src.part.rels[embed].target_part
            el.set(qn("r:embed"), new.part.relate_to(img_part, RT.IMAGE))
    return new


def content_boxes(slide, min_left_in=0.0):
    """Tekstboxen met inhoud (geen placeholders/markers), gesorteerd op (top,left)."""
    out = []
    for sh in slide.shapes:
        if sh.has_text_frame and sh.shape_type == 17:
            t = sh.text_frame.text.strip()
            if t and t not in ("■", "•", "✓") and sh.left >= Inches(min_left_in):
                out.append(sh)
    out.sort(key=lambda s: (round(s.top / 914400, 1), s.left))
    return out


def slide_title_box(slide):
    boxes = content_boxes(slide)
    return boxes[0] if boxes else None


def add_jump(shape, source_slide, target_slide):
    """Interne §3.5-jump: slide-rel + ppaction://hlinksldjump."""
    cNvPr = shape._element.find(".//" + qn("p:cNvPr"))
    if cNvPr is None:
        return
    rId = source_slide.part.relate_to(target_slide.part, RT.SLIDE)
    hl = cNvPr.find(qn("a:hlinkClick"))
    if hl is None:
        hl = cNvPr.makeelement(qn("a:hlinkClick"), {})
        cNvPr.append(hl)
    hl.set(qn("r:id"), rId)
    hl.set("action", JUMP_ACTION)


def reorder_slides(prs, order):
    """sldIdLst herschikken naar gewenste visuele volgorde."""
    sldIdLst = prs.slides._sldIdLst
    id_by_part = {}
    for sldId in list(sldIdLst):
        rId = sldId.get(qn("r:id"))
        id_by_part[prs.part.rels[rId].target_part] = sldId
        sldIdLst.remove(sldId)
    for sl in order:
        sldIdLst.append(id_by_part[sl.part])


def detect_kind(data):
    t = str(data.get("type", "")).lower()
    if "toolbox" in t:
        return "toolbox"
    if "awareness" in t:
        return "awareness"
    return "course"


def strip_old_jumps(slide):
    """Bestaande hlinksldjump-rels verwijderen (voorkomt aliasing)."""
    for cNvPr in slide._element.iter(qn("p:cNvPr")):
        hl = cNvPr.find(qn("a:hlinkClick"))
        if hl is not None and hl.get("action") == JUMP_ACTION:
            old = hl.get(qn("r:id"))
            if old in slide.part.rels:
                slide.part.drop_rel(old)
            cNvPr.remove(hl)


def add_pagenum(slide, n, total, color=JVG_TEKST):
    box = slide.shapes.add_textbox(Inches(12.3), Inches(7.05), Inches(0.85),
                                   Inches(0.3))
    p = box.text_frame.paragraphs[0]
    p.alignment = PP_ALIGN.RIGHT
    r = p.add_run()
    r.text = f"{n} / {total}"
    r.font.size = Pt(9)
    r.font.color.rgb = RGBColor.from_string(color)


# ------------------------------------------------------------- JvG HSEQ ----

JVG_TITEL, JVG_INTRO, JVG_MODTPL, JVG_MAAT, JVG_VER, JVG_REF, JVG_KERN, JVG_AFS = \
    0, 1, 4, 7, 8, 9, 10, 11


def _vul_module(slide, titel, bullets_4):
    """Module-slide vullen: kop = titel, max 4 bulletboxen (lijst = meerregelig)."""
    boxes = content_boxes(slide, min_left_in=0.7)
    kop = boxes[0]
    set_box_text(kop, titel)
    for i, sh in enumerate(boxes[1:5]):
        if i < len(bullets_4):
            if isinstance(bullets_4[i], (list, tuple)):
                set_box_lines(sh, bullets_4[i])
            else:
                set_box_text(sh, bullets_4[i])
        else:
            rm(sh)


def build_jvg_hseq(data, out_path):
    kind = detect_kind(data)
    prs = Presentation(str(PROJ / "formats/JVG_HSEQ_MASTER_FORMAT.pptx"))
    S = list(prs.slides)

    modules = data.get("modules", [])
    quiz = data.get("quiz", [])
    leerdoelen = data.get("leerdoelen", [])
    bronnen = data.get("bronnenlijst", [])
    kernpunten = data.get("kernpunten", [])

    mods = modules[:3] if kind == "toolbox" else modules
    # maatregelen-module over ALLE modules zoeken (toolbox knipt mods anders in)
    maat_mod = next((m for m in modules if re.search(
        r"maatregel|afspraken|prevent|stop", m["titel"], re.I)),
        mods[-1] if mods else None)
    ver_mod = next((m for m in mods if re.search(
        r"verantwoord|rol|taak", m["titel"], re.I)), None)
    inhoud_mods = [m for m in mods if m is not maat_mod and m is not ver_mod]

    weg = [2, 3, 5, 6, 12]
    if kind == "toolbox" or not ver_mod:
        weg.append(JVG_VER)
    # NB: deleties pas op het EIND — vóór duplicatie ontstaan partname-botsingen

    # 1) titelslide
    label = {"toolbox": "TOOLBOX TALK", "awareness": "AWARENESS TRAINING",
             "course": "COURSE"}[kind]
    for sh in S[JVG_TITEL].shapes:
        if not sh.has_text_frame:
            continue
        t = sh.text_frame.text.strip()
        if t.startswith("AWARENESS"):
            set_box_text(sh, label)
        elif "Kwik" in t:
            titel_txt = data.get("titel", "Training")
            set_box_text(sh, titel_txt)
            # lange titels: font verkleinen zodat hij niet uit de box loopt
            if len(titel_txt) > 38:
                nieuwe_grootte = 30
            elif len(titel_txt) > 26:
                nieuwe_grootte = 38
            else:
                nieuwe_grootte = None
            if nieuwe_grootte:
                for r in sh.text_frame.paragraphs[0].runs:
                    r.font.size = Pt(nieuwe_grootte)
        elif "Olie, Gas" in t:
            set_box_text(sh, data.get("ondertitel", ""))
        elif t.startswith("Niveau"):
            # type-label normaliseren naar gekozen type (geen rauwe JSON-string)
            duur = data.get("duur", "")
            set_box_text(sh, f"{label.title()}  |  Duur: {duur}" if duur else label.title())

    # 2) modules
    sectie_slides = []  # (naam, slide) voor agenda-jumps
    mod_slide_objs = []  # moduleslides in volgorde van aanmaken
    for mi, mod in enumerate(inhoud_mods):
        bl = mod.get("bullets", [])
        if kind == "toolbox" and len(bl) > 4:
            chunks = [[bl[0], bl[1], bl[2], bl[3:]]]
        else:
            chunks = [bl[i:i + 4] for i in range(0, len(bl), 4)] or [[]]
        for ci, chunk in enumerate(chunks):
            slide = S[JVG_MODTPL] if (mi == 0 and ci == 0) else \
                dup_slide(prs, S[JVG_MODTPL])
            titel = mod["titel"] if len(chunks) == 1 else \
                f"{mod['titel']} ({ci + 1}/{len(chunks)})"
            _vul_module(slide, titel, chunk)
            if ci == 0:
                sectie_slides.append((mod["titel"], slide))
            mod_slide_objs.append(slide)

    # 3) maatregelen (3 kolommen)
    if maat_mod:
        s = S[JVG_MAAT]
        set_box_text(slide_title_box(s), f"Maatregelen: {maat_mod['titel']}")
        vlak = [sh for sh in s.shapes if sh.has_text_frame and sh.shape_type == 17
                and sh.top > Inches(4.4) and sh.text_frame.text.strip() not in ("■",)]
        vlak.sort(key=lambda x: x.left)
        kops = [sh for sh in vlak if sh.top < Inches(4.9)]
        teksten = [sh for sh in vlak if sh.top >= Inches(4.9)]
        bl = maat_mod.get("bullets", [])[:3]
        for i in range(3):
            if i < len(kops):
                prefix = bl[i].split(":")[0] if i < len(bl) and ":" in bl[i][:40] \
                    else f"Maatregel {i + 1}"
                if len(prefix) > 26:
                    prefix = prefix[:25] + "…"
                set_box_text(kops[i], f"{i + 1}. {prefix}")
            if i < len(teksten):
                if i < len(bl):
                    txt = bl[i].split(":", 1)[1].strip() if ":" in bl[i][:40] else bl[i]
                    set_box_text(teksten[i], txt)
        sectie_slides.append(("Maatregelen", s))

    # 4) verantwoordelijkheden (2 kolommen)
    if ver_mod and kind != "toolbox":
        s = S[JVG_VER]
        set_box_text(slide_title_box(s), f"Verantwoordelijkheden: {ver_mod['titel']}")
        links = [sh for sh in s.shapes if sh.has_text_frame and sh.shape_type == 17
                 and Inches(2.4) < sh.top < Inches(6.6) and sh.left < Inches(5.5)
                 and sh.text_frame.text.strip() not in ("■",)]
        rechts = [sh for sh in s.shapes if sh.has_text_frame and sh.shape_type == 17
                  and Inches(2.4) < sh.top < Inches(6.6) and sh.left > Inches(5.5)
                  and sh.text_frame.text.strip() not in ("■",)]
        bl = ver_mod.get("bullets", [])
        helft = (len(bl) + 1) // 2
        set_box_text(links[0], "De Werknemer (Uitvoerend)")
        set_box_lines(
            sorted(links[1:], key=lambda x: x.top)[0] if len(links) > 1 else links[0],
            bl[:helft])
        for extra in sorted(links[1:], key=lambda x: x.top)[1:]:
            rm(extra)
        set_box_text(rechts[0], "Leidinggevende / HSEQ")
        set_box_lines(
            sorted(rechts[1:], key=lambda x: x.top)[0] if len(rechts) > 1 else rechts[0],
            bl[helft:])
        for extra in sorted(rechts[1:], key=lambda x: x.top)[1:]:
            rm(extra)
        sectie_slides.append(("Verantwoordelijkheden", s))

    # 5) quiz
    quiz_slides = []
    if kind == "awareness" and quiz:
        slide = dup_slide(prs, S[JVG_MODTPL])
        regels = []
        for q in quiz[:4]:
            juist = q["antwoorden"][q["correct"]] if q.get("antwoorden") else ""
            regels.append([f"Vraag: {q['vraag']}", f"✓ {juist}"])
        _vul_module(slide, "Quiz — test je kennis", regels[:4])
        sectie_slides.append(("Quiz", slide))
    elif kind == "course" and quiz:
        quiz_slides = []
        for qi, q in enumerate(quiz):
            slide = dup_slide(prs, S[JVG_MODTPL])
            opties = [f"{chr(65 + i)}) {a}" for i, a in
                      enumerate(q.get("antwoorden", []))]
            juist = q.get("antwoorden", [""])[q.get("correct", 0)]
            uitleg = q.get("uitleg", "")
            _vul_module(slide, f"Quiz ({qi + 1}/{len(quiz)})", [
                [q["vraag"]], opties,
                [f"✓ Juist: {juist}"] + ([uitleg] if uitleg else [])
            ])
            quiz_slides.append(slide)
        if quiz_slides:
            sectie_slides.append(("Quiz", quiz_slides[0]))

    # 6) referenties
    s = S[JVG_REF]
    kop = slide_title_box(s)
    set_box_text(kop, "Referenties & Compliance")
    vlak = [sh for sh in s.shapes if sh.has_text_frame and sh.shape_type == 17
            and sh.top > Inches(2.0) and sh.text_frame.text.strip() not in ("■",)]
    vlak.sort(key=lambda x: x.top)
    # bronnen verdelen over beschikbare boxen (i.p.v. alles in één kleine box)
    per_box = math.ceil(len(bronnen) / max(1, len(vlak))) if bronnen else 0
    for bi, box in enumerate(vlak):
        stuk = bronnen[bi * per_box:(bi + 1) * per_box]
        if stuk:
            set_box_lines(box, stuk)
        else:
            rm(box)
    sectie_slides.append(("Referenties", s))

    # 7) kernpunten (kop+body paren)
    s = S[JVG_KERN]
    kop = slide_title_box(s)
    set_box_text(kop, f"Samenvatting: de {min(len(kernpunten), 4)} kernregels")
    vlak = [sh for sh in s.shapes if sh.has_text_frame and sh.shape_type == 17
            and sh.top > Inches(2.2) and sh.text_frame.text.strip() not in ("■",)]
    # per kolom sorteren (links→rechts, binnen kolom boven→onder) zodat
    # kop/body-paren kloppen: [kop1, body1, kop2, body2, ...]
    vlak.sort(key=lambda x: (round(x.left / 914400), x.top))
    for i, sh in enumerate(vlak):
        k = i // 2
        if k >= len(kernpunten):
            rm(sh)
        elif i % 2 == 0:
            set_box_text(sh, f"{k + 1}. Kernpunt")
        else:
            set_box_text(sh, kernpunten[k])
    sectie_slides.append(("Samenvatting", s))

    # 8) afsluiting
    s = S[JVG_AFS]
    for sh in s.shapes:
        if not sh.has_text_frame:
            continue
        t = sh.text_frame.text.strip()
        if t.startswith("Veiligheid is geen optie"):
            if kind == "toolbox" and quiz:
                set_box_text(sh, f"Discussie: {quiz[0]['vraag']}")
        elif t.startswith("Neem contact op"):
            set_box_text(sh, "Vragen? Neem contact op met uw HSEQ-afdeling of "
                             "leidinggevende.")

    # 9) intro/agenda + jumps
    s = S[JVG_INTRO]
    set_box_text(slide_title_box(s), "Introductie, Doelstelling en Agenda")
    links = [sh for sh in s.shapes if sh.has_text_frame and sh.shape_type == 17
             and sh.left < Inches(6.5) and sh.top > Inches(2.5)
             and sh.text_frame.text.strip() not in ("■",)]
    links.sort(key=lambda x: x.top)
    if links:
        set_box_text(links[0], "Doelstelling")
        if len(links) > 1:
            set_box_lines(links[1], leerdoelen[:3] or [data.get("ondertitel", "")])
        for extra in links[2:]:
            rm(extra)
    agenda = [sh for sh in s.shapes if sh.has_text_frame and sh.shape_type == 17
              and sh.left > Inches(7.0) and sh.top > Inches(2.4)
              and sh.text_frame.text.strip() not in ("■",)]
    agenda.sort(key=lambda x: x.top)
    bulletes = [sh for sh in s.shapes if sh.has_text_frame
                and sh.text_frame.text.strip() == "■" and sh.left > Inches(7.0)]
    bulletes.sort(key=lambda x: x.top)
    for i, sh in enumerate(agenda):
        if i < len(sectie_slides):
            naam, target = sectie_slides[i]
            set_box_text(sh, naam)
            add_jump(sh, s, target)
        else:
            rm(sh)
            if i < len(bulletes):
                rm(bulletes[i])

    # 11) opruimen + gewenste volgorde
    for i in weg:
        delete_slide(prs, S[i])
    volgorde = [S[JVG_TITEL], S[JVG_INTRO]] + mod_slide_objs
    if maat_mod:
        volgorde.append(S[JVG_MAAT])
    volgorde += quiz_slides + [S[JVG_REF], S[JVG_KERN], S[JVG_AFS]]
    aanwezig = {sl.part for sl in volgorde}
    volgorde += [sl for sl in prs.slides if sl.part not in aanwezig]
    reorder_slides(prs, volgorde)

    # paginanummering n/N (titelslide exempt)
    total = len(prs.slides)
    for i, sl in enumerate(prs.slides, 1):
        if sl is not S[JVG_TITEL]:
            add_pagenum(sl, i, total)

    out = Path(out_path)
    out.parent.mkdir(parents=True, exist_ok=True)
    prs.save(str(out))
    return out, kind, total


# ----------------------------------------------------------- WINTERSHALL ---

def _ws_apply(slide, title_txt, sub_txt, bullet_txts):
    """Wintershall content-vulling (classificeer op geometrie, zie master-build)."""
    title = sub = None
    bullets = []
    for sh in slide.shapes:
        if not sh.has_text_frame or sh.shape_type != 17:
            continue
        if sh.width is None or sh.height is None:
            continue
        w_in, l_in, t_in = sh.width / 914400, sh.left / 914400, sh.top / 914400
        txt = sh.text_frame.text.strip()
        if not txt and w_in < 3:
            continue
        if w_in < 1.0 and l_in < 0.4:      # sidebar-labels
            continue
        if t_in < 0.7 and w_in >= 3:
            title = sh
        elif 0.7 <= t_in < 1.15 and w_in >= 3:
            sub = sh
        elif t_in >= 1.15 and w_in >= 3 and l_in >= 0.7:
            bullets.append(sh)
    bullets.sort(key=lambda s: (s.top, s.left))
    if title is not None and title_txt is not None:
        set_box_text(title, title_txt)
    if sub is not None and sub_txt is not None:
        set_box_text(sub, sub_txt)
    for i, sh in enumerate(bullets):
        if i < len(bullet_txts):
            if isinstance(bullet_txts[i], (list, tuple)):
                set_box_lines(sh, bullet_txts[i])
            else:
                set_box_text(sh, bullet_txts[i])
        else:
            rm(sh)


def _ws_add_textbox(slide, x, y, w, h, text, size, color, bold=False):
    box = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
    tf = box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    r = p.add_run()
    r.text = text
    r.font.name = WS_FONT
    r.font.size = Pt(size)
    r.font.bold = bold
    r.font.color.rgb = RGBColor.from_string(color)
    return box


def build_wintershall(data, out_path):
    kind = detect_kind(data)
    prs = Presentation(str(PROJ / "formats/WINTERSHALL_MASTER_FORMAT.pptx"))
    S = list(prs.slides)  # 10: titel menu leerdoelen kaarten wetgeving
    #                        piramide tabel rasci quiz kernpunten
    T, MENU, LEER, KAART, WET, PIR, TAB, RASCI, QUIZ, KERN = range(10)

    modules = data.get("modules", [])
    quiz = data.get("quiz", [])
    bronnen = data.get("bronnenlijst", [])
    kernpunten = data.get("kernpunten", [])
    leerdoelen = data.get("leerdoelen", [])

    # type-adaptatie: slides weg of behouden
    # (KAART valt altijd weg: 2-modules-gepropt is vervangen door 1 module/slide
    #  volgens het goedgekeurde Stof-deck-patroon)
    weg = [KAART]
    if kind == "toolbox":
        weg += [LEER, PIR, TAB, RASCI, QUIZ]
    elif kind == "awareness":
        weg += [PIR, TAB, RASCI]
    else:
        weg += [RASCI]  # geen RASCI-content in JSON-structuur
    # NB: deleties pas op het EIND (partname-botsingen voorkomen)

    # 1) titel
    s = S[T]
    for sh in s.shapes:
        if sh.has_text_frame:
            if sh.is_placeholder:
                set_box_text(sh, data.get("titel", "Training"))
            elif "Ondertitel" in sh.text_frame.text:
                ws_label = {"toolbox": "Toolbox Talk",
                            "awareness": "Awareness Training",
                            "course": "Course"}[kind]
                set_box_text(sh, f"{ws_label} | {data.get('duur', '')}"
                                 f" — {data.get('ondertitel', '')}")

    # 2) moduleslides: ÉÉN module per slide — patroon van het goedgekeurde
    #    Stof-deck (titel + subtitel + max 6 volle-breedte bullets), gebouwd
    #    op het WET-template (vóórdat WET zelf gevuld wordt)
    mod_slides = []
    for mi, mod in enumerate(modules):
        slide = dup_slide(prs, S[WET])
        blts = mod.get("bullets", [])
        bl = blts[:5]
        rest = blts[5:]
        if rest:
            bl.append(rest)  # overschot als extra regels in de laatste box
        _ws_apply(slide, mod["titel"],
                  f"Module {mi + 1} van {len(modules)}", bl)
        mod_slides.append(slide)

    # 3) leerdoelen
    if LEER not in weg:
        _ws_apply(S[LEER], "Leerdoelen", "Wat kan de deelnemer na deze training?",
                  leerdoelen)

    # 4) wetgeving/bronnen
    if WET not in weg:
        _ws_apply(S[WET], "Wetgeving & bronnen", "Relevant kader en referenties",
                  bronnen)

    # 5) piramide (course): opbouw = module-titels
    if PIR not in weg and modules:
        s = S[PIR]
        _ws_apply(s, "Opbouw van de training", None, [])
        trapz = sorted([sh for sh in s.shapes if sh.shape_type == 1
                        and sh.has_text_frame
                        and sh.text_frame.text.strip()], key=lambda x: x.top)
        for i, sh in enumerate(trapz):
            paras = sh.text_frame.paragraphs
            naam = modules[min(i, len(modules) - 1)]["titel"] if i > 0 else \
                "Kernpunten"
            if paras and paras[0].runs:
                paras[0].runs[0].text = f"{len(trapz) - i}. {naam}"
                for r in paras[0].runs[1:]:
                    r._r.getparent().remove(r._r)
            if len(paras) > 1 and paras[1].runs:
                paras[1].runs[0].text = \
                    modules[min(i, len(modules) - 1)].get("bullets", [""])[0][:80] \
                    if i > 0 else kernpunten[0][:80] if kernpunten else ""
                for r in paras[1].runs[1:]:
                    r._r.getparent().remove(r._r)

    # 6) tabel (course): uit module['tabel']
    if TAB not in weg:
        tab_mod = next((m for m in modules if m.get("tabel")), None)
        if tab_mod:
            s = S[TAB]
            _ws_apply(s, f"Tabel: {tab_mod['titel']}", None, [])
            oude = [sh for sh in s.shapes if sh.has_table]
            for sh in oude:
                rm(sh)
            tbl_data = tab_mod["tabel"]
            if isinstance(tbl_data, dict):
                headers = tbl_data.get("headers", [])
                rows = tbl_data.get("rows", [])
            else:
                headers, rows = tbl_data[0], tbl_data[1:]
            grafiek = s.shapes.add_table(len(rows) + 1, len(headers),
                                         Inches(1.0), Inches(1.6),
                                         Inches(8.4), Inches(0.4 * (len(rows) + 1)))
            tbl = grafiek.table
            for c, htxt in enumerate(headers):
                cell = tbl.cell(0, c)
                cell.fill.solid()
                cell.fill.fore_color.rgb = RGBColor.from_string(WS_PRIMARY)
                cell.text = str(htxt)
                for p in cell.text_frame.paragraphs:
                    for r in p.runs:
                        r.font.name = WS_FONT
                        r.font.size = Pt(12)
                        r.font.bold = True
                        r.font.color.rgb = RGBColor.from_string("FFFFFF")
            for ri, row in enumerate(rows, 1):
                for ci, val in enumerate(row):
                    cell = tbl.cell(ri, ci)
                    cell.fill.solid()
                    cell.fill.fore_color.rgb = RGBColor.from_string(
                        "FFFFFF" if ri % 2 else WS_PANEL)
                    cell.text = str(val)
                    for p in cell.text_frame.paragraphs:
                        for r in p.runs:
                            r.font.name = WS_FONT
                            r.font.size = Pt(11)
                            r.font.color.rgb = RGBColor.from_string(WS_BODY)
        else:
            weg.append(TAB)

    # 7) quiz
    quiz_slides = []
    if QUIZ not in weg and quiz:
        if kind == "awareness":
            _ws_apply(S[QUIZ], "Quiz", "Test je kennis", [
                [f"{q['vraag']}  ✓ {q['antwoorden'][q['correct']]}"]
                for q in quiz[:4]])
            quiz_slides.append(S[QUIZ])
        else:
            for qi, q in enumerate(quiz):
                slide = S[QUIZ] if qi == 0 else dup_slide(prs, S[QUIZ])
                opties = [f"{chr(65 + i)}) {a}" for i, a in
                          enumerate(q.get("antwoorden", []))]
                juist = q.get("antwoorden", [""])[q.get("correct", 0)]
                _ws_apply(slide, f"Quiz ({qi + 1}/{len(quiz)})",
                          "Kies het juiste antwoord",
                          [q["vraag"]] + opties + [f"✓ Juist: {juist}"])
                quiz_slides.append(slide)
    # quiz-slides: zwevende content uit de originele stof-master opruimen
    # (✓-lijst rechts: FFP3 filter, Handschoenen, enz.)
    for sl in quiz_slides:
        for sh in list(sl.shapes):
            if sh.has_text_frame and sh.left is not None \
                    and sh.left > Inches(7.3) and sh.width < Inches(3):
                rm(sh)

    # 8) kernpunten
    _ws_apply(S[KERN], "Kernpunten — samenvatting", None,
              [f"✓ {k}" for k in kernpunten])

    # 9) menu + sidebar-jumps herbestemmen
    s = S[MENU]
    secties = [("Welkom", S[MENU])]
    if LEER not in weg:
        secties.append(("Leerdoelen", S[LEER]))
    if mod_slides:
        secties.append(("Content", mod_slides[0]))
    if WET not in weg:
        secties.append(("Wetgeving", S[WET]))
    if PIR not in weg:
        secties.append(("Opbouw", S[PIR]))
    if TAB not in weg and any(m.get("tabel") for m in modules):
        secties.append(("Tabel", S[TAB]))
    if quiz_slides:
        secties.append(("Quiz", quiz_slides[0]))
    secties.append(("Kernpunten", S[KERN]))

    ws_label2 = {"toolbox": "Toolbox Talk", "awareness": "Awareness Training",
                 "course": "Course"}[kind]
    agenda = [f"{ws_label2} — duur {data.get('duur', '')}"] + \
             [m["titel"] for m in modules]
    if len(agenda) > 4:
        agenda = agenda[:3] + [agenda[3:]]
    _ws_apply(s, "Welkom bij de training", "Menu | Kies een sectie", agenda)

    # sidebar-nav: geometrisch detecteren (w<1in, links), oude jumps strippen,
    # labels herbestemmen + nieuwe jumps zetten
    for sl in prs.slides:
        strip_old_jumps(sl)
    # mapping slide → actieve sectie-index (voor oranje indicator-blokje)
    sectie_index = {}
    for si_, (naam_, tgt_) in enumerate(secties):
        sectie_index[tgt_.part] = si_
    ci_ = next((i for i, (n, _) in enumerate(secties) if n == "Content"), None)
    for sl in mod_slides:
        sectie_index.setdefault(sl.part, ci_)
    qi_ = next((i for i, (n, _) in enumerate(secties) if n == "Quiz"), None)
    for sl in quiz_slides:
        sectie_index.setdefault(sl.part, qi_)
    for sl in prs.slides:
        navs = []
        for sh in sl.shapes:
            if not sh.has_text_frame or sh.width is None or sh.left is None:
                continue
            if sh.width < Inches(1.0) and sh.left < Inches(0.4) \
                    and sh.text_frame.text.strip():
                navs.append(sh)
        navs.sort(key=lambda x: x.top)
        # indicator: smal AUTO_SHAPE-blokje links (oranje accent)
        ind = next((sh for sh in sl.shapes
                    if sh.shape_type == MSO_SHAPE_TYPE.AUTO_SHAPE
                    and sh.width is not None and 0 < sh.width < Inches(0.15)
                    and sh.height is not None
                    and Inches(0.15) < sh.height < Inches(0.7)
                    and (sh.left or 0) < Inches(0.3)), None)
        if not navs:
            if ind is not None:
                rm(ind)
            continue
        for i, sh in enumerate(navs):
            if i < len(secties):
                naam, target = secties[i]
                set_box_text(sh, naam)
                add_jump(sh, sl, target)
            else:
                rm(sh)
        # oranje blokje naast het nav-item van de actieve sectie plaatsen
        aidx = sectie_index.get(sl.part)
        if aidx is not None and aidx < len(navs):
            if ind is None:
                # master mist een indicator (bijv. kernpunten-slide): oranje
                # accent-blokje aanmaken in Wintershall-huisstijl
                ind = sl.shapes.add_shape(
                    MSO_SHAPE.RECTANGLE, Inches(0), navs[aidx].top,
                    Inches(0.05), Inches(0.35))
                ind.fill.solid()
                ind.fill.fore_color.rgb = RGBColor.from_string("FF9900")
                ind.line.fill.background()
            ind.top = navs[aidx].top
            ind.left = max(0, navs[aidx].left - ind.width)
        elif ind is not None:
            rm(ind)

    # opruimen + gewenste volgorde
    for i in weg:
        delete_slide(prs, S[i])
    volgorde = [S[T], S[MENU]]
    if LEER not in weg:
        volgorde.append(S[LEER])
    volgorde += mod_slides
    if WET not in weg:
        volgorde.append(S[WET])
    if PIR not in weg:
        volgorde.append(S[PIR])
    if TAB not in weg and S[TAB].part in {sl.part for sl in prs.slides}:
        volgorde.append(S[TAB])
    volgorde += quiz_slides + [S[KERN]]
    aanwezig = {sl.part for sl in volgorde}
    volgorde += [sl for sl in prs.slides if sl.part not in aanwezig]
    reorder_slides(prs, volgorde)

    # paginanummering n/N, alleen paginanummer (WINTERSHALL-REGEL: 0x JvG)
    total = len(prs.slides)
    for i, sl in enumerate(prs.slides, 1):
        if sl is not S[T]:
            add_pagenum(sl, i, total, color=WS_PRIMARY)

    out = Path(out_path)
    out.parent.mkdir(parents=True, exist_ok=True)
    prs.save(str(out))
    return out, kind, total
