#!/usr/bin/env python3
"""
Deliverables Gatekeeper — Uniforme Deliverables Borging
=======================================================
Mechanische afdwinging van MASTER_SOP v8.8-regels die voorheen alleen
procedureel waren. Drie run-modes:

  --phase entry        Vóór eerste subagent-spawn (Fase 0-1 gate)
  --phase artefact     Direct na elke subagent-run (fysiek artefact-check)
  --phase deliverables Vóór oplevering (Golden Standard diff + cleanup + changelog)

Exit codes: 0 = PASS, 1 = FAIL (met per-check rapportage op stdout)

Auteur: Repair Clan subagent — 2026-08-24
SOP-referentie: MASTER_SOP v8.8 §1.3, §1.13, §3, §3.5, §4.3, §5.2, §5.4, §6
"""

import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone

FASE1_SUBMAPPEN = ["assets", "correspondence", "deliverables", "logs", "research", "working", "archive"]

# Golden Standard, afgeleid van 2026-audit-koole-mercury-003057 ( fysiek geverifieerd 24-08-2026)
GOLDEN_STANDARD = {
    "documenttypes": [
        {"naam": "audit_rapport", "root_md": r"^audit_rapport_.*\.md$", "docx": r"^docx/(Compliance_)?[Aa]udit.*\.docx$", "gescheiden": True},
        {"naam": "herzieningsvoorstel", "root_md": r"^herzieningsvoorstel_.*\.md$", "docx": r"^docx/Herzieningsvoorstel.*\.docx$", "gescheiden": True},
        {"naam": "checklist", "checklists_md": r"^checklists/.*[Cc]hecklist.*\.md$", "checklists_pdf": r"^checklists/.*[Cc]hecklist.*\.pdf$"},
    ],
    # Patronen die NOOIT in deliverables/ mogen staan (combined-detectie)
    "verboden_patronen": [
        (r"_en_[Hh]erzieningsvoorstel.*\.docx$", "Gecombineerd DOCX (audit+herziening) — Mercury levert gescheiden bestanden"),
        (r"_en_[Hh]erzieningsvoorstel.*\.html$", "Gecombineerd HTML (audit+herziening) — Mercury levert gescheiden bestanden"),
    ],
}

ANTI_CORRUPTIE_PATROON = re.compile(
    r"(QA\s*Engineer\s*:?\s*Kas|Director\s*[-–—]\s*Nood-?QC|Kas.*Nood-?QC)", re.IGNORECASE
)

MIN_ARTIFACT_BYTES = 512  #QC/productierapporten: minimaal 0,5 KB om "leeg bestand" te vangen


def now() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")


def result(check: str, ok: bool, detail: str = "") -> bool:
    status = "✅ PASS" if ok else "❌ FAIL"
    line = f"{status}  {check}"
    if detail:
        line += f" — {detail}"
    print(line)
    return ok


def alle_bestanden(root: str) -> list:
    out = []
    for dirpath, _dirs, files in os.walk(root):
        for f in files:
            out.append(os.path.relpath(os.path.join(dirpath, f), root))
    return sorted(out)


