# ============================================================
# MODULE: Agent Task Executor — Background thread for AI task execution
# HSEQ Intelligence Dashboard
# ============================================================

import json
import sqlite3
import threading
import time
import traceback
import urllib.request
import os
import logging

from indexer import DB_PATH

log = logging.getLogger('agent_executor')

# ── AI Config (same as app.py) ──────────────────────────────────────────────
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')

# ── 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 op basis van de verstrekte gegevens. Structuur: 1) Bedrijfsgegevens 2) Inventarisatie gevaren 3) Risicobeoordeling (risicomatrix) 4) Maatregelen 5) Evaluatie. Gebruik de volgende input:",
    "tra_maken": "Stel een Taakrisicoanalyse (TRA) op voor de beschreven werkzaamheden. Structuur: 1) Taakbeschrijving 2) Stappenplan 3) Gefaren per stap 4) Risicobeoordeling 5) Beheersmaatregelen 6) Restrisico. Gebruik de volgende input:",
    "training_prep": "Bereid een training voor met: 1) Leerdoelen 2) Inhoud (gestructureerd in modules) 3) Praktijkvoorbeelden 4) Quizvragen 5) Evaluatieformulier. Gebruik de volgende input:",
    "compliance_check": "Voer een compliance check uit tegen de genoemde wetgeving. Structuur: 1) Scope 2) Toepasselijke wetgeving per onderwerp 3) Gap-analyse 4) Prioritering 5) Actieplan. Gebruik de volgende input:",
    "audit_prep": "Bereid een audit voor met: 1) Auditdoel en scope 2) Criteria 3) Checklist (gecategoriseerd) 4) Documentenlijst 5) Tips voor auditees. Gebruik de volgende input:",
    "incident_analyse": "Voer een uitgebreide incidentanalyse uit: 1) Feitenrelaas 2) Oorzakenanalyse (5-Why / visgraat) 3) Contributerende factoren 4) Verbetermaatregelen 5) Opvolging. Gebruik de volgende input:",
}

API_TIMEOUT = 120  # seconds


