#!/usr/bin/env python3
"""
generate_presentatie.py — Presentatie_Voorjaarsconventie_2027_v1.0.pptx
=======================================================================
Project : 2026-voorjaarsconventie-110an
District: 110AN | Conventie: April 2027 | Thema 2026-2027: "Samen verder bouwen!"

BRON: meta-dossier_v1.json (22 bronnen; leidend: Handboek Lions Conventies vs06,
Conventie Roulatieschema, Huishoudelijk Reglement MD 110 mei 2021,
Werkplan AN 2026-2027 draft v1, Districtbegroting 110 AN 25-26,
agenda-110AN-2026-2027.json, Verslag Najaarsconventie 17-10-2024)

CONTROLESET (TierVerify §17 — moet consistent terugkomen in elke deliverable):
  - District      : 110AN
  - Conventietype : Voorjaarsconventie (District Convention)
  - Datum         : April 2027 (exacte datum t.b.v. roulatie)
  - Locatie       : Te bepalen op basis van roulatie (landelijke voorjaarsconventie
                    2026-2027 staat volgens roulatieschema GR 6-5-2022 op naam 110AZ)
  - Governance    : Huishoudelijk Reglement MD 110 mei 2021
  - Stemrechten   : Per club 1 stem per 10 leden (min. 1, max. 5)
  - Quorum        : Meeste artikelen >50% van de stemgerechtigden
  - Voorzitter    : District Governor 2026-2027
  - Secretaris    : Cabinet Secretary 2026-2027

SPECIFICATIES:
  - python-pptx; corporate master indien aanwezig (fallback: leeg 16:9 deck)
  - JvG logo: 754x676 px (ratio 1.115:1) -> 1.5" x 1.35", rechtsboven op elke slide
  - 10 slides: Titel, Agenda, Doel & Scope, Governance & Stemrechten,
    Programma & Sprekers, Locatie & Logistiek, Budget Overzicht,
    Risico's & Maatregelen, Communicatieplan, Volgende Stappen & Actiepunten

GEBRUIK: python3 generate_presentatie.py
"""

import os
import sys
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE

# =====================================================================
# CONFIGURATIE
# =====================================================================
BASE = "/root/projects/jg/2026-voorjaarsconventie-110an"
LOGO_PATH = os.path.join(BASE, "assets", "logo_jvg.png")          # 754x676 px, ratio 1.115:1
CORPORATE_MASTER = os.path.join(BASE, "assets", "corporate_master.pptx")
OUTPUT_PATH = os.path.join(BASE, "working",
                           "Presentatie_Voorjaarsconventie_2027_v1.0.pptx")

# JvG logo: 1.5" breed x 1.35" hoog (754/676 = 1.115; 1.5/1.35 = 1.111 — OK)
LOGO_W = Inches(1.5)
LOGO_H = Inches(1.35)
LOGO_TOP = Inches(0.22)
LOGO_RIGHT_MARGIN = Inches(0.25)

# 16:9 geometrie
SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)
MARGIN_L = Inches(0.55)
CONTENT_W = Inches(12.23)

# Corporate huisstijl (JvG / Lions blauw-groen)
C_PRIMARY = RGBColor(0x1F, 0x3A, 0x63)    # donker corporate blauw
C_ACCENT = RGBColor(0x00, 0x6A, 0x4E)     # Lions groen accent
C_TEXT = RGBColor(0x21, 0x21, 0x21)       # bijna-zwart
C_MUTED = RGBColor(0x5A, 0x64, 0x73)      # grijs
C_WHITE = RGBColor(0xFF, 0xFF, 0xFF)
C_BG_TITLE = RGBColor(0x1F, 0x3A, 0x63)   # titelslide: volledig blauw
C_BAR = RGBColor(0x00, 0x6A, 0x4E)        # accentbalk onder titel
C_LIGHT = RGBColor(0xEF, 0xF3, 0xF8)      # licht tabelvlak
C_FOOT_LIGHT = RGBColor(0xC9, 0xD4, 0xE3) # voettekst op donkere bg

FONT = "Calibri"
FONT_TITLE = "Calibri"

VERSION_TAG = "v1.0 · Working draft · Status: ter bespreking"
DOC_TITLE = "Presentatie_Voorjaarsconventie_2027_v1.0.pptx"

