# ============================================================
# MODULE: Document Scanner & Analyser — Standalone Module
# HSEQ Intelligence Dashboard
# Upload, server file explorer, ZAI GLM analyse, DOCX export
# Versie: 1.0.0 | Datum: 2026-04-20
# ============================================================

import os
import json
import base64
import sqlite3
import shutil
import requests
import urllib.request
import urllib.error
import mimetypes
from datetime import datetime
from flask import jsonify, request, render_template_string, send_file
from indexer import DB_PATH

# ─── Constanten ────────────────────────────────────────────────────────────

UPLOAD_DIR = os.path.join(os.path.dirname(__file__), 'uploads', 'scanner_temp')
ALLOWED_EXTENSIONS = {'pdf', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'docx', 'txt'}
MAX_FILE_SIZE = 50 * 1024 * 1024  # 50 MB
SERVER_BASE_PATH = '/root/Documents'
ZAI_API_URL = os.environ.get('AI_API_URL', 'https://api.z.ai/api/coding/paas/v4/chat/completions')
ZAI_API_KEY = os.environ.get('AI_API_KEY') or '43a6c7e3d7b240daafae006e8488f674.ivYYRRLySgUwgqVE'
ZAI_MODELS = {'glm-5-turbo': 'Snel & efficiënt (standaard)', 'glm-5.1': 'Diepgaande HSEQ-analyse'}
GEMINI_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}'

# ─── Database Init ─────────────────────────────────────────────────────────

def init_db_doc_scanner():
    """Maak scanner tabellen aan in de centrale SQLite database."""
    os.makedirs(UPLOAD_DIR, exist_ok=True)
    conn = sqlite3.connect(DB_PATH, check_same_thread=False)
    c = conn.cursor()

    c.execute('''CREATE TABLE IF NOT EXISTS scanner_analyses (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT,
        context TEXT,
        language TEXT DEFAULT 'NL',
        file_count INTEGER DEFAULT 0,
        file_names TEXT,
        status TEXT DEFAULT 'pending',
        result_json TEXT,
        error_msg TEXT,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        completed_at TEXT
    )''')

    c.execute('''CREATE TABLE IF NOT EXISTS scanner_files (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        analysis_id INTEGER,
        filename TEXT NOT NULL,
        filepath TEXT NOT NULL,
        file_size INTEGER DEFAULT 0,
        file_type TEXT,
        source TEXT DEFAULT 'upload',
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (analysis_id) REFERENCES scanner_analyses(id)
    )''')

    conn.commit()
    conn.close()


# ─── Hulpfuncties ──────────────────────────────────────────────────────────

def _allowed_file(filename):
    """Controleer of bestandsextensie toegestaan is."""
    ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
    return ext in ALLOWED_EXTENSIONS


def _safe_path(requested_path):
    """Valideer dat een pad binnen SERVER_BASE_PATH of UPLOAD_DIR blijft (path traversal protectie)."""
    real_base = os.path.realpath(SERVER_BASE_PATH)
    real_upload = os.path.realpath(UPLOAD_DIR)
    real_target = os.path.realpath(requested_path)
    if real_target.startswith(real_base) or real_target.startswith(real_upload):
        return real_target
    return None


def _human_size(size_bytes):
    """Converteer bytes naar leesbare vorm."""
    if size_bytes < 1024:
        return f"{size_bytes} B"
    elif size_bytes < 1024 * 1024:
        return f"{size_bytes / 1024:.1f} KB"
    else:
        return f"{size_bytes / (1024 * 1024):.1f} MB"


def _read_file_as_base64(filepath):
    """Lees bestand en retourneer (base64_data, mime_type)."""
    mime_type, _ = mimetypes.guess_type(filepath)
    if mime_type is None:
        ext = filepath.rsplit('.', 1)[-1].lower()
        mime_map = {
            'pdf': 'application/pdf',
            'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
            'gif': 'image/gif', 'webp': 'image/webp',
            'txt': 'text/plain', 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        }
        mime_type = mime_map.get(ext, 'application/octet-stream')

    with open(filepath, 'rb') as f:
        data = f.read()
    return base64.b64encode(data).decode('utf-8'), mime_type


def _call_zai_api(system_prompt, user_prompt, model, api_key, image_parts=None, max_tokens=8192):
    """Laag-niveau ZAI API call met multimodale support."""
    if image_parts:
        user_content = [{"type": "text", "text": user_prompt}] + image_parts
    else:
        user_content = user_prompt

    payload = {
        "model": model,
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_content}
        ],
        "temperature": 0.3,
        "max_tokens": max_tokens
    }
    try:
        resp = requests.post(ZAI_API_URL, json=payload, headers={
            'Authorization': f'Bearer {api_key}'
        }, timeout=300)
        resp.raise_for_status()
        result = resp.json()
        choices = result.get('choices', [])
        if choices:
            return choices[0].get('message', {}).get('content', ''), None
        return None, 'ZAI retourneerde geen choices.'
    except requests.exceptions.HTTPError as e:
        err_msg = ''
        try: err_msg = e.response.json().get('error', {}).get('message', str(e))
        except: err_msg = str(e)
        return None, f'ZAI API fout (HTTP {e.response.status_code}): {err_msg}'
    except Exception as e:
        return None, f'ZAI API fout: {str(e)}'


