# ============================================================
# MODULE: Agent Task Executor — Background thread for AI task execution
# HSEQ Intelligence Dashboard
# ARCHITECTURE UPGRADE v2.0 — Multi-Deliverable Suite Generation
# ============================================================

import json
import sqlite3
import threading
import time
import traceback
import urllib.request
import os
import logging
import re
import ssl
from datetime import datetime

from indexer import DB_PATH

log = logging.getLogger('agent_executor')

# ── AI Config ────────────────────────────────────────────────────────────────
AI_PROVIDER = os.environ.get('AI_PROVIDER', 'zai')
OPENROUTER_URL = os.environ.get('OPENROUTER_URL', 'https://openrouter.ai/api/v1/chat/completions')
OPENROUTER_KEY = os.environ.get('OPENROUTER_API_KEY', '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')
OPENROUTER_MODEL = os.environ.get('OPENROUTER_MODEL', 'google/gemma-3-27b-it:free')
AI_API_URL = os.environ.get('AI_API_URL', 'https://api.z.ai/api/coding/paas/v4/chat/completions')
AI_API_KEY = os.environ.get('AI_API_KEY', '43a6c7e3d7b240daafae006e8488f674.ivYYRRLySgUwgqVE')
AI_MODEL = os.environ.get('AI_MODEL', 'glm-5.1')

# ── Project paths (dynamic per task) ─────────────────────────────────────────
_DEFAULT_PROJECT_DIR = '/root/projects/jg/2026-hseq-dashboard'
_SOP_DIR = '/root/projects/jg'

# Module-level defaults (overridden per-task in _resolve_project_dir)
PROJECT_DIR = _DEFAULT_PROJECT_DIR
DELIV_HTML = os.path.join(PROJECT_DIR, 'deliverables', 'html')
DELIV_DOCX = os.path.join(PROJECT_DIR, 'deliverables', 'docx')
DELIV_XLSX = os.path.join(PROJECT_DIR, 'deliverables', 'xlsx')
DELIV_PPTX = os.path.join(PROJECT_DIR, 'deliverables', 'pptx')
LOG_DIR = os.path.join(PROJECT_DIR, 'logs')
for d in [DELIV_HTML, DELIV_DOCX, DELIV_XLSX, DELIV_PPTX, LOG_DIR]:
    os.makedirs(d, exist_ok=True)


def _resolve_project_dir(task_desc, context):
    """Determine project directory from task context. MASTER_SOP compliant."""
    global PROJECT_DIR, DELIV_HTML, DELIV_DOCX, DELIV_XLSX, DELIV_PPTX, LOG_DIR

    project_dir = None

    # 1. Check for OPSLAG: directive in context
    if context and 'OPSLAG:' in context:
        m = re.search(r'OPSLAG:\s*(\S+)', context)
        if m:
            project_dir = m.group(1).rstrip('/')

    # 2. Auto-generate project name from task description
    if not project_dir or not os.path.isdir(os.path.dirname(project_dir)):
        safe = re.sub(r'[^A-Za-z0-9]+', '-', task_desc.lower())[:40].strip('-')
        ts = datetime.now().strftime('%Y-%m')
        project_dir = os.path.join('/root/projects/jg', f'{ts}-{safe}')

    # Ensure full MASTER_SOP directory structure
    for subdir in ['assets', 'correspondence', 'deliverables/docx', 'deliverables/html',
                   'deliverables/pdf', 'deliverables/media', 'deliverables/scorm',
                   'deliverables/pptx', 'deliverables/xlsx', 'logs', 'research', 'working', 'archive']:
        os.makedirs(os.path.join(project_dir, subdir), exist_ok=True)

    PROJECT_DIR = project_dir
    DELIV_HTML = os.path.join(project_dir, 'deliverables', 'html')
    DELIV_DOCX = os.path.join(project_dir, 'deliverables', 'docx')
    DELIV_XLSX = os.path.join(project_dir, 'deliverables', 'xlsx')
    DELIV_PPTX = os.path.join(project_dir, 'deliverables', 'pptx')
    LOG_DIR = os.path.join(project_dir, 'logs')

    log.info(f'Project directory resolved: {PROJECT_DIR}')
    return project_dir

# ── Deliverables Matrix (MASTER_SOP §3.5) ───────────────────────────────────
DELIVERABLE_MATRIX = {
    "rie_opstellen": [
        {"file": "RI&E Document", "format": "docx", "desc": "Volledige Risico-Inventarisatie & Evaluatie"},
        {"file": "Risicotabel", "format": "xlsx", "desc": "Risicomatrix met scores en maatregelen"},
        {"file": "RI&E Rapport", "format": "html", "desc": "Web-viewable rapport met styling"},
    ],
    "tra_maken": [
        {"file": "TRA Document", "format": "docx", "desc": "Taakrisicoanalyse volledig"},
        {"file": "TRA Checklist", "format": "xlsx", "desc": "Controlepunten per werkstap"},
        {"file": "TRA Rapport", "format": "html", "desc": "Web-viewable rapport"},
    ],
    "training_prep": [
        {"file": "Training Presentatie", "format": "html", "desc": "Slide-deck (HTML-presentatie)"},
        {"file": "Training Inhoud", "format": "docx", "desc": "Docentenhandleiding / cursusinhoud"},
        {"file": "Quiz", "format": "html", "desc": "Interactieve kennistoets"},
    ],
    "compliance_check": [
        {"file": "Compliance Rapport", "format": "docx", "desc": "Uitgebreid rapport met bevindingen"},
        {"file": "Gap-analyse", "format": "xlsx", "desc": "Tabel met gaps, prioriteit en acties"},
        {"file": "Compliance Dashboard", "format": "html", "desc": "Web-viewable overzicht"},
    ],
    "audit_prep": [
        {"file": "Audit Voorbereiding", "format": "docx", "desc": "Checklist en voorbereiding"},
        {"file": "Audit Checklist", "format": "xlsx", "desc": "Toetsbare checklist"},
        {"file": "Audit Rapport", "format": "html", "desc": "Web-viewable rapport"},
    ],
    "incident_analyse": [
        {"file": "Incident Rapport", "format": "docx", "desc": "Volledige incidentanalyse"},
        {"file": "Actielijst", "format": "xlsx", "desc": "Verbetermaatregelen met verantwoordelijken"},
        {"file": "Incident Dashboard", "format": "html", "desc": "Web-viewable rapport"},
    ],
    "_default": [
        {"file": "Rapport", "format": "docx", "desc": "Hoofddocument"},
        {"file": "Bijlagen", "format": "xlsx", "desc": "Tabellen en checklists"},
        {"file": "Rapport", "format": "html", "desc": "Web-viewable versie"},
    ],
}