# =====================================================================
# CONTROLESET (TierVerify §17 — bron: meta-dossier sources.md)
# =====================================================================
CT_DISTRICT = "110AN"
CT_DATUM = "April 2027"
CT_STEMRECHTEN = "1 stem per 10 leden (min. 1, max. 5)"
CT_QUORUM = ">50% van de stemgerechtigden"
CT_REGELGEVING = "Huishoudelijk Reglement MD 110 (mei 2021)"
CT_VOORZITTER = "District Governor 2026-2027"
CT_SECRETARIS = "Cabinet Secretary 2026-2027"
CT_THEMA = "Samen verder bouwen!"

# =====================================================================
# HELPERS
# =====================================================================


def new_presentation() -> Presentation:
    """Laad corporate master indien aanwezig; anders leeg 16:9 deck."""
    if os.path.exists(CORPORATE_MASTER):
        prs = Presentation(CORPORATE_MASTER)
    else:
        prs = Presentation()
    prs.slide_width = SLIDE_W
    prs.slide_height = SLIDE_H
    return prs


def blank_slide(prs: Presentation):
    """Lege slide (layout 6) — volledige opmaakcontrole, master-onafhankelijk."""
    return prs.slides.add_slide(prs.slide_layouts[6])


def add_logo(slide):
    """JvG logo (754x676 px, ratio 1.115:1) rechtsboven: exact 1.5" x 1.35".
    Fallback: gemarkeerde placeholder als asset nog niet is aangeleverd."""
    left = SLIDE_W - LOGO_W - LOGO_RIGHT_MARGIN
    if os.path.exists(LOGO_PATH):
        slide.shapes.add_picture(LOGO_PATH, left, LOGO_TOP,
                                 width=LOGO_W, height=LOGO_H)
    else:
        box = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, LOGO_TOP,
                                     LOGO_W, LOGO_H)
        box.fill.solid()
        box.fill.fore_color.rgb = C_LIGHT
        box.line.color.rgb = C_PRIMARY
        tf = box.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = "JvG"
        p.alignment = PP_ALIGN.CENTER
        for run in p.runs:
            run.font.size = Pt(12)
            run.font.color.rgb = C_MUTED
            run.font.name = FONT
        p2 = tf.add_paragraph()
        p2.text = "[logo 754x676]"
        p2.alignment = PP_ALIGN.CENTER
        for run in p2.runs:
            run.font.size = Pt(8)
            run.font.color.rgb = C_MUTED
            run.font.name = FONT


def add_footer(slide, page_no: int, total: int, light: bool = False):
    """Voettekst: documentnaam, versie, controleset-regel, paginanummer."""
    box = slide.shapes.add_textbox(Inches(0.55), Inches(7.08),
                                   Inches(12.23), Inches(0.32))
    tf = box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.text = (f"{DOC_TITLE}  ·  {VERSION_TAG}  ·  District {CT_DISTRICT} · "
              f"{CT_DATUM}  ·  Slide {page_no}/{total}")
    p.alignment = PP_ALIGN.LEFT
    for run in p.runs:
        run.font.size = Pt(9)
        run.font.color.rgb = C_FOOT_LIGHT if light else C_MUTED
        run.font.name = FONT


def add_title_bar(slide, title: str, subtitle: str = ""):
    """Standaard content-slide: titel, accentbalk, optionele subtitel."""
    box = slide.shapes.add_textbox(Inches(0.55), Inches(0.30),
                                   Inches(10.8), Inches(0.75))
    tf = box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.text = title
    for run in p.runs:
        run.font.size = Pt(30)
        run.font.bold = True
        run.font.color.rgb = C_PRIMARY
        run.font.name = FONT_TITLE
    bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.55), Inches(1.04),
                                 Inches(2.2), Inches(0.06))
    bar.fill.solid()
    bar.fill.fore_color.rgb = C_BAR
    bar.line.fill.background()
    if subtitle:
        sub = slide.shapes.add_textbox(Inches(0.55), Inches(1.14),
                                       Inches(11.0), Inches(0.4))
        tf = sub.text_frame
        tf.word_wrap = True
        p = tf.paragraphs[0]
        p.text = subtitle
        for run in p.runs:
            run.font.size = Pt(13)
            run.font.italic = True
            run.font.color.rgb = C_MUTED
            run.font.name = FONT


