"""Message tool for sending messages to users."""

from pathlib import Path
from typing import Any, Awaitable, Callable

from agno.tools import Toolkit
from asedclaw.bus.events import OutboundMessage


class MessageTool(Toolkit):
    """Toolkit to send messages and attachments to users on chat channels.

    Key behaviour (mirrors openclaw's publish_stop_flag pattern):
    - After a successful `message()` call agno stops the current agent run
      (stop_after_tool_call=True).  This prevents the LLM from generating
      another turn or calling the tool again for the same response.
    - The agent loop's `_assemble_reply` detects `_sent_in_turn=True` and
      suppresses the final text echo, avoiding duplicate delivery.
    """

    def __init__(
        self,
        send_callback: Callable[[OutboundMessage], Awaitable[None]] | None = None,
        default_channel: str = "",
        default_chat_id: str = "",
        default_message_id: str | None = None,
        workspace: Path | None = None,
    ):
        self._send_callback = send_callback
        self._default_channel = default_channel
        self._default_chat_id = default_chat_id
        self._default_message_id = default_message_id
        self._workspace = workspace
        self._sent_in_turn: bool = False
        self._calls_in_turn: int = 0
        self._sent_fingerprints: set[tuple[str, str, str, tuple[str, ...]]] = set()
        self._sent_media_paths: set[str] = set()
        self._sent_texts: list[str] = []
        # stop_after_tool_call_tools stops the agno agent run immediately after
        # message() returns, preventing the LLM from looping or echoing the reply.
        super().__init__(
            name="message",
            tools=[self.message],
            stop_after_tool_call_tools=["message"],
        )

    def set_context(self, channel: str, chat_id: str, message_id: str | None = None) -> None:
        """Set the current message context."""
        self._default_channel = channel
        self._default_chat_id = chat_id
        self._default_message_id = message_id

    def set_send_callback(self, callback: Callable[[OutboundMessage], Awaitable[None]]) -> None:
        """Set the callback for sending messages."""
        self._send_callback = callback

    def start_turn(self) -> None:
        """Reset per-turn send tracking."""
        self._sent_in_turn = False
        self._calls_in_turn = 0
        self._sent_fingerprints.clear()
        self._sent_media_paths.clear()
        self._sent_texts.clear()

    @property
    def sent_media_paths(self) -> set[str]:
        """Paths successfully sent via message tool this turn (read-only)."""
        return set(self._sent_media_paths)

    @property
    def sent_texts(self) -> list[str]:
        """Text content successfully sent via message tool this turn (read-only)."""
        return list(self._sent_texts)

    def _normalize_media(self, media: Any, **kwargs: Any) -> tuple[list[str], list[str]]:
        """
        Normalize attachments from various argument shapes.

        Supports:
        - media: str | list[str] | list[dict]
        - kwargs aliases: attachments/files/file/file_path/path
        """
        candidates: list[Any] = []
        warnings: list[str] = []

        if media is not None:
            if isinstance(media, list):
                candidates.extend(media)
            else:
                candidates.append(media)

        for key in ("attachments", "files"):
            value = kwargs.get(key)
            if isinstance(value, list):
                candidates.extend(value)
            elif value is not None:
                candidates.append(value)
        for key in ("file", "file_path", "path"):
            value = kwargs.get(key)
            if value is not None:
                candidates.append(value)

        result: list[str] = []
        for item in candidates:
            path_value: str | None = None
            if isinstance(item, str):
                path_value = item
            elif isinstance(item, dict):
                for key in ("path", "file_path", "file"):
                    value = item.get(key)
                    if isinstance(value, str) and value.strip():
                        path_value = value
                        break

            if not path_value:
                warnings.append(f"ignored invalid media item: {item!r}")
                continue

            p = Path(path_value).expanduser()
            if not p.is_absolute() and self._workspace is not None:
                p = self._workspace / p
            try:
                resolved = p.resolve()
            except Exception:
                resolved = p
            if not resolved.is_file():
                warnings.append(f"media file not found: {resolved}")
                continue
            result.append(str(resolved))

        # Keep order while de-duplicating.
        deduped = list(dict.fromkeys(result))
        return deduped, warnings

    async def message(
        self,
        content: str,
        channel: str | None = None,
        chat_id: str | None = None,
        message_id: str | None = None,
        media: list[str] | None = None,
        **kwargs: Any
    ) -> str:
        """Send your final response to the user and end the current agent turn.

        **IMPORTANT**: Calling this tool immediately stops the agent run.
        The agent will NOT generate any further output after this call.
        Use it exactly ONCE when you have a complete answer or update ready.
        Do NOT call it multiple times for the same response — one call is enough.

        Args:
            content (str): The message text to send. Supports Markdown formatting.
                           Required. Must not be empty.
            channel (str | None): Target channel name (e.g. "telegram", "ased").
                                  Defaults to the current conversation channel.
                                  Leave unset in almost all cases.
            chat_id (str | None): Target chat/conversation ID.
                                  Defaults to the current conversation.
                                  Leave unset in almost all cases.
            message_id (str | None): ID of the user message being replied to.
                                     Leave unset; the system fills this automatically.
            media (list[str] | None): Local file paths to attach to the message.
                                      Relative paths are resolved from the workspace root.
                                      Only existing files are attached; missing ones are skipped.
                                      Example: ["reports/summary.pdf", "/tmp/chart.png"]

        Returns:
            str: "Message delivered." on success, or an error string on failure.

        Examples:
            message(content="任务已完成，请查看附件。", media=["output/report.pdf"])
            message(content="搜索结果如下：\\n- 结果1\\n- 结果2")
            message(content="请查收日志文件。", media=["logs/app.log"])
        """
        self._calls_in_turn += 1
        channel = channel or self._default_channel
        chat_id = chat_id or self._default_chat_id
        message_id = message_id or self._default_message_id
        normalized_media, media_warnings = self._normalize_media(media, **kwargs)

        if not channel or not chat_id:
            return "Error: No target channel/chat specified."

        if not self._send_callback:
            return "Error: Message sending not configured."

        if self._calls_in_turn > 4:
            return "Error: message tool called too many times in one turn."

        fingerprint = (channel, chat_id, content.strip(), tuple(normalized_media))
        if fingerprint in self._sent_fingerprints:
            return "Message already sent in this turn. Do not repeat the same call."

        msg = OutboundMessage(
            channel=channel,
            chat_id=chat_id,
            content=content,
            media=normalized_media,
            metadata={
                "message_id": message_id,
            },
        )

        try:
            await self._send_callback(msg)
            self._sent_fingerprints.add(fingerprint)
            self._sent_media_paths.update(normalized_media)
            if content and content.strip():
                self._sent_texts.append(content.strip())
            if channel == self._default_channel and chat_id == self._default_chat_id:
                self._sent_in_turn = True
            # Return a minimal, neutral confirmation.
            # Verbose channel details are intentionally omitted to prevent
            # the LLM from treating the result as a prompt for further action.
            if media_warnings:
                return f"Message delivered. (warnings: {'; '.join(media_warnings)})"
            return "Message delivered."
        except Exception as e:
            return f"Error sending message: {str(e)}"

    async def execute(
        self,
        content: str,
        channel: str | None = None,
        chat_id: str | None = None,
        message_id: str | None = None,
        media: list[str] | None = None,
        **kwargs: Any
    ) -> str:
        return await self.message(content, channel, chat_id, message_id, media, **kwargs)
