from __future__ import annotations

import logging
import mimetypes
from pathlib import Path
from typing import Any
from typing import Literal, cast

from llm_async.models import Tool

from minibot.adapters.files.local_storage import LocalFileStorage
from minibot.app.event_bus import EventBus
from minibot.core.agent_runtime import AgentMessage, AppendMessageDirective, MessagePart, MessageRole, ToolResult
from minibot.core.channels import ChannelFileResponse
from minibot.core.events import OutboundFileEvent
from minibot.llm.tools.arg_utils import optional_int, optional_str, require_non_empty_str
from minibot.llm.tools.base import ToolBinding, ToolContext
from minibot.llm.tools.schema_utils import nullable_boolean, nullable_integer, nullable_string, strict_object
from minibot.shared.path_utils import to_posix_relative


class FileStorageTool:
    _IMAGE_MIME_PREFIX = "image/"

    def __init__(
        self,
        storage: LocalFileStorage,
        event_bus: EventBus | None = None,
    ) -> None:
        self._storage = storage
        self._event_bus = event_bus
        self._logger = logging.getLogger("minibot.tools.file_storage")

    def bindings(self) -> list[ToolBinding]:
        return [
            ToolBinding(tool=self._filesystem_schema(), handler=self._filesystem),
            ToolBinding(tool=self._artifact_insert_schema(), handler=self._self_insert_artifact),
            ToolBinding(tool=self._list_files_schema(), handler=self._list_files),
            ToolBinding(tool=self._glob_files_schema(), handler=self._glob_files),
            ToolBinding(tool=self._file_info_schema(), handler=self._file_info),
            ToolBinding(tool=self._create_file_schema(), handler=self._create_file),
            ToolBinding(tool=self._move_file_schema(), handler=self._move_file),
            ToolBinding(tool=self._delete_file_schema(), handler=self._delete_file),
            ToolBinding(tool=self._send_file_schema(), handler=self._send_file),
            ToolBinding(tool=self._self_insert_artifact_schema(), handler=self._self_insert_artifact),
        ]

    def _filesystem_schema(self) -> Tool:
        return Tool(
            name="filesystem",
            description=("Managed workspace file operations. Use action=list|glob|info|write|move|delete|send."),
            parameters=strict_object(
                properties={
                    "action": {
                        "type": "string",
                        "enum": ["list", "glob", "info", "write", "move", "delete", "send"],
                        "description": "Filesystem operation to perform.",
                    },
                    "folder": nullable_string("Optional folder path for list/glob."),
                    "pattern": nullable_string("Glob pattern for action=glob."),
                    "limit": nullable_integer(minimum=1, description="Optional result limit for action=glob."),
                    "path": nullable_string("Path for info/write/delete/send."),
                    "content": nullable_string("Content for action=write."),
                    "overwrite": nullable_boolean("Overwrite flag for action=write/move."),
                    "source_path": nullable_string("Source path for action=move."),
                    "destination_path": nullable_string("Destination path for action=move."),
                    "target": {
                        **nullable_string("Target kind filter for action=delete."),
                        "enum": ["any", "file", "folder", None],
                    },
                    "recursive": nullable_boolean("Recursive delete for action=delete."),
                    "caption": nullable_string("Optional caption for action=send."),
                },
                required=[
                    "action",
                    "folder",
                    "pattern",
                    "limit",
                    "path",
                    "content",
                    "overwrite",
                    "source_path",
                    "destination_path",
                    "target",
                    "recursive",
                    "caption",
                ],
            ),
        )

    def _artifact_insert_schema(self) -> Tool:
        return Tool(
            name="artifact_insert",
            description=(
                "Insert a managed file artifact into conversation context for multimodal analysis. "
                "Equivalent to self_insert_artifact."
            ),
            parameters=self._self_insert_artifact_schema().parameters,
        )

    def _list_files_schema(self) -> Tool:
        return Tool(
            name="list_files",
            description=(
                "List files and folders in the managed workspace. "
                "Use this to explore available files, verify file existence, or browse folder contents. "
                "Returns entries with names, types (file/folder), sizes, and modification times."
            ),
            parameters=strict_object(
                properties={
                    "folder": nullable_string("Optional folder relative to the managed root. Defaults to root.")
                },
                required=["folder"],
            ),
        )

    def _create_file_schema(self) -> Tool:
        return Tool(
            name="create_file",
            description=(
                "Create a new text or markdown file in the managed workspace. "
                "Use this to save notes, create documents, store structured data, or generate text-based content. "
                "Supports creating files in subdirectories (e.g., 'notes/today.md'). "
                "Set overwrite=true to replace existing files, or false to prevent accidental overwrites."
            ),
            parameters=strict_object(
                properties={
                    "path": {
                        "type": "string",
                        "description": "Relative file path (for example notes/today.md).",
                    },
                    "content": {
                        "type": "string",
                        "description": "Text content to write into the file.",
                    },
                    "overwrite": nullable_boolean("Set true to replace existing files."),
                },
                required=["path", "content", "overwrite"],
            ),
        )

    def _glob_files_schema(self) -> Tool:
        return Tool(
            name="glob_files",
            description=(
                "Search for files matching a glob pattern in the managed workspace. "
                "Use this to find specific file types (e.g., '**/*.png' for images, '**/*.md' for markdown), "
                "locate files by naming pattern (e.g., 'screenshot_*.png'), or discover files in nested directories. "
                "Supports wildcards: * (any characters), ** (recursive directories), ? (single character)."
            ),
            parameters=strict_object(
                properties={
                    "pattern": {
                        "type": "string",
                        "description": "Glob pattern (for example **/*.md or uploads/**/*.png).",
                    },
                    "folder": nullable_string("Optional folder relative to managed root to scope search."),
                    "limit": nullable_integer(
                        minimum=1,
                        description="Maximum number of matches to return. Defaults to all matches.",
                    ),
                },
                required=["pattern", "folder", "limit"],
            ),
        )

    def _file_info_schema(self) -> Tool:
        return Tool(
            name="file_info",
            description=(
                "Get detailed metadata for a specific file in the managed workspace. "
                "Returns file size, MIME type, creation/modification times, and existence status. "
                "Use this to check file properties, verify file type, or get precise information before operations."
            ),
            parameters=strict_object(
                properties={"path": {"type": "string", "description": "Relative file path under managed root."}},
                required=["path"],
            ),
        )

    def _send_file_schema(self) -> Tool:
        return Tool(
            name="send_file",
            description=(
                "Send a file from the managed workspace to the user via the current channel (Telegram/Console). "
                "The file will be delivered to the same chat where the user's message originated. "
                "Use this to share screenshots, images, documents, or any files generated by tools or agents. "
                "For Telegram: sends as photo (images) or document (other files). For Console: displays file path. "
                "Path must be relative to workspace root (e.g., 'browser/screenshot.png', 'notes/summary.txt')."
            ),
            parameters=strict_object(
                properties={
                    "path": {
                        "type": "string",
                        "description": "Relative file path under the managed root.",
                    },
                    "caption": nullable_string("Optional caption sent with the file."),
                },
                required=["path", "caption"],
            ),
        )

    def _move_file_schema(self) -> Tool:
        return Tool(
            name="move_file",
            description=(
                "Move or rename a file within the managed workspace. "
                "Use this to reorganize files, rename files for clarity, move files between folders, "
                "or restructure the workspace organization. Both source and destination must be within workspace. "
                "Set overwrite=true to replace existing destination files, or false to prevent accidental overwrites."
            ),
            parameters=strict_object(
                properties={
                    "source_path": {
                        "type": "string",
                        "description": "Existing relative file path under managed root.",
                    },
                    "destination_path": {
                        "type": "string",
                        "description": "Target relative file path under managed root.",
                    },
                    "overwrite": nullable_boolean("Set true to replace an existing destination file."),
                },
                required=["source_path", "destination_path", "overwrite"],
            ),
        )

    def _delete_file_schema(self) -> Tool:
        return Tool(
            name="delete_file",
            description=(
                "Delete a file or folder from the managed workspace. "
                "Use this to clean up temporary files, remove outdated content, or maintain workspace hygiene. "
                "Set target='file' to only delete files, 'folder' to only delete folders, or 'any' for both. "
                "Set recursive=true to delete non-empty folders and their contents, or false for empty folders only. "
                "CAUTION: Deletion is permanent and cannot be undone."
            ),
            parameters=strict_object(
                properties={
                    "path": {
                        "type": "string",
                        "description": "Relative path under managed root.",
                    },
                    "target": {
                        **nullable_string("Target kind filter. Use folder to only delete folders."),
                        "enum": ["any", "file", "folder", None],
                    },
                    "recursive": nullable_boolean("Set true to delete non-empty folders recursively."),
                },
                required=["path", "target", "recursive"],
            ),
        )

    def _self_insert_artifact_schema(self) -> Tool:
        return Tool(
            name="self_insert_artifact",
            description=(
                "Inject a file from managed workspace into the conversation context for multimodal analysis. "
                "Use this to analyze images (vision), read documents, or include files in conversation history. "
                "When as='image': enables vision analysis of images (requires vision-capable model). "
                "When as='file': includes file metadata for reference. "
                "The injected content becomes part of conversation context and can be analyzed in messages. "
                "Path must be relative to workspace root (e.g., 'uploads/photo.jpg', 'browser/screenshot.png')."
            ),
            parameters=strict_object(
                properties={
                    "path": {
                        "type": "string",
                        "description": "Relative file path under managed root (for example uploads/friends.jpg).",
                    },
                    "as": {
                        "type": "string",
                        "enum": ["image", "file"],
                        "description": "How to represent this file in injected message content.",
                    },
                    "role": {
                        **nullable_string("Target role for injected message. Defaults to user."),
                        "enum": ["user", "system", None],
                    },
                    "text": nullable_string("Optional text prepended before injected file/image part."),
                    "mime": nullable_string("Optional MIME hint."),
                    "filename": nullable_string("Optional display filename for file mode."),
                },
                required=["path", "as", "role", "text", "mime", "filename"],
            ),
        )

    async def _list_files(self, payload: dict[str, Any], _: ToolContext) -> dict[str, Any]:
        folder = optional_str(payload.get("folder"))
        entries = self._storage.list_files(folder)
        return {
            "root_dir": str(self._storage.root_dir),
            "folder": folder or ".",
            "entries": entries,
            "count": len(entries),
        }

    async def _glob_files(self, payload: dict[str, Any], _: ToolContext) -> dict[str, Any]:
        pattern = require_non_empty_str(payload, "pattern")
        folder = optional_str(payload.get("folder"))
        limit = optional_int(
            payload.get("limit"),
            field="limit",
            min_value=1,
            allow_float=False,
            allow_string=False,
            type_error="Expected integer value",
            min_error="Expected integer value >= 1",
        )
        entries = self._storage.glob_files(pattern=pattern, folder=folder, limit=limit)
        return {
            "root_dir": str(self._storage.root_dir),
            "folder": folder or ".",
            "pattern": pattern,
            "limit": limit,
            "entries": entries,
            "count": len(entries),
        }

    async def _create_file(self, payload: dict[str, Any], _: ToolContext) -> dict[str, Any]:
        path = require_non_empty_str(payload, "path")
        content = require_non_empty_str(payload, "content")
        overwrite = bool(payload.get("overwrite") or False)
        result = self._storage.create_text_file(path=path, content=content, overwrite=overwrite)
        return {
            "ok": True,
            "path": result["path"],
            "bytes_written": result["bytes_written"],
            "overwrite": overwrite,
        }

    async def _file_info(self, payload: dict[str, Any], _: ToolContext) -> dict[str, Any]:
        path = require_non_empty_str(payload, "path")
        info = self._storage.file_info(path)
        return {
            "ok": True,
            **info,
        }

    async def _send_file(self, payload: dict[str, Any], context: ToolContext) -> dict[str, Any]:
        if self._event_bus is None:
            raise ValueError("send_file is unavailable because no event bus is configured")
        if not context.channel:
            raise ValueError("channel context is required")
        if context.chat_id is None:
            raise ValueError("chat context is required")

        path = require_non_empty_str(payload, "path")
        caption = optional_str(payload.get("caption"))
        absolute_path = self._storage.resolve_existing_file(path)

        await self._event_bus.publish(
            OutboundFileEvent(
                response=ChannelFileResponse(
                    channel=context.channel,
                    chat_id=context.chat_id,
                    file_path=str(absolute_path),
                    caption=caption,
                )
            )
        )
        return {
            "ok": True,
            "path": str(path),
            "chat_id": context.chat_id,
            "channel": context.channel,
            "sent": True,
        }

    async def _move_file(self, payload: dict[str, Any], _: ToolContext) -> dict[str, Any]:
        source_path = require_non_empty_str(payload, "source_path")
        destination_path = require_non_empty_str(payload, "destination_path")
        overwrite = bool(payload.get("overwrite") or False)
        result = self._storage.move_file(
            source_path=source_path,
            destination_path=destination_path,
            overwrite=overwrite,
        )
        return {
            "ok": True,
            "source_path": result["source_path"],
            "destination_path": result["destination_path"],
            "overwrite": overwrite,
        }

    async def _delete_file(self, payload: dict[str, Any], _: ToolContext) -> dict[str, Any]:
        path = require_non_empty_str(payload, "path")
        target = (optional_str(payload.get("target")) or "any").lower()
        if target not in {"any", "file", "folder"}:
            raise ValueError("target must be one of: any, file, folder")
        recursive = bool(payload.get("recursive") or False)
        resolved_target = cast(Literal["any", "file", "folder"], target)
        result = self._storage.delete_file(path, recursive=recursive, target=resolved_target)
        deleted_count = int(result.get("deleted_count", 0))
        resolved_path = str(result["path"])
        target_type = str(result.get("target_type") or "path")
        message = (
            f"Deleted {target_type} successfully: {resolved_path}"
            if deleted_count > 0
            else f"No file or folder found to delete: {resolved_path}"
        )
        return {
            "ok": True,
            "path": result["path"],
            "deleted": bool(result.get("deleted", False)),
            "deleted_count": deleted_count,
            "target": target,
            "recursive": recursive,
            "target_type": target_type,
            "message": message,
        }

    async def _self_insert_artifact(self, payload: dict[str, Any], _: ToolContext) -> ToolResult:
        path = require_non_empty_str(payload, "path")
        insert_as = require_non_empty_str(payload, "as").lower()
        if insert_as not in {"image", "file"}:
            return ToolResult(
                content={
                    "status": "error",
                    "code": "invalid_as",
                    "message": "as must be either 'image' or 'file'",
                }
            )
        role = optional_str(payload.get("role")) or "user"
        if role not in {"user", "system"}:
            return ToolResult(
                content={
                    "status": "error",
                    "code": "invalid_role",
                    "message": "role must be user or system",
                }
            )

        text = optional_str(payload.get("text"))
        mime_hint = optional_str(payload.get("mime"))
        filename_hint = optional_str(payload.get("filename"))
        self._logger.debug(
            "self_insert_artifact resolving managed path",
            extra={"path": path, "managed_root": str(self._storage.root_dir)},
        )

        try:
            absolute_path = self._storage.resolve_existing_file(path)
        except ValueError as exc:
            reason = str(exc)
            self._logger.debug(
                "self_insert_artifact rejected input",
                extra={"path": path, "reason": reason},
            )
            if reason == "file does not exist":
                code = "file_not_found"
            elif reason in {
                "path is not a file",
                "path must be relative to managed root",
                "path escapes managed root",
            }:
                code = "invalid_path"
            else:
                code = "invalid_path"
            return ToolResult(content={"status": "error", "code": code, "message": reason})

        relative_path = to_posix_relative(absolute_path, self._storage.root_dir)
        resolved_mime = self._resolve_mime(absolute_path, mime_hint)
        filename = filename_hint or absolute_path.name
        file_size = absolute_path.stat().st_size

        if insert_as == "image" and not resolved_mime.startswith(self._IMAGE_MIME_PREFIX):
            self._logger.debug(
                "self_insert_artifact rejected non-image mime for image mode",
                extra={"path": relative_path, "mime": resolved_mime},
            )
            return ToolResult(
                content={
                    "status": "error",
                    "code": "unsupported_mime",
                    "message": f"managed file MIME {resolved_mime} is not an image",
                }
            )

        parts: list[MessagePart] = []
        if text is not None:
            parts.append(MessagePart(type="text", text=text))
        source = {"type": "managed_file", "path": relative_path}
        if insert_as == "image":
            parts.append(MessagePart(type="image", source=source, mime=resolved_mime))
        else:
            parts.append(MessagePart(type="file", source=source, mime=resolved_mime, filename=filename))

        directives = [
            AppendMessageDirective(
                type="append_message",
                message=AgentMessage(role=cast(MessageRole, role), content=parts),
            )
        ]
        self._logger.debug(
            "self_insert_artifact created append_message directive",
            extra={
                "path": relative_path,
                "mime": resolved_mime,
                "size": file_size,
                "insert_as": insert_as,
                "role": role,
            },
        )
        return ToolResult(
            content={
                "status": "ok",
                "path": relative_path,
                "mime": resolved_mime,
                "size": file_size,
            },
            directives=directives,
        )

    async def _filesystem(self, payload: dict[str, Any], context: ToolContext) -> dict[str, Any] | ToolResult:
        action = (optional_str(payload.get("action")) or "").lower()
        if action == "list":
            return await self._list_files({"folder": payload.get("folder")}, context)
        if action == "glob":
            return await self._glob_files(
                {
                    "pattern": payload.get("pattern"),
                    "folder": payload.get("folder"),
                    "limit": payload.get("limit"),
                },
                context,
            )
        if action == "info":
            return await self._file_info({"path": payload.get("path")}, context)
        if action == "write":
            return await self._create_file(
                {
                    "path": payload.get("path"),
                    "content": payload.get("content"),
                    "overwrite": payload.get("overwrite"),
                },
                context,
            )
        if action == "move":
            return await self._move_file(
                {
                    "source_path": payload.get("source_path"),
                    "destination_path": payload.get("destination_path"),
                    "overwrite": payload.get("overwrite"),
                },
                context,
            )
        if action == "delete":
            return await self._delete_file(
                {
                    "path": payload.get("path"),
                    "target": payload.get("target"),
                    "recursive": payload.get("recursive"),
                },
                context,
            )
        if action == "send":
            return await self._send_file(
                {
                    "path": payload.get("path"),
                    "caption": payload.get("caption"),
                },
                context,
            )
        raise ValueError("action must be one of: list, glob, info, write, move, delete, send")

    @staticmethod
    def _resolve_mime(path: Path, mime_hint: str | None) -> str:
        if isinstance(mime_hint, str) and mime_hint.strip():
            return mime_hint.strip().lower()
        guessed, _ = mimetypes.guess_type(str(path), strict=False)
        if isinstance(guessed, str) and guessed:
            return guessed.lower()
        return "application/octet-stream"