def add_bullets(slide, items, top=Inches(1.70), height=Inches(5.25),
                width=None, left=MARGIN_L, size=16):
    """Bullets. Elke item: string, of tuple (tekst, level, opts).
    opts: bold (bool), color (RGBColor), size (Pt), space_after (Pt)."""
    width = width or CONTENT_W
    box = slide.shapes.add_textbox(left, top, width, height)
    tf = box.text_frame
    tf.word_wrap = True
    tf.vertical_anchor = MSO_ANCHOR.TOP
    first = True
    for item in items:
        if isinstance(item, tuple):
            text = item[0]
            level = item[1] if len(item) > 1 else 0
            opts = item[2] if len(item) > 2 else {}
        else:
            text, level, opts = item, 0, {}
        p = tf.paragraphs[0] if first else tf.add_paragraph()
        first = False
        if not text:
            p.text = ""
            continue
        bullet_char = {0: "▪  ", 1: "–  ", 2: "·  "}.get(level, "▪  ")
        p.text = f"{bullet_char}{text}"
        p.level = min(level, 4)
        p.space_after = opts.get("space_after", Pt(5))
        fsize = opts.get("size", Pt(size - 2 * level))
        for run in p.runs:
            run.font.size = fsize
            run.font.bold = opts.get("bold", False)
            run.font.color.rgb = opts.get("color", C_TEXT)
            run.font.name = FONT
    return box


def add_table(slide, rows, top=Inches(2.0), left=MARGIN_L, width=CONTENT_W,
              height=None, col_widths=None, header=True, font_size=13):
    """Gestylde tabel; rij 0 = koptekst."""
    nrows, ncols = len(rows), len(rows[0])
    height = height or Inches(0.36 * nrows)
    shape = slide.shapes.add_table(nrows, ncols, left, top, width, height)
    table = shape.table
    if col_widths:
        total = float(sum(col_widths))
        for i, w in enumerate(col_widths):
            table.columns[i].width = Emu(int(width * (w / total)))
    for r, row in enumerate(rows):
        for c, val in enumerate(row):
            cell = table.cell(r, c)
            cell.text = str(val)
            cell.margin_left = Inches(0.08)
            cell.margin_right = Inches(0.08)
            cell.margin_top = Inches(0.02)
            cell.margin_bottom = Inches(0.02)
            cell.vertical_anchor = MSO_ANCHOR.MIDDLE
            for p in cell.text_frame.paragraphs:
                for run in p.runs:
                    run.font.size = Pt(font_size)
                    run.font.name = FONT
                    if r == 0 and header:
                        run.font.bold = True
                        run.font.color.rgb = C_WHITE
                    else:
                        run.font.color.rgb = C_TEXT
            if r == 0 and header:
                cell.fill.solid()
                cell.fill.fore_color.rgb = C_PRIMARY
            elif r % 2 == 0:
                cell.fill.solid()
                cell.fill.fore_color.rgb = C_LIGHT
    return table


def add_note(slide, text, top=Inches(6.45), width=None, left=MARGIN_L):
    """Bron-/opmerkingsregel onder de inhoud (blijft boven voettekst op 7.08")."""
    box = slide.shapes.add_textbox(left, top, width or CONTENT_W, Inches(0.35))
    tf = box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.text = text
    for run in p.runs:
        run.font.size = Pt(10)
        run.font.italic = True
        run.font.color.rgb = C_MUTED
        run.font.name = FONT


# =====================================================================
# SLIDES
# =====================================================================