def _call_zai(file_paths, context, language, model='glm-5-turbo'):
    """
    Roep de ZAI/GLM API aan met bestandspaden.
    file_paths: lijst van (filepath, filename)
    model: 'glm-5-turbo' (standaard) of 'glm-5.1' (diepgaand)
    Retourneert: (analyse_tekst, foutmelding) — bij succes is foutmelding None.

    Voor grote documenten (>40K tekens): chunked analyse met synthese.
    """
    api_key = os.environ.get('AI_API_KEY') or ZAI_API_KEY
    if not api_key:
        return None, 'ZAI_API_KEY niet geconfigureerd in omgevingsvariabelen.'

    lang_label = 'Nederlands' if language == 'NL' else 'English'
    context_block = f"\nAnalyse-context: {context}" if context else ""

    # ── FASE 1: Alle bestanden inlezen ─────────────────────────────
    MAX_SINGLE_CHARS = 40000  # Tot dit formaat: enkele API call
    documents_block = ""
    image_parts = []
    pdf_chunks = []  # Voor chunked analyse

    for filepath, filename in file_paths:
        try:
            ext = filepath.rsplit('.', 1)[-1].lower() if '.' in filepath else ''
            if ext == 'pdf':
                import fitz
                doc = fitz.open(filepath)
                total_pages = len(doc)
                pages_text = [doc[i].get_text() for i in range(total_pages)]
                doc.close()
                content = '\n'.join(pages_text)

                # Groot document? Chunked analyse
                if len(content) > MAX_SINGLE_CHARS:
                    chunk_size = 60000
                    for i in range(0, len(content), chunk_size):
                        chunk = content[i:i+chunk_size]
                        page_range = f"pagina's ~{i * total_pages // len(content) + 1}-{min((i+chunk_size) * total_pages // len(content), total_pages)}"
                        pdf_chunks.append((filename, chunk, page_range, total_pages))
                    continue  # Niet toevoegen aan documents_block

            elif ext in ('png', 'jpg', 'jpeg', 'gif', 'webp'):
                import mimetypes
                mime = mimetypes.guess_type(filepath)[0] or 'image/png'
                with open(filepath, 'rb') as f:
                    img_b64 = base64.b64encode(f.read()).decode('utf-8')
                image_parts.append({
                    "type": "image_url",
                    "image_url": {"url": f"data:{mime};base64,{img_b64}"}
                })
                documents_block += f"\n\n--- DOCUMENT: {filename} ({mime}) ---\n[Afbeelding is bijgevoegd als multimodale content]\n--- EINDE DOCUMENT ---"
                continue
            elif ext == 'docx':
                try:
                    import docx
                    d = docx.Document(filepath)
                    content = '\n'.join([p.text for p in d.paragraphs])
                except ImportError:
                    with open(filepath, 'rb') as f:
                        content = f.read().decode('utf-8', errors='replace')[:MAX_SINGLE_CHARS]
                total_pages = None
            else:
                with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
                    content = f.read()
                total_pages = None

            documents_block += f"\n\n--- DOCUMENT: {filename} ---\n{content}\n--- EINDE DOCUMENT ---"

        except Exception as e:
            documents_block += f"\n\n--- DOCUMENT: {filename} ---\n[Kon bestand niet lezen: {str(e)}]\n--- EINDE DOCUMENT ---"

    # ── FASE 2: Chunked analyse voor grote PDF's ──────────────────
    if pdf_chunks:
        return _chunked_analysis(pdf_chunks, documents_block, image_parts,
                                  context_block, lang_label, model, api_key)

    # ── FASE 3: Standaard enkele call ─────────────────────────────
    system_prompt = (
        "Je bent een senior HSEQ documentanalist met 20+ jaar ervaring in de zware industrie, "
        "specifiek bij BRZO/Seveso-bedrijven. Je analyseert documenten op HSEQ-aspecten: "
        "veiligheid, compliance, risicobeheersing, milieuregelgeving en kwaliteit."
    )
    user_prompt = (
        f"Analyseer de volgende {len(file_paths)} document(en) in het {lang_label}.{context_block}\n\n"
        f"{documents_block}\n\n"
        "Genereer een gestructureerde analyse met de volgende secties:\n\n"
        "## 1. Leeswijzer\n"
        "Beschrijf kort de context en het doel van deze documentenset.\n\n"
        "## 2. Executive Summary\n"
        "Geef de kernboodschap in maximaal 200 woorden.\n\n"
        "## 3. HSEQ Highlights Matrix\n"
        "Maak een tabel met de volgende kolommen:\n"
        "| Thema | Risico | Compliance-eis | Actiepunt | Prioriteit |\n"
        "|-------|--------|---------------|-----------|------------|\n\n"
        "## 4. Synergieën & Hiaten\n"
        "- Synergieën: overlap of versterking tussen documenten\n"
        "- Hiaten: ontbrekende informatie of tegenstrijdigheden\n\n"
        "BELANGRIJK: Analyseer uitsluitend de tekst die hierboven is meegeleverd. "
        "Verzin NOOIT content, reconstrueer NOOIT ontbrekende teksten. "
        "Gebruik professionele HSEQ-terminologie. Wees feitelijk en beknopt. Geen AI-clichés."
    )

    return _call_zai_api(system_prompt, user_prompt, model, api_key, image_parts)


def _chunked_analysis(pdf_chunks, extra_docs, image_parts, context_block, lang_label, model, api_key):
    """
    Chunked analyse: analyseer elk deel met een volledige rapport-structuur.
    Geen synthese-call — elk chunk levert direct een gestructureerd rapport.
    Het eerste chunk bevat de leeswijzer en summary, de rest vullen de matrix en details.
    """
    system_prompt = (
        "Je bent een senior HSEQ documentanalist met 20+ jaar ervaring in de zware industrie, "
        "specifiek bij BRZO/Seveso-bedrijven."
    )

    chunk_results = []
    total_chunks = len(pdf_chunks)

    for idx, (filename, chunk_text, page_range, total_pages) in enumerate(pdf_chunks):
        is_first = (idx == 0)
        is_last = (idx == total_chunks - 1)

        if is_first:
            # Eerste chunk: leeswijzer + summary + begin matrix
            chunk_prompt = (
                f"Dit is DEEL 1/{total_chunks} van document '{filename}' ({total_pages} pagina's, {page_range}).\n\n"
                f"Analyseer dit deel en genereer:\n\n"
                "## 1. Leeswijzer\n"
                "Beschrijf het document, scope, doelgroep, wettelijk kader.\n\n"
                "## 2. Executive Summary\n"
                "Kernboodschap in max 250 woorden.\n\n"
                "## 3. Documentstructuur (deel 1)\n"
                "Welke hoofdstukken/secties komen in dit deel aan bod? Bevindingen per sectie.\n\n"
                "## 4. HSEQ Highlights (deel 1)\n"
                "| Thema | Risico | Compliance-eis | Actiepunt | Prioriteit |\n"
                "|-------|--------|---------------|-----------|------------|\n\n"
                f"TEKST:\n{chunk_text}\n\n"
                "Feitelijk. Specifieke waarden. Geen AI-clichés."
            )
        elif is_last:
            # Laatste chunk: actielijst + hiaten
            chunk_prompt = (
                f"Dit is DEEL {idx+1}/{total_chunks} (LAATSTE) van document '{filename}' ({page_range}).\n\n"
                f"Analyseer dit deel en genereer:\n\n"
                "## Documentstructuur (vervolg)\n"
                "Welke hoofdstukken/secties komen in dit deel aan bod? Bevindingen per sectie.\n\n"
                "## HSEQ Highlights (vervolg)\n"
                "| Thema | Risico | Compliance-eis | Actiepunt | Prioriteit |\n"
                "|-------|--------|---------------|-----------|------------|\n\n"
                "## Normen, Waarden & Scenario's\n"
                "Specifieke normatieve waarden, scenario's, berekeningen uit dit deel.\n\n"
                "## 6. Synergieën & Hiaten\n"
                "Hiaten, tegenstrijdigheden, aanbevelingen.\n\n"
                "## 7. Actielijst voor Inrichtinghouder\n"
                "Genummerde actiepunten met prioriteit (Hoog/Middel/Laar).\n\n"
                f"TEKST:\n{chunk_text}\n\n"
                "Feitelijk. Specifieke waarden. Geen AI-clichés."
            )
        else:
            # Middelste chunks: structuur + highlights
            chunk_prompt = (
                f"Dit is DEEL {idx+1}/{total_chunks} van document '{filename}' ({page_range}).\n\n"
                f"Analyseer dit deel en genereer:\n\n"
                "## Documentstructuur (vervolg)\n"
                "Welke hoofdstukken/secties? Bevindingen per sectie.\n\n"
                "## HSEQ Highlights (vervolg)\n"
                "| Thema | Risico | Compliance-eis | Actiepunt | Prioriteit |\n"
                "|-------|--------|---------------|-----------|------------|\n\n"
                "## Normen & Scenario's\n"
                "Specifieke waarden, scenario's, berekeningen.\n\n"
                f"TEKST:\n{chunk_text}\n\n"
                "Feitelijk. Specifieke waarden. Geen AI-clichés."
            )

        result, error = _call_zai_api(system_prompt, chunk_prompt, model, api_key, max_tokens=4096)
        if error:
            chunk_results.append(f"\n[Fout in deel {idx+1} ({page_range}): {error}]\n")
        else:
            chunk_results.append(f"\n--- {page_range} ---\n{result}\n")

    # Combineer tot eindrapport (geen extra API call)
    final_report = '\n'.join(chunk_results)
    return final_report, None


