# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""Workspace and file management tools (read, write, search, folders, web, translate)."""

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 (
    _getOrCreateInstanceFolder,
    _getOrCreateTempFolder,
    _looksLikeBinary,
    _resolveFileScope,
    _MAX_TOOL_RESULT_CHARS,
)

logger = logging.getLogger(__name__)


import uuid as _uuid

def _registerWorkspaceTools(registry: ToolRegistry, services):
    """Auto-extracted from registerCoreTools."""
    import uuid as _uuid

    # ---- Read-only tools ----

    def _applyOffsetLimit(text: str, offset: int = None, limit: int = None) -> str:
        """Apply line-based offset/limit to text content, returning numbered lines."""
        if offset is None and limit is None:
            return None
        lines = text.split("\n")
        totalLines = len(lines)
        startLine = max(0, (offset or 1) - 1)
        endLine = min(totalLines, startLine + (limit or 200))
        selected = lines[startLine:endLine]
        numbered = "\n".join(f"{i + startLine + 1}|{line}" for i, line in enumerate(selected))
        header = f"[Lines {startLine + 1}-{endLine} of {totalLines} total]\n"
        return header + numbered

    async def _readFile(args: Dict[str, Any], context: Dict[str, Any]):
        fileId = args.get("fileId", "")
        offset = args.get("offset")
        limit = args.get("limit")
        if not fileId:
            return ToolResult(toolCallId="", toolName="readFile", success=False, error="fileId is required")
        try:
            knowledgeService = services.getService("knowledge") if hasattr(services, "getService") else None

            # 1) Knowledge Store: return already-extracted text chunks
            if knowledgeService:
                fileStatus = knowledgeService.getFileStatus(fileId)
                if fileStatus == "indexed":
                    chunks = knowledgeService._knowledgeDb.getContentChunks(fileId)
                    textChunks = [
                        c for c in (chunks or [])
                        if c.get("contentType") != "image" and c.get("data")
                    ]
                    if textChunks:
                        assembled = "\n\n".join(c["data"] for c in textChunks)
                        chunked = _applyOffsetLimit(assembled, offset, limit)
                        if chunked is not None:
                            return ToolResult(toolCallId="", toolName="readFile", success=True, data=chunked)
                        if len(assembled) > _MAX_TOOL_RESULT_CHARS:
                            assembled = assembled[:_MAX_TOOL_RESULT_CHARS] + f"\n\n[Truncated – showing first {_MAX_TOOL_RESULT_CHARS} chars of {len(assembled)}. Use offset/limit to read specific sections.]"
                        return ToolResult(
                            toolCallId="", toolName="readFile", success=True,
                            data=assembled,
                        )
                elif fileStatus in ("processing", "embedding", "extracted"):
                    return ToolResult(
                        toolCallId="", toolName="readFile", success=True,
                        data=f"[File {fileId} is currently being processed (status: {fileStatus}). Try again shortly.]",
                    )

            # 2) Not indexed yet: try on-demand extraction
            chatService = services.chat
            fileInfo = chatService.getFileInfo(fileId)
            if not fileInfo:
                return ToolResult(toolCallId="", toolName="readFile", success=True, data="File not found.")

            fileName = fileInfo.get("fileName", fileId)
            mimeType = fileInfo.get("mimeType", "")

            _BINARY_TYPES = ("application/pdf", "image/", "application/vnd.", "application/zip",
                             "application/x-zip", "application/x-tar", "application/x-7z",
                             "application/msword", "application/octet-stream",
                             "message/rfc822")
            isBinary = any(mimeType.startswith(t) for t in _BINARY_TYPES)

            rawBytes = chatService.getFileData(fileId)
            if not rawBytes:
                return ToolResult(toolCallId="", toolName="readFile", success=True, data="File data not accessible.")

            if not isBinary:
                isBinary = _looksLikeBinary(rawBytes)

            if isBinary:
                try:
                    from modules.serviceCenter.services.serviceExtraction.subRegistry import ExtractorRegistry, ChunkerRegistry
                    from modules.serviceCenter.services.serviceExtraction.subPipeline import runExtraction
                    from modules.datamodels.datamodelExtraction import ExtractionOptions

                    extracted = runExtraction(
                        ExtractorRegistry(), ChunkerRegistry(),
                        rawBytes, fileName, mimeType, ExtractionOptions(),
                    )

                    contentObjects = []
                    for part in extracted.parts:
                        tg = (part.typeGroup or "").lower()
                        ct = "image" if tg == "image" else "text"
                        if not part.data or not part.data.strip():
                            continue
                        contentObjects.append({
                            "contentObjectId": part.id,
                            "contentType": ct,
                            "data": part.data,
                            "contextRef": {
                                "containerPath": fileName,
                                "location": part.label or "file",
                                **(part.metadata or {}),
                            },
                        })

                    if contentObjects:
                        if knowledgeService:
                            try:
                                userId = context.get("userId", "")
                                _fiId, _mId = _resolveFileScope(fileId, context)
                                await knowledgeService.indexFile(
                                    fileId=fileId, fileName=fileName, mimeType=mimeType,
                                    userId=userId, contentObjects=contentObjects,
                                    featureInstanceId=_fiId,
                                    mandateId=_mId,
                                )
                            except Exception as e:
                                logger.warning(f"readFile: knowledge indexing failed for {fileId}: {e}")

                        joined = ""
                        if knowledgeService:
                            _chunks = knowledgeService._knowledgeDb.getContentChunks(fileId)
                            _textChunks = [
                                c for c in (_chunks or [])
                                if c.get("contentType") != "image" and c.get("data")
                            ]
                            if _textChunks:
                                joined = "\n\n".join(c["data"] for c in _textChunks)
                        if not joined:
                            textParts = [o["data"] for o in contentObjects if o["contentType"] != "image"]
                            joined = "\n\n".join(textParts) if textParts else ""
                        if joined:
                            chunked = _applyOffsetLimit(joined, offset, limit)
                            if chunked is not None:
                                return ToolResult(toolCallId="", toolName="readFile", success=True, data=chunked)
                            if len(joined) > _MAX_TOOL_RESULT_CHARS:
                                joined = joined[:_MAX_TOOL_RESULT_CHARS] + f"\n\n[Truncated – showing first {_MAX_TOOL_RESULT_CHARS} chars of {len(joined)}. Use offset/limit to read specific sections.]"
                            return ToolResult(
                                toolCallId="", toolName="readFile", success=True,
                                data=joined,
                            )
                        imgCount = sum(1 for o in contentObjects if o["contentType"] == "image")
                        return ToolResult(
                            toolCallId="", toolName="readFile", success=True,
                            data=f"[Extracted {len(contentObjects)} content objects from '{fileName}' "
                                 f"({imgCount} images, no readable text). "
                                 f"Use describeImage(fileId='{fileId}') to analyze visual content.]",
                        )
                except Exception as extractErr:
                    logger.warning(f"readFile extraction failed for {fileId} ({fileName}): {extractErr}")

                return ToolResult(
                    toolCallId="", toolName="readFile", success=True,
                    data=f"[Binary file: '{fileName}', type={mimeType}, size={len(rawBytes)} bytes. "
                         f"Text extraction not available. Use describeImage for images.]",
                )

            # 3) Text file: decode raw bytes
            for encoding in ("utf-8", "utf-8-sig", "latin-1"):
                try:
                    text = rawBytes.decode(encoding)
                    if text.strip():
                        _fileNeedNeutralize = False
                        try:
                            from modules.datamodels.datamodelFiles import FileItem as _FI
                            from modules.interfaces.interfaceDbManagement import ComponentObjects as _CO
                            _fRec = _CO().db._loadRecord(_FI, fileId)
                            if _fRec:
                                _fG = (lambda k, d=None: _fRec.get(k, d)) if isinstance(_fRec, dict) else (lambda k, d=None: getattr(_fRec, k, d))
                                _fileNeedNeutralize = bool(_fG("neutralize", False))
                        except Exception as e:
                            logger.warning(f"readFile: neutralize flag check failed for {fileId}: {e}")
                        if _fileNeedNeutralize:
                            try:
                                _nSvc = services.getService("neutralization") if hasattr(services, "getService") else None
                                if _nSvc and hasattr(_nSvc, 'processTextAsync'):
                                    _nResult = await _nSvc.processTextAsync(text, fileId)
                                    if _nResult and _nResult.get("neutralized_text"):
                                        text = _nResult["neutralized_text"]
                                        logger.debug(f"readFile: neutralized text for file {fileId}")
                                    else:
                                        logger.warning(f"readFile: neutralization failed for file {fileId}, blocking text (fail-safe)")
                                        return ToolResult(toolCallId="", toolName="readFile", success=True,
                                                          data="[File requires neutralization but neutralization failed. Content blocked for data protection.]")
                                else:
                                    logger.warning(f"readFile: neutralization required but service unavailable for file {fileId}")
                                    return ToolResult(toolCallId="", toolName="readFile", success=True,
                                                      data="[File requires neutralization but service unavailable. Content blocked for data protection.]")
                            except Exception as _nErr:
                                logger.error(f"readFile: neutralization error for file {fileId}: {_nErr}")
                                return ToolResult(toolCallId="", toolName="readFile", success=True,
                                                  data="[File requires neutralization but an error occurred. Content blocked for data protection.]")
                        chunked = _applyOffsetLimit(text, offset, limit)
                        if chunked is not None:
                            return ToolResult(toolCallId="", toolName="readFile", success=True, data=chunked)
                        if len(text) > _MAX_TOOL_RESULT_CHARS:
                            text = text[:_MAX_TOOL_RESULT_CHARS] + f"\n\n[Truncated – showing first {_MAX_TOOL_RESULT_CHARS} chars of {len(text)}. Use offset/limit to read specific sections.]"
                        return ToolResult(
                            toolCallId="", toolName="readFile", success=True,
                            data=text,
                        )
                except (UnicodeDecodeError, ValueError):
                    continue

            return ToolResult(
                toolCallId="", toolName="readFile", success=True,
                data="File is empty or could not be decoded.",
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="readFile", success=False, error=str(e))

    async def _listFiles(args: Dict[str, Any], context: Dict[str, Any]):
        try:
            chatService = services.chat
            files = chatService.listFiles(
                folderId=args.get("folderId"),
                tags=args.get("tags"),
                search=args.get("search"),
            )
            fileList = "\n".join(
                f"- {f.get('fileName', 'unknown')} (id: {f.get('id', '?')}, "
                f"type: {f.get('mimeType', '?')}, size: {f.get('fileSize', '?')}, "
                f"tags: {f.get('tags', [])}, status: {f.get('status', 'n/a')})"
                for f in files
            ) if files else "No files found."
            return ToolResult(toolCallId="", toolName="listFiles", success=True, data=fileList)
        except Exception as e:
            return ToolResult(toolCallId="", toolName="listFiles", success=False, error=str(e))

    async def _searchInFileContent(args: Dict[str, Any], context: Dict[str, Any]):
        import re as _re
        fileId = args.get("fileId", "")
        query = args.get("query", "")
        contextLines = args.get("contextLines", 2)
        if not fileId or not query:
            return ToolResult(toolCallId="", toolName="searchInFileContent", success=False, error="fileId and query are required")
        try:
            chatService = services.chat
            rawBytes = chatService.getFileData(fileId)
            if not rawBytes:
                return ToolResult(toolCallId="", toolName="searchInFileContent", success=False, error="File data not accessible")
            try:
                content = rawBytes.decode("utf-8")
            except UnicodeDecodeError:
                content = rawBytes.decode("latin-1", errors="replace")

            lines = content.split("\n")
            pattern = _re.compile(_re.escape(query), _re.IGNORECASE)
            matches = []
            for i, line in enumerate(lines):
                if pattern.search(line):
                    start = max(0, i - contextLines)
                    end = min(len(lines), i + contextLines + 1)
                    snippet = "\n".join(f"{j + 1}|{lines[j]}" for j in range(start, end))
                    matches.append(snippet)

            if not matches:
                return ToolResult(toolCallId="", toolName="searchInFileContent", success=True,
                                  data=f"No matches for '{query}' in file.")

            shown = matches[:20]
            resultText = f"Found {len(matches)} match(es) for '{query}':\n\n" + "\n---\n".join(shown)
            if len(matches) > 20:
                resultText += f"\n\n... and {len(matches) - 20} more matches"
            return ToolResult(toolCallId="", toolName="searchInFileContent", success=True, data=resultText)
        except Exception as e:
            return ToolResult(toolCallId="", toolName="searchInFileContent", success=False, error=str(e))

    async def _listFolders(args: Dict[str, Any], context: Dict[str, Any]):
        try:
            chatService = services.chat
            folders = chatService.listFolders(parentId=args.get("parentId"))
            folderList = "\n".join(
                f"- {f.get('name', 'unnamed')} (id: {f.get('id', '?')})"
                for f in folders
            ) if folders else "No folders found."
            return ToolResult(toolCallId="", toolName="listFolders", success=True, data=folderList)
        except Exception as e:
            return ToolResult(toolCallId="", toolName="listFolders", success=False, error=str(e))

    async def _webSearch(args: Dict[str, Any], context: Dict[str, Any]):
        query = args.get("query", "")
        if not query:
            return ToolResult(toolCallId="", toolName="webSearch", success=False, error="query is required")
        try:
            webService = services.getService("web")
            result = await webService.performWebResearch(
                prompt=query,
                urls=[],
                country=None,
                language=args.get("language"),
            )
            summary = result.get("summary", "") if isinstance(result, dict) else str(result)
            return ToolResult(
                toolCallId="", toolName="webSearch", success=True,
                data=summary or str(result)
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="webSearch", success=False, error=str(e))

    # ---- Write tools ----

    async def _tagFile(args: Dict[str, Any], context: Dict[str, Any]):
        fileId = args.get("fileId", "")
        tags = args.get("tags", [])
        if not fileId:
            return ToolResult(toolCallId="", toolName="tagFile", success=False, error="fileId is required")
        try:
            chatService = services.chat
            chatService.interfaceDbComponent.updateFile(fileId, {"tags": tags})
            return ToolResult(
                toolCallId="", toolName="tagFile", success=True,
                data=f"Tags updated to {tags} for file {fileId}"
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="tagFile", success=False, error=str(e))

    async def _moveFile(args: Dict[str, Any], context: Dict[str, Any]):
        fileId = args.get("fileId", "")
        targetFolderId = args.get("targetFolderId")
        if not fileId:
            return ToolResult(toolCallId="", toolName="moveFile", success=False, error="fileId is required")
        try:
            chatService = services.chat
            chatService.interfaceDbComponent.updateFile(fileId, {"folderId": targetFolderId})
            return ToolResult(
                toolCallId="", toolName="moveFile", success=True,
                data=f"File {fileId} moved to folder {targetFolderId or 'root'}"
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="moveFile", success=False, error=str(e))

    async def _createFolder(args: Dict[str, Any], context: Dict[str, Any]):
        name = args.get("name", "")
        if not name:
            return ToolResult(toolCallId="", toolName="createFolder", success=False, error="name is required")
        try:
            chatService = services.chat
            folder = chatService.createFolder(name=name, parentId=args.get("parentId"))
            return ToolResult(
                toolCallId="", toolName="createFolder", success=True,
                data=f"Folder '{name}' created (id: {folder.get('id', '?')})"
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="createFolder", success=False, error=str(e))

    async def _writeFile(args: Dict[str, Any], context: Dict[str, Any]):
        content = args.get("content", "")
        mode = args.get("mode", "create")
        fileId = args.get("fileId", "")
        name = args.get("name", "")

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

        try:
            chatService = services.chat
            dbMgmt = chatService.interfaceDbComponent

            if mode == "append":
                if not fileId:
                    return ToolResult(toolCallId="", toolName="writeFile", success=False, error="fileId is required for mode=append")
                file = dbMgmt.getFile(fileId)
                if not file:
                    return ToolResult(toolCallId="", toolName="writeFile", success=False, error=f"File {fileId} not found")
                existingData = dbMgmt.getFileData(fileId) or b""
                try:
                    existingText = existingData.decode("utf-8")
                except UnicodeDecodeError:
                    existingText = existingData.decode("latin-1", errors="replace")
                newContent = existingText + content
                dbMgmt.updateFileData(fileId, newContent.encode("utf-8"))
                dbMgmt.updateFile(fileId, {"fileSize": len(newContent.encode("utf-8"))})
                return ToolResult(
                    toolCallId="", toolName="writeFile", success=True,
                    data=f"Appended {len(content)} chars to '{file.fileName}' (id: {fileId}, total: {len(newContent)} chars)",
                    sideEvents=[{"type": "fileUpdated", "data": {"fileId": fileId, "fileName": file.fileName}}],
                )

            if mode == "overwrite":
                if not fileId:
                    return ToolResult(toolCallId="", toolName="writeFile", success=False, error="fileId is required for mode=overwrite")
                file = dbMgmt.getFile(fileId)
                if not file:
                    return ToolResult(toolCallId="", toolName="writeFile", success=False, error=f"File {fileId} not found")
                dbMgmt.updateFileData(fileId, content.encode("utf-8"))
                dbMgmt.updateFile(fileId, {"fileSize": len(content.encode("utf-8"))})
                return ToolResult(
                    toolCallId="", toolName="writeFile", success=True,
                    data=f"Overwritten '{file.fileName}' (id: {fileId}, {len(content)} chars)",
                    sideEvents=[{"type": "fileUpdated", "data": {"fileId": fileId, "fileName": file.fileName}}],
                )

            # mode == "create" (default)
            if not name:
                return ToolResult(toolCallId="", toolName="writeFile", success=False, error="name is required for mode=create")
            fileItem, _ = dbMgmt.saveUploadedFile(content.encode("utf-8"), name)
            fiId = context.get("featureInstanceId") or (services.featureInstanceId if services else "")
            if fiId:
                dbMgmt.updateFile(fileItem.id, {"featureInstanceId": fiId})
            if args.get("folderId"):
                dbMgmt.updateFile(fileItem.id, {"folderId": args["folderId"]})
            elif fiId:
                instanceFolderId = _getOrCreateInstanceFolder(chatService, fiId, context.get("mandateId", ""))
                if instanceFolderId:
                    dbMgmt.updateFile(fileItem.id, {"folderId": instanceFolderId})
            if args.get("tags"):
                dbMgmt.updateFile(fileItem.id, {"tags": args["tags"]})
            return ToolResult(
                toolCallId="", toolName="writeFile", success=True,
                data=f"File '{name}' created (id: {fileItem.id})",
                sideEvents=[{
                    "type": "fileCreated",
                    "data": {
                        "fileId": fileItem.id,
                        "fileName": name,
                        "mimeType": fileItem.mimeType,
                        "fileSize": fileItem.fileSize,
                    },
                }],
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="writeFile", success=False, error=str(e))

    # ---- Register all tools ----

    registry.register(
        "readFile", _readFile,
        description=(
            "Read the content of a file. Returns full content by default. "
            "For large files, use offset and limit to read specific line ranges. "
            "When truncated, the response tells the total line count so you can paginate."
        ),
        parameters={
            "type": "object",
            "properties": {
                "fileId": {"type": "string", "description": "The file ID to read"},
                "offset": {"type": "integer", "description": "Start reading from this line number (1-based). Omit for full file."},
                "limit": {"type": "integer", "description": "Max number of lines to return (default: all). Use with offset for chunked reading."},
            },
            "required": ["fileId"]
        },
        readOnly=True
    )

    registry.register(
        "listFiles", _listFiles,
        description=(
            "List files in the local workspace. Filter by folder, tags, or search term. "
            "For external data sources, use browseDataSource instead."
        ),
        parameters={
            "type": "object",
            "properties": {
                "folderId": {"type": "string", "description": "Filter by folder ID"},
                "tags": {"type": "array", "items": {"type": "string"}, "description": "Filter by tags (any match)"},
                "search": {"type": "string", "description": "Search in file names and descriptions"},
            }
        },
        readOnly=True
    )

    registry.register(
        "searchInFileContent", _searchInFileContent,
        description=(
            "Search for text within a file's content. Returns matching lines with context. "
            "Case-insensitive. Use to locate specific text before using replaceInFile, "
            "or to find relevant sections in a large file before reading with offset/limit."
        ),
        parameters={
            "type": "object",
            "properties": {
                "fileId": {"type": "string", "description": "The file ID to search in"},
                "query": {"type": "string", "description": "Text to search for (case-insensitive)"},
                "contextLines": {"type": "integer", "description": "Number of context lines around each match (default: 2)"},
            },
            "required": ["fileId", "query"]
        },
        readOnly=True
    )

    registry.register(
        "listFolders", _listFolders,
        description="List folders in the local workspace. For external data sources, use browseDataSource instead.",
        parameters={
            "type": "object",
            "properties": {
                "parentId": {"type": "string", "description": "Parent folder ID (omit for root)"},
            }
        },
        readOnly=True
    )

    registry.register(
        "webSearch", _webSearch,
        description="Search the web for general information. Use readUrl to fetch content from a known URL instead.",
        parameters={
            "type": "object",
            "properties": {"query": {"type": "string", "description": "Search query"}},
            "required": ["query"]
        },
        readOnly=True
    )

    registry.register(
        "tagFile", _tagFile,
        description="Set or update tags on a file for categorization and filtering via listFiles.",
        parameters={
            "type": "object",
            "properties": {
                "fileId": {"type": "string", "description": "The file ID"},
                "tags": {"type": "array", "items": {"type": "string"}, "description": "Tags to set"},
            },
            "required": ["fileId", "tags"]
        },
        readOnly=False
    )

    registry.register(
        "moveFile", _moveFile,
        description="Move a file to a different folder in the local workspace.",
        parameters={
            "type": "object",
            "properties": {
                "fileId": {"type": "string", "description": "The file ID to move"},
                "targetFolderId": {"type": "string", "description": "Target folder ID (null for root)"},
            },
            "required": ["fileId"]
        },
        readOnly=False
    )

    registry.register(
        "createFolder", _createFolder,
        description="Create a new folder in the local workspace.",
        parameters={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Folder name"},
                "parentId": {"type": "string", "description": "Parent folder ID (omit for root)"},
            },
            "required": ["name"]
        },
        readOnly=False
    )

    registry.register(
        "writeFile", _writeFile,
        description=(
            "Create, append, or overwrite a file. Modes:\n"
            "- create (default): create a new file (name required).\n"
            "- append: append content to an existing file (fileId required). "
            "Use for large content that exceeds a single tool call (~8000 chars per call).\n"
            "- overwrite: replace entire file content (fileId required)."
        ),
        parameters={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "File name (required for mode=create)"},
                "content": {"type": "string", "description": "Content to write/append"},
                "mode": {"type": "string", "enum": ["create", "append", "overwrite"], "description": "Write mode (default: create)"},
                "fileId": {"type": "string", "description": "File ID (required for mode=append/overwrite)"},
                "folderId": {"type": "string", "description": "Target folder ID (mode=create only)"},
                "tags": {"type": "array", "items": {"type": "string"}, "description": "Tags (mode=create only)"},
            },
            "required": ["content"]
        },
        readOnly=False
    )

    # ---- Phase 1: deleteFile, renameFile, readUrl, translateText ----

    async def _deleteFile(args: Dict[str, Any], context: Dict[str, Any]):
        fileId = args.get("fileId", "")
        if not fileId:
            return ToolResult(toolCallId="", toolName="deleteFile", success=False, error="fileId is required")
        try:
            chatService = services.chat
            file = chatService.interfaceDbComponent.getFile(fileId)
            if not file:
                return ToolResult(toolCallId="", toolName="deleteFile", success=False, error=f"File {fileId} not found")
            fileName = file.fileName
            try:
                knowledgeService = services.getService("knowledge")
                if knowledgeService and hasattr(knowledgeService, "removeFile"):
                    knowledgeService.removeFile(fileId)
            except Exception as e:
                logger.warning(f"deleteFile: knowledge store cleanup failed for {fileId}: {e}")
            chatService.interfaceDbComponent.deleteFile(fileId)
            return ToolResult(
                toolCallId="", toolName="deleteFile", success=True,
                data=f"File '{fileName}' (id: {fileId}) deleted",
                sideEvents=[{"type": "fileDeleted", "data": {"fileId": fileId, "fileName": fileName}}],
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="deleteFile", success=False, error=str(e))

    async def _renameFile(args: Dict[str, Any], context: Dict[str, Any]):
        fileId = args.get("fileId", "")
        newName = args.get("newName", "")
        if not fileId or not newName:
            return ToolResult(toolCallId="", toolName="renameFile", success=False, error="fileId and newName are required")
        try:
            chatService = services.chat
            chatService.interfaceDbComponent.updateFile(fileId, {"fileName": newName})
            return ToolResult(
                toolCallId="", toolName="renameFile", success=True,
                data=f"File {fileId} renamed to '{newName}'",
                sideEvents=[{"type": "fileUpdated", "data": {"fileId": fileId, "fileName": newName}}],
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="renameFile", success=False, error=str(e))

    async def _readUrl(args: Dict[str, Any], context: Dict[str, Any]):
        url = args.get("url", "")
        if not url:
            return ToolResult(toolCallId="", toolName="readUrl", success=False, error="url is required")
        try:
            webService = services.getService("web")
            result = await webService._performWebCrawl(
                instruction="Extract all content from this page",
                urls=[url],
                maxDepth=1,
                maxWidth=1,
            )
            if isinstance(result, list) and result:
                content = "\n\n".join(
                    item.get("content", "") or item.get("text", "") or str(item)
                    for item in result if item
                )
            elif isinstance(result, dict):
                content = result.get("content", "") or result.get("summary", "") or str(result)
            else:
                content = str(result) if result else "No content retrieved"
            _MAX = 30000
            if len(content) > _MAX:
                content = content[:_MAX] + f"\n\n... (truncated at {_MAX} chars)"
            return ToolResult(toolCallId="", toolName="readUrl", success=True, data=content)
        except Exception as e:
            return ToolResult(toolCallId="", toolName="readUrl", success=False, error=str(e))

    async def _translateText(args: Dict[str, Any], context: Dict[str, Any]):
        text = args.get("text", "")
        targetLanguage = args.get("targetLanguage", "")
        if not text or not targetLanguage:
            return ToolResult(toolCallId="", toolName="translateText", success=False, error="text and targetLanguage are required")
        try:
            from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
            mandateId = context.get("mandateId", "")
            voiceInterface = getVoiceInterface(currentUser=None, mandateId=mandateId)
            sourceLanguage = args.get("sourceLanguage", "auto")
            result = await voiceInterface.translateText(text, sourceLanguage=sourceLanguage, targetLanguage=targetLanguage)
            if result and result.get("success"):
                translated = result.get("translated_text", "")
                return ToolResult(toolCallId="", toolName="translateText", success=True, data=translated)
            return ToolResult(toolCallId="", toolName="translateText", success=False, error=result.get("error", "Translation failed"))
        except Exception as e:
            return ToolResult(toolCallId="", toolName="translateText", success=False, error=str(e))

    registry.register(
        "deleteFile", _deleteFile,
        description="Permanently delete a file from the local workspace.",
        parameters={
            "type": "object",
            "properties": {
                "fileId": {"type": "string", "description": "The file ID to delete"},
            },
            "required": ["fileId"]
        },
        readOnly=False
    )

    registry.register(
        "renameFile", _renameFile,
        description="Rename a file in the local workspace. Include the file extension in the new name.",
        parameters={
            "type": "object",
            "properties": {
                "fileId": {"type": "string", "description": "The file ID to rename"},
                "newName": {"type": "string", "description": "New file name including extension"},
            },
            "required": ["fileId", "newName"]
        },
        readOnly=False
    )

    registry.register(
        "readUrl", _readUrl,
        description=(
            "Read and extract content from a specific URL. "
            "Use when the user provides a specific URL to read, or when you need to fetch content from a known web page. "
            "For general information searches, use webSearch instead."
        ),
        parameters={
            "type": "object",
            "properties": {
                "url": {"type": "string", "description": "The URL to read"},
            },
            "required": ["url"]
        },
        readOnly=True
    )

    registry.register(
        "translateText", _translateText,
        description=(
            "Translate text to a target language using Google Cloud Translation. "
            "More efficient than AI translation for large text volumes. "
            "Use ISO language codes (e.g. 'en', 'de', 'fr', 'es', 'it', 'pt', 'zh', 'ja', 'ko', 'ar')."
        ),
        parameters={
            "type": "object",
            "properties": {
                "text": {"type": "string", "description": "Text to translate"},
                "targetLanguage": {"type": "string", "description": "Target language ISO code (e.g. 'en', 'de', 'fr')"},
                "sourceLanguage": {"type": "string", "description": "Source language ISO code (default: auto-detect)"},
            },
            "required": ["text", "targetLanguage"]
        },
        readOnly=True
    )

    # ---- Phase 2: deleteFolder, renameFolder, moveFolder, copyFile, editFile ----

    async def _deleteFolder(args: Dict[str, Any], context: Dict[str, Any]):
        folderId = args.get("folderId", "")
        recursive = args.get("recursive", False)
        if not folderId:
            return ToolResult(toolCallId="", toolName="deleteFolder", success=False, error="folderId is required")
        try:
            chatService = services.chat
            result = chatService.interfaceDbComponent.deleteFolder(folderId, recursive=recursive)
            summary = f"Deleted {result.get('deletedFolders', 1)} folder(s) and {result.get('deletedFiles', 0)} file(s)"
            return ToolResult(
                toolCallId="", toolName="deleteFolder", success=True, data=summary,
                sideEvents=[{"type": "folderDeleted", "data": {"folderId": folderId, **result}}],
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="deleteFolder", success=False, error=str(e))

    async def _renameFolder(args: Dict[str, Any], context: Dict[str, Any]):
        folderId = args.get("folderId", "")
        newName = args.get("newName", "")
        if not folderId or not newName:
            return ToolResult(toolCallId="", toolName="renameFolder", success=False, error="folderId and newName are required")
        try:
            chatService = services.chat
            chatService.interfaceDbComponent.renameFolder(folderId, newName)
            return ToolResult(
                toolCallId="", toolName="renameFolder", success=True,
                data=f"Folder {folderId} renamed to '{newName}'",
                sideEvents=[{"type": "folderUpdated", "data": {"folderId": folderId, "name": newName}}],
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="renameFolder", success=False, error=str(e))

    async def _moveFolder(args: Dict[str, Any], context: Dict[str, Any]):
        folderId = args.get("folderId", "")
        targetParentId = args.get("targetParentId")
        if not folderId:
            return ToolResult(toolCallId="", toolName="moveFolder", success=False, error="folderId is required")
        try:
            chatService = services.chat
            chatService.interfaceDbComponent.moveFolder(folderId, targetParentId)
            return ToolResult(
                toolCallId="", toolName="moveFolder", success=True,
                data=f"Folder {folderId} moved to {targetParentId or 'root'}",
                sideEvents=[{"type": "folderUpdated", "data": {"folderId": folderId, "parentId": targetParentId}}],
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="moveFolder", success=False, error=str(e))

    async def _copyFile(args: Dict[str, Any], context: Dict[str, Any]):
        fileId = args.get("fileId", "")
        if not fileId:
            return ToolResult(toolCallId="", toolName="copyFile", success=False, error="fileId is required")
        try:
            chatService = services.chat
            copiedFile = chatService.interfaceDbComponent.copyFile(
                fileId,
                targetFolderId=args.get("targetFolderId"),
                newFileName=args.get("newFileName"),
            )
            return ToolResult(
                toolCallId="", toolName="copyFile", success=True,
                data=f"File copied as '{copiedFile.fileName}' (id: {copiedFile.id})",
                sideEvents=[{
                    "type": "fileCreated",
                    "data": {"fileId": copiedFile.id, "fileName": copiedFile.fileName,
                             "mimeType": copiedFile.mimeType, "fileSize": copiedFile.fileSize},
                }],
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="copyFile", success=False, error=str(e))

    async def _replaceInFile(args: Dict[str, Any], context: Dict[str, Any]):
        fileId = args.get("fileId", "")
        oldText = args.get("oldText", "")
        newText = args.get("newText", "")
        replaceAll = args.get("replaceAll", False)
        if not fileId or not oldText:
            return ToolResult(toolCallId="", toolName="replaceInFile", success=False, error="fileId and oldText are required")
        try:
            chatService = services.chat
            dbMgmt = chatService.interfaceDbComponent
            file = dbMgmt.getFile(fileId)
            if not file:
                return ToolResult(toolCallId="", toolName="replaceInFile", success=False, error=f"File {fileId} not found")
            if not dbMgmt.isTextMimeType(file.mimeType):
                return ToolResult(
                    toolCallId="", toolName="replaceInFile", success=False,
                    error=f"Cannot edit binary file ({file.mimeType}). Only text-based files are supported."
                )
            rawData = dbMgmt.getFileData(fileId)
            if not rawData:
                return ToolResult(toolCallId="", toolName="replaceInFile", success=False, error="File has no content")
            try:
                oldContent = rawData.decode("utf-8")
            except UnicodeDecodeError:
                return ToolResult(toolCallId="", toolName="replaceInFile", success=False, error="File content is not valid UTF-8 text")

            count = oldContent.count(oldText)
            if count == 0:
                return ToolResult(
                    toolCallId="", toolName="replaceInFile", success=False,
                    error="oldText not found in file. Use readFile or searchInFileContent to verify the exact text."
                )
            if count > 1 and not replaceAll:
                return ToolResult(
                    toolCallId="", toolName="replaceInFile", success=False,
                    error=f"oldText found {count} times. Set replaceAll=true or provide more surrounding context to make it unique."
                )

            newContent = oldContent.replace(oldText, newText) if replaceAll else oldContent.replace(oldText, newText, 1)

            editId = str(_uuid.uuid4())
            label = f"all {count} occurrences" if replaceAll else "1 occurrence"
            return ToolResult(
                toolCallId="", toolName="replaceInFile", success=True,
                data=f"Edit proposed for '{file.fileName}': replaced {label}. Waiting for user review.",
                sideEvents=[{
                    "type": "fileEditProposal",
                    "data": {
                        "id": editId,
                        "fileId": fileId,
                        "fileName": file.fileName,
                        "mimeType": file.mimeType,
                        "oldContent": oldContent,
                        "newContent": newContent,
                    },
                }],
            )
        except Exception as e:
            return ToolResult(toolCallId="", toolName="replaceInFile", success=False, error=str(e))

    registry.register(
        "deleteFolder", _deleteFolder,
        description="Delete a folder from the local workspace. Set recursive=true to delete all contents.",
        parameters={
            "type": "object",
            "properties": {
                "folderId": {"type": "string", "description": "The folder ID to delete"},
                "recursive": {"type": "boolean", "description": "If true, delete folder and all contents (files and subfolders). Default: false"},
            },
            "required": ["folderId"]
        },
        readOnly=False
    )

    registry.register(
        "renameFolder", _renameFolder,
        description="Rename a folder in the local workspace.",
        parameters={
            "type": "object",
            "properties": {
                "folderId": {"type": "string", "description": "The folder ID to rename"},
                "newName": {"type": "string", "description": "New folder name"},
            },
            "required": ["folderId", "newName"]
        },
        readOnly=False
    )

    registry.register(
        "moveFolder", _moveFolder,
        description="Move a folder to a different parent in the local workspace.",
        parameters={
            "type": "object",
            "properties": {
                "folderId": {"type": "string", "description": "The folder ID to move"},
                "targetParentId": {"type": "string", "description": "Target parent folder ID (null/omit for root)"},
            },
            "required": ["folderId"]
        },
        readOnly=False
    )

    registry.register(
        "copyFile", _copyFile,
        description="Create an independent copy of a file in the local workspace.",
        parameters={
            "type": "object",
            "properties": {
                "fileId": {"type": "string", "description": "The file ID to copy"},
                "targetFolderId": {"type": "string", "description": "Target folder for the copy (default: same folder)"},
                "newFileName": {"type": "string", "description": "New file name (default: same name, auto-numbered if duplicate)"},
            },
            "required": ["fileId"]
        },
        readOnly=False
    )

    registry.register(
        "replaceInFile", _replaceInFile,
        description=(
            "Replace specific text in an existing file. The edit is shown to the user for "
            "review (accept/reject) before being applied. Provide enough surrounding context "
            "in oldText to make the match unique (at least 2-3 lines). "
            "Use readFile or searchInFileContent first to identify the exact text to replace."
        ),
        parameters={
            "type": "object",
            "properties": {
                "fileId": {"type": "string", "description": "The file ID to edit"},
                "oldText": {"type": "string", "description": "Exact text to find and replace (must be unique unless replaceAll=true)"},
                "newText": {"type": "string", "description": "The replacement text"},
                "replaceAll": {"type": "boolean", "description": "Replace all occurrences (default: false)"},
            },
            "required": ["fileId", "oldText", "newText"]
        },
        readOnly=False
    )