def slide_01_titel(prs, total):
    """1) Titel — Conventie 110AN, April 2027."""
    s = blank_slide(prs)
    bg = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_W, SLIDE_H)
    bg.fill.solid()
    bg.fill.fore_color.rgb = C_BG_TITLE
    bg.line.fill.background()

    add_logo(s)
    add_footer(s, 1, total, light=True)

    box = s.shapes.add_textbox(Inches(0.9), Inches(2.05), Inches(11.5), Inches(3.6))
    tf = box.text_frame
    tf.word_wrap = True

    p = tf.paragraphs[0]
    p.text = "Voorjaarsconventie 2027"
    p.alignment = PP_ALIGN.CENTER
    p.space_after = Pt(10)
    for run in p.runs:
        run.font.size = Pt(50)
        run.font.bold = True
        run.font.color.rgb = C_WHITE
        run.font.name = FONT_TITLE

    p = tf.add_paragraph()
    p.text = f"District {CT_DISTRICT}  |  {CT_DATUM}"
    p.alignment = PP_ALIGN.CENTER
    p.space_after = Pt(6)
    for run in p.runs:
        run.font.size = Pt(26)
        run.font.color.rgb = C_WHITE
        run.font.name = FONT

    p = tf.add_paragraph()
    p.text = f'Thema Lions-jaar 2026-2027: "{CT_THEMA}"'
    p.alignment = PP_ALIGN.CENTER
    p.space_after = Pt(6)
    for run in p.runs:
        run.font.size = Pt(16)
        run.font.italic = True
        run.font.color.rgb = C_FOOT_LIGHT
        run.font.name = FONT

    p = tf.add_paragraph()
    p.text = (f"Voorzitter: {CT_VOORZITTER}  ·  Secretaris: {CT_SECRETARIS}\n"
              f"Regelgeving: {CT_REGELGEVING}")
    p.alignment = PP_ALIGN.CENTER
    for run in p.runs:
        run.font.size = Pt(12)
        run.font.color.rgb = C_FOOT_LIGHT
        run.font.name = FONT

    # Accentbalk boven titelblok
    bar = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(5.42), Inches(1.92),
                             Inches(2.5), Inches(0.07))
    bar.fill.solid()
    bar.fill.fore_color.rgb = C_ACCENT
    bar.line.fill.background()


def slide_02_agenda(prs, total):
    """2) Agenda — opbouw van deze presentatie."""
    s = blank_slide(prs)
    add_logo(s)
    add_footer(s, 2, total)
    add_title_bar(s, "Agenda",
                  "Opbouw van deze presentatie — voorbereiding Voorjaarsconventie 2027")
    add_bullets(s, [
        ("1.  Titel & introductie — Conventie 110AN, April 2027", 0, {"bold": True}),
        ("2.  Doel & Scope — statutaire doelstellingen en afbakening", 0, {"bold": True}),
        ("3.  Governance & Stemrechten", 0, {"bold": True}),
        (f"Stemrecht per club: {CT_STEMRECHTEN}; quorum {CT_QUORUM} "
         f"(HR MD 110 mei 2021)", 1),
        ("4.  Programma & Sprekers", 0, {"bold": True}),
        ("5.  Locatie & Logistiek — locatie te bepalen op basis van roulatie", 0, {"bold": True}),
        ("6.  Budget Overzicht — referentie: Districtsbegroting (conventiepost € 3.500)", 0, {"bold": True}),
        ("7.  Risico's & Maatregelen", 0, {"bold": True}),
        ("8.  Communicatieplan — oproeping minimaal 30 dagen vooraf", 0, {"bold": True}),
        ("9.  Volgende Stappen & Actiepunten", 0, {"bold": True}),
    ], top=Inches(1.75), size=17)
    add_note(s, "Volgorde volgt de goedgekeurde deliverable-opbouw uit het Projectplan "
                "PVA v1.0; spreker: projectleider PVA.")


def slide_03_doel_scope(prs, total):
    """3) Doel & Scope."""
    s = blank_slide(prs)
    add_logo(s)
    add_footer(s, 3, total)
    add_title_bar(s, "Doel & Scope",
                  "Wat de Voorjaarsconventie 2027 moet waarmaken — District 110AN")
    add_bullets(s, [
        ("Doel", 0, {"bold": True, "color": C_PRIMARY}),
        ("Statutaire districtsvergadering: besluitvorming over werkplan, "
         "begroting en jaarrekening district 110AN", 1),
        ("Verkiezingen/benoemingen die volgens LCI-statuten en HR MD 110 "
         "aan de conventie zijn voorbehouden", 1),
        ("Deelname van alle Lions-clubs in good standing; verbinding en "
         "inspiratie rond het thema \"Samen verder bouwen!\"", 1),
        ("Scope", 0, {"bold": True, "color": C_PRIMARY}),
        ("Bestuurlijk/juridisch: agendapunten conform oproeping gouverneursraad "
         "(HR art. 4: voorstellen GR, clubs, kandidaat-DG's en PDG's)", 1),
        ("Financieel: districtsconventie valt onder post \"1 Districtsconventie\" "
         "in de Districtsbegroting", 1),
        ("Buiten scope: Nationale Voorjaarsconventie (29 mei 2027, organisatie "
         "landelijk/gastheer-district) en Kabinetsvergaderingen", 1),
        ("Randvoorwaarden", 0, {"bold": True, "color": C_PRIMARY}),
        ("Clubs in good standing; aanmelding stemgerechtigden uiterlijk 48 uur "
         "vóór aanvang (HR art. 2)", 1),
    ], size=15)
    add_note(s, "Bron: Huishoudelijk Reglement MD 110 mei 2021 (art. 2-4); "
                "Handboek MD 110 Lions Conventies vs06; meta-dossier sources.md.")