def _generate_docx(analysis_text, file_names, title, language):
    """Genereer een DOCX Kennisdossier. Fallback naar None als python-docx ontbreekt."""
    try:
        from docx import Document
        from docx.shared import Pt, Inches, Cm, RGBColor
        from docx.enum.text import WD_ALIGN_PARAGRAPH
        from docx.enum.section import WD_ORIENT
    except ImportError:
        return None

    doc = Document()

    # Standaard stijl
    style = doc.styles['Normal']
    font = style.font
    font.name = 'Inter'
    font.size = Pt(11)
    font.color.rgb = RGBColor(0x33, 0x33, 0x33)

    # Pagina marges
    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)

    # Header
    header = doc.sections[0].header
    hp = header.paragraphs[0]
    hp.text = 'JVG Consultancy — HSEQ Document Analyse'
    hp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    hf = hp.runs[0] if hp.runs else hp.add_run()
    hf.font.size = Pt(8)
    hf.font.color.rgb = RGBColor(0x00, 0x33, 0x66)

    # Footer
    footer = doc.sections[0].footer
    fp = footer.paragraphs[0]
    fp.text = f'Versie 1.0 | {datetime.now().strftime("%d-%m-%Y")} | Vertrouwelijk'
    fp.alignment = WD_ALIGN_PARAGRAPH.CENTER

    # Titel
    title_p = doc.add_heading(title or 'HSEQ Kennisdossier', level=0)
    title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    for run in title_p.runs:
        run.font.color.rgb = RGBColor(0x00, 0x33, 0x66)

    # Meta-informatie
    meta = doc.add_paragraph()
    meta.alignment = WD_ALIGN_PARAGRAPH.CENTER
    meta_run = meta.add_run(
        f"Aanmaakdatum: {datetime.now().strftime('%d-%m-%Y %H:%M')}\n"
        f"Aantal bronnen: {len(file_names)}\n"
        f"Taal: {language}"
    )
    meta_run.font.size = Pt(9)
    meta_run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)

    doc.add_page_break()

    # Inhoudsopgave placeholder
    doc.add_heading('Inhoudsopgave', level=1)
    doc.add_paragraph('[Automatisch gegenereerde inhoudsopgave — bijwerken in Word]', style='Intense Quote')

    doc.add_page_break()

    # Bronvermeldingen
    doc.add_heading('Bronvermeldingen', level=1)
    for i, fn in enumerate(file_names, 1):
        doc.add_paragraph(f'{i}. {fn}', style='List Number')
    doc.add_paragraph()

    # Analyse-inhoud (Markdown → Word secties)
    sections = analysis_text.split('## ')
    for section in sections:
        section = section.strip()
        if not section:
            continue
        lines = section.split('\n', 1)
        heading = lines[0].strip()
        body_text = lines[1].strip() if len(lines) > 1 else ''

        if heading:
            # Converteer Markdown tabel naar Word tabel
            if '|' in body_text and heading.lower().startswith(('3', 'highlights', 'matrix')):
                doc.add_heading(heading, level=2)
                _markdown_table_to_docx(doc, body_text)
            else:
                doc.add_heading(heading, level=2)
                # Verwerk paragrafen
                for para in body_text.split('\n\n'):
                    para = para.strip()
                    if para:
                        p = doc.add_paragraph(para)
                        p.paragraph_format.space_after = Pt(6)

    # Opslaan naar temp
    output_dir = os.path.join(UPLOAD_DIR, 'exports')
    os.makedirs(output_dir, exist_ok=True)
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    output_path = os.path.join(output_dir, f'kennisdossier_{timestamp}.docx')
    doc.save(output_path)
    return output_path


def _markdown_table_to_docx(doc, text):
    """Converteer een Markdown tabel naar een Word tabel."""
    from docx.shared import Pt, RGBColor
    rows = [r.strip() for r in text.split('\n') if r.strip().startswith('|')]
    if len(rows) < 2:
        doc.add_paragraph(text)
        return

    # Parse headers
    headers = [c.strip() for c in rows[0].split('|')[1:-1]]
    # Skip separator row
    data_rows = []
    for r in rows[2:]:
        cells = [c.strip() for c in r.split('|')[1:-1]]
        data_rows.append(cells)

    if not headers:
        doc.add_paragraph(text)
        return

    table = doc.add_table(rows=1 + len(data_rows), cols=len(headers))
    table.style = 'Light Grid Accent 1'

    # Header rij
    for i, h in enumerate(headers):
        cell = table.rows[0].cells[i]
        cell.text = h
        for p in cell.paragraphs:
            for run in p.runs:
                run.font.bold = True
                run.font.size = Pt(10)
                run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)

    # Data rijen
    for ri, row in enumerate(data_rows):
        for ci, val in enumerate(row):
            if ci < len(headers):
                table.rows[ri + 1].cells[ci].text = val

    doc.add_paragraph()


# ─── Routes ────────────────────────────────────────────────────────────────

