"""MultimodalAgent — processes files into multimodal LLM messages.

Extends AgentTemplate to accept files (local paths or HTTP URLs) via
``context["files"]``, automatically downloading, converting, and encoding
them before sending to the LLM.

Usage::

    agent = MultimodalAgent(config)
    result = await Runner.run(
        agent,
        messages=[{"role": "user", "content": "Analyze these files"}],
        context={
            "files": [
                {"url": "https://signed-url...", "file_name": "img.png"},
                {"path": "/local/doc.pdf", "content_type": "application/pdf"},
            ]
        },
    )

Content type routing:
    - Text (.txt, .md, .csv, .json, ...) → injected as text in the prompt
    - Office (.docx, .pptx) → converted to text, injected in the prompt
    - Images (.png, .jpg, .gif, .webp) → base64, multimodal image content
    - PDF (.pdf) → base64, multimodal document content
    - Unsupported → skipped with warning

When used as agent-as-tool, overrides ``as_tool()`` to expose a ``files``
parameter so the calling agent can pass file entries.
"""

from __future__ import annotations

import os
from typing import Any

from fluxibly.agent.agent_template import AgentTemplate
from fluxibly.agent.base import AgentConfig
from fluxibly.agent.utils.multimodal import (
    convert_office_to_text,
    detect_content_type,
    download_file,
    encode_visual_content,
    is_office_type,
    is_text_type,
    is_visual_type,
    read_text_file,
)
from fluxibly.logging import Logger

logger = Logger(component="multimodal_agent")