def slide_04_governance(prs, total):
    """4) Governance & Stemrechten — incl. controleset-kern in tabel."""
    s = blank_slide(prs)
    add_logo(s)
    add_footer(s, 4, total)
    add_title_bar(s, "Governance & Stemrechten",
                  f"Kader: {CT_REGELGEVING} — Conventiereglement MD 110")
    add_table(s, [
        ["Regel", "Bepaling", "Bron"],
        ["Stemrecht per club", CT_STEMRECHTEN, "HR MD 110 / LCI-statuten"],
        ["Quorum", CT_QUORUM + " (meeste artikelen)", "HR MD 110"],
        ["Oproeping", "Minimaal 30 dagen vooraf, incl. agenda + bijlagen", "HR art. 2.1"],
        ["Aanmelding", "Uiterlijk 48 uur vóór aanvang; club wijst stemgerechtigde "
         "uiterlijk 24 uur vóór aanvang", "HR art. 2.1-2.2"],
        ["Good standing", "Voorwaarde voor stemrecht (MD-statuten)", "HR art. 2.1"],
        ["Stemprocedure", "Stemkaarten voor zaken, stembiljetten voor personen; "
         "stembureau telt en verifieert quorum", "HR art. 3.2-3.5 / art. 16"],
        ["Voorstellen", "GR, elke club in good standing, kandidaat-DG's, PDG's "
         "(schriftelijk conceptbesluit)", "HR art. 4"],
        ["Voorzitter", CT_VOORZITTER, "Controleset / Werkplan 2026-2027"],
    ], top=Inches(1.75), col_widths=[2.2, 6.2, 3.0], font_size=12)
    add_bullets(s, [
        ("Referentiemeting najaarsconventie 17-10-2024: 66 aanwezigen, 20 clubs, "
         "42 stemmen — stembureau conform HR samengesteld.", 0, {"size": Pt(12)}),
    ], top=Inches(6.05), height=Inches(0.7))
    add_note(s, "Stemrecht: 1 stem per 10 leden of deel daarvan — minimaal 1, "
                "maximaal 5 stemmen per club; quorum voor besluitvorming >50%.", top=Inches(6.55))


def slide_05_programma(prs, total):
    """5) Programma & Sprekers — bouwstenen conform HR + najaarsconventie-formaat."""
    s = blank_slide(prs)
    add_logo(s)
    add_footer(s, 5, total)
    add_title_bar(s, "Programma & Sprekers",
                  "Bouwstenen — definitieve agenda volgt met de oproeping (≥30 dagen vooraf)")
    add_table(s, [
        ["Onderdeel", "Inhoud / spreker"],
        ["Registratie & opening", "Registratie stemgerechtigden vanaf 1 uur vóór opening; "
         "opening door de voorzitter (DG 2026-2027)"],
        ["Vaststelling verslag", "Goedkeuring verslag vorige districtsvergadering"],
        ["Jaarverslag & decharge", "Districtspenningmeester; advies kascommissie; "
         "decharge districtsbestuur"],
        ["Verkiezingen", "Conceptbesluiten stembureau; stembiljetten voor personen "
         "(kandidaten stellen zich voor)"],
        ["Werkplan & begroting", "Presentatie taakstellend werkplan + begroting; "
         "advies raad van advies (HR art. 5)"],
        ["Voorstellen & moties", "Behandeling voorstellen GR/clubs; eventuele herstemmingen"],
        ["Inspiratie & informeel", "Aansluitend programma; ontmoeting clubs — "
         "in lijn met thema \"Samen verder bouwen!\""],
    ], top=Inches(1.75), col_widths=[3.2, 8.5], font_size=12)
    add_note(s, "Format gebaseerd op Verslag Districtsconventie/LOA 17-10-2024 "
                "(Alkmaar) en Handboek MD 110 Lions Conventies vs06.", top=Inches(6.35))