def register_doc_scanner_routes(flask_app, page_func, BASE_PATH="/hseq-dashboard"):
    """Registreer alle Document Scanner routes."""

    # ─── Hoofdpagina ──────────────────────────────────────────────────
    @flask_app.route(BASE_PATH + '/doc-scanner')
    def doc_scanner_page():
        body = _render_main_page().replace('{{base_path}}', BASE_PATH)
        return page_func(body_html=body, active='doc-scanner', page_title='Document Scanner & Analyser')

    # ─── Bestanden uploaden ────────────────────────────────────────────
    @flask_app.route(BASE_PATH + '/doc-scanner/upload', methods=['POST'])
    def doc_scanner_upload():
        if 'files' not in request.files:
            return jsonify({'error': 'Geen bestanden ontvangen.'}), 400

        files = request.files.getlist('files')
        uploaded = []
        errors = []

        for f in files:
            if not f.filename:
                continue
            if not _allowed_file(f.filename):
                errors.append(f'{f.filename}: formaat niet ondersteund.')
                continue

            # Controleer bestandsgrootte
            f.seek(0, 2)
            size = f.tell()
            f.seek(0)
            if size > MAX_FILE_SIZE:
                errors.append(f'{f.filename}: te groot (max 50 MB).')
                continue

            # Sla op
            safe_name = f.filename.replace('/', '_').replace('\\', '_')
            dest = os.path.join(UPLOAD_DIR, safe_name)
            counter = 1
            base, ext = os.path.splitext(safe_name)
            while os.path.exists(dest):
                dest = os.path.join(UPLOAD_DIR, f'{base}_{counter}{ext}')
                counter += 1

            f.save(dest)
            uploaded.append({
                'name': os.path.basename(dest),
                'path': dest,
                'size': _human_size(os.path.getsize(dest)),
                'type': ext.lstrip('.').upper()
            })

        return jsonify({'uploaded': uploaded, 'errors': errors})

    # ─── Server file explorer — mappenlijst ────────────────────────────
    @flask_app.route(BASE_PATH + '/doc-scanner/browse')
    def doc_scanner_browse():
        req_path = request.args.get('path', SERVER_BASE_PATH)
        safe = _safe_path(req_path)
        if not safe or not os.path.isdir(safe):
            return jsonify({'error': 'Ongeldig pad.'}), 400

        ext_filter = request.args.get('ext', '')
        ext_list = set(ext_filter.split(',')) if ext_filter else ALLOWED_EXTENSIONS

        folders = []
        files = []

        try:
            for entry in sorted(os.scandir(safe), key=lambda e: (not e.is_dir(), e.name.lower())):
                if entry.is_dir():
                    folders.append({'name': entry.name, 'path': entry.path})
                elif entry.is_file():
                    ext = entry.name.rsplit('.', 1)[-1].lower() if '.' in entry.name else ''
                    if ext in ext_list:
                        files.append({
                            'name': entry.name,
                            'path': entry.path,
                            'size': _human_size(entry.stat().st_size),
                            'type': ext.upper()
                        })
        except PermissionError:
            return jsonify({'error': 'Geen toegang tot deze map.'}), 403

        return jsonify({
            'current_path': safe,
            'parent': os.path.dirname(safe) if safe != os.path.realpath(SERVER_BASE_PATH) else None,
            'folders': folders,
            'files': files
        })

    # ─── Analyse starten ───────────────────────────────────────────────
    @flask_app.route(BASE_PATH + '/doc-scanner/analyse', methods=['POST'])
    def doc_scanner_analyse():
        data = request.get_json(force=True)
        file_paths = data.get('files', [])
        context = data.get('context', '').strip()
        language = data.get('language', 'NL')

        if not file_paths:
            return jsonify({'error': 'Selecteer minimaal één bestand.'}), 400

        # Valideer alle paden
        validated = []
        for fp in file_paths:
            safe = _safe_path(fp)
            if safe and os.path.isfile(safe):
                validated.append(safe)

        if not validated:
            return jsonify({'error': 'Geen geldige bestanden gevonden.'}), 400

        # Bouw (path, name) tuples voor _call_zai (leest direct vanaf disk)
        file_refs = [(vp, os.path.basename(vp)) for vp in validated]

        # Sla analyse op in database
        conn = sqlite3.connect(DB_PATH, check_same_thread=False)
        c = conn.cursor()
        c.execute(
            'INSERT INTO scanner_analyses (title, context, language, file_count, file_names, status) VALUES (?,?,?,?,?,?)',
            (context[:200] if context else 'Document Analyse', context, language, len(validated),
             json.dumps([os.path.basename(v) for v in validated]), 'processing')
        )
        analysis_id = c.lastrowid

        for vp in validated:
            ext = vp.rsplit('.', 1)[-1].lower() if '.' in vp else ''
            c.execute(
                'INSERT INTO scanner_files (analysis_id, filename, filepath, file_size, file_type, source) VALUES (?,?,?,?,?,?)',
                (analysis_id, os.path.basename(vp), vp, os.path.getsize(vp), ext, 'server')
            )

        conn.commit()
        conn.close()

        # Start analyse op de achtergrond via ZAI chunked engine
        import subprocess, sys
        subprocess.Popen(
            [sys.executable, '/root/projects/jg/HSEQ/HSEQ-Intelligence-Monitor/app/_analyse_worker.py',
             json.dumps(file_refs), context, language, 'glm-5-turbo', str(analysis_id)],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )

        return jsonify({
            'analysis_id': analysis_id,
            'status': 'processing',
            'message': 'Analyse gestart. Even geduld...'
        })

    # ─── Analyse status polling ────────────────────────────────────────
    @flask_app.route(BASE_PATH + '/doc-scanner/analyse/status/<int:analysis_id>')
    def doc_scanner_analyse_status(analysis_id):
        conn = sqlite3.connect(DB_PATH, check_same_thread=False)
        c = conn.cursor()
        row = c.execute('SELECT status, result_json, error_msg FROM scanner_analyses WHERE id=?', (analysis_id,)).fetchone()
        conn.close()
        if not row:
            return jsonify({'error': 'Analyse niet gevonden.'}), 404
        status, result_json, error_msg = row
        resp = {'status': status, 'analysis_id': analysis_id}
        if status == 'completed' and result_json:
            resp['result'] = json.loads(result_json).get('text', '')
        elif status == 'error':
            resp['error'] = error_msg
        return jsonify(resp)

    # ─── Analysegeschiedenis ───────────────────────────────────────────
    @flask_app.route(BASE_PATH + '/doc-scanner/history')
    def doc_scanner_history():
        conn = sqlite3.connect(DB_PATH, check_same_thread=False)
        conn.row_factory = sqlite3.Row
        rows = conn.execute(
            'SELECT id, title, file_count, status, created_at, completed_at FROM scanner_analyses ORDER BY id DESC LIMIT 50'
        ).fetchall()
        conn.close()
        return jsonify([dict(r) for r in rows])

    # ─── DOCX Export ───────────────────────────────────────────────────
    @flask_app.route(BASE_PATH + '/doc-scanner/export-docx', methods=['POST'])
    def doc_scanner_export_docx():
        data = request.get_json(force=True)
        analysis_text = data.get('result', '')
        file_names = data.get('file_names', [])
        title = data.get('title', 'HSEQ Kennisdossier')
        language = data.get('language', 'NL')

        if not analysis_text:
            return jsonify({'error': 'Geen analyseresultaat om te exporteren.'}), 400

        path = _generate_docx(analysis_text, file_names, title, language)
        if path is None:
            # Fallback: python-docx niet beschikbaar
            return jsonify({'error': 'DOCX export niet beschikbaar (python-docx niet geïnstalleerd).'}), 501

        return jsonify({'download_url': f'{BASE_PATH}/doc-scanner/download?file={os.path.basename(path)}'})

    # ─── Download DOCX ────────────────────────────────────────────────
    @flask_app.route(BASE_PATH + '/doc-scanner/download')
    def doc_scanner_download():
        filename = request.args.get('file', '')
        if not filename or '..' in filename or '/' in filename:
            return jsonify({'error': 'Ongeldig bestand.'}), 400
        filepath = os.path.join(UPLOAD_DIR, 'exports', filename)
        if not os.path.isfile(filepath):
            return jsonify({'error': 'Bestand niet gevonden.'}), 404
        return send_file(filepath, as_attachment=True, download_name=filename)

    # ─── Oude uploads wissen ──────────────────────────────────────────
    @flask_app.route(BASE_PATH + '/doc-scanner/cleanup', methods=['POST'])
    def doc_scanner_cleanup():
        """Verwijder tijdelijke upload bestanden ouder dan 24 uur."""
        removed = 0
        cutoff = datetime.now().timestamp() - 86400
        for f in os.listdir(UPLOAD_DIR):
            fp = os.path.join(UPLOAD_DIR, f)
            if os.path.isfile(fp) and os.path.getmtime(fp) < cutoff:
                os.remove(fp)
                removed += 1
        return jsonify({'removed': removed})