class TaskExecutor:
    """Background thread executor for agent tasks."""

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

    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 started')

    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 status(self):
        conn = sqlite3.connect(DB_PATH)
        pending = conn.execute("SELECT COUNT(*) FROM agent_tasks WHERE status='pending'").fetchone()[0]
        conn.close()
        return {
            'executor_running': self.running,
            'current_task': self.current_task_id,
            'pending_tasks': pending,
            'poll_interval': self.poll_interval
        }

    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()

    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:
            prompt = self._build_prompt(agent_id, workflow_id, task_desc, context, metadata_json)
            messages = [
                {"role": "system", "content": prompt['system']},
                {"role": "user", "content": prompt['user']}
            ]
            result = self._ai_call(messages)
            deliverable_path = None
            try:
                deliverable_path = self._generate_deliverable(task_id, agent_id, agent_name, task_desc, result)
            except Exception as deliv_err:
                log.warning(f'Task {task_id}: deliverable generation failed: {deliv_err}')
            self._update_task(task_id, 'completed', result, deliverable_path)
            if deliverable_path:
                try:
                    self._register_deliverable(task_id, agent_id, agent_name, task_desc, deliverable_path, result)
                except Exception as reg_err:
                    log.warning(f'Task {task_id}: DB registration failed: {reg_err}')
            log.info(f'Task {task_id} completed')
        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

    def _generate_deliverable(self, task_id, agent_id, agent_name, task_desc, ai_text):
        import os, re
        from datetime import datetime
        project_dir = '/root/projects/jg/2026-hseq-dashboard'
        deliv_dir = os.path.join(project_dir, 'deliverables', 'html')
        os.makedirs(deliv_dir, exist_ok=True)
        ts = datetime.now().strftime('%Y%m%d_%H%M')
        safe_agent = re.sub(r'[^A-Za-z0-9_-]', '_', agent_name.lower())
        safe_task = re.sub(r'[^A-Za-z0-9_-]', '_', task_desc.lower())[:40]
        filename = f'HSEQ_{safe_agent}_{safe_task}_{ts}_v1.0.html'
        filepath = os.path.join(deliv_dir, filename)
        html_content = self._md2html(ai_text)
        now_str = datetime.now().strftime('%d %B %Y')
        year_str = str(datetime.now().year)
        # Load JvG logo (white variant for dark header)
        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
        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}"
            ".priority-high{color:#EF4444;font-weight:700}"
            ".priority-mid{color:#F59E0B;font-weight:600}"
            ".priority-low{color:#00A859;font-weight:600}"
            "@media print{body{font-size:11px;-webkit-print-color-adjust:exact;print-color-adjust:exact}.header,.footer,.callout,.callout-warning,.callout-info,.callout-success,.callout-critical,th{print-color-adjust:exact;-webkit-print-color-adjust:exact}}"
        )
        body = (
            f'<!DOCTYPE html><html lang="nl"><head><meta charset="UTF-8">'
            f'<title>{task_desc} — JvG Consultancy</title>'
            f'<style>{css}</style></head><body>'
            f'<div class="header">'
            f'<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>'
            f'<div>Versie 1.0</div></div></div>'
            f'<div class="content">'
            f'<div class="doc-meta"><table>'
            f'<tr><td>Document:</td><td>{task_desc}</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'{html_content}'
            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>'
            f'</body></html>'
        )
        with open(filepath, 'w', encoding='utf-8') as f:
            f.write(body)
        self._update_changelog(project_dir, filename, agent_name, task_desc, filepath)
        return filepath

    def _md2html(self, md):
        import re
        html = md
        # Callouts first (before other blockquote processing)
        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)
        # Remaining blockquotes
        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 formatting
        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)
        html = re.sub(r'<p>\s*</p>', '', html)
        return html

    def _update_changelog(self, project_dir, filename, agent_name, task_desc, filepath):
        import os
        from datetime import datetime
        log_dir = os.path.join(project_dir, 'logs')
        os.makedirs(log_dir, exist_ok=True)
        changelog_path = os.path.join(log_dir, 'changelog.md')
        ts = datetime.now().strftime('%Y-%m-%d %H:%M')
        entry = '\n## ' + ts + ' — ' + filename + '\n- **Agent**: ' + agent_name + '\n- **Taak**: ' + task_desc + '\n- **Bestand**: ' + filepath + '\n- **Versie**: v1.0\n'
        with open(changelog_path, 'a', encoding='utf-8') as f:
            f.write(entry)

    def _register_deliverable(self, task_id, agent_id, agent_name, task_desc, filepath, ai_text):
        """Register deliverable in the app's deliverables DB (MASTER SOP §4.3, §6)."""
        import os
        from datetime import datetime
        try:
            db_path = DB_PATH
            conn = sqlite3.connect(db_path)
            ts = datetime.now().strftime('%Y-%m-%d %H:%M')
            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, 'html', 'approved', '1.0')
                )
                conn.commit()
                log.info(f'Task {task_id}: deliverable registered in DB')
            except sqlite3.OperationalError:
                log.warning(f'Task {task_id}: deliverables table not found, skipping DB registration')
            conn.close()
        except Exception as e:
            log.warning(f'Task {task_id}: _register_deliverable error: {e}')


    def _build_prompt(self, agent_id, workflow_id, task_desc, context, metadata_json):
        personality = AGENT_PERSONALITIES.get(agent_id, DEFAULT_PERSONALITY)

        # Load MASTER_SOP and MASTER_STYLEGUIDE context
        sop_context = ''
        styleguide_context = ''
        try:
            for path, label in [
                ('/root/.openclaw/workspace/MASTER_SOP.md', 'MASTER_SOP'),
                ('/root/.openclaw/workspace/MASTER_STYLEGUIDE.md', 'MASTER_STYLEGUIDE')
            ]:
                import os
                if os.path.exists(path):
                    with open(path, 'r', encoding='utf-8') as f:
                        content = f.read()
                    if label == 'MASTER_SOP':
                        sop_context = '\n### ' + label + ' (belangrijkste secties):\n' + content[:3000]
                    else:
                        styleguide_context = '\n### ' + label + ' (belangrijkste secties):\n' + content[:2500]
        except Exception:
            pass

        system = f"""{personality}

## ⚠️ VERPLICHT: MASTER_SOP & MASTER_STYLEGUIDE
Je MOET je output baseren op de volgende richtlijnen. Afwijking is niet toegestaan.
{sop_context}
{styleguide_context}

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

## OUTPUT FORMAAT (VERPLICHT)
Je output MOET een gestructureerd HSEQ rapport zijn met EXACT deze secties (in Markdown):

### 1. Scope & Doelstelling
Wat wordt onderzocht/beoordeeld. Doelgroep en toepassingsgebied.

### 2. Beleid & Wetgeving
Relevante wetgeving (Arbowet, BRZO, Seveso, Omgevingswet, etc.). Verwijs naar specifieke artikelen.

### 3. Analyse & Bevindingen
Gestructureerde bevindingen met prioriteitsclassificatie:
- **🔴 Hoog** — Kritieke afwijking, direct actie vereist
- **🟡 Midden** — Verbetering nodig binnen korte termijn
- **🟢 Laag** — Aanbeveling voor lange termijn

### 4. Risicobeoordeling
Risicotabel met kolommen: | Gefaar | Waarschijnlijkheid | Impact | Risicoklasse | Bestaande barrière |

### 5. Aanbevelingen
Genummerde, concrete aanbevelingen met prioriteit. Gebruik bullets.

### 6. Actielijst
TABEL met kolommen: | # | Actie | Verantwoordelijke | Deadline | Status | Prioriteit |

### 7. Bronvermelding
Gebruikte bronnen, referenties en verwezen wetgeving.

## CALL-OUTS (VERPLICHT bij relevante punten)
- > ⚠️ [waarschuwing] — voor veiligheidsrisico's en verplichtingen
- > ℹ️ [info] — voor context en uitleg
- > ✅ [succes] — voor goedgekeurde elementen en best practices

## 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.
Geen data dumps. Geen vage samenvattingen. Wereldklasse of niets.
Taal: Nederlands (tenzij anders aangegeven)."""

        user_parts = []
        if workflow_id and workflow_id in WORKFLOW_PROMPTS:
            user_parts.append(WORKFLOW_PROMPTS[workflow_id])
            if context:
                try:
                    answers = json.loads(context)
                    for k, v in answers.items():
                        user_parts.append(f"- {k}: {v}")
                except (json.JSONDecodeError, TypeError):
                    user_parts.append(f"Context: {context}")
        else:
            user_parts.append(task_desc)
            if context:
                user_parts.append(f"\nContext:\n{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}")
            except (json.JSONDecodeError, TypeError):
                pass

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

    def _ai_call(self, messages, max_retries=3):
        import time, ssl
        for attempt in range(max_retries):
            try:
                if AI_PROVIDER == 'zai':
                    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=120, context=ctx) as resp:
                        data = json.loads(resp.read().decode('utf-8'))
                        return data.get('choices', [{}])[0].get('message', {}).get('content', '')
                else:
                    payload = json.dumps({
                        "model": AI_MODEL,
                        "messages": messages,
                        "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}"
                    })
                    with urllib.request.urlopen(req, timeout=120) 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 ""

    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()


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

def register_executor_routes(flask_app):
    @flask_app.route('/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('/api/agents/executor/restart', methods=['POST'])
    def executor_restart():
        executor.stop()
        executor.start()
        return jsonify({'ok': True, 'message': 'Executor herstart'})

    @flask_app.route('/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'})