def slide_06_locatie(prs, total):
    """6) Locatie & Logistiek."""
    s = blank_slide(prs)
    add_logo(s)
    add_footer(s, 6, total)
    add_title_bar(s, "Locatie & Logistiek",
                  "Locatie wordt bepaald op basis van het Conventie Roulatieschema")
    add_bullets(s, [
        ("Locatie", 0, {"bold": True, "color": C_PRIMARY}),
        ("Conventie Roulatieschema (vastgesteld GR 6-5-2022): landelijke "
         "voorjaarsconventie 2026-2027 op naam van 110AZ; 110AN is aan de beurt "
         "voor 2025-2026-cyclus geweest — exacte datum/locatie April 2027 nog "
         "definitief vast te stellen", 1),
        ("Kriteria locatie: zaalcapaciteit naar verwachte opkomst (referentie "
         "2024: ±66 personen), registratieruimte, AV, bereikbaarheid", 1),
        ("Logistiek", 0, {"bold": True, "color": C_PRIMARY}),
        ("Registratiebureau: open vanaf 1 uur vóór opening; uitreiking stemkaarten "
         "(zaken) en stembiljetten (personen)", 1),
        ("Stembureau: tijdig samenstellen + positie telling per stemblok; "
         "reservestembiljetten voor herstemmingen (GR zorgt hiervoor)", 1),
        ("Agenda 2026-2027 geeft rond maart 2027 reeds \"Voorjaarsconventie??\" "
         "(19-20 mrt) — datum April 2027 definitief vaststellen in GR", 1),
        ("Aansluitend op landelijke kalender: NVJC-upload 28 apr 2027, publicatie "
         "stukken Nationale voorjaarsconventie 29 apr 2027, Nationale "
         "Voorjaarsconventie 29 mei 2027", 1),
    ], size=14)
    add_note(s, "Bron: Conventie Roulatieschema (GR 6-5-2022); agenda-110AN-2026-2027.json; "
                "HR MD 110 art. 3; Handboek Lions Conventies vs06.")


def slide_07_budget(prs, total):
    """7) Budget Overzicht — referentie Districtsbegroting 110 AN."""
    s = blank_slide(prs)
    add_logo(s)
    add_footer(s, 7, total)
    add_title_bar(s, "Budget Overzicht",
                  "Kader: Districtsbegroting 110 AN (referentie 2025-2026)")
    add_table(s, [
        ["Post (begroting district)", "Bedrag (ref. 25-26)", "Toelichting"],
        ["1 Districtsconventie", "€ 3.500", "Directe referentiepost voor deze conventie"],
        ["1 Ledenontmoetings- en inspiratieavond", "€ 3.000", "Ter vergelijking; geen deel van scope"],
        ["1 Stamppotavond MJF/PDG", "€ 2.500", "Ter vergelijking"],
        ["Districtskosten totaal (post District)", "€ 9.750", "Conventie is grootste enkelvoudige post"],
        ["Uitgaven district totaal", "€ 29.500", "Tegenover inkomsten € 24.500 (saldo −€ 5.000)"],
    ], top=Inches(1.75), col_widths=[5.2, 2.6, 4.6], font_size=12)
    add_bullets(s, [
        ("Begroting 2026-2027 (Werkplan AN draft v1) ligt ter vaststelling — "
         "conventiebudget April 2027 daarin verankeren.", 0),
        ("Budgetbewaking: besteding conform besluit conventie; afwijkingen terug "
         "naar GR; jaarrapportage aan volgende conventie.", 0),
        ("Contributiebasis referentie: 1.200 leden × € 20 (begroting 25-26).", 0),
    ], top=Inches(4.85), size=13)
    add_note(s, "Bron: Districtbegroting 110 AN 25-26 (EUR-bedragen exact overgenomen); "
                "Werkplan en begroting AN 2026-2027 draft v1.")