# ---------------------------------------------------------------------------
# PHASE: ENTRY — vóór eerste subagent-spawn (MASTER_SOP §3 Fase 0-1)
# ---------------------------------------------------------------------------
def phase_entry(project: str) -> int:
    print(f"\n=== PHASE-ENTRY GATE — {project} — {now()} ===")
    ok = True

    if not os.path.isdir(project):
        print("❌ FAIL  Projectmap bestaat niet — Fase 1 initialisatie vereist vóór spawn")
        return 1

    # 1. Fase-1 submappen
    ontbrekend = [m for m in FASE1_SUBMAPPEN if not os.path.isdir(os.path.join(project, m))]
    ok &= result("Fase-1 submappen compleet (7)", not ontbrekend,
                 f"ontbrekend: {', '.join(ontbrekend)}" if ontbrekend else "assets, correspondence, deliverables, logs, research, working, archive")

    # 2. Prompt-log aanwezig
    plog = os.path.join(project, "project_prompts.log")
    ok &= result("project_prompts.log aanwezig (§1.13)", os.path.isfile(plog))

    # 3. Prompt-log timing: moet vóór álle working-artefacten dateren
    if os.path.isfile(plog):
        plog_mtime = os.path.getmtime(plog)
        working = os.path.join(project, "working")
        latere = []
        if os.path.isdir(working):
            for f in alle_bestanden(working):
                fp = os.path.join(working, f)
                if os.path.getmtime(fp) < plog_mtime - 5:
                    latere.append(f)
        # 'latere' bevat working-bestanden die OUDER zijn dan de prompt-log → log is retroactief
        ok &= result("Prompt-log datering correct (vóór working-artefacten)", not latere,
                     f"retroactief t.o.v. {len(latere)} artefacten, o.a. {latere[:3]}" if latere else "")
    else:
        ok = False

    # 4. Bronverzameling
    src = os.path.join(project, "research", "sources.md")
    ok &= result("research/sources.md aanwezig (Fase 1 bronverzameling)", os.path.isfile(src))

    # 5. GO-log (advies-check: waarschuwt, blokkeert nog niet — SOP v8.9-voorstel)
    golog = os.path.join(project, "logs", "go_decision.log")
    if not os.path.isfile(golog):
        print("⚠️  ADVIES  logs/go_decision.log ontbreekt — expliciet GO-moment onbewezen (SOP v8.9-voorstel §1.7)")

    print(f"\n{'🎉 ENTRY-PASS' if ok else '⛔ ENTRY-FAIL — NIET spawnen; eerst Fase 0-1 afronden'}")
    return 0 if ok else 1


# ---------------------------------------------------------------------------
# PHASE: ARTEFACT — direct na elke subagent-run (§1.3 Artefact Recovery)
# ---------------------------------------------------------------------------
def phase_artefact(path: str, min_bytes: int) -> int:
    print(f"\n=== ARTEFACT-CHECK — {path} — {now()} ===")
    ok = True
    ok &= result("Bestand bestaat (fysiek artefact)", os.path.isfile(path))
    if not ok:
        print("\n⛔ ARTEFACT-FAIL — start Artefact Recovery Protocol:")
        print("   1. Re-spawn subagent met OUTPUT-CONTRACT (pad + naam + formaat + min-grootte)")
        print("   2. NOOIT zelf produceren (§1.3/§5.4: zelf-fixen = Critical System Failure)")
        return 1

    size = os.path.getsize(path)
    ok &= result(f"Minimale grootte ≥ {min_bytes} bytes", size >= min_bytes, f"actueel: {size} bytes")

    ext = os.path.splitext(path)[1].lower()
    validatie = {
        ".md": (lambda t: len(re.findall(r"^#{1,3} ", t, re.M)) >= 1, "minimaal 1 heading"),
        ".json": (lambda t: bool(json.loads(t)), "parseerbaar"),
        ".docx": (lambda t: len(t) > 1000, "non-triviale binary"),
        ".html": (lambda t: "</html>" in t.lower(), "sluitende </html>-tag"),
    }
    if ext in validatie:
        fn, eis = validatie[ext]
        try:
            if ext == ".docx":
                with open(path, "rb") as fh:
                    content = fh.read()
            else:
                with open(path, "r", errors="replace") as fh:
                    content = fh.read()
            inhoud_ok = fn(content)
        except Exception as e:
            inhoud_ok = False
            eis = f"fout bij lezen: {e}"
        ok &= result(f"Formaatvalidatie {ext} ({eis})", inhoud_ok)

    print(f"\n{'🎉 ARTEFACT-PASS' if ok else '⛔ ARTEFACT-FAIL — Artefact Recovery Protocol van start'}")
    return 0 if ok else 1