# ─── HTML Templates ────────────────────────────────────────────────────────

def _render_main_page():
    """Genereer de hoofdpagina HTML voor de Document Scanner."""
    return '''
<style>
/* ── Scoped CSS voor Doc Scanner Module ── */
.ds-wrap{font-family:'Inter',system-ui,sans-serif;color:#333;max-width:1200px;margin:0 auto}
.ds-header{background:#003366;color:#fff;padding:24px 32px;border-radius:6px;margin-bottom:20px}
.ds-header h1{margin:0 0 4px;font-size:1.5rem;font-weight:700}
.ds-header p{margin:0;opacity:.85;font-size:.9rem}
.ds-card{background:#F8F9FA;border:1px solid #E5E7EB;border-radius:6px;padding:20px;margin-bottom:16px}
.ds-card h2{margin:0 0 12px;font-size:1.1rem;color:#003366}

/* Tabs */
.ds-tabs{display:flex;gap:0;margin-bottom:16px;border-bottom:2px solid #E5E7EB}
.ds-tab{padding:12px 24px;cursor:pointer;font-weight:600;color:#666;border-bottom:3px solid transparent;transition:all .2s}
.ds-tab:hover{color:#003366}
.ds-tab.active{color:#003366;border-bottom-color:#003366}
.ds-tab-content{display:none}
.ds-tab-content.active{display:block}

/* Upload zone */
.ds-dropzone{border:2px dashed #003366;border-radius:8px;padding:48px 24px;text-align:center;background:#fff;cursor:pointer;transition:all .2s}
.ds-dropzone:hover,.ds-dropzone.dragover{background:#E8F0FE;border-color:#4A90D9}
.ds-dropzone-icon{font-size:2.5rem;margin-bottom:8px}
.ds-dropzone-text{color:#666;font-size:.95rem}

/* File list */
.ds-file-list{margin-top:16px;max-height:300px;overflow-y:auto}
.ds-file-item{display:flex;align-items:center;gap:10px;padding:8px 12px;border-bottom:1px solid #E5E7EB}
.ds-file-item:nth-child(even){background:#F8F9FA}
.ds-file-item label{flex:1;cursor:pointer;font-size:.9rem}
.ds-file-item .ds-file-meta{font-size:.8rem;color:#888}

/* Server explorer */
.ds-explorer-path{background:#fff;border:1px solid #E5E7EB;border-radius:4px;padding:8px 12px;margin-bottom:12px;font-family:monospace;font-size:.85rem;color:#555;display:flex;align-items:center;gap:8px}
.ds-explorer-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:8px}
.ds-explorer-item{display:flex;align-items:center;gap:10px;padding:10px 12px;background:#fff;border:1px solid #E5E7EB;border-radius:4px;cursor:pointer;transition:background .15s}
.ds-explorer-item:hover{background:#E8F0FE}
.ds-explorer-item input[type=checkbox]{accent-color:#003366;transform:scale(1.2)}
.ds-folder-icon{color:#E8A317;font-size:1.2rem}
.ds-file-icon{color:#003366;font-size:1.1rem}

/* Controls */
.ds-controls{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-top:16px;padding:16px;background:#fff;border:1px solid #E5E7EB;border-radius:6px}
.ds-btn{padding:12px 24px;border:none;border-radius:6px;font-weight:600;cursor:pointer;font-size:.9rem;transition:opacity .2s}
.ds-btn:hover{opacity:.9}
.ds-btn-primary{background:#003366;color:#fff}
.ds-btn-success{background:#00A859;color:#fff}
.ds-btn-danger{background:#E03131;color:#fff}
.ds-btn:disabled{opacity:.4;cursor:not-allowed}
.ds-context-input{flex:1;min-width:200px;padding:10px 14px;border:1px solid #E5E7EB;border-radius:6px;font-size:.9rem}
.ds-select{padding:10px 14px;border:1px solid #E5E7EB;border-radius:6px;font-size:.9rem;background:#fff}

/* Resultaat */
.ds-result{margin-top:20px}
.ds-result-content{background:#fff;border:1px solid #E5E7EB;border-radius:6px;padding:24px;line-height:1.7}
.ds-result-content h2{color:#003366;border-bottom:2px solid #E5E7EB;padding-bottom:8px;margin-top:24px}
.ds-result-content h3{color:#003366;margin-top:16px}
.ds-result-content table{width:100%;border-collapse:collapse;margin:12px 0}
.ds-result-content th{background:#E5E7EB;padding:8px 12px;text-align:left;font-weight:600;font-size:.9rem}
.ds-result-content td{padding:8px 12px;border-bottom:1px solid #E5E7EB;font-size:.9rem}
.ds-result-content tr:nth-child(even) td{background:#F8F9FA}
.ds-result-content ul,.ds-result-content ol{padding-left:20px}
.ds-result-content li{margin-bottom:4px}

/* Progress */
.ds-progress{display:none;margin-top:16px;padding:16px;background:#fff;border:1px solid #E5E7EB;border-radius:6px;text-align:center}
.ds-progress.active{display:block}
.ds-spinner{display:inline-block;width:24px;height:24px;border:3px solid #E5E7EB;border-top-color:#003366;border-radius:50%;animation:ds-spin 1s linear infinite}
@keyframes ds-spin{to{transform:rotate(360deg)}}

/* Historie */
.ds-history{margin-top:24px}
.ds-history-item{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;background:#fff;border:1px solid #E5E7EB;border-radius:4px;margin-bottom:6px}
.ds-history-meta{font-size:.85rem;color:#666}
.ds-status-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:.75rem;font-weight:600}
.ds-status-completed{background:#D3F9D8;color:#2B8A3E}
.ds-status-error{background:#FFE3E3;color:#E03131}
.ds-status-processing{background:#FFF3BF;color:#E8A317}

/* Filter bar */
.ds-filter-bar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;align-items:center}
.ds-filter-chip{padding:4px 12px;border-radius:16px;font-size:.8rem;cursor:pointer;border:1px solid #E5E7EB;background:#fff;transition:all .15s}
.ds-filter-chip:hover{border-color:#003366}
.ds-filter-chip.active{background:#003366;color:#fff;border-color:#003366}

/* Responsive */
@media(max-width:768px){
  .ds-controls{flex-direction:column}
  .ds-explorer-grid{grid-template-columns:1fr}
  .ds-header{padding:16px}
}
</style>

<div class="ds-wrap">
  <!-- Header -->
  <div class="ds-header">
    <h1>📄 Document Scanner & Analyser</h1>
    <p>Upload of selecteer HSEQ-documenten voor geautomatiseerde analyse via AI</p>
  </div>

  <!-- Tabs -->
  <div class="ds-tabs">
    <div class="ds-tab active" onclick="DsApp.switchTab('upload')">⬆️ Upload</div>
    <div class="ds-tab" onclick="DsApp.switchTab('server')">🗂️ Server Bestanden</div>
    <div class="ds-tab" onclick="DsApp.switchTab('history')">📋 Historie</div>
  </div>

  <!-- Tab: Upload -->
  <div class="ds-tab-content active" id="ds-tab-upload">
    <div class="ds-card">
      <h2>Bestanden Uploaden</h2>
      <div class="ds-dropzone" id="ds-dropzone"
           ondragover="DsApp.onDragOver(event)"
           ondragleave="DsApp.onDragLeave(event)"
           ondrop="DsApp.onDrop(event)"
           onclick="document.getElementById('ds-file-input').click()">
        <div class="ds-dropzone-icon">📁</div>
        <div class="ds-dropzone-text">
          Sleep bestanden hierheen of <strong>klik om te bladeren</strong><br>
          <small>PDF, PNG, JPG, GIF, WebP, DOCX, TXT — max 50 MB per bestand</small>
        </div>
      </div>
      <input type="file" id="ds-file-input" multiple accept=".pdf,.png,.jpg,.jpeg,.gif,.webp,.docx,.txt" style="display:none" onchange="DsApp.onFileSelect(event)">
      <div class="ds-file-list" id="ds-upload-list"></div>
    </div>
  </div>

  <!-- Tab: Server Explorer -->
  <div class="ds-tab-content" id="ds-tab-server">
    <div class="ds-card">
      <h2>Server Bestanden</h2>
      <div class="ds-filter-bar" id="ds-ext-filters"></div>
      <div class="ds-explorer-path" id="ds-current-path">
        <button class="ds-btn" style="padding:4px 10px;font-size:.8rem" onclick="DsApp.navigateUp()">⬆️</button>
        <span id="ds-path-text">/root/projects/jg/</span>
      </div>
      <div class="ds-explorer-grid" id="ds-explorer-grid">
        <em style="color:#888">Map laden...</em>
      </div>
      <div class="ds-file-list" id="ds-server-file-list"></div>
    </div>
  </div>

  <!-- Tab: Historie -->
  <div class="ds-tab-content" id="ds-tab-history">
    <div class="ds-card">
      <h2>Analysegeschiedenis</h2>
      <div id="ds-history-list">
        <em style="color:#888">Geschiedenis laden...</em>
      </div>
    </div>
  </div>

  <!-- Gedeelde Controls -->
  <div class="ds-controls">
    <input type="text" class="ds-context-input" id="ds-context" placeholder="Analyse-context (optioneel): bijv. 'BRZO compliance check voor opslag x'">
    <select class="ds-select" id="ds-language">
      <option value="NL">🇳🇱 Nederlands</option>
      <option value="EN">🇬🇧 English</option>
    </select>
    <select class="ds-select" id="ds-model">
      <option value="glm-5-turbo">⚡ GLM-5 Turbo (snel)</option>
      <option value="glm-5.1">🧠 GLM-5.1 (diepgaand)</option>
    </select>
    <button class="ds-btn ds-btn-primary" id="ds-analyse-btn" disabled onclick="DsApp.startAnalyse()">
      🔍 Analyseren
    </button>
  </div>

  <!-- Progress -->
  <div class="ds-progress" id="ds-progress">
    <div class="ds-spinner"></div>
    <p style="margin-top:8px;color:#555"><strong>Analyse wordt uitgevoerd...</strong><br><small>Dit kan enkele minuten duren afhankelijk van het aantal bestanden.</small></p>
  </div>

  <!-- Resultaat -->
  <div class="ds-result" id="ds-result" style="display:none">
    <div class="ds-card">
      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
        <h2 style="margin:0">📊 Analyseresultaat</h2>
        <div style="display:flex;gap:8px">
          <button class="ds-btn ds-btn-success" onclick="DsApp.exportDocx()">📥 Export DOCX</button>
          <button class="ds-btn ds-btn-primary" onclick="DsApp.copyResult()">📋 Kopieer</button>
        </div>
      </div>
      <div class="ds-result-content" id="ds-result-content"></div>
    </div>
  </div>
</div>

<script>
/* ── Scoped JavaScript (IIFE namespace) ── */
const DsApp = (() => {
  // State
  const state = {
    uploadedFiles: [],     // {name, path, size, type}
    serverFiles: [],       // {name, path, size, type}
    currentPath: '/root/projects/jg/',
    lastResult: null,
    lastFileNames: [],
    activeExtFilter: ''
  };

  // ── Tabs ──────────────────────────────────────────────────────────
  function switchTab(tab) {
    document.querySelectorAll('.ds-tab').forEach((t, i) => {
      t.classList.toggle('active', ['upload','server','history'][i] === tab);
    });
    document.querySelectorAll('.ds-tab-content').forEach(c => c.classList.remove('active'));
    document.getElementById('ds-tab-' + tab).classList.add('active');
    if (tab === 'server') browsePath(state.currentPath);
    if (tab === 'history') loadHistory();
  }

  // ── Upload ────────────────────────────────────────────────────────
  function onDragOver(e) {
    e.preventDefault();
    document.getElementById('ds-dropzone').classList.add('dragover');
  }
  function onDragLeave(e) {
    document.getElementById('ds-dropzone').classList.remove('dragover');
  }
  function onDrop(e) {
    e.preventDefault();
    document.getElementById('ds-dropzone').classList.remove('dragover');
    const files = e.dataTransfer.files;
    uploadFiles(files);
  }
  function onFileSelect(e) {
    uploadFiles(e.target.files);
    e.target.value = '';
  }

  function uploadFiles(fileList) {
    const fd = new FormData();
    for (const f of fileList) fd.append('files', f);

    fetch('{{base_path}}/doc-scanner/upload', {method:'POST', body: fd, credentials:'same-origin'})
      .then(r => r.json())
      .then(data => {
        if (data.uploaded) {
          state.uploadedFiles.push(...data.uploaded);
          renderFileList('upload');
        }
        if (data.errors && data.errors.length) {
          alert('Fouten:\\n' + data.errors.join('\\n'));
        }
        updateAnalyseBtn();
      })
      .catch(err => alert('Upload fout: ' + err));
  }

  function removeUploadedFile(idx) {
    state.uploadedFiles.splice(idx, 1);
    renderFileList('upload');
    updateAnalyseBtn();
  }

  // ── Server Explorer ───────────────────────────────────────────────
  function browsePath(path) {
    let url = '{{base_path}}/doc-scanner/browse?path=' + encodeURIComponent(path);
    if (state.activeExtFilter) url += '&ext=' + state.activeExtFilter;

    fetch(url, {credentials:'same-origin'}).then(r => r.json()).then(data => {
      if (data.error) { alert(data.error); return; }
      state.currentPath = data.current_path;
      document.getElementById('ds-path-text').textContent = data.current_path;

      const grid = document.getElementById('ds-explorer-grid');
      grid.innerHTML = '';

      if (data.parent) {
        grid.innerHTML += `<div class="ds-explorer-item" onclick="DsApp.navigateUp()">
          <span class="ds-folder-icon">📁</span><span>.. (omhoog)</span></div>`;
      }

      for (const f of data.folders) {
        grid.innerHTML += `<div class="ds-explorer-item" onclick="DsApp.browsePath('${f.path.replace(/'/g,"\\\\'")}')">
          <span class="ds-folder-icon">📁</span><span>${escHtml(f.name)}</span></div>`;
      }

      const sfl = document.getElementById('ds-server-file-list');
      sfl.innerHTML = '';
      state.serverFiles = data.files;

      for (let i = 0; i < data.files.length; i++) {
        const f = data.files[i];
        sfl.innerHTML += `<div class="ds-file-item">
          <input type="checkbox" id="ds-sf-${i}" onchange="DsApp.updateAnalyseBtn()">
          <label for="ds-sf-${i}"><span class="ds-file-icon">📄</span> ${escHtml(f.name)}</label>
          <span class="ds-file-meta">${escHtml(f.size)} | ${escHtml(f.type)}</span>
        </div>`;
      }
      updateAnalyseBtn();
    }).catch(err => alert('Browse fout: ' + err));
  }

  function navigateUp() {
    const parent = state.currentPath.replace(/[/][^/]+[/]?$/, '') || '/root/projects/jg';
    browsePath(parent);
  }

  function setExtFilter(ext) {
    state.activeExtFilter = ext;
    document.querySelectorAll('.ds-filter-chip').forEach(c => {
      c.classList.toggle('active', c.dataset.ext === ext);
    });
    browsePath(state.currentPath);
  }

  // ── Render ────────────────────────────────────────────────────────
  function renderFileList(source) {
    const el = document.getElementById('ds-upload-list');
    el.innerHTML = '';
    state.uploadedFiles.forEach((f, i) => {
      el.innerHTML += `<div class="ds-file-item">
        <input type="checkbox" id="ds-uf-${i}" checked onchange="DsApp.updateAnalyseBtn()">
        <label for="ds-uf-${i}">📄 ${escHtml(f.name)}</label>
        <span class="ds-file-meta">${escHtml(f.size)} | ${escHtml(f.type)}</span>
        <button class="ds-btn ds-btn-danger" style="padding:4px 8px;font-size:.75rem" onclick="DsApp.removeUploadedFile(${i})">✕</button>
      </div>`;
    });
  }

  function updateAnalyseBtn() {
    const sel = getSelectedFiles();
    document.getElementById('ds-analyse-btn').disabled = sel.length === 0;
  }

  function getSelectedFiles() {
    const paths = [];
    // Upload bestanden
    state.uploadedFiles.forEach((f, i) => {
      const cb = document.getElementById('ds-uf-' + i);
      if (cb && cb.checked) paths.push(f.path);
    });
    // Server bestanden
    state.serverFiles.forEach((f, i) => {
      const cb = document.getElementById('ds-sf-' + i);
      if (cb && cb.checked) paths.push(f.path);
    });
    return paths;
  }

  // ── Analyse ───────────────────────────────────────────────────────
  function startAnalyse() {
    const files = getSelectedFiles();
    if (!files.length) return;

    const context = document.getElementById('ds-context').value;
    const language = document.getElementById('ds-language').value;
    const model = document.getElementById('ds-model').value;

    document.getElementById('ds-progress').classList.add('active');
    document.getElementById('ds-result').style.display = 'none';
    document.getElementById('ds-analyse-btn').disabled = true;

    fetch('{{base_path}}/doc-scanner/analyse', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({files, context, language, model}),
      credentials: 'same-origin'
    })
    .then(r => r.json())
    .then(data => {
      if (data.error) {
        document.getElementById('ds-progress').classList.remove('active');
        document.getElementById('ds-analyse-btn').disabled = false;
        alert('Analyse fout: ' + data.error);
        return;
      }
      // Analyse gestart — poll voor resultaat
      pollAnalysis(data.analysis_id);
    })
    .catch(err => {
      document.getElementById('ds-progress').classList.remove('active');
      document.getElementById('ds-analyse-btn').disabled = false;
      alert('Fout: ' + err);
    });
  }

  function pollAnalysis(analysisId) {
    var attempts = 0;
    var maxAttempts = 200; // 10 minuten max max (120 x 3s)
    var pollInterval = setInterval(function() {
      attempts++;
      if (attempts > maxAttempts) {
        clearInterval(pollInterval);
        document.getElementById('ds-progress').classList.remove('active');
        document.getElementById('ds-analyse-btn').disabled = false;
        alert('Analyse duurt te lang. Probeer het later opnieuw of gebruik een kleiner bestand.');
        return;
      }
      fetch('{{base_path}}/doc-scanner/analyse/status/' + analysisId, {credentials: 'same-origin'})
        .then(function(r) { return r.json(); })
        .then(function(data) {
          if (data.status === 'completed') {
            clearInterval(pollInterval);
            document.getElementById('ds-progress').classList.remove('active');
            document.getElementById('ds-analyse-btn').disabled = false;
            state.lastResult = data.result;
            state.lastFileNames = [];
            renderResult(data.result);
          } else if (data.status === 'error') {
            clearInterval(pollInterval);
            document.getElementById('ds-progress').classList.remove('active');
            document.getElementById('ds-analyse-btn').disabled = false;
            alert('Analyse fout: ' + (data.error || 'Onbekende fout'));
          }
          // status === 'processing' → blijf pollen
        })
        .catch(function(err) {
          clearInterval(pollInterval);
          document.getElementById('ds-progress').classList.remove('active');
          document.getElementById('ds-analyse-btn').disabled = false;
          alert('Poll fout: ' + err);
        });
    }, 3000);
  }

  function renderResult(md) {
    document.getElementById('ds-result').style.display = 'block';
    // Eenvoudige Markdown → HTML conversie
    let html = escHtml(md);
    // Headers
    html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
    html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
    // Vetgedrukt
    html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
    // Tabellen
    var lines = html.split(String.fromCharCode(10));
    var inTable = false;
    var tableHtml = '';
    var resultParts = [];
    for (var li = 0; li < lines.length; li++) {
      var line = lines[li].trim();
      if (line.indexOf('|') === 0 && line.lastIndexOf('|') === line.length - 1) {
        if (!inTable) { inTable = true; tableHtml = '<table>'; }
        var cells = line.substring(1, line.length - 1).split('|');
        if (li + 1 < lines.length && /^[|][\\s-|]+[|]$/.test(lines[li + 1].trim())) {
          li++; continue;
        }
        var tag = tableHtml === '<table>' ? 'th' : 'td';
        tableHtml += '<tr>' + cells.map(function(c) { return '<' + tag + '>' + c.trim() + '</' + tag + '>'; }).join('') + '</tr>';
      } else {
        if (inTable) { tableHtml += '</table>'; resultParts.push(tableHtml); inTable = false; tableHtml = ''; }
        resultParts.push(lines[li]);
      }
    }
    if (inTable) { tableHtml += '</table>'; resultParts.push(tableHtml); }
    html = resultParts.join(String.fromCharCode(10));
    // Lijsten
    html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
    html = html.replace(/(<li>.*<\\/li>[\\n]?)+/g, '<ul>$&</ul>');
    // Paragrafen
    var NL = String.fromCharCode(10);
    html = html.replace(new RegExp(NL + NL, 'g'), '</p><p>');
    html = '<p>' + html + '</p>';
    html = html.replace(/<p><\\/p>/g, '');

    document.getElementById('ds-result-content').innerHTML = html;
    document.getElementById('ds-result').scrollIntoView({behavior:'smooth'});
  }

  // ── Export ─────────────────────────────────────────────────────────
  function exportDocx() {
    if (!state.lastResult) return;
    const context = document.getElementById('ds-context').value;

    fetch('{{base_path}}/doc-scanner/export-docx', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({
        result: state.lastResult,
        file_names: state.lastFileNames,
        title: context || 'HSEQ Kennisdossier',
        language: document.getElementById('ds-language').value
      }),
      credentials: 'same-origin'
    })
    .then(r => r.json())
    .then(data => {
      if (data.error) { alert(data.error); return; }
      if (data.download_url) {
        window.location.href = data.download_url;
      }
    })
    .catch(err => alert('Export fout: ' + err));
  }

  function copyResult() {
    const el = document.getElementById('ds-result-content');
    const range = document.createRange();
    range.selectNodeContents(el);
    window.getSelection().removeAllRanges();
    window.getSelection().addRange(range);
    document.execCommand('copy');
    window.getSelection().removeAllRanges();
    alert('Resultaat gekopieerd naar klembord.');
  }

  // ── Historie ──────────────────────────────────────────────────────
  function loadHistory() {
    fetch('{{base_path}}/doc-scanner/history', {credentials:'same-origin'})
      .then(r => r.json())
      .then(rows => {
        const el = document.getElementById('ds-history-list');
        if (!rows.length) {
          el.innerHTML = '<em style="color:#888">Nog geen analyses uitgevoerd.</em>';
          return;
        }
        el.innerHTML = rows.map(r => {
          const badge = r.status === 'completed' ? 'ds-status-completed' :
                        r.status === 'error' ? 'ds-status-error' : 'ds-status-processing';
          const date = r.created_at ? r.created_at.substring(0, 16).replace('T', ' ') : '';
          return '<div class="ds-history-item"><div><strong>' + escHtml(r.title || 'Analyse #' + r.id) + '</strong><span class="ds-history-meta"> — ' + r.file_count + ' bestand(en) — ' + date + '</span></div><span class="ds-status-badge ' + badge + '">' + r.status + '</span></div>';
        }).join('');
      })
      .catch(function() {
        document.getElementById('ds-history-list').innerHTML = '<em style="color:#888">Kon geschiedenis niet laden.</em>';
      });
  }

  // ── Hulp ──────────────────────────────────────────────────────────
  function escHtml(s) {
    const d = document.createElement('div');
    d.textContent = s || '';
    return d.innerHTML;
  }

  // ── Init ──────────────────────────────────────────────────────────
  function init() {
    var filters = document.getElementById('ds-ext-filters');
    ['','pdf','png','jpg','docx','txt'].forEach(function(ext) {
      var label = ext || 'Alle';
      filters.innerHTML += '<span class="ds-filter-chip ' + (ext === '' ? 'active' : '') + '" data-ext="' + ext + '" onclick="DsApp.setExtFilter(&quot;' + ext + '&quot;)">' + label + '</span>';
    });
    // Initieel: knop disabled zolang er geen bestanden zijn
    updateAnalyseBtn();
  }

  // Publieke API
  return {
    switchTab, onDragOver, onDragLeave, onDrop, onFileSelect,
    removeUploadedFile, browsePath, navigateUp, setExtFilter,
    updateAnalyseBtn, startAnalyse, exportDocx, copyResult,
    loadHistory, init
  };
})();

// Start
document.addEventListener('DOMContentLoaded', DsApp.init);
</script>
'''


# ─── Integratie-instructies ────────────────────────────────────────────────
#
# Voeg de volgende regels toe aan het EINDE van app.py:
#
#   from module_doc_scanner import init_db_doc_scanner, register_doc_scanner_routes
#   init_db_doc_scanner()
#   register_doc_scanner_routes(app, page, BASE_PATH)
#
# ─── Einde module ──────────────────────────────────────────────────────────