def slide_08_risicos(prs, total):
    """8) Risico's & Maatregelen."""
    s = blank_slide(prs)
    add_logo(s)
    add_footer(s, 8, total)
    add_title_bar(s, "Risico's & Maatregelen",
                  "Top-risico's met beheersmaatregel en eigenaar")
    add_table(s, [
        ["Risico", "Maatregel", "Eigenaar"],
        ["Quorum niet gehaald (>50% stemgerechtigden vereist)",
         "Vroege oproeping (≥30 dagen), aanmeldtracking met 48-uursdrempel, "
         "herinneringen aan clubs (24-uurs aanwijzing)", "Cabinet Secretary"],
        ["Datum/locatie niet tijdig vastgesteld (roulatie)",
         "Datum April 2027 + locatie agenderen in GR; opties voorbereiden "
         "langs roulatieschema (110AZ-cyclus 2026-2027)", "Gouverneursraad"],
        ["Stemgerechtigden niet correct geregistreerd",
         "Stembureau + registratiebureau conform HR art. 3; telling direct "
         "na sluiting registratie aan voorzitter", "Stembureau"],
        ["Besluitvorming loopt uit / herstemmingen",
         "Strakke agenda, voorstellen tijdig rond (HR art. 4-5), "
         "reservestembiljetten beschikbaar", "Voorzitter (DG)"],
        ["Budgetoverschrijding",
         "Budgetplafond € 3.500 (ref. 25-26); aanbesteding catering/locatie; "
         "tussentijdse bestuursrapportage", "Districtspenningmeester"],
    ], top=Inches(1.75), col_widths=[3.6, 6.4, 2.3], font_size=11)
    add_note(s, "Kans x effect-score per risico in het Risicoregister (bijlage "
                "Projectplan PVA v1.0).", top=Inches(6.40))


def slide_09_comunicatie(prs, total):
    """9) Communicatieplan."""
    s = blank_slide(prs)
    add_logo(s)
    add_footer(s, 9, total)
    add_title_bar(s, "Communicatieplan",
                  "Van oproeping tot verslaglegging — conform HR MD 110 en districtskalender")
    add_table(s, [
        ["Moment", "Actie", "Kanaal / eigenaar"],
        ["T-6 maanden", "Definitieve datum + locatie vaststellen (GR); agenderen",
         "GR / Cabinet Secretary"],
        ["T-30 dagen (verplicht)", "Oproeping: agenda, bijlagen, registratietijd, "
         "good-standing- en 48-uursvermelding", "Gouverneursraad"],
        ["T-30 tot T-2", "Aanmeldingen monitoren; clubs herinneren aan aanwijzing "
         "stemgerechtigden (24-uurstermijn)", "Cabinet Secretary"],
        ["T-1 maand / periodiek", "Nieuwsbrief district (kalender geeft 29-3-2027 "
         "en 26-4-2027) + clubbezoeken DG", "DC-team"],
        ["T-7 dagen", "Schriftelijke adviezen raad van advies verspreiden "
         "(HR art. 5); laatste reminder", "GR"],
        ["Dag zelf", "Registratie vanaf 1 uur vóór opening; tellingen na "
         "sluiting registratie", "Registratiebureau"],
        ["Na afloop", "Verslag (minutes) binnen 2 weken; opvolging actiepunten; "
         "input NVJC-publicatie (29-4-2027)", "Districtssecretaris"],
    ], top=Inches(1.75), col_widths=[2.6, 6.0, 3.4], font_size=11)
    add_note(s, "Bron: HR MD 110 art. 2-3 en 5; agenda-110AN-2026-2027.json "
                "(nieuwsbriefdata, NVJC-upload 28-4-2027).", top=Inches(6.40))