# ---------------------------------------------------------------------------
# PHASE: DELIVERABLES — vóór oplevering (Golden Standard diff + cleanup)
# ---------------------------------------------------------------------------
def phase_deliverables(project: str, manifest: str | None) -> int:
    print(f"\n=== DELIVERABLES GATE — {project} — {now()} ===")
    droot = os.path.join(project, "deliverables")
    if not os.path.isdir(droot):
        print("❌ FAIL  deliverables/ map bestaat niet")
        return 1

    gs = GOLDEN_STANDARD
    if manifest and os.path.isfile(manifest):
        with open(manifest) as fh:
            gs = json.load(fh)
        print(f"   (extern manifest geladen: {manifest})")

    bestanden = [b for b in alle_bestanden(droot) if not b.startswith(".")]
    ok = True

    # Normaliseer elk patroon: match tegen relatief pad binnen deliverables/
    def pat_hits(pat: str) -> list:
        rx = re.compile(pat)
        return [b for b in bestanden if rx.search(b)]

    # 1. Golden Standard: per documenttype verplichte bestanden aanwezig
    for dt in gs["documenttypes"]:
        for key in ("root_md", "docx", "checklists_md", "checklists_pdf", "html", "json"):
            if key in dt:
                hits = pat_hits(dt[key])
                ok &= result(f"[{dt['naam']}] {key} aanwezig", bool(hits),
                             f"geen match op {dt[key]}" if not hits else ", ".join(hits[:2]))

    # 2. Verboden patronen (combined-detectie)
    for pat, melding in gs.get("verboden_patronen", []):
        hits = pat_hits(pat)
        ok &= result(f"Geen verboden patroon: {pat}", not hits,
                     f"{melding} → {', '.join(hits)} (verplaats naar archive/ + CHANGELOG-entry)" if hits else "")

    # 3. CHANGELOG-actualiteit: elk fysiek deliverable-bestand vermeld?
    clog = os.path.join(droot, "CHANGELOG.md")
    if os.path.isfile(clog):
        with open(clog, errors="replace") as fh:
            cl = fh.read()
        niet_genoemd = [b for b in bestanden if b != "CHANGELOG.md" and os.path.basename(b) not in cl]
        ok &= result("CHANGELOG vermeldt alle deliverable-bestanden (§6)", not niet_genoemd,
                     f"niet genoemd: {', '.join(niet_genoemd[:5])}" if niet_genoemd else "")
    else:
        ok &= result("CHANGELOG.md aanwezig in deliverables/ (§6)", False)

    # 4. Versietags in bestandsnamen (§5.2 check 5)
    zonder_tag = [b for b in bestanden
                  if b != "CHANGELOG.md" and b.endswith((".md", ".docx", ".html", ".pdf", ".json"))
                  and not re.search(r"v\d+\.\d+", b, re.IGNORECASE)]
    ok &= result("Versietag in elke bestandsnaam (§5.2 #5)", not zonder_tag,
                 f"zonder tag: {', '.join(zonder_tag[:5])}" if zonder_tag else "")

    # 5. Anti-corruptie scan (§5.4): Director als QC-auditor = VOID
    working = os.path.join(project, "working")
    if os.path.isdir(working):
        for b in alle_bestanden(working):
            if re.search(r"qa_?.*(report|rapport)", b, re.IGNORECASE) and b.endswith((".md", ".txt")):
                fp = os.path.join(working, b)
                with open(fp, errors="replace") as fh:
                    if ANTI_CORRUPTIE_PATROON.search(fh.read()):
                        ok &= result(f"Anti-corruptie scan {b} (§5.4)", False,
                                     "Director (Kas) vermeld als QA-auditor → rapport VOID; her-audit door nieuwe QC-subagent vereist")

    print(f"\n{'🎉 DELIVERABLES-PASS' if ok else '⛔ DELIVERABLES-FAIL — niet leveren; fixes hierboven vermelden'}")
    return 0 if ok else 1


def main():
    ap = argparse.ArgumentParser(description="Deliverables Gatekeeper — uniforme deliverables borging")
    ap.add_argument("phase", choices=["entry", "artefact", "deliverables"])
    ap.add_argument("project", nargs="?", help="Projectmap (entry/deliverables)")
    ap.add_argument("--file", help="Verwacht artefact-pad (artefact)")
    ap.add_argument("--min-bytes", type=int, default=MIN_ARTIFACT_BYTES)
    ap.add_argument("--manifest", help="Extern golden-standard manifest (JSON)")
    args = ap.parse_args()

    if args.phase == "entry":
        sys.exit(phase_entry(args.project))
    if args.phase == "artefact":
        if not args.file:
            print("❌ --file vereist bij --phase artefact"); sys.exit(2)
        sys.exit(phase_artefact(args.file, args.min_bytes))
    sys.exit(phase_deliverables(args.project, args.manifest))


if __name__ == "__main__":
    main()