# ── Agent personality prompts ───────────────────────────────────────────────
AGENT_PERSONALITIES = {
    "hseq_specialist": "Je bent een HSEQ Specialist met diepe expertise in BRZO, Arbowet, Milieuregelgeving en kwaliteitsmanagement. Je antwoorden zijn feitelijk, beknopt en direct toepasbaar.",
    "compliance_auditor": "Je bent een Compliance Auditor gespecialiseerd in wet- en regelgeving audits. Je werkt systematisch en produceert gestructureerde bevindingen met prioritering.",
    "risk_management": "Je bent een Risk Management Specialist met expertise in HAZOP, LOPA, QRA en Bow-Tie analyse. Je denkt in risicomatrices en barrières.",
    "qa_engineer": "Je bent een QA Engineer met diepe kennis van ISO 9001, KAM-systemen en procesvalidatie. Je output is gestructureerd en controleerbaar.",
    "technical_writer": "Je bent een Technical Writer die heldere, gestructureerde documentatie produceert. Procedures, handleidingen en rapporten zijn jouw specialiteit.",
    "environmental_compliance": "Je bent een Environmental Compliance Manager met expertise in emissies, vergunningen en de Omgevingswet.",
    "training_generator": "Je bent een Training Generator die effectieve trainingen ontwerpt: toolbox talks, e-learning modules en VCA-voorerichte materialen.",
    "presentation_specialist": "Je bent een Presentation Specialist die professionele presentaties en rapportages ontwerpt met visuele elementen.",
    "software_architect": "Je bent een Software Architect met expertise in Python, Flask, database design en API ontwikkeling.",
    "project_manager": "Je bent een Project Manager die planningen, resource allocatie en risicomanagement overzichten produceert.",
}

DEFAULT_PERSONALITY = "Je bent een HSEQ AI-assistent. Je antwoorden zijn feitelijk, beknopt en hyper-professioneel."

# ── Workflow prompt templates ────────────────────────────────────────────────
WORKFLOW_PROMPTS = {
    "rie_opstellen": """Stel een volledige Risico-Inventarisatie & Evaluatie (RI&E) op.

OUTPUT FORMAAT: Je MOET een JSON-object retourneren met deze structuur:
{
  "title": "...",
  "docx_content": {
    "sections": [
      {"heading": "1. Bedrijfsgegevens", "body": "..."},
      {"heading": "2. Inventarisatie Gevaren", "body": "..."},
      {"heading": "3. Risicobeoordeling", "body": "..."},
      {"heading": "4. Maatregelen", "body": "..."},
      {"heading": "5. Evaluatie", "body": "..."}
    ]
  },
  "xlsx_tables": {
    "risicomatrix": {
      "headers": ["Gevaar", "Bron", "Mogelijk gevolg", "W", "A", "Risicoklasse", "Bestaande maatregel", "Aanbeveling"],
      "rows": [["...", "...", "...", 3, 2, "M", "...", "..."]]
    },
    "actielijst": {
      "headers": ["#", "Actie", "Verantwoordelijke", "Deadline", "Status", "Prioriteit"],
      "rows": [["1", "...", "...", "...", "Open", "Hoog"]]
    }
  },
  "tables_for_html": [
    {"title": "Risicomatrix", "headers": ["Gevaar", "Bron", "Gevolg", "W", "A", "Klasse", "Maatregel", "Aanbeveling"], "rows": []}
  ]
}

Belangrijk: De JSON MOET geldig zijn. Alle tekst in Nederlands. Maak REALISTIEKE inhoud op basis van de input. Minimaal 8 risico's in de matrix, minimaal 6 acties in de actielijst.

Gebruik de volgende input:""",

    "tra_maken": """Stel een Taakrisicoanalyse (TRA) op.

OUTPUT FORMAAT: Je MOET een JSON-object retourneren:
{
  "title": "...",
  "docx_content": {
    "sections": [
      {"heading": "1. Taakbeschrijving", "body": "..."},
      {"heading": "2. Stappenplan", "body": "..."},
      {"heading": "3. Gefaren per Stap", "body": "..."},
      {"heading": "4. Risicobeoordeling", "body": "..."},
      {"heading": "5. Beheersmaatregelen", "body": "..."},
      {"heading": "6. Restrisico", "body": "..."}
    ]
  },
  "xlsx_tables": {
    "tra_checklist": {
      "headers": ["Stap", "Gevaar", "Risico voor", "W", "A", "Klasse", "Beheersmaatregel", "PBM", "Restrisico"],
      "rows": [["...", "...", "...", 3, 2, "M", "...", "...", "Laag"]]
    }
  },
  "tables_for_html": []
}

Gebruik de volgende input:""",

    "training_prep": """Bereid een training voor.

OUTPUT FORMAAT: Je MOET een JSON-object retourneren:
{
  "title": "...",
  "docx_content": {
    "sections": [
      {"heading": "1. Leerdoelen", "body": "..."},
      {"heading": "2. Module 1: ...", "body": "..."},
      {"heading": "3. Module 2: ...", "body": "..."},
      {"heading": "4. Praktijkvoorbeelden", "body": "..."},
      {"heading": "5. Evaluatie", "body": "..."}
    ]
  },
  "xlsx_tables": {},
  "tables_for_html": [],
  "quiz_questions": [
    {"question": "...", "options": ["A) ...", "B) ...", "C) ...", "D) ..."], "correct": 0}
  ]
}

Gebruik de volgende input:""",

    "compliance_check": """Voer een compliance check uit.

OUTPUT FORMAAT: Je MOET een JSON-object retourneren:
{
  "title": "...",
  "docx_content": {
    "sections": [
      {"heading": "1. Scope", "body": "..."},
      {"heading": "2. Toepasselijke Wetgeving", "body": "..."},
      {"heading": "3. Gap-analyse", "body": "..."},
      {"heading": "4. Prioritering", "body": "..."},
      {"heading": "5. Actieplan", "body": "..."},
      {"heading": "6. Bronvermelding", "body": "..."}
    ]
  },
  "xlsx_tables": {
    "gap_analyse": {
      "headers": ["#", "Wetgeving", "Eis", "Huidige situatie", "Gap", "Risico", "Actie", "Prioriteit"],
      "rows": [["1", "...", "...", "...", "...", "...", "...", "Hoog"]]
    },
    "actielijst": {
      "headers": ["#", "Actie", "Verantwoordelijke", "Deadline", "Status"],
      "rows": [["1", "...", "...", "...", "Open"]]
    }
  },
  "tables_for_html": []
}

Gebruik de volgende input:""",

    "audit_prep": """Bereid een audit voor.

OUTPUT FORMAAT: Je MOET een JSON-object retourneren:
{
  "title": "...",
  "docx_content": {
    "sections": [
      {"heading": "1. Auditdoel en Scope", "body": "..."},
      {"heading": "2. Auditcriteria", "body": "..."},
      {"heading": "3. Checklist", "body": "..."},
      {"heading": "4. Documentenlijst", "body": "..."},
      {"heading": "5. Tips voor Auditees", "body": "..."}
    ]
  },
  "xlsx_tables": {
    "audit_checklist": {
      "headers": ["#", "Controlepunt", "Referentie", "Status", "Bevinding", "Prioriteit"],
      "rows": [["1", "...", "...", "Open", "...", "Hoog"]]
    }
  },
  "tables_for_html": []
}

Gebruik de volgende input:""",

    "incident_analyse": """Voer een incidentanalyse uit.

OUTPUT FORMAAT: Je MOET een JSON-object retourneren:
{
  "title": "...",
  "docx_content": {
    "sections": [
      {"heading": "1. Feitenrelaas", "body": "..."},
      {"heading": "2. Oorzakenanalyse (5-Why)", "body": "..."},
      {"heading": "3. Contributerende Factoren", "body": "..."},
      {"heading": "4. Verbetermaatregelen", "body": "..."},
      {"heading": "5. Opvolging", "body": "..."}
    ]
  },
  "xlsx_tables": {
    "actielijst": {
      "headers": ["#", "Maatregel", "Type", "Verantwoordelijke", "Deadline", "Status", "Prioriteit"],
      "rows": [["1", "...", "Direct/Structuur", "...", "...", "Open", "Hoog"]]
    }
  },
  "tables_for_html": []
}

Gebruik de volgende input:""",
}

