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.action_dispatcher import dispatch_action
from minibot.llm.tools.base import ToolBinding, ToolContext
from minibot.llm.tools.description_loader import load_tool_description
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")
        self._filesystem_handlers = {
            "list": self._filesystem_list,
            "glob": self._filesystem_glob,
            "info": self._filesystem_info,
            "write": self._filesystem_write,
            "move": self._filesystem_move,
            "delete": self._filesystem_delete,
            "send": self._filesystem_send,
        }

    def bindings(self) -> list[ToolBinding]:
        return [
            ToolBinding(tool=self._filesystem_schema(), handler=self._filesystem),
            ToolBinding(tool=self._glob_files_schema(), handler=self._glob_files),
            ToolBinding(tool=self._read_file_schema(), handler=self._read_file),
            ToolBinding(tool=self._self_insert_artifact_schema(), handler=self._self_insert_artifact),
        ]

    def _filesystem_schema(self) -> Tool:
        return Tool(
            name="filesystem",
            description=load_tool_description("filesystem"),
            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 _glob_files_schema(self) -> Tool:
        return Tool(
            name="glob_files",
            description=load_tool_description("glob_files"),
            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 _read_file_schema(self) -> Tool:
        return Tool(
            name="read_file",
            description=load_tool_description("read_file"),
            parameters=strict_object(
                properties={"path": {"type": "string", "description": "Relative file path under managed root."}},
                required=["path"],
            ),
        )

    def _self_insert_artifact_schema(self) -> Tool:
        return Tool(
            name="self_insert_artifact",
            description=load_tool_description("self_insert_artifact"),
            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"))
        raw_entries = self._storage.list_files(folder)
        entries = [self._entry_with_canonical_paths(entry) for entry in raw_entries]
        return {
            "action": "list",
            "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",
        )
        raw_entries = self._storage.glob_files(pattern=pattern, folder=folder, limit=limit)
        entries = [self._entry_with_canonical_paths(entry) for entry in raw_entries]
        return {
            "action": "glob",
            "root_dir": str(self._storage.root_dir),
            "folder": folder or ".",
            "pattern": pattern,
            "limit": limit,
            "entries": entries,
            "count": len(entries),
        }

    async def _read_file(self, payload: dict[str, Any], _: ToolContext) -> dict[str, Any]:
        path = require_non_empty_str(payload, "path")
        result = self._storage.read_text_file(path)
        return {
            "action": "read",
            **result,
            **self._canonical_path_payload(str(result["path"])),
        }

    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 {
            "action": "write",
            "ok": True,
            "path": result["path"],
            "bytes_written": result["bytes_written"],
            "overwrite": overwrite,
            **self._canonical_path_payload(str(result["path"])),
        }

    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 {
            "action": "info",
            "ok": True,
            **info,
            **self._canonical_path_payload(str(info["path"])),
        }

    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 {
            "action": "send",
            "ok": True,
            "path": str(path),
            "chat_id": context.chat_id,
            "channel": context.channel,
            "sent": True,
            **self._canonical_path_payload(str(path)),
        }

    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 {
            "action": "move",
            "ok": True,
            "source_path": result["source_path"],
            "destination_path": result["destination_path"],
            "overwrite": overwrite,
            **self._canonical_path_payload(str(result["source_path"]), prefix="source_"),
            **self._canonical_path_payload(str(result["destination_path"]), prefix="destination_"),
        }

    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 {
            "action": "delete",
            "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,
            **self._canonical_path_payload(resolved_path),
        }

    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_list(self, payload: dict[str, Any], context: ToolContext) -> dict[str, Any]:
        return await self._list_files({"folder": payload.get("folder")}, context)

    async def _filesystem_glob(self, payload: dict[str, Any], context: ToolContext) -> dict[str, Any]:
        return await self._glob_files(
            {
                "pattern": payload.get("pattern"),
                "folder": payload.get("folder"),
                "limit": payload.get("limit"),
            },
            context,
        )

    async def _filesystem_info(self, payload: dict[str, Any], context: ToolContext) -> dict[str, Any]:
        return await self._file_info({"path": payload.get("path")}, context)

    async def _filesystem_write(self, payload: dict[str, Any], context: ToolContext) -> dict[str, Any]:
        return await self._create_file(
            {
                "path": payload.get("path"),
                "content": payload.get("content"),
                "overwrite": payload.get("overwrite"),
            },
            context,
        )

    async def _filesystem_move(self, payload: dict[str, Any], context: ToolContext) -> dict[str, Any]:
        return await self._move_file(
            {
                "source_path": payload.get("source_path"),
                "destination_path": payload.get("destination_path"),
                "overwrite": payload.get("overwrite"),
            },
            context,
        )

    async def _filesystem_delete(self, payload: dict[str, Any], context: ToolContext) -> dict[str, Any]:
        return await self._delete_file(
            {
                "path": payload.get("path"),
                "target": payload.get("target"),
                "recursive": payload.get("recursive"),
            },
            context,
        )

    async def _filesystem_send(self, payload: dict[str, Any], context: ToolContext) -> dict[str, Any]:
        return await self._send_file(
            {
                "path": payload.get("path"),
                "caption": payload.get("caption"),
            },
            context,
        )

    async def _filesystem(self, payload: dict[str, Any], context: ToolContext) -> dict[str, Any] | ToolResult:
        action = (optional_str(payload.get("action")) or "").lower()
        return await dispatch_action(
            action=action,
            payload=payload,
            context=context,
            handlers=self._filesystem_handlers,
            error_message="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"

    def _entry_with_canonical_paths(self, entry: dict[str, Any]) -> dict[str, Any]:
        path_value = entry.get("path")
        if not isinstance(path_value, str) or not path_value.strip():
            return dict(entry)
        return {
            **entry,
            **self._canonical_path_payload(path_value),
        }

    def _canonical_path_payload(self, path_value: str, *, prefix: str = "") -> dict[str, Any]:
        resolved = self._storage.resolve_file(path_value)
        absolute = resolved.resolve().as_posix()
        if resolved.is_relative_to(self._storage.root_dir):
            relative = to_posix_relative(resolved, self._storage.root_dir)
            scope = "inside_root"
        else:
            relative = None
            scope = "outside_root"
        return {
            f"{prefix}path_relative": relative,
            f"{prefix}path_absolute": absolute,
            f"{prefix}path_scope": scope,
        }