def slide_10_volgende_stappen(prs, total):
    """10) Volgende Stappen & Actiepunten."""
    s = blank_slide(prs)
    add_logo(s)
    add_footer(s, 10, total)
    add_title_bar(s, "Volgende Stappen & Actiepunten",
                  "Naar definitieve oproeping Voorjaarsconventie 2027 — District 110AN")
    add_table(s, [
        ["#", "Actie", "Eigenaar", "Deadline"],
        ["1", "Definitieve datum April 2027 + locatie vaststellen (roulatie)",
         "Gouverneursraad", "Q3-2026"],
        ["2", "Conventiebudget verankeren in begroting 2026-2027 (€ 3.500 ref.)",
         "Districtspenningmeester", "Vaststelling begroting"],
        ["3", "Stembureau + registratiebureau samenstellen",
         "GR / Cabinet Secretary", "T-60 dagen"],
        ["4", "Oproeping met agenda + bijlagen versturen",
         "Gouverneursraad", "T-30 dagen (verplicht)"],
        ["5", "Aanmeld- en stemrechtentracking (48/24-uursdrempels)",
         "Cabinet Secretary", "Doorlopend"],
        ["6", "Voorstellen GR/clubs/kandidaat-DG's verzamelen",
         "GR + clubs", "T-30 dagen"],
        ["7", "Programma, sprekers en informeel gedeelte afronden",
         "Organisatiecommissie", "T-14 dagen"],
        ["8", "Nieuwsbrief-aandacht + kalender bijwerken (incl. NVJC 28/29-4-2027)",
         "DC-team", "29-3-2027 / 26-4-2027"],
    ], top=Inches(1.75), col_widths=[0.6, 5.6, 3.2, 2.8], font_size=11)
    add_bullets(s, [
        ("Besluit gevraagd: akkoord met aanpak, datum-voorstel April 2027 en "
         "opdracht aan GR voor de oproeping.", 0, {"bold": True}),
    ], top=Inches(6.30), height=Inches(0.6))
    add_note(s, "Contact: District Governor 2026-2027 (voorzitter) · "
                "Cabinet Secretary 2026-2027 (secretaris) — overeenkomstig controleset "
                "meta-dossier v1.", top=Inches(6.70))


# =====================================================================
# CONTROLESET-VALIDATIE (TierVerify §17)
# =====================================================================

def validate_controleset(prs) -> None:
    """Controleer dat de verplichte controleset-feiten in het deck voorkomen."""
    import re
    all_text = []
    for slide in prs.slides:
        for shape in slide.shapes:
            if shape.has_text_frame:
                all_text.append(shape.text_frame.text)
            if getattr(shape, "has_table", False) and shape.has_table:
                for row in shape.table.rows:
                    for cell in row.cells:
                        all_text.append(cell.text)
    blob = "\n".join(all_text)
    checks = {
        "District 110AN": CT_DISTRICT in blob,
        "April 2027": CT_DATUM in blob,
        "Stemrecht 1/10 (min 1, max 5)": ("1 stem per 10 leden" in blob
                                          and "min. 1, max. 5" in blob),
        "Quorum >50%": ">50%" in blob,
        "Regelgeving HR MD 110": "Huishoudelijk Reglement MD 110" in blob,
    }
    failed = [k for k, ok in checks.items() if not ok]
    for k, ok in checks.items():
        print(f"  {'PASS' if ok else 'FAIL'}  {k}")
    if failed:
        raise SystemExit(f"Controleset-validatie mislukt: {failed}")


# =====================================================================
# MAIN
# =====================================================================

def main():
    prs = new_presentation()
    total = 10
    slide_01_titel(prs, total)
    slide_02_agenda(prs, total)
    slide_03_doel_scope(prs, total)
    slide_04_governance(prs, total)
    slide_05_programma(prs, total)
    slide_06_locatie(prs, total)
    slide_07_budget(prs, total)
    slide_08_risicos(prs, total)
    slide_09_comunicatie(prs, total)
    slide_10_volgende_stappen(prs, total)

    print("Controleset-validatie (TierVerify §17):")
    validate_controleset(prs)

    os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
    prs.save(OUTPUT_PATH)
    print(f"OK  Presentatie opgeslagen: {OUTPUT_PATH}")
    print(f"OK  Aantal slides: {len(prs.slides)}")
    print(f"OK  Logo: {LOGO_PATH if os.path.exists(LOGO_PATH) else 'ontbreekt — placeholder gebruikt'}")
    print(f"OK  Master: {'corporate_master.pptx' if os.path.exists(CORPORATE_MASTER) else 'standaard leeg 16:9 deck'}")


if __name__ == "__main__":
    main()