# Fallback prompt for direct agent tasks (no workflow)
DIRECT_TASK_PROMPT = """
OUTPUT FORMAAT: Je MOET een JSON-object retourneren met deze structuur:
{
  "title": "...",
  "docx_content": {
    "sections": [
      {"heading": "1. Inleiding & Scope", "body": "..."},
      {"heading": "2. Analyse & Bevindingen", "body": "..."},
      {"heading": "3. Risicobeoordeling", "body": "..."},
      {"heading": "4. Aanbevelingen", "body": "..."},
      {"heading": "5. Actielijst", "body": "..."},
      {"heading": "6. Bronvermelding", "body": "..."}
    ]
  },
  "xlsx_tables": {
    "actielijst": {
      "headers": ["#", "Actie", "Verantwoordelijke", "Deadline", "Status", "Prioriteit"],
      "rows": [["1", "...", "...", "...", "Open", "Hoog"]]
    }
  },
  "tables_for_html": []
}

Belangrijk: De JSON MOET geldig zijn. Gebruik Nederlandse tekst. Maak REALISTIEKE inhoud.
"""

API_TIMEOUT = 180  # seconds (increased for JSON output)


class TaskExecutor:
    """Background thread executor for agent tasks — Multi-Deliverable Suite."""

    def __init__(self):
        self._thread = None
        self._stop_event = threading.Event()
        self.running = False
        self.current_task_id = None
        self.poll_interval = 5

    def start(self):
        if self._thread and self._thread.is_alive():
            return
        self._stop_event.clear()
        self.running = True
        self._thread = threading.Thread(target=self._loop, daemon=True, name='agent_executor')
        self._thread.start()
        log.info('Agent executor v2.0 started (Multi-Deliverable Suite)')

    def stop(self):
        self._stop_event.set()
        self.running = False
        if self._thread:
            self._thread.join(timeout=10)
        log.info('Agent executor stopped')

    def restart(self):
        self.stop()
        self.start()

    def get_pending_count(self):
        conn = sqlite3.connect(DB_PATH)
        try:
            return conn.execute("SELECT COUNT(*) FROM agent_tasks WHERE status='pending'").fetchone()[0]
        finally:
            conn.close()

    def status(self):
        return {
            'executor_running': self.running,
            'current_task': self.current_task_id,
            'pending_tasks': self.get_pending_count(),
            'poll_interval': self.poll_interval
        }

    # ── Main loop ──────────────────────────────────────────────────────────
    def _loop(self):
        while not self._stop_event.is_set():
            try:
                task = self._fetch_next()
                if task:
                    self._execute(task)
                else:
                    self._stop_event.wait(self.poll_interval)
            except Exception as e:
                log.error(f'Executor loop error: {e}')
                self._stop_event.wait(self.poll_interval)

    def _fetch_next(self):
        conn = sqlite3.connect(DB_PATH)
        conn.execute('BEGIN IMMEDIATE')
        try:
            row = conn.execute(
                "SELECT id, agent_id, agent_name, workflow_id, task_description, context, metadata "
                "FROM agent_tasks WHERE status='pending' ORDER BY created_at ASC LIMIT 1"
            ).fetchone()
            if row:
                conn.execute(
                    "UPDATE agent_tasks SET status='running', started_at=datetime('now') WHERE id=?",
                    (row[0],))
                conn.commit()
                self.current_task_id = row[0]
            return row
        except Exception:
            conn.rollback()
            raise
        finally:
            conn.close()

    # ── Execute task ───────────────────────────────────────────────────────
    def _execute(self, task):
        task_id, agent_id, agent_name, workflow_id, task_desc, context, metadata_json = task
        log.info(f'Executing task {task_id}: {agent_name} - {task_desc[:80]}')
        try:
            # Resolve project directory (MASTER_SOP compliant)
            _resolve_project_dir(task_desc, context)
            prompt = self._build_prompt(agent_id, workflow_id, task_desc, context, metadata_json)
            messages = [
                {"role": "system", "content": prompt['system']},
                {"role": "user", "content": prompt['user']}
            ]
            raw_result = self._ai_call(messages)

            # Parse JSON from AI response
            parsed = self._parse_json_response(raw_result, task_desc)
            if not parsed:
                raise ValueError("AI response kon niet als JSON worden geparsed. Fallback naar HTML-only.")

            # Generate Multi-Deliverable Suite
            deliverables = self._generate_suite(task_id, agent_id, agent_name, task_desc, workflow_id, parsed)

            # Build result summary
            summary = f"## Generated Deliverables\n"
            for d in deliverables:
                summary += f"- **{d['label']}** ({d['format']}): {d['path']}\n"
            summary += f"\n### Content Preview\n{raw_result[:1500]}"

            self._update_task(task_id, 'completed', summary, json.dumps(deliverables))

            # Register in deliverables DB
            for d in deliverables:
                try:
                    self._register_deliverable(task_id, agent_id, agent_name, task_desc, d['path'], d['format'])
                except Exception as reg_err:
                    log.warning(f'Task {task_id}: DB registration failed for {d["path"]}: {reg_err}')

            self._update_changelog(task_id, agent_name, task_desc, deliverables)
            log.info(f'Task {task_id} completed — {len(deliverables)} deliverables generated')

        except Exception as e:
            error_msg = f'{type(e).__name__}: {str(e)}\n{traceback.format_exc()}'
            self._update_task(task_id, 'failed', error_msg)
            log.error(f'Task {task_id} failed: {e}')
        finally:
            self.current_task_id = None

    # ── Parse JSON response ────────────────────────────────────────────────
    def _parse_json_response(self, raw, task_desc):
        """Extract JSON from AI response (handles markdown code blocks)."""
        # Try direct JSON parse
        try:
            return json.loads(raw)
        except json.JSONDecodeError:
            pass

        # Try extracting from markdown code block
        patterns = [
            r'```json\s*\n(.*?)\n\s*```',
            r'```\s*\n(.*?)\n\s*```',
            r'\{[\s\S]*\}',  # Last resort: find outermost JSON object
        ]
        for pat in patterns:
            m = re.search(pat, raw, re.DOTALL)
            if m:
                try:
                    return json.loads(m.group(1) if m.lastindex else m.group(0))
                except (json.JSONDecodeError, AttributeError):
                    continue
        return None

    # ── Multi-Deliverable Suite Generator ───────────────────────────────────
    def _generate_suite(self, task_id, agent_id, agent_name, task_desc, workflow_id, parsed):
        """Generate the full deliverable suite: DOCX + HTML + XLSX."""
        deliverables = []
        ts = datetime.now().strftime('%Y%m%d_%H%M')
        safe_agent = re.sub(r'[^A-Za-z0-9_-]', '_', agent_name.lower())[:20]
        safe_task = re.sub(r'[^A-Za-z0-9_-]', '_', task_desc.lower())[:30]
        title = parsed.get('title', task_desc)

        # 1. DOCX — check if docx_content was provided by AI
        docx_content = parsed.get('docx_content', {})
        sections = docx_content.get('sections', [])
        if not sections:
            log.warning(f'Task {task_id}: NO docx_content in AI response — generating fallback DOCX from title')
            # Build minimal docx_content from available data
            parsed['docx_content'] = {
                'sections': [{'heading': title, 'body': task_desc + '\n\n*Automatisch gegenereerd — AI leverde geen gestructureerde content.*'}]
            }
            if 'xlsx_tables' in parsed and parsed['xlsx_tables']:
                for tbl_name, tbl_data in parsed['xlsx_tables'].items():
                    parsed['docx_content']['sections'].append({
                        'heading': f'Bijlage: {tbl_name.replace("_", " ").title()}',
                        'body': f'Tabel is beschikbaar in het XLSX-bestand.'
                    })
        try:
            docx_path = os.path.join(DELIV_DOCX, f'HSEQ_{safe_agent}_{safe_task}_{ts}_v1.0.docx')
            self._generate_docx(docx_path, title, agent_name, parsed)
            deliverables.append({'label': f'{title} (DOCX)', 'format': 'docx', 'path': docx_path})
            log.info(f'Task {task_id}: DOCX generated → {docx_path}')
        except Exception as e:
            log.warning(f'Task {task_id}: DOCX generation failed: {e}')

        # 2. XLSX (if tables present)
        xlsx_tables = parsed.get('xlsx_tables', {})
        if xlsx_tables:
            try:
                xlsx_path = os.path.join(DELIV_XLSX, f'HSEQ_{safe_agent}_{safe_task}_{ts}_v1.0.xlsx')
                self._generate_xlsx(xlsx_path, title, xlsx_tables)
                deliverables.append({'label': f'{title} Tabellen (XLSX)', 'format': 'xlsx', 'path': xlsx_path})
                log.info(f'Task {task_id}: XLSX generated → {xlsx_path}')
            except Exception as e:
                log.warning(f'Task {task_id}: XLSX generation failed: {e}')

        # 3. HTML (always)
        try:
            html_path = os.path.join(DELIV_HTML, f'HSEQ_{safe_agent}_{safe_task}_{ts}_v1.0.html')
            self._generate_html(html_path, title, agent_name, parsed)
            deliverables.append({'label': f'{title} (HTML)', 'format': 'html', 'path': html_path})
            log.info(f'Task {task_id}: HTML generated → {html_path}')
        except Exception as e:
            log.warning(f'Task {task_id}: HTML generation failed: {e}')

        # 4. PPTX (for training_generator / presentation_specialist)
        if agent_id in ('training_generator', 'presentation_specialist'):
            try:
                pptx_path = os.path.join(DELIV_PPTX, f'HSEQ_{safe_agent}_{safe_task}_{ts}_v1.0.pptx')
                self._generate_pptx(pptx_path, title, agent_name, parsed)
                deliverables.append({'label': f'{title} (PPTX)', 'format': 'pptx', 'path': pptx_path})
                log.info(f'Task {task_id}: PPTX generated → {pptx_path}')
            except Exception as e:
                log.warning(f'Task {task_id}: PPTX generation failed: {e}')

        return deliverables

    # ── DOCX Generator ─────────────────────────────────────────────────────
    def _generate_docx(self, filepath, title, agent_name, parsed):
        try:
            from docx import Document
            from docx.shared import Pt, Cm, RGBColor
            from docx.enum.text import WD_ALIGN_PARAGRAPH
            from docx.enum.table import WD_TABLE_ALIGNMENT
            from docx.oxml.ns import nsdecls
            from docx.oxml import parse_xml
        except ImportError:
            log.error('python-docx not installed. Run: pip3 install python-docx')
            raise

        doc = Document()
        style = doc.styles['Normal']
        style.font.name = 'Calibri'
        style.font.size = Pt(11)
        style.paragraph_format.space_after = Pt(6)
        style.paragraph_format.line_spacing = 1.15

        for level in range(1, 4):
            hs = doc.styles[f'Heading {level}']
            hs.font.name = 'Calibri'
            hs.font.color.rgb = RGBColor(0, 51, 102)
            hs.font.size = Pt([16, 13, 11][level - 1])
            hs.font.bold = True
            hs.paragraph_format.space_before = Pt([18, 12, 8][level - 1])

        for section in doc.sections:
            section.top_margin = Cm(2.5)
            section.bottom_margin = Cm(2.5)
            section.left_margin = Cm(2.5)
            section.right_margin = Cm(2.5)

        # Title page
        for _ in range(6):
            doc.add_paragraph()
        p = doc.add_paragraph()
        p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = p.add_run(title)
        run.font.size = Pt(24)
        run.font.bold = True
        run.font.color.rgb = RGBColor(0, 51, 102)
        doc.add_paragraph()
        p = doc.add_paragraph()
        p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = p.add_run(f'JvG Consultancy — HSEQ Intelligence')
        run.font.size = Pt(14)
        run.font.color.rgb = RGBColor(0, 51, 102)
        p = doc.add_paragraph()
        p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = p.add_run(f'Versie 1.0 | {datetime.now().strftime("%d %B %Y")}')
        run.font.size = Pt(12)
        for _ in range(4):
            doc.add_paragraph()
        p = doc.add_paragraph()
        p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = p.add_run(f'Agent: {agent_name}')
        run.font.size = Pt(11)
        p = doc.add_paragraph()
        p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = p.add_run('Classificatie: Intern — Directeur J. van Gemert')
        run.font.size = Pt(11)
        doc.add_page_break()

        # Sections
        docx_content = parsed.get('docx_content', {})
        sections = docx_content.get('sections', [])

        for sec in sections:
            heading = sec.get('heading', '')
            body = sec.get('body', '')

            if heading:
                doc.add_heading(heading, level=1)

            if body:
                # Split on double newline for paragraphs
                paragraphs = body.split('\n\n')
                for para_text in paragraphs:
                    para_text = para_text.strip()
                    if not para_text:
                        continue
                    # Check if it's a table
                    if '|' in para_text and para_text.count('|') >= 3:
                        self._add_table_from_md(doc, para_text)
                    elif para_text.startswith('- ') or para_text.startswith('* '):
                        # Bullet list
                        for line in para_text.split('\n'):
                            line = line.strip()
                            if line.startswith('- ') or line.startswith('* '):
                                p = doc.add_paragraph(line[2:], style='List Bullet')
                                for run in p.runs:
                                    run.font.size = Pt(11)
                    else:
                        p = doc.add_paragraph(para_text)
                        for run in p.runs:
                            run.font.size = Pt(11)

        # Tables from xlsx_tables as appendix
        xlsx_tables = parsed.get('xlsx_tables', {})
        if xlsx_tables:
            doc.add_page_break()
            doc.add_heading('Bijlage: Tabellen', level=1)
            for tbl_name, tbl_data in xlsx_tables.items():
                doc.add_heading(tbl_name.replace('_', ' ').title(), level=2)
                headers = tbl_data.get('headers', [])
                rows = tbl_data.get('rows', [])
                if headers and rows:
                    self._add_formatted_table(doc, headers, rows)

        doc.save(filepath)

    def _add_table_from_md(self, doc, md_text):
        lines = [l.strip() for l in md_text.split('\n') if '|' in l]
        if len(lines) < 2:
            return
        headers = [c.strip() for c in lines[0].split('|')[1:-1]]
        rows = []
        for line in lines[2:]:  # Skip separator line
            cells = [c.strip() for c in line.split('|')[1:-1]]
            if cells and not all(set(c) <= {'-', ' ', ':'} for c in cells):
                rows.append(cells)
        if headers:
            self._add_formatted_table(doc, headers, rows)

    def _add_formatted_table(self, doc, headers, rows):
        from docx.oxml.ns import nsdecls
        from docx.oxml import parse_xml
        table = doc.add_table(rows=1 + len(rows), cols=len(headers))
        table.style = 'Table Grid'
        table.alignment = 2  # CENTER
        for i, h in enumerate(headers):
            cell = table.rows[0].cells[i]
            cell.text = h
            shading = parse_xml(f'<w:shd {nsdecls("w")} w:fill="003366"/>')
            cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(255, 255, 255)
            cell.paragraphs[0].runs[0].font.bold = True
            cell.paragraphs[0].runs[0].font.size = Pt(10)
            cell._tc.get_or_add_tcPr().append(shading)
        for r_idx, row in enumerate(rows):
            for c_idx, val in enumerate(row):
                if c_idx < len(table.rows[r_idx + 1].cells):
                    cell = table.rows[r_idx + 1].cells[c_idx]
                    cell.text = str(val)
                    for p in cell.paragraphs:
                        for run in p.runs:
                            run.font.size = Pt(10)
        doc.add_paragraph()

    # ── XLSX Generator ─────────────────────────────────────────────────────
    def _generate_xlsx(self, filepath, title, xlsx_tables):
        try:
            from openpyxl import Workbook
            from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
        except ImportError:
            log.error('openpyxl not installed. Run: pip3 install openpyxl')
            raise

        wb = Workbook()
        first = True
        header_fill = PatternFill(start_color='003366', end_color='003366', fill_type='solid')
        header_font = Font(name='Calibri', size=10, bold=True, color='FFFFFF')
        cell_font = Font(name='Calibri', size=10)
        thin_border = Border(
            left=Side(style='thin', color='D1D5DB'),
            right=Side(style='thin', color='D1D5DB'),
            top=Side(style='thin', color='D1D5DB'),
            bottom=Side(style='thin', color='D1D5DB')
        )
        alt_fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')

        for tbl_name, tbl_data in xlsx_tables.items():
            headers = tbl_data.get('headers', [])
            rows = tbl_data.get('rows', [])
            if not headers:
                continue

            ws = wb.active if first else wb.create_sheet(title=tbl_name[:31])
            first = False
            ws.title = tbl_name[:31]

            # Title row
            ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(headers))
            title_cell = ws.cell(row=1, column=1, value=f'{title} — {tbl_name.replace("_", " ").title()}')
            title_cell.font = Font(name='Calibri', size=12, bold=True, color='003366')
            ws.row_dimensions[1].height = 30

            # Date
            ws.cell(row=2, column=1, value=f'Versie 1.0 | {datetime.now().strftime("%d-%m-%Y")}').font = Font(size=9, color='868E96')

            # Headers
            header_row = 4
            for c_idx, h in enumerate(headers, 1):
                cell = ws.cell(row=header_row, column=c_idx, value=h)
                cell.fill = header_fill
                cell.font = header_font
                cell.alignment = Alignment(horizontal='center', vertical='center')
                cell.border = thin_border

            # Data rows
            for r_idx, row in enumerate(rows, header_row + 1):
                for c_idx, val in enumerate(row, 1):
                    cell = ws.cell(row=r_idx, column=c_idx, value=val)
                    cell.font = cell_font
                    cell.border = thin_border
                    cell.alignment = Alignment(vertical='center', wrap_text=True)
                    if (r_idx - header_row) % 2 == 0:
                        cell.fill = alt_fill

            # Auto-width (approximate)
            for c_idx in range(1, len(headers) + 1):
                max_len = max(len(str(headers[c_idx - 1])), 10)
                for row in rows:
                    if c_idx - 1 < len(row):
                        max_len = max(max_len, len(str(row[c_idx - 1])))
                ws.column_dimensions[chr(64 + c_idx) if c_idx <= 26 else 'A' + chr(64 + c_idx - 26)].width = min(max_len + 4, 40)

        wb.save(filepath)

    # ── HTML Generator (enhanced) ──────────────────────────────────────────
    def _generate_html(self, filepath, title, agent_name, parsed):
        import os
        now_str = datetime.now().strftime('%d %B %Y')
        year_str = str(datetime.now().year)

        logo_html = ''
        try:
            with open('/root/projects/jg/assets/branding/logo-white-base64.txt', 'r') as lf:
                logo_data = lf.read().strip()
            logo_html = f'<img src="{logo_data}" alt="JvG Consultancy" style="height:40px;width:auto;margin-right:16px;">'
        except Exception:
            pass

        # Build content from parsed JSON
        content_html = ''
        docx_content = parsed.get('docx_content', {})
        for sec in docx_content.get('sections', []):
            heading = sec.get('heading', '')
            body = sec.get('body', '')
            if heading:
                content_html += f'<h2>{heading}</h2>\n'
            if body:
                content_html += self._md2html(body) + '\n'

        # Add tables from xlsx_tables
        xlsx_tables = parsed.get('xlsx_tables', {})
        if xlsx_tables:
            content_html += '<h2>Bijlage: Tabellen</h2>\n'
            for tbl_name, tbl_data in xlsx_tables.items():
                content_html += f'<h3>{tbl_name.replace("_", " ").title()}</h3>\n'
                headers = tbl_data.get('headers', [])
                rows = tbl_data.get('rows', [])
                if headers:
                    content_html += '<table><thead><tr>'
                    for h in headers:
                        content_html += f'<th>{h}</th>'
                    content_html += '</tr></thead><tbody>'
                    for row in rows:
                        content_html += '<tr>'
                        for cell in row:
                            content_html += f'<td>{cell}</td>'
                        content_html += '</tr>'
                    content_html += '</tbody></table>\n'

        # Quiz (if present)
        quiz = parsed.get('quiz_questions', [])
        if quiz:
            content_html += '<h2>Kennistoets</h2>\n'
            for i, q in enumerate(quiz, 1):
                content_html += f'<div class="quiz-q"><strong>Vraag {i}:</strong> {q.get("question", "")}</div>\n'
                for j, opt in enumerate(q.get('options', [])):
                    content_html += f'<div class="quiz-opt"><label><input type="radio" name="q{i}" value="{j}"> {opt}</label></div>\n'
                content_html += '<div class="quiz-reveal" onclick="this.textContent=this.textContent===\'Toon antwoord\'?\'Het correcte antwoord is: ' + chr(65 + q.get("correct", 0)) + '\'">Toon antwoord</div>\n'

        css = (
            "@page{size:A4;margin:20mm 15mm}*{margin:0;padding:0;box-sizing:border-box}"
            "body{font-family:'Segoe UI',Roboto,Arial,sans-serif;color:#1F2937;line-height:1.6;font-size:13px;background:#fff}"
            ".header{background:linear-gradient(135deg,#003366,#004488);color:#fff;padding:24px 40px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 6px rgba(0,0,0,.1)}"
            ".header-left{display:flex;align-items:center}"
            ".header h1{font-size:20px;font-weight:700;margin:0}"
            ".header .subtitle{font-size:12px;opacity:.85;margin-top:4px}"
            ".header .meta{font-size:11px;opacity:.9;text-align:right}"
            ".badge{display:inline-block;padding:3px 12px;border-radius:12px;font-size:10px;font-weight:600;color:#fff;background:#3B82F6;margin-bottom:6px}"
            ".content{padding:30px 40px;max-width:1200px;margin:0 auto}"
            "h2{font-size:17px;color:#003366;margin:28px 0 12px;padding-bottom:6px;border-bottom:2px solid #E5E7EB;font-weight:600}"
            "h3{font-size:14px;color:#1F2937;margin:20px 0 8px;font-weight:600}"
            "table{width:100%;border-collapse:collapse;margin:12px 0;font-size:12px;border:1px solid #E5E7EB}"
            "th{background:#003366;color:#fff;padding:10px 12px;text-align:left;font-weight:600}"
            "td{padding:8px 12px;border-bottom:1px solid #E5E7EB}"
            "tr:nth-child(even){background:#F8F9FA}"
            "tr:hover{background:#F0F4F8}"
            "ul,ol{margin:8px 0 8px 24px}li{margin:4px 0}"
            ".callout{padding:14px 18px;border-radius:6px;margin:14px 0;font-size:12px;line-height:1.5}"
            ".callout-warning{background:#FFF7ED;border-left:4px solid #F59E0B;color:#92400E}"
            ".callout-info{background:#EFF6FF;border-left:4px solid #3B82F6;color:#1E40AF}"
            ".callout-success{background:#F0FDF4;border-left:4px solid #00A859;color:#065F46}"
            ".callout-critical{background:#FFF7ED;border-left:4px solid #FF6D00;color:#9A3412}"
            ".footer{background:#374151;color:#fff;padding:16px 40px;font-size:10px;display:flex;justify-content:space-between;align-items:center}"
            "blockquote{border-left:3px solid #003366;padding:10px 16px;margin:12px 0;background:#F8F9FA;color:#374151}"
            "strong{color:#003366}"
            ".doc-meta{background:#F8F9FA;border:1px solid #E5E7EB;border-radius:6px;padding:14px 18px;margin:20px 0;font-size:11px}"
            ".doc-meta table{border:none;margin:0}"
            ".doc-meta td{border:none;padding:3px 12px 3px 0;font-size:11px}"
            ".doc-meta td:first-child{font-weight:600;color:#374151;white-space:nowrap}"
            ".quiz-q{padding:12px;background:#F0F7FF;border-radius:6px;margin:10px 0;font-weight:600}"
            ".quiz-opt{padding:6px 12px 6px 24px;cursor:pointer}"
            ".quiz-opt:hover{background:#F0F4F8}"
            ".quiz-reveal{padding:6px 12px;margin:4px 0 12px;color:#003366;cursor:pointer;font-size:12px;font-style:italic}"
            "@media print{body{font-size:11px;-webkit-print-color-adjust:exact;print-color-adjust:exact}.header,.footer,.callout,th{print-color-adjust:exact;-webkit-print-color-adjust:exact}}"
        )

        html = (
            f'<!DOCTYPE html><html lang="nl"><head><meta charset="UTF-8">'
            f'<title>{title} — JvG Consultancy</title><style>{css}</style></head><body>'
            f'<div class="header"><div class="header-left">{logo_html}<div><h1>JvG Consultancy</h1>'
            f'<div class="subtitle">HSEQ Intelligence Deliverable</div></div></div>'
            f'<div class="meta"><div class="badge">{agent_name}</div>'
            f'<div style="margin-top:6px">{now_str}</div><div>Versie 1.0</div></div></div>'
            f'<div class="content">'
            f'<div class="doc-meta"><table>'
            f'<tr><td>Document:</td><td>{title}</td></tr>'
            f'<tr><td>Agent:</td><td>{agent_name}</td></tr>'
            f'<tr><td>Datum:</td><td>{now_str}</td></tr>'
            f'<tr><td>Versie:</td><td>1.0</td></tr>'
            f'<tr><td>Classificatie:</td><td>Intern — Directeur J. van Gemert</td></tr>'
            f'</table></div>'
            f'{content_html}'
            f'</div>'
            f'<div class="footer">'
            f'<span>&copy; {year_str} JvG Consultancy — Safety &middot; Governance &middot; Advisory</span>'
            f'<span>Autorisatie: Intern — Directeur J. van Gemert</span>'
            f'</div></body></html>'
        )

        with open(filepath, 'w', encoding='utf-8') as f:
            f.write(html)

    # ── PPTX Generator ────────────────────────────────────────────────────
    def _generate_pptx(self, filepath, title, agent_name, parsed):
        try:
            from pptx import Presentation
            from pptx.util import Pt, Inches, Cm
            from pptx.dml.color import RGBColor
            from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
        except ImportError:
            log.error('python-pptx not installed. Run: pip3 install python-pptx')
            raise

        prs = Presentation()
        prs.slide_width = Inches(13.333)
        prs.slide_height = Inches(7.5)

        brand_color = RGBColor(0, 51, 102)
        white = RGBColor(255, 255, 255)
        light_gray = RGBColor(248, 249, 250)

        def add_text_box(slide, left, top, width, height, text, font_size=18, bold=False, color=brand_color, alignment=PP_ALIGN.LEFT):
            txBox = slide.shapes.add_textbox(left, top, width, height)
            tf = txBox.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.text = text
            p.font.size = Pt(font_size)
            p.font.bold = bold
            p.font.color.rgb = color
            p.alignment = alignment
            return txBox

        def add_background(slide, color=brand_color):
            bg = slide.background
            fill = bg.fill
            fill.solid()
            fill.fore_color.rgb = color

        # Slide 1: Title
        slide = prs.slides.add_slide(prs.slide_layouts[6])  # blank
        add_background(slide, brand_color)
        add_text_box(slide, Inches(1), Inches(1.5), Inches(11), Inches(1.5), title, 44, True, white, PP_ALIGN.CENTER)
        add_text_box(slide, Inches(1), Inches(3.5), Inches(11), Inches(0.8), 'JvG Consultancy — HSEQ Intelligence', 20, False, RGBColor(180, 200, 220), PP_ALIGN.CENTER)
        add_text_box(slide, Inches(1), Inches(4.5), Inches(11), Inches(0.6), f'Versie 1.0 | {datetime.now().strftime("%d %B %Y")} | {agent_name}', 14, False, RGBColor(150, 170, 190), PP_ALIGN.CENTER)

        # Content slides
        sections = parsed.get('docx_content', {}).get('sections', [])
        for sec in sections[:10]:  # max 10 slides
            heading = sec.get('heading', '')
            body = sec.get('body', '')
            if not heading:
                continue

            slide = prs.slides.add_slide(prs.slide_layouts[6])
            # Header bar
            shape = slide.shapes.add_shape(1, Inches(0), Inches(0), prs.slide_width, Inches(1.2))  # rectangle
            shape.fill.solid()
            shape.fill.fore_color.rgb = brand_color
            shape.line.fill.background()
            tf = shape.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.text = heading
            p.font.size = Pt(28)
            p.font.bold = True
            p.font.color.rgb = white
            p.alignment = PP_ALIGN.LEFT
            tf.margin_left = Inches(0.8)
            tf.vertical_anchor = MSO_ANCHOR.MIDDLE

            # Body content
            if body:
                # Clean markdown
                clean = body.replace('**', '').replace('##', '').replace('###', '').replace('`', '')
                # Truncate for slide
                if len(clean) > 600:
                    clean = clean[:597] + '...'
                add_text_box(slide, Inches(0.8), Inches(1.6), Inches(11.5), Inches(5.2), clean, 16, False, RGBColor(31, 41, 55))

        # Closing slide
        slide = prs.slides.add_slide(prs.slide_layouts[6])
        add_background(slide, brand_color)
        add_text_box(slide, Inches(1), Inches(2.5), Inches(11), Inches(1.5), 'JvG Consultancy', 40, True, white, PP_ALIGN.CENTER)
        add_text_box(slide, Inches(1), Inches(4.2), Inches(11), Inches(0.8), 'Safety · Governance · Advisory', 20, False, RGBColor(180, 200, 220), PP_ALIGN.CENTER)
        add_text_box(slide, Inches(1), Inches(5.2), Inches(11), Inches(0.6), f'© {datetime.now().year} JvG Consultancy — Intern', 12, False, RGBColor(150, 170, 190), PP_ALIGN.CENTER)

        prs.save(filepath)

    # ── MD to HTML converter ───────────────────────────────────────────────
    def _md2html(self, md):
        html = md
        # Callouts
        html = re.sub(r'^> \s*⚠️\s*(.+)$', r'<div class="callout callout-warning">⚠️ \1</div>', html, flags=re.MULTILINE)
        html = re.sub(r'^> \s*ℹ️\s*(.+)$', r'<div class="callout callout-info">ℹ️ \1</div>', html, flags=re.MULTILINE)
        html = re.sub(r'^> \s*✅\s*(.+)$', r'<div class="callout callout-success">✅ \1</div>', html, flags=re.MULTILINE)
        html = re.sub(r'^> \s*🔴\s*(.+)$', r'<div class="callout callout-critical">🔴 \1</div>', html, flags=re.MULTILINE)
        html = re.sub(r'^> (.+)$', r'<blockquote>\1</blockquote>', html, flags=re.MULTILINE)
        # Headers
        html = re.sub(r'^### (.+)$', r'<h3>\1</h3>', html, flags=re.MULTILINE)
        html = re.sub(r'^## (.+)$', r'<h2>\1</h2>', html, flags=re.MULTILINE)
        html = re.sub(r'^# (.+)$', r'<h2>\1</h2>', html, flags=re.MULTILINE)
        # Inline
        html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
        html = re.sub(r'\*(.+?)\*', r'<em>\1</em>', html)
        # Tables
        lines = html.split('\n')
        in_table = False
        tbl = []
        out = []
        for line in lines:
            if '|' in line and not line.strip().startswith('<'):
                cells = [c.strip() for c in line.split('|')[1:-1]]
                if all(set(c) <= {'-', ' ', ':'} for c in cells):
                    continue
                if not in_table:
                    tbl = ['<table>', '<thead><tr>' + ''.join(f'<th>{c}</th>' for c in cells) + '</tr></thead><tbody>']
                    in_table = True
                else:
                    tbl.append('<tr>' + ''.join(f'<td>{c}</td>' for c in cells) + '</tr>')
            else:
                if in_table:
                    tbl.append('</tbody></table>')
                    out.append('\n'.join(tbl))
                    tbl = []
                    in_table = False
                out.append(line)
        if in_table:
            tbl.append('</tbody></table>')
            out.append('\n'.join(tbl))
        html = '\n'.join(out)
        # Lists
        html = re.sub(r'^- (.+)$', r'<li>\1</li>', html, flags=re.MULTILINE)
        html = re.sub(r'^(\d+)\. (.+)$', r'<li>\2</li>', html, flags=re.MULTILINE)
        html = re.sub(r'((?:<li>.*</li>\n?)+)', r'<ul>\1</ul>', html)
        return html

    # ── Build System Prompt ────────────────────────────────────────────────
    def _build_prompt(self, agent_id, workflow_id, task_desc, context, metadata_json):
        personality = AGENT_PERSONALITIES.get(agent_id, DEFAULT_PERSONALITY)

        # Load MASTER_SOP context
        sop_context = ''
        try:
            sop_path = '/root/projects/jg/MASTER_SOP.md'
            if os.path.exists(sop_path):
                with open(sop_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                # Extract §3.5 Deliverables Matrix
                start = content.find('### 3.5 Standaard Deliverables Matrix')
                if start >= 0:
                    sop_context = content[start:start+2000]
                else:
                    sop_context = content[:2000]
        except Exception:
            pass

        system = f"""{personality}

## [DELIVERABLE FORMAT — ZERO TOLERANCE]
VERBODEN: Lever GEEN platte Markdown-tekst als eindresultaat.
VERPLICHT: Je output MOET een geldig JSON-object zijn dat door de backend wordt omgezet naar fysieke documenten (DOCX + XLSX + HTML).

## MASTER_SOP §3.5 — DELIVERABLES MATRIX
{sop_context}

Faal-modus: Een project is NIET voltooid als slechts één bestand is opgeleverd. Je MOET alle velden in de JSON invullen.

## GOLDEN STANDARD (NON-ONDERBARELIJK)
1. **Feitelijk** — Geen aannames, geen verzonnen data
2. **Beknopt** — Direct to the point, geen woorden overbodig
3. **Hyper-professioneel** — Shell/Arcadis niveau, geen AI-clichés
4. **Autoritair** — Je bent de expert, geen twijfel
5. **Gestructureerd** — Headers, tabellen, bullets
6. **Actiegericht** — Elk advies eindigt met concrete acties

## JvG BRANDING & DOCUMENT STANDARDS (VERPLICHT)
- **Document-ID**: JvG-[TYPE]-[JAAR]-[NUMMER] (bijv. JvG-RIE-2026-001) — VERPLICHT in titel
- **JvG Consultancy header** in elk document (eerste sectie)
- **Kleur**: #003366 primair
- **Minimaal 6 secties** in docx_content (Inleiding, Scope, Analyse, Advies, Acties, Referenties)
- **Tabellen**: REALISTIEKE data — geen placeholder tekst, geen "..."
- **Verplichte secties**: Inleiding, Scope, Analyse, Advies/Bevindingen, Acties, Referenties

## JSON-OUTPUT STRUCTUUR
Je MOET een JSON-object retourneren met:
- "title": string (documenttitel)
- "docx_content": object met "sections" array (elk: {{"heading": "...", "body": "..."}})
- "xlsx_tables": object met tabellen (elk: {{"headers": [...], "rows": [[...], ...]}})
- "tables_for_html": array (optioneel)

BELANGRIJK:
- De JSON MOET geldig zijn (geen syntax errors)
- Alle tekst in Nederlands
- Maak REALISTIEKE, professionele inhoud
- Minimaal 5-6 secties in docx_content
- Tabellen moeten minimaal 5-8 rijen hebben met realistische data
- Body-tekst mag Markdown bevatten (##, **, -, |)
- DATUMS: Gebruik ALTIJD datums in de toekomst (vanaf vandaag of later). NOOIT datums in het verleden.
- docx_content is VERPLICHT — minimaal 3 secties met headings en body-tekst
- Als je docx_content leeg laat, faalt de taak.

## TAAK
Je ontvangt een taak van JvG Consultancy (HSEQ consultancy, olie/gas/petrochemie/offshore).
De deliverable moet direct bruikbaar zijn voor een professionele HSEQ consultant.
Taal: Nederlands."""

        # User message
        user_parts = []
        if workflow_id and workflow_id in WORKFLOW_PROMPTS:
            user_parts.append(WORKFLOW_PROMPTS[workflow_id])
        else:
            user_parts.append(DIRECT_TASK_PROMPT)

        if context:
            user_parts.append(f"\n## Input Data\n{context}")
        else:
            user_parts.append(f"\n## Taak\n{task_desc}")

        # KB context
        if metadata_json:
            try:
                meta = json.loads(metadata_json)
                kb = meta.get('kb_context', '')
                if kb:
                    user_parts.append(f"\n## Relevante Kennisbank Extracten\n{kb[:3000]}")
            except (json.JSONDecodeError, TypeError):
                pass

        return {'system': system, 'user': '\n'.join(user_parts)}

    # ── AI Call ────────────────────────────────────────────────────────────
    def _ai_call(self, messages, max_retries=3):
        for attempt in range(max_retries):
            try:
                payload = json.dumps({
                    "model": AI_MODEL,
                    "messages": messages,
                    "stream": False,
                    "temperature": 0.7,
                    "max_tokens": 16000
                }).encode('utf-8')
                req = urllib.request.Request(AI_API_URL, data=payload, headers={
                    "Content-Type": "application/json",
                    "Authorization": f"Bearer {AI_API_KEY}"
                })
                ctx = ssl.create_default_context()
                ctx.check_hostname = False
                ctx.verify_mode = ssl.CERT_NONE
                with urllib.request.urlopen(req, timeout=API_TIMEOUT, context=ctx) as resp:
                    data = json.loads(resp.read().decode('utf-8'))
                    return data.get('choices', [{}])[0].get('message', {}).get('content', '')
            except Exception as e:
                log.warning(f'AI call attempt {attempt+1} failed: {e}')
                if attempt == max_retries - 1:
                    raise
                time.sleep(2 ** attempt)
        return ""

    # ── DB Operations ──────────────────────────────────────────────────────
    def _update_task(self, task_id, status, result, deliverable_file=None):
        conn = sqlite3.connect(DB_PATH)
        conn.execute('BEGIN IMMEDIATE')
        try:
            conn.execute(
                "UPDATE agent_tasks SET status=?, result=?, deliverable_file=?, completed_at=datetime('now') WHERE id=?",
                (status, result, deliverable_file, task_id))
            conn.commit()
        except Exception:
            conn.rollback()
            raise
        finally:
            conn.close()

    def _register_deliverable(self, task_id, agent_id, agent_name, task_desc, filepath, file_type):
        try:
            conn = sqlite3.connect(DB_PATH)
            safe_name = os.path.basename(filepath)
            try:
                conn.execute(
                    "INSERT INTO deliverables (title, agent_name, file_path, file_type, status, version, created_at) VALUES (?,?,?,?,?,?,datetime('now'))",
                    (task_desc, agent_name, filepath, file_type, 'approved', '1.0'))
                conn.commit()
            except sqlite3.OperationalError:
                log.warning(f'Deliverables table not found, skipping registration')
            conn.close()
        except Exception as e:
            log.warning(f'_register_deliverable error: {e}')

    def _update_changelog(self, task_id, agent_name, task_desc, deliverables):
        ts = datetime.now().strftime('%Y-%m-%d')
        changelog_path = os.path.join(LOG_DIR, 'changelog.md')
        header = '| Versie | Datum | Wijziging | Agent |\n|--------|-------|-----------|-------|\n'
        row = f'| v1.0 | {ts} | {task_desc[:60]} | {agent_name} |\n'
        if not os.path.exists(changelog_path):
            with open(changelog_path, 'w', encoding='utf-8') as f:
                f.write(f'# Changelog — {os.path.basename(PROJECT_DIR)}\n\n{header}{row}')
        else:
            with open(changelog_path, 'a', encoding='utf-8') as f:
                f.write(row)


# ── Singleton ────────────────────────────────────────────────────────────────
executor = TaskExecutor()


def register_executor_routes(flask_app, BASE_PATH="/hseq-dashboard"):

    @flask_app.route(BASE_PATH + '/api/agents/executor/status')
    def executor_status():
        return jsonify({
            'running': executor.running,
            'current_task': executor.current_task_id,
            'pending_tasks': executor.get_pending_count()
        })

    @flask_app.route(BASE_PATH + '/api/agents/executor/restart', methods=['POST'])
    def executor_restart():
        executor.stop()
        executor.start()
        return jsonify({'ok': True, 'message': 'Executor herstart (v2.0 Multi-Deliverable)'})

    @flask_app.route(BASE_PATH + '/api/agents/task/<int:task_id>/retry', methods=['POST'])
    def task_retry(task_id):
        conn = sqlite3.connect(DB_PATH)
        conn.execute("UPDATE agent_tasks SET status='pending', result=NULL, started_at=NULL, completed_at=NULL WHERE id=?", (task_id,))
        conn.commit()
        conn.close()
        return jsonify({'ok': True, 'message': f'Taak {task_id} opnieuw in queue'})