class MultimodalAgent(AgentTemplate):
    """Agent that processes files into multimodal LLM messages.

    Accepts files via ``context["files"]``. Each entry is a dict with:
        - ``url`` (str): HTTP(S) URL to download, OR
        - ``path`` (str): local file path to read directly
        - ``file_name`` (str, optional): display name / extension hint
        - ``content_type`` (str, optional): explicit MIME type

    Files are processed in ``pre_forward()`` before the LLM call:
        - Text/office files are read/converted and appended to the prompt
        - Visual files (images, PDFs) are base64-encoded as multimodal parts
        - Downloaded files are saved to ``{sandbox_dir}/multimodal/``
          and cleaned up by the Runner's sandbox lifecycle
    """

    def __init__(self, config: AgentConfig) -> None:
        super().__init__(config)

    async def pre_forward(
        self,
        messages: list[dict[str, Any]],
        context: dict[str, Any],
    ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
        """Process files from context and build multimodal messages.

        Pops ``files`` from context, downloads/reads each file, and modifies
        the last user message to include text attachments and visual content.
        """
        files = context.pop("files", None)
        if not files:
            return messages, context

        text_attachments: list[str] = []
        visual_parts: list[dict[str, Any]] = []

        # Determine download directory
        download_dir = self._get_download_dir()

        for entry in files:
            try:
                local_path, file_name, content_type = await self._resolve_file(
                    entry, download_dir
                )

                if is_text_type(content_type):
                    text = read_text_file(local_path)
                    text_attachments.append(f"\n--- {file_name} ---\n{text}")
                    logger.info(
                        "Attached text file: {name} ({ct})",
                        name=file_name,
                        ct=content_type,
                    )

                elif is_office_type(content_type):
                    text = convert_office_to_text(local_path, content_type)
                    text_attachments.append(f"\n--- {file_name} ---\n{text}")
                    logger.info(
                        "Converted office file: {name} ({ct})",
                        name=file_name,
                        ct=content_type,
                    )

                elif is_visual_type(content_type):
                    part = encode_visual_content(local_path, content_type)
                    visual_parts.append(part)
                    logger.info(
                        "Encoded visual file: {name} ({ct})",
                        name=file_name,
                        ct=content_type,
                    )

                else:
                    logger.warning(
                        "Unsupported file type, skipping: {name} ({ct})",
                        name=file_name,
                        ct=content_type,
                    )

            except Exception as e:
                file_name = entry.get(
                    "file_name", entry.get("url", entry.get("path", "unknown"))
                )
                logger.error(
                    "Failed to process file {name}: {err}",
                    name=file_name,
                    err=e,
                )

        # Modify the last user message with processed content
        if text_attachments or visual_parts:
            messages = self._inject_into_messages(
                messages, text_attachments, visual_parts
            )

        return messages, context

    def as_tool(
        self,
        tool_name: str | None = None,
        tool_description: str | None = None,
    ) -> dict:
        """Expose this agent as a callable tool with a ``files`` parameter.

        Overrides BaseAgent.as_tool() to add ``files`` alongside ``input``,
        enabling calling agents to pass file entries for multimodal analysis.
        """
        name = tool_name or f"agent_{self.config.name}"
        desc = (
            tool_description
            or self.config.description
            or f"Analyze files via {self.config.name}"
        )
        return {
            "type": "function",
            "function": {
                "name": name,
                "description": desc,
                "parameters": {
                    "type": "object",
                    "properties": {
                        "input": {
                            "type": "string",
                            "description": "The analysis query or task",
                        },
                        "files": {
                            "type": "array",
                            "description": (
                                "Files to analyze. Each item has 'url' "
                                "(signed HTTP URL), 'file_name', and "
                                "optionally 'content_type'."
                            ),
                            "items": {
                                "type": "object",
                                "properties": {
                                    "url": {"type": "string"},
                                    "file_name": {"type": "string"},
                                    "content_type": {"type": "string"},
                                },
                            },
                        },
                    },
                    "required": ["input"],
                },
            },
        }

    # ── Private helpers ──────────────────────────────────────────

    def _get_download_dir(self) -> str:
        """Return the directory for downloaded files."""
        if self.sandbox_dir:
            download_dir = os.path.join(self.sandbox_dir, "multimodal")
        else:
            import tempfile

            download_dir = os.path.join(
                tempfile.gettempdir(), "fluxibly_multimodal"
            )
            logger.warning(
                "No sandbox_dir set — using fallback: {dir}",
                dir=download_dir,
            )
        os.makedirs(download_dir, exist_ok=True)
        return download_dir

    async def _resolve_file(
        self,
        entry: dict[str, Any],
        download_dir: str,
    ) -> tuple[str, str, str]:
        """Resolve a file entry to (local_path, file_name, content_type).

        Downloads from URL if needed, detects content type.
        """
        file_name = entry.get("file_name", "")
        explicit_ct = entry.get("content_type")

        if "url" in entry:
            url = entry["url"]
            local_path = await download_file(url, download_dir, file_name)
            if not file_name:
                file_name = os.path.basename(local_path)
        elif "path" in entry:
            local_path = entry["path"]
            if not file_name:
                file_name = os.path.basename(local_path)
        else:
            raise ValueError(f"File entry must have 'url' or 'path': {entry}")

        content_type = detect_content_type(file_name, explicit_ct)
        return local_path, file_name, content_type

    @staticmethod
    def _inject_into_messages(
        messages: list[dict[str, Any]],
        text_attachments: list[str],
        visual_parts: list[dict[str, Any]],
    ) -> list[dict[str, Any]]:
        """Inject text and visual content into the last user message."""
        # Find the last user message
        for i in range(len(messages) - 1, -1, -1):
            if messages[i]["role"] == "user":
                original = messages[i]["content"]

                if isinstance(original, str):
                    full_text = original
                    if text_attachments:
                        full_text += "\n" + "\n".join(text_attachments)

                    if visual_parts:
                        # Convert to multimodal content list
                        content: list[dict[str, Any]] = [
                            {"type": "text", "text": full_text}
                        ]
                        content.extend(visual_parts)
                        messages[i] = {**messages[i], "content": content}
                    else:
                        messages[i] = {**messages[i], "content": full_text}

                elif isinstance(original, list):
                    # Already multimodal — append to existing text part
                    # and add visual parts
                    if text_attachments:
                        attachment_text = "\n".join(text_attachments)
                        for part in original:
                            if part.get("type") == "text":
                                part["text"] += "\n" + attachment_text
                                break
                        else:
                            original.insert(
                                0,
                                {"type": "text", "text": attachment_text},
                            )
                    original.extend(visual_parts)

                break

        return messages
