# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Media and utility tools (render, TTS, STT, image gen, charts, neutralize, code exec)."""

import logging
from typing import Any, Dict, List, Optional

from modules.serviceCenter.services.serviceAgent.datamodelAgent import ToolResult
from modules.serviceCenter.services.serviceAgent.toolRegistry import ToolRegistry

from modules.serviceCenter.services.serviceAgent.coreTools._helpers import (
    _getOrCreateTempFolder,
    _looksLikeBinary,
    _resolveFileScope,
    _MAX_TOOL_RESULT_CHARS,
)

logger = logging.getLogger(__name__)


def _registerMediaTools(registry: ToolRegistry, services):
    """Auto-extracted from registerCoreTools."""
    # ---- Document rendering tool ----

    def _markdownToDocumentJson(markdown: str, title: str, language: str = "de") -> Dict[str, Any]:
        """Convert markdown content to the standard document JSON format expected by renderers."""
        import re as _re

        sections = []
        order = 0
        lines = markdown.split("\n")
        i = 0

        def _nextId():
            nonlocal order
            order += 1
            return f"s_{order}"

        while i < len(lines):
            line = lines[i]

            # --- Headings ---
            headingMatch = _re.match(r'^(#{1,6})\s+(.+)', line)
            if headingMatch:
                level = len(headingMatch.group(1))
                text = headingMatch.group(2).strip()
                sections.append({
                    "id": _nextId(), "content_type": "heading", "order": order,
                    "elements": [{"content": {"text": text, "level": level}}],
                })
                i += 1
                continue

            # --- Fenced code blocks ---
            codeMatch = _re.match(r'^```(\w*)', line)
            if codeMatch:
                lang = codeMatch.group(1) or "text"
                codeLines = []
                i += 1
                while i < len(lines) and not lines[i].startswith("```"):
                    codeLines.append(lines[i])
                    i += 1
                i += 1
                sections.append({
                    "id": _nextId(), "content_type": "code_block", "order": order,
                    "elements": [{"content": {"code": "\n".join(codeLines), "language": lang}}],
                })
                continue

            # --- Tables ---
            tableMatch = _re.match(r'^\|(.+)\|$', line)
            if tableMatch and (i + 1) < len(lines) and _re.match(r'^\|[\s\-:|]+\|$', lines[i + 1]):
                headerCells = [c.strip() for c in tableMatch.group(1).split("|")]
                i += 2
                rows = []
                while i < len(lines) and _re.match(r'^\|(.+)\|$', lines[i]):
                    rowCells = [c.strip() for c in lines[i][1:-1].split("|")]
                    rows.append(rowCells)
                    i += 1
                sections.append({
                    "id": _nextId(), "content_type": "table", "order": order,
                    "elements": [{"content": {"headers": headerCells, "rows": rows}}],
                })
                continue

            # --- Bullet / numbered lists ---
            listMatch = _re.match(r'^(\s*)([-*+]|\d+[.)]) (.+)', line)
            if listMatch:
                isNumbered = bool(_re.match(r'\d+[.)]', listMatch.group(2)))
                items = []
                while i < len(lines) and _re.match(r'^(\s*)([-*+]|\d+[.)]) (.+)', lines[i]):
                    m = _re.match(r'^(\s*)([-*+]|\d+[.)]) (.+)', lines[i])
                    items.append({"text": m.group(3).strip()})
                    i += 1
                sections.append({
                    "id": _nextId(), "content_type": "bullet_list", "order": order,
                    "elements": [{"content": {"items": items, "list_type": "numbered" if isNumbered else "bullet"}}],
                })
                continue

            # --- Empty lines (skip) ---
            if not line.strip():
                i += 1
                continue

            # --- Images: ![alt](file:fileId) or ![alt](url) ---
            imgMatch = _re.match(r'^!\[([^\]]*)\]\(([^)]+)\)', line)
            if imgMatch:
                altText = imgMatch.group(1).strip() or "Image"
                src = imgMatch.group(2).strip()
                fileId = ""
                if src.startswith("file:"):
                    fileId = src[5:]
                sections.append({
                    "id": _nextId(), "content_type": "image", "order": order,
                    "elements": [{
                        "content": {
                            "altText": altText,
                            "base64Data": "",
                            "_fileRef": fileId,
                            "_srcUrl": src if not fileId else "",
                        }
                    }],
                })
                i += 1
                continue

            # --- Paragraph (collect consecutive non-empty lines) ---
            paraLines = []
            while i < len(lines) and lines[i].strip() and not _re.match(r'^(#{1,6}\s|```|\|.+\||!\[|(\s*)([-*+]|\d+[.)]) )', lines[i]):
                paraLines.append(lines[i])
                i += 1
            if paraLines:
                sections.append({
                    "id": _nextId(), "content_type": "paragraph", "order": order,
                    "elements": [{"content": {"text": " ".join(paraLines)}}],
                })
                continue

            i += 1

        if not sections:
            sections.append({
                "id": _nextId(), "content_type": "paragraph", "order": order,
                "elements": [{"content": {"text": markdown.strip() or "(empty)"}}],
            })

        return {
            "metadata": {
                "split_strategy": "single_document",
                "source_documents": [],
                "extraction_method": "agent_rendering",
                "title": title,
                "language": language,
            },
            "documents": [{
                "id": "doc_1",
                "title": title,
                "sections": sections,
            }],
        }

    async def _renderDocument(args: Dict[str, Any], context: Dict[str, Any]):
        """Render agent-produced markdown content into any document format via the RendererRegistry."""
        import re as _re
        sourceFileId = (args.get("sourceFileId") or "").strip()
        content = args.get("content", "")
        if not isinstance(content, str):
            content = str(content) if content is not None else ""
        outputFormat = args.get("outputFormat", "pdf")
        title = args.get("title", "Document")
        language = args.get("language", "de")

        if sourceFileId:
            try:
                dbMgmt = services.chat.interfaceDbComponent
                fileRow = dbMgmt.getFile(sourceFileId)
                if not fileRow:
                    return ToolResult(
                        toolCallId="",
                        toolName="renderDocument",
                        success=False,
                        error=f"sourceFileId not found: {sourceFileId}",
                    )
                rawBytes = dbMgmt.getFileData(sourceFileId)
                if not rawBytes:
                    return ToolResult(
                        toolCallId="",
                        toolName="renderDocument",
                        success=False,
                        error=f"sourceFileId has no data: {sourceFileId}",
                    )
                try:
                    content = rawBytes.decode("utf-8")
                except UnicodeDecodeError:
                    content = rawBytes.decode("latin-1", errors="replace")
            except Exception as e:
                return ToolResult(
                    toolCallId="",
                    toolName="renderDocument",
                    success=False,
                    error=f"Could not read sourceFileId: {e}",
                )

        if not (content or "").strip():
            return ToolResult(
                toolCallId="",
                toolName="renderDocument",
                success=False,
                error=(
                    "Provide non-empty `content` (markdown) or `sourceFileId` (id of a .md/.txt from writeFile). "
                    "For long documents use writeFile create+append, then renderDocument(sourceFileId=...)."
                ),
            )

        modelMaxTokens = context.get("modelMaxOutputTokens", 0)
        _inlineCharLimit = int(modelMaxTokens * 3 * 0.5) if modelMaxTokens > 0 else 6000
        _inlineCharLimit = max(_inlineCharLimit, 3000)

        if not sourceFileId and len(content) > _inlineCharLimit:
            return ToolResult(
                toolCallId="",
                toolName="renderDocument",
                success=False,
                error=(
                    f"Inline `content` is {len(content)} chars — over the {_inlineCharLimit} char limit "
                    f"(derived from model output budget of {modelMaxTokens} tokens). "
                    "Large documents must use the file path:\n"
                    "1. writeFile(mode='create', name='draft.md', content=<first ~5000 chars>)\n"
                    "2. writeFile(mode='append', fileId=<id>, content=<next chunk>) — repeat as needed\n"
                    "3. renderDocument(sourceFileId=<id>, outputFormat='pdf', title='...')\n"
                    "This avoids output truncation entirely."
                ),
            )

        try:
            structuredContent = _markdownToDocumentJson(content, title, language)

            # Resolve image file references (file:fileId) to base64 data from Knowledge Store
            knowledgeService = None
            try:
                knowledgeService = services.getService("knowledge")
            except Exception as e:
                logger.warning(f"renderDocument: knowledge service unavailable: {e}")
            resolvedImages = 0
            for doc in structuredContent.get("documents", []):
                for section in doc.get("sections", []):
                    if section.get("content_type") != "image":
                        continue
                    for element in section.get("elements", []):
                        contentObj = element.get("content", {})
                        fileRef = contentObj.get("_fileRef", "")
                        if not fileRef or contentObj.get("base64Data"):
                            continue
                        if knowledgeService:
                            chunks = knowledgeService._knowledgeDb.getContentChunks(fileRef)
                            imageChunks = [c for c in (chunks or []) if c.get("contentType") == "image"]
                            if imageChunks:
                                contentObj["base64Data"] = imageChunks[0].get("data", "")
                                chunkMime = imageChunks[0].get("contextRef", {}).get("mimeType", "image/png")
                                contentObj["mimeType"] = chunkMime
                                resolvedImages += 1
                        if not contentObj.get("base64Data"):
                            try:
                                rawBytes = services.chat.getFileData(fileRef)
                                if rawBytes:
                                    import base64 as _b64
                                    contentObj["base64Data"] = _b64.b64encode(rawBytes).decode("ascii")
                                    contentObj["mimeType"] = "image/png"
                                    resolvedImages += 1
                            except Exception as e:
                                logger.warning(f"renderDocument: image resolve failed for fileRef={fileRef}: {e}")
                        contentObj.pop("_fileRef", None)
                        contentObj.pop("_srcUrl", None)

            sectionCount = len(structuredContent.get("documents", [{}])[0].get("sections", []))
            logger.info(f"renderDocument: parsed {sectionCount} sections from markdown ({len(content)} chars), resolved {resolvedImages} image(s), format={outputFormat}")

            generationService = services.getService("generation")
            documents = await generationService.renderReport(
                extractedContent=structuredContent,
                outputFormat=outputFormat,
                language=language,
                title=title,
                userPrompt=content,
            )

            if not documents:
                return ToolResult(toolCallId="", toolName="renderDocument", success=False, error="Rendering produced no output")

            savedFiles = []
            sideEvents = []
            chatService = services.chat

            sanitizedTitle = _re.sub(r'[^\w._-]', '_', title, flags=_re.UNICODE).strip('_') or "document"

            for doc in documents:
                docData = doc.documentData if hasattr(doc, "documentData") else b""
                docName = doc.filename if hasattr(doc, "filename") else f"{sanitizedTitle}.{outputFormat}"
                docMime = doc.mimeType if hasattr(doc, "mimeType") else "application/octet-stream"

                if not docName.lower().endswith(f".{outputFormat}"):
                    docName = f"{sanitizedTitle}.{outputFormat}"

                fileItem = None
                if hasattr(chatService.interfaceDbComponent, "saveGeneratedFile"):
                    fileItem = chatService.interfaceDbComponent.saveGeneratedFile(docData, docName, docMime)
                else:
                    fileItem, _ = chatService.interfaceDbComponent.saveUploadedFile(docData, docName)

                if fileItem:
                    fid = fileItem.id if hasattr(fileItem, "id") else fileItem.get("id", "?")
                    fiId = context.get("featureInstanceId") or (services.featureInstanceId if services else "")
                    if fiId:
                        chatService.interfaceDbComponent.updateFile(fid, {"featureInstanceId": fiId})
                    tempFolderId = _getOrCreateTempFolder(chatService)
                    if tempFolderId:
                        chatService.interfaceDbComponent.updateFile(fid, {"folderId": tempFolderId})
                    savedFiles.append(f"- {docName} (id: {fid})")
                    sideEvents.append({
                        "type": "fileCreated",
                        "data": {
                            "fileId": fid,
                            "fileName": docName,
                            "mimeType": docMime,
                            "fileSize": len(docData),
                        },
                    })

            result = f"Rendered {len(documents)} document(s):\n" + "\n".join(savedFiles)
            return ToolResult(toolCallId="", toolName="renderDocument", success=True, data=result, sideEvents=sideEvents)

        except Exception as e:
            logger.error(f"renderDocument failed: {e}")
            return ToolResult(toolCallId="", toolName="renderDocument", success=False, error=str(e))

    registry.register(
        "renderDocument", _renderDocument,
        description=(
            "Render markdown into a document file (PDF, DOCX, XLSX, PPTX, CSV, HTML, MD, JSON, TXT). "
            "For long documents: write markdown with writeFile (mode=create then append chunks), then call this tool with "
            "`sourceFileId` only (tiny JSON — avoids model output truncation). For short docs you may pass `content` inline. "
            "Images: ![alt text](file:fileId) in the markdown."
        ),
        parameters={
            "type": "object",
            "properties": {
                "content": {
                    "type": "string",
                    "description": "Full markdown inline. Prefer `sourceFileId` when the document is large (many KB).",
                },
                "sourceFileId": {
                    "type": "string",
                    "description": "Chat file id of markdown saved via writeFile (create+append). Use this instead of `content` for long PDFs.",
                },
                "outputFormat": {"type": "string", "description": "Target format: pdf, docx, xlsx, pptx, csv, html, md, json, txt", "default": "pdf"},
                "title": {"type": "string", "description": "Document title", "default": "Document"},
                "language": {"type": "string", "description": "Document language (ISO 639-1)", "default": "de"},
            },
        },
        readOnly=False,
    )

    # ── textToSpeech tool ──────────────────────────────────────────────
    def _stripMarkdownForTts(text: str) -> str:
        """Strip markdown formatting so TTS reads clean speech text."""
        import re as _re
        t = text
        t = _re.sub(r'\*\*(.+?)\*\*', r'\1', t)
        t = _re.sub(r'\*(.+?)\*', r'\1', t)
        t = _re.sub(r'__(.+?)__', r'\1', t)
        t = _re.sub(r'_(.+?)_', r'\1', t)
        t = _re.sub(r'`[^`]+`', lambda m: m.group(0)[1:-1], t)
        t = _re.sub(r'^#{1,6}\s*', '', t, flags=_re.MULTILINE)
        t = _re.sub(r'^\s*[-*+]\s+', '', t, flags=_re.MULTILINE)
        t = _re.sub(r'^\s*\d+\.\s+', '', t, flags=_re.MULTILINE)
        t = _re.sub(r'\[(.+?)\]\(.+?\)', r'\1', t)
        t = _re.sub(r'!\[.*?\]\(.*?\)', '', t)
        t = _re.sub(r'\n{3,}', '\n\n', t)
        return t.strip()

    async def _textToSpeech(args: Dict[str, Any], context: Dict[str, Any]):
        """Convert text to speech using Google Cloud TTS, deliver audio via SSE."""
        import base64 as _b64
        text = args.get("text", "")
        language = args.get("language", "auto")
        voiceName = args.get("voiceName")

        if not text:
            return ToolResult(toolCallId="", toolName="textToSpeech", success=False, error="text is required")

        cleanText = _stripMarkdownForTts(text)
        if not cleanText:
            return ToolResult(toolCallId="", toolName="textToSpeech", success=False, error="text is empty after stripping markdown")

        try:
            from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
            mandateId = context.get("mandateId", "")
            voiceInterface = getVoiceInterface(currentUser=None, mandateId=mandateId)

            _ISO_TO_BCP47 = {
                "de": "de-DE", "en": "en-US", "fr": "fr-FR", "it": "it-IT",
                "es": "es-ES", "pt": "pt-BR", "nl": "nl-NL", "pl": "pl-PL",
                "ru": "ru-RU", "ja": "ja-JP", "zh": "zh-CN", "ko": "ko-KR",
                "ar": "ar-XA", "hi": "hi-IN", "tr": "tr-TR", "sv": "sv-SE",
            }

            if language == "auto":
                try:
                    snippet = cleanText[:500]
                    detectResult = await voiceInterface.detectLanguage(snippet)
                    if detectResult and detectResult.get("success"):
                        detected = detectResult.get("language", "de")
                        language = _ISO_TO_BCP47.get(detected, detected)
                        if "-" not in language:
                            language = _ISO_TO_BCP47.get(language, f"{language}-{language.upper()}")
                        logger.info(f"textToSpeech: auto-detected language '{detected}' -> '{language}'")
                    else:
                        language = "de-DE"
                except Exception as detectErr:
                    logger.warning(f"textToSpeech: language detection failed: {detectErr}, defaulting to de-DE")
                    language = "de-DE"

            if not voiceName:
                try:
                    from modules.datamodels.datamodelUam import UserVoicePreferences
                    from modules.interfaces.interfaceDbApp import getRootInterface
                    userId = context.get("userId", "")
                    if userId:
                        rootIf = getRootInterface()
                        prefRecords = rootIf.db.getRecordset(
                            UserVoicePreferences,
                            recordFilter={"userId": userId}
                        )
                        if prefRecords:
                            allPrefs = [
                                r if isinstance(r, dict) else r.model_dump() if hasattr(r, "model_dump") else r
                                for r in prefRecords
                            ]
                            _mid = str(mandateId or "").strip()
                            scopedPref = next((p for p in allPrefs if str(p.get("mandateId") or "").strip() == _mid), None)
                            globalPref = next((p for p in allPrefs if not str(p.get("mandateId") or "").strip()), None)

                            def _resolveVoiceFromMap(prefDict, lang):
                                vm = (prefDict or {}).get("ttsVoiceMap", {}) or {}
                                if not isinstance(vm, dict) or not vm:
                                    return None
                                baseLang = lang.split("-")[0].lower() if isinstance(lang, str) and lang else ""
                                langNorm = str(lang or "").strip()
                                if langNorm in vm:
                                    entry = vm[langNorm]
                                    return entry.get("voiceName") if isinstance(entry, dict) else entry
                                if baseLang and baseLang in vm:
                                    entry = vm[baseLang]
                                    return entry.get("voiceName") if isinstance(entry, dict) else entry
                                if baseLang:
                                    for mk, mv in vm.items():
                                        mkn = str(mk).lower()
                                        if mkn == baseLang or mkn.startswith(f"{baseLang}-"):
                                            return mv.get("voiceName") if isinstance(mv, dict) else mv
                                return None

                            voiceName = (
                                _resolveVoiceFromMap(scopedPref, language)
                                or _resolveVoiceFromMap(globalPref, language)
                                or _resolveVoiceFromMap(allPrefs[0], language)
                            )
                            if not voiceName:
                                for candidate in [globalPref, scopedPref, allPrefs[0]]:
                                    if candidate and candidate.get("ttsVoice") and candidate.get("ttsLanguage") == language:
                                        voiceName = candidate["ttsVoice"]
                                        break
                            if voiceName:
                                logger.info(f"textToSpeech: using configured voice '{voiceName}' for language '{language}'")
                except Exception as prefErr:
                    logger.debug(f"textToSpeech: could not load voice preferences: {prefErr}")

            ttsResult = await voiceInterface.textToSpeech(
                text=cleanText,
                languageCode=language,
                voiceName=voiceName,
            )

            if not ttsResult or not ttsResult.get("success"):
                errMsg = ttsResult.get("error", "TTS call failed") if ttsResult else "TTS returned None"
                return ToolResult(toolCallId="", toolName="textToSpeech", success=False, error=errMsg)

            audioContent = ttsResult.get("audioContent", "")
            if not audioContent:
                return ToolResult(toolCallId="", toolName="textToSpeech", success=False, error="TTS returned no audio")

            if isinstance(audioContent, bytes):
                audioB64 = _b64.b64encode(audioContent).decode("ascii")
            elif isinstance(audioContent, str):
                audioB64 = audioContent
            else:
                audioB64 = str(audioContent)

            audioFormat = ttsResult.get("audioFormat", "mp3")
            charCount = len(cleanText)
            usedVoice = voiceName or "default"
            logger.info(f"textToSpeech: generated {audioFormat} audio for {charCount} chars, language={language}, voice={usedVoice}")

            return ToolResult(
                toolCallId="", toolName="textToSpeech", success=True,
                data=f"Audio generated ({charCount} characters, language={language}, voice={usedVoice}). Playing in chat.",
                sideEvents=[{
                    "type": "voiceResponse",
                    "data": {
                        "audio": audioB64,
                        "format": audioFormat,
                        "language": language,
                        "charCount": charCount,
                    },
                }],
            )

        except ImportError:
            return ToolResult(toolCallId="", toolName="textToSpeech", success=False,
                              error="Voice interface not available (missing dependency)")
        except Exception as e:
            logger.error(f"textToSpeech failed: {e}")
            return ToolResult(toolCallId="", toolName="textToSpeech", success=False, error=str(e))

    registry.register(
        "textToSpeech", _textToSpeech,
        description=(
            "Convert text to speech audio. The audio is played directly in the chat. "
            "Use this when the user asks you to read something aloud, narrate, or speak. "
            "Language is auto-detected from the text content. You do NOT need to specify a language."
        ),
        parameters={
            "type": "object",
            "properties": {
                "text": {"type": "string", "description": "The text to convert to speech. Can include markdown (will be stripped automatically)."},
                "language": {"type": "string", "description": "BCP-47 language code (e.g. de-DE, en-US) or 'auto' for automatic detection", "default": "auto"},
                "voiceName": {"type": "string", "description": "Optional specific voice name. If omitted, uses the configured voice for the detected language."},
            },
            "required": ["text"],
        },
        readOnly=False,
    )

    # ── generateImage tool ─────────────────────────────────────────────

    async def _generateImage(args: Dict[str, Any], context: Dict[str, Any]):
        """Generate an image from a text prompt using AI (DALL-E)."""
        import re as _re

        prompt = (args.get("prompt") or "").strip()
        style = (args.get("style") or "").strip() or None
        title = (args.get("title") or "").strip() or "Generated Image"

        if not prompt:
            return ToolResult(toolCallId="", toolName="generateImage", success=False, error="prompt is required")

        try:
            from modules.serviceCenter.services.serviceGeneration.paths.imagePath import ImageGenerationPath

            imagePath = ImageGenerationPath(services)
            aiResponse = await imagePath.generateImages(
                userPrompt=prompt,
                count=1,
                style=style,
                format="png",
                title=title,
            )

            if not aiResponse.documents:
                return ToolResult(toolCallId="", toolName="generateImage", success=False, error="Image generation returned no image data")

            sideEvents = []
            savedFiles = []
            chatService = services.chat
            sanitizedTitle = _re.sub(r'[^\w._-]', '_', title, flags=_re.UNICODE).strip('_') or "generated_image"

            for doc in aiResponse.documents:
                docData = doc.documentData if hasattr(doc, "documentData") else b""
                docName = doc.documentName if hasattr(doc, "documentName") else f"{sanitizedTitle}.png"
                docMime = doc.mimeType if hasattr(doc, "mimeType") else "image/png"

                if not docName.lower().endswith(".png"):
                    docName = f"{sanitizedTitle}.png"

                fileItem = None
                if hasattr(chatService.interfaceDbComponent, "saveGeneratedFile"):
                    fileItem = chatService.interfaceDbComponent.saveGeneratedFile(docData, docName, docMime)
                else:
                    fileItem, _ = chatService.interfaceDbComponent.saveUploadedFile(docData, docName)

                if fileItem:
                    fid = fileItem.id if hasattr(fileItem, "id") else fileItem.get("id", "?")
                    fiId = context.get("featureInstanceId") or (services.featureInstanceId if services else "")
                    if fiId:
                        chatService.interfaceDbComponent.updateFile(fid, {"featureInstanceId": fiId})
                    tempFolderId = _getOrCreateTempFolder(chatService)
                    if tempFolderId:
                        chatService.interfaceDbComponent.updateFile(fid, {"folderId": tempFolderId})
                    savedFiles.append(f"- {docName} (id: {fid})")
                    sideEvents.append({
                        "type": "fileCreated",
                        "data": {
                            "fileId": fid,
                            "fileName": docName,
                            "mimeType": docMime,
                            "fileSize": len(docData),
                        },
                    })

            result = f"Generated {len(aiResponse.documents)} image(s):\n" + "\n".join(savedFiles)
            return ToolResult(toolCallId="", toolName="generateImage", success=True, data=result, sideEvents=sideEvents)

        except Exception as e:
            logger.error(f"generateImage failed: {e}")
            return ToolResult(toolCallId="", toolName="generateImage", success=False, error=str(e))

    registry.register(
        "generateImage", _generateImage,
        description=(
            "Generate an image from a text description using AI (DALL-E). "
            "The generated image is saved as a file in the workspace. "
            "Use this when the user asks to create, generate, draw, or design an image, illustration, icon, logo, diagram, or any visual content. "
            "Provide a detailed, descriptive prompt for best results."
        ),
        parameters={
            "type": "object",
            "properties": {
                "prompt": {"type": "string", "description": "Detailed description of the image to generate. Be specific about subject, composition, colors, style, and mood."},
                "style": {"type": "string", "description": "Optional style modifier (e.g. 'photorealistic', 'watercolor', 'digital art', 'minimalist', 'sketch')"},
                "title": {"type": "string", "description": "Title/filename for the generated image", "default": "Generated Image"},
            },
            "required": ["prompt"],
        },
        readOnly=False,
    )

    # ── createChart tool ─────────────────────────────────────────────────

    async def _createChart(args: Dict[str, Any], context: Dict[str, Any]):
        """Create a data chart as PNG image using matplotlib."""
        import re as _re

        chartType = (args.get("chartType") or "bar").strip().lower()
        title = (args.get("title") or "Chart").strip()
        labels = args.get("labels") or []
        datasets = args.get("datasets") or []
        xLabel = (args.get("xLabel") or "").strip()
        yLabel = (args.get("yLabel") or "").strip()
        width = min(max(args.get("width") or 10, 4), 20)
        height = min(max(args.get("height") or 6, 3), 14)
        colors = args.get("colors") or None

        if not datasets:
            return ToolResult(toolCallId="", toolName="createChart", success=False, error="datasets is required (list of {label, values})")

        try:
            import matplotlib
            matplotlib.use("Agg")
            import logging as _mpllog
            _mpllog.getLogger("matplotlib").setLevel(_mpllog.WARNING)
            import matplotlib.pyplot as plt
            import io

            _DEFAULT_COLORS = [
                "#4285F4", "#EA4335", "#FBBC04", "#34A853", "#FF6D01",
                "#46BDC6", "#7B61FF", "#F538A0", "#00ACC1", "#AB47BC",
            ]
            usedColors = colors if colors and len(colors) >= len(datasets) else _DEFAULT_COLORS

            fig, ax = plt.subplots(figsize=(width, height))
            fig.patch.set_facecolor("#FFFFFF")
            ax.set_facecolor("#FAFAFA")

            if chartType in ("pie", "donut"):
                values = datasets[0].get("values", []) if datasets else []
                explode = [0.02] * len(values)
                wedges, texts, autotexts = ax.pie(
                    values, labels=labels, autopct="%1.1f%%",
                    colors=usedColors[:len(values)], explode=explode,
                    textprops={"fontsize": 9},
                )
                if chartType == "donut":
                    ax.add_artist(plt.Circle((0, 0), 0.55, fc="white"))
                ax.set_title(title, fontsize=14, fontweight="bold", pad=16)

            else:
                import numpy as _np
                x = _np.arange(len(labels)) if labels else _np.arange(max(len(d.get("values", [])) for d in datasets))
                barWidth = 0.8 / max(len(datasets), 1)

                for i, ds in enumerate(datasets):
                    dsLabel = ds.get("label", f"Series {i+1}")
                    values = ds.get("values", [])
                    color = usedColors[i % len(usedColors)]

                    if chartType == "bar":
                        offset = (i - len(datasets) / 2 + 0.5) * barWidth
                        ax.bar(x + offset, values, barWidth, label=dsLabel, color=color, edgecolor="white", linewidth=0.5)
                    elif chartType == "horizontalbar":
                        offset = (i - len(datasets) / 2 + 0.5) * barWidth
                        ax.barh(x + offset, values, barWidth, label=dsLabel, color=color, edgecolor="white", linewidth=0.5)
                    elif chartType == "line":
                        ax.plot(x[:len(values)], values, marker="o", markersize=5, label=dsLabel, color=color, linewidth=2)
                    elif chartType == "area":
                        ax.fill_between(x[:len(values)], values, alpha=0.3, color=color)
                        ax.plot(x[:len(values)], values, label=dsLabel, color=color, linewidth=2)
                    elif chartType == "scatter":
                        ax.scatter(x[:len(values)], values, label=dsLabel, color=color, s=50, edgecolors="white", linewidth=0.5)
                    else:
                        ax.bar(x, values, label=dsLabel, color=color)

                if labels:
                    if chartType == "horizontalbar":
                        ax.set_yticks(x)
                        ax.set_yticklabels(labels, fontsize=9)
                    else:
                        ax.set_xticks(x)
                        ax.set_xticklabels(labels, fontsize=9, rotation=45 if len(labels) > 6 else 0, ha="right" if len(labels) > 6 else "center")

                ax.set_title(title, fontsize=14, fontweight="bold", pad=12)
                if xLabel:
                    ax.set_xlabel(xLabel, fontsize=10)
                if yLabel:
                    ax.set_ylabel(yLabel, fontsize=10)
                if len(datasets) > 1:
                    ax.legend(fontsize=9, framealpha=0.9)
                ax.grid(axis="y", alpha=0.3, linestyle="--")
                ax.spines["top"].set_visible(False)
                ax.spines["right"].set_visible(False)

            plt.tight_layout()
            buf = io.BytesIO()
            fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
            plt.close(fig)
            pngData = buf.getvalue()

            chatService = services.chat
            sanitizedTitle = _re.sub(r'[^\w._-]', '_', title, flags=_re.UNICODE).strip('_') or "chart"
            fileName = f"{sanitizedTitle}.png"

            if hasattr(chatService.interfaceDbComponent, "saveGeneratedFile"):
                fileItem = chatService.interfaceDbComponent.saveGeneratedFile(pngData, fileName, "image/png")
            else:
                fileItem, _ = chatService.interfaceDbComponent.saveUploadedFile(pngData, fileName)

            fid = fileItem.id if hasattr(fileItem, "id") else fileItem.get("id", "?") if isinstance(fileItem, dict) else "?"
            fiId = context.get("featureInstanceId") or (services.featureInstanceId if services else "")
            if fiId and fid != "?":
                chatService.interfaceDbComponent.updateFile(fid, {"featureInstanceId": fiId})
            tempFolderId = _getOrCreateTempFolder(chatService)
            if tempFolderId and fid != "?":
                chatService.interfaceDbComponent.updateFile(fid, {"folderId": tempFolderId})

            sideEvents = [{"type": "fileCreated", "data": {
                "fileId": fid, "fileName": fileName,
                "mimeType": "image/png", "fileSize": len(pngData),
            }}]
            return ToolResult(
                toolCallId="", toolName="createChart", success=True,
                data=f"Chart saved as '{fileName}' (id: {fid}, {len(pngData)} bytes). "
                     f"Embed in documents with: ![{title}](file:{fid})",
                sideEvents=sideEvents,
            )

        except Exception as e:
            logger.error(f"createChart failed: {e}", exc_info=True)
            return ToolResult(toolCallId="", toolName="createChart", success=False, error=str(e))

    registry.register(
        "createChart", _createChart,
        description=(
            "Create a data chart/graph as a PNG image using matplotlib. "
            "Supported types: bar, horizontalBar, line, area, scatter, pie, donut. "
            "The chart is saved as a file in the workspace. "
            "Use the returned fileId to embed in documents via renderDocument: ![title](file:fileId). "
            "Provide structured data with labels and datasets."
        ),
        parameters={
            "type": "object",
            "properties": {
                "chartType": {
                    "type": "string",
                    "enum": ["bar", "horizontalBar", "line", "area", "scatter", "pie", "donut"],
                    "description": "Chart type (default: bar)",
                },
                "title": {"type": "string", "description": "Chart title"},
                "labels": {
                    "type": "array", "items": {"type": "string"},
                    "description": "X-axis labels / category names",
                },
                "datasets": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "label": {"type": "string", "description": "Series name (legend)"},
                            "values": {"type": "array", "items": {"type": "number"}, "description": "Data values"},
                        },
                        "required": ["values"],
                    },
                    "description": "Data series to plot",
                },
                "xLabel": {"type": "string", "description": "X-axis label"},
                "yLabel": {"type": "string", "description": "Y-axis label"},
                "colors": {
                    "type": "array", "items": {"type": "string"},
                    "description": "Custom hex colors for series (e.g. ['#4285F4', '#EA4335'])",
                },
                "width": {"type": "number", "description": "Figure width in inches (4-20, default 10)"},
                "height": {"type": "number", "description": "Figure height in inches (3-14, default 6)"},
            },
            "required": ["datasets"],
        },
        readOnly=False,
    )

    # ── Phase 3: speechToText, detectLanguage, neutralizeData, executeCode ──

    async def _speechToText(args: Dict[str, Any], context: Dict[str, Any]):
        fileId = args.get("fileId", "")
        if not fileId:
            return ToolResult(toolCallId="", toolName="speechToText", success=False, error="fileId is required")
        try:
            chatService = services.chat
            audioData = chatService.interfaceDbComponent.getFileData(fileId)
            if not audioData:
                return ToolResult(toolCallId="", toolName="speechToText", success=False, error=f"No data found for file {fileId}")
            from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
            mandateId = context.get("mandateId", "")
            voiceInterface = getVoiceInterface(currentUser=None, mandateId=mandateId)
            language = args.get("language", "de-DE")
            result = await voiceInterface.speechToText(audioData, language=language)
            if result and result.get("success"):
                transcript = result.get("text", "")
                confidence = result.get("confidence", 0)
                return ToolResult(
                    toolCallId="", toolName="speechToText", success=True,
                    data=f"Transcript (confidence: {confidence:.0%}):\n{transcript}"
                )
            return ToolResult(toolCallId="", toolName="speechToText", success=False, error=result.get("error", "Transcription failed"))
        except Exception as e:
            return ToolResult(toolCallId="", toolName="speechToText", success=False, error=str(e))

    async def _detectLanguage(args: Dict[str, Any], context: Dict[str, Any]):
        text = args.get("text", "")
        if not text:
            return ToolResult(toolCallId="", toolName="detectLanguage", success=False, error="text is required")
        try:
            from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
            mandateId = context.get("mandateId", "")
            voiceInterface = getVoiceInterface(currentUser=None, mandateId=mandateId)
            result = await voiceInterface.detectLanguage(text)
            if result and result.get("success"):
                lang = result.get("language", "unknown")
                return ToolResult(toolCallId="", toolName="detectLanguage", success=True, data=f"Detected language: {lang}")
            return ToolResult(toolCallId="", toolName="detectLanguage", success=False, error=result.get("error", "Detection failed"))
        except Exception as e:
            return ToolResult(toolCallId="", toolName="detectLanguage", success=False, error=str(e))

    async def _neutralizeData(args: Dict[str, Any], context: Dict[str, Any]):
        text = args.get("text", "")
        fileId = args.get("fileId", "")
        if not text and not fileId:
            return ToolResult(toolCallId="", toolName="neutralizeData", success=False, error="text or fileId is required")
        try:
            neutralizationService = services.getService("neutralization")
            if not neutralizationService:
                return ToolResult(toolCallId="", toolName="neutralizeData", success=False, error="Neutralization service not available")
            if not neutralizationService.interfaceDbComponent:
                neutralizationService.interfaceDbComponent = services.chat.interfaceDbComponent
            if text:
                result = await neutralizationService.processTextAsync(text, fileId or None)
            else:
                result = neutralizationService.processFile(fileId)
            if result:
                neutralized = result.get("neutralized_text", "") or result.get("result", str(result))
                return ToolResult(toolCallId="", toolName="neutralizeData", success=True, data=neutralized)
            return ToolResult(toolCallId="", toolName="neutralizeData", success=False, error="Neutralization returned no result")
        except Exception as e:
            return ToolResult(toolCallId="", toolName="neutralizeData", success=False, error=str(e))

    async def _executeCode(args: Dict[str, Any], context: Dict[str, Any]):
        code = args.get("code", "")
        language = args.get("language", "python")
        if not code:
            return ToolResult(toolCallId="", toolName="executeCode", success=False, error="code is required")
        if language != "python":
            return ToolResult(toolCallId="", toolName="executeCode", success=False, error=f"Language '{language}' not supported. Only 'python' is available.")
        try:
            from modules.serviceCenter.services.serviceAgent.sandboxExecutor import executePython
            result = await executePython(code)
            if result.get("success"):
                output = result.get("output", "(no output)")
                return ToolResult(toolCallId="", toolName="executeCode", success=True, data=output)
            error = result.get("error", "Execution failed")
            tb = result.get("traceback", "")
            return ToolResult(toolCallId="", toolName="executeCode", success=False, error=f"{error}\n{tb}" if tb else error)
        except Exception as e:
            return ToolResult(toolCallId="", toolName="executeCode", success=False, error=str(e))

    registry.register(
        "speechToText", _speechToText,
        description="Transcribe an audio file to text using speech recognition. Returns the transcript with confidence score.",
        parameters={
            "type": "object",
            "properties": {
                "fileId": {"type": "string", "description": "Audio file ID from the workspace"},
                "language": {"type": "string", "description": "BCP-47 language code (e.g. 'de-DE', 'en-US'). Default: 'de-DE'"},
            },
            "required": ["fileId"]
        },
        readOnly=True
    )

    registry.register(
        "detectLanguage", _detectLanguage,
        description="Detect the language of a text snippet. Returns ISO 639-1 code (e.g. 'de', 'en').",
        parameters={
            "type": "object",
            "properties": {
                "text": {"type": "string", "description": "Text to analyze"},
            },
            "required": ["text"]
        },
        readOnly=True
    )

    registry.register(
        "neutralizeData", _neutralizeData,
        description="Anonymize text or file content by replacing personal data (names, addresses, etc.) with placeholders. Non-destructive -- returns the anonymized copy.",
        parameters={
            "type": "object",
            "properties": {
                "text": {"type": "string", "description": "Text to anonymize"},
                "fileId": {"type": "string", "description": "File ID to anonymize (alternative to text)"},
            },
        },
        readOnly=True
    )

    registry.register(
        "executeCode", _executeCode,
        description=(
            "Execute Python code in a sandboxed environment for calculations and data analysis. "
            "Available modules: math, statistics, json, csv, re, datetime, collections, itertools, functools, decimal, fractions, random. "
            "No file system, network, or OS access. Max 30s execution time. "
            "Use print() to produce output."
        ),
        parameters={
            "type": "object",
            "properties": {
                "code": {"type": "string", "description": "Python code to execute"},
                "language": {"type": "string", "description": "Programming language (only 'python' supported)", "default": "python"},
            },
            "required": ["code"]
        },
        readOnly=True
    )
