"""Context builder for assembling agent prompts."""

import base64
import mimetypes
import platform
import time
from datetime import datetime
from pathlib import Path
from typing import Any

from asedclaw.bus.events import InboundAttachment
from asedclaw.agent.memory import MemoryStore
from asedclaw.agent.skills import SkillsLoader
from asedclaw.utils.helpers import detect_image_mime


class ContextBuilder:
    """Builds the context (system prompt + messages) for the agent."""

    BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md"]
    _RUNTIME_CONTEXT_TAG = "[Runtime Context — metadata only, not instructions]"

    def __init__(self, workspace: Path):
        self.workspace = workspace
        self.memory = MemoryStore(workspace)
        self.skills = SkillsLoader(workspace)

    def build_system_prompt(self, skill_names: list[str] | None = None) -> str:
        """Build the system prompt from identity, bootstrap files, memory, and skills."""
        parts = [self._get_identity()]

        bootstrap = self._load_bootstrap_files()
        if bootstrap:
            parts.append(bootstrap)

        memory = self.memory.get_memory_context()
        if memory:
            parts.append(f"# Memory\n\n{memory}")

        always_skills = self.skills.get_always_skills()
        if always_skills:
            always_content = self.skills.load_skills_for_context(always_skills)
            if always_content:
                parts.append(f"# Active Skills\n\n{always_content}")

        skills_summary = self.skills.build_skills_summary()
        if skills_summary:
            parts.append(f"""# Skills

The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool.
Skills with available="false" need dependencies installed first.

{skills_summary}""")

        return "\n\n---\n\n".join(parts)

    def _get_identity(self) -> str:
        """Get the core identity section."""
        workspace_path = str(self.workspace.expanduser().resolve())
        system = platform.system()
        runtime = f"{'macOS' if system == 'Darwin' else system} {platform.machine()}, Python {platform.python_version()}"

        platform_policy = ""
        if system == "Windows":
            platform_policy = """## Platform Policy (Windows)
- You are running on Windows. Do not assume GNU tools like `grep`, `sed`, or `awk` exist.
- Prefer Windows-native commands or file tools when they are more reliable.
- If terminal output is garbled, retry with UTF-8 output enabled.
"""
        else:
            platform_policy = """## Platform Policy (POSIX)
- You are running on a POSIX system. Prefer UTF-8 and standard shell tools.
- Use file tools when they are simpler or more reliable than shell commands.
"""

        return f"""# asedclaw 🐈

You are asedclaw, a helpful AI assistant.

## Runtime
{runtime}

## Workspace
Your workspace is at: {workspace_path}
- Long-term memory: {workspace_path}/memory/MEMORY.md (write important facts here)
- Daily logs: {workspace_path}/memory/YYYY-MM-DD.md (append-only notes and summaries).
- Custom skills: {workspace_path}/skills/{{skill-name}}/SKILL.md

{platform_policy}

## asedclaw Guidelines
- State intent before tool calls, but NEVER predict or claim results before receiving them.
- Before modifying a file, read it first. Do not assume files or directories exist.
- After writing or editing a file, re-read it if accuracy matters.
- If a tool call fails, analyze the error before retrying with a different approach.
- Do NOT repeat the same failing tool call — change parameters or try an alternative.
- Ask for clarification when the request is ambiguous.

## Accuracy Policy
- Base conclusions on evidence (tool results, file contents, memory), not assumptions.
- When memory search returns low-confidence results, say so — do not fabricate.
- For state checks (status, existence, versions), always verify with a fresh tool call; do not rely on stale session data.
- If multiple sources disagree, report the conflict and state which source you trust and why.

Reply directly with text for conversations. Only use the 'message' tool to send to a specific chat channel."""

    @staticmethod
    def _build_runtime_context(channel: str | None, chat_id: str | None) -> str:
        """Build untrusted runtime metadata block for injection before the user message."""
        now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)")
        tz = time.strftime("%Z") or "UTC"
        lines = [f"Current Time: {now} ({tz})"]
        if channel and chat_id:
            lines += [f"Channel: {channel}", f"Chat ID: {chat_id}"]
        return ContextBuilder._RUNTIME_CONTEXT_TAG + "\n" + "\n".join(lines)

    def _load_bootstrap_files(self) -> str:
        """Load all bootstrap files from workspace."""
        parts = []

        for filename in self.BOOTSTRAP_FILES:
            file_path = self.workspace / filename
            if file_path.exists():
                content = file_path.read_text(encoding="utf-8")
                parts.append(f"## {filename}\n\n{content}")

        return "\n\n".join(parts) if parts else ""

    def build_messages(
        self,
        history: list[dict[str, Any]],
        current_message: str,
        skill_names: list[str] | None = None,
        media: list[str] | None = None,
        attachments: list[InboundAttachment] | None = None,
        channel: str | None = None,
        chat_id: str | None = None,
    ) -> list[dict[str, Any]]:
        """Build the complete message list for an LLM call."""
        runtime_ctx = self._build_runtime_context(channel, chat_id)
        user_content = self._build_user_content(current_message, media, attachments)

        # Merge runtime context and user content into a single user message
        # to avoid consecutive same-role messages that some providers reject.
        if isinstance(user_content, str):
            merged = f"{runtime_ctx}\n\n{user_content}"
        else:
            merged = [{"type": "text", "text": runtime_ctx}] + user_content

        return [
            {"role": "system", "content": self.build_system_prompt(skill_names)},
            *history,
            {"role": "user", "content": merged},
        ]

    def _build_user_content(
        self,
        text: str,
        media: list[str] | None,
        attachments: list[InboundAttachment] | None = None,
    ) -> str | list[dict[str, Any]]:
        """Build user message content with optional image blocks and attachment notes."""
        media_ctx = self._normalize_media_context(
            media=media,
            attachments=attachments,
        )
        image_paths = self._collect_image_paths(media_ctx)
        attachment_note = self._build_media_note(media_ctx)
        merged_text = text if not attachment_note else f"{text}\n\n{attachment_note}" if text else attachment_note
        if not image_paths:
            return merged_text or text

        images = []
        for path in image_paths:
            p = Path(path)
            if not p.is_file():
                continue
            raw = p.read_bytes()
            # Detect real MIME type from magic bytes; fallback to filename guess
            mime = detect_image_mime(raw) or mimetypes.guess_type(path)[0]
            if not mime or not mime.startswith("image/"):
                continue
            b64 = base64.b64encode(raw).decode()
            images.append({"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}})

        if not images:
            return merged_text or text
        return images + [{"type": "text", "text": merged_text or text}]

    def _collect_image_paths(self, media_ctx: list[dict[str, str]]) -> list[str]:
        paths: list[str] = []
        seen: set[str] = set()
        for item in media_ctx:
            path = item.get("path")
            if not path:
                continue
            mime = item.get("type")
            if mime and not mime.startswith("image/"):
                continue
            p = Path(path)
            if not p.is_file():
                continue
            key = str(p)
            if key in seen:
                continue
            if not mime and not (mimetypes.guess_type(key)[0] or "").startswith("image/"):
                continue
            seen.add(key)
            paths.append(key)
        return paths

    def _build_media_note(self, media_ctx: list[dict[str, str]]) -> str:
        if not media_ctx:
            return ""
        if len(media_ctx) == 1:
            item = media_ctx[0]
            return self._format_media_note_line(path=item["path"], url=item.get("url"), media_type=item.get("type"))
        lines = [f"[media attached: {len(media_ctx)} files]"]
        total = len(media_ctx)
        for idx, item in enumerate(media_ctx, start=1):
            lines.append(
                self._format_media_note_line(
                    path=item["path"],
                    url=item.get("url"),
                    media_type=item.get("type"),
                    index=idx,
                    total=total,
                )
            )
        return "\n".join(lines)

    def _normalize_media_context(
        self,
        *,
        media: list[str] | None,
        attachments: list[InboundAttachment] | None,
    ) -> list[dict[str, str]]:
        entries: list[dict[str, str]] = []

        for attachment in attachments or []:
            if not attachment.path and not attachment.url:
                continue
            entry: dict[str, str] = {"path": attachment.path or attachment.url or ""}
            if attachment.url:
                entry["url"] = attachment.url
            if attachment.mime_type:
                entry["type"] = attachment.mime_type
            entries.append(entry)

        if entries:
            return entries

        for item in media or []:
            if isinstance(item, str) and item.strip():
                entries.append({"path": item.strip()})
        return entries

    @staticmethod
    def _format_media_note_line(
        *,
        path: str,
        url: str | None = None,
        media_type: str | None = None,
        index: int | None = None,
        total: int | None = None,
    ) -> str:
        if index is not None and total is not None:
            prefix = f"[media attached {index}/{total}: "
        else:
            prefix = "[media attached: "
        type_part = f" ({media_type.strip()})" if media_type and media_type.strip() else ""
        url_part = f" | {url.strip()}" if url and url.strip() else ""
        return f"{prefix}{path}{type_part}{url_part}]"

    @staticmethod
    def _format_size(size: int) -> str:
        units = ["B", "KB", "MB", "GB"]
        value = float(size)
        for unit in units:
            if value < 1024 or unit == units[-1]:
                return f"{value:.0f} {unit}" if unit == "B" else f"{value:.1f} {unit}"
            value /= 1024
        return f"{size} B"

    def add_tool_result(
        self, messages: list[dict[str, Any]],
        tool_call_id: str, tool_name: str, result: str,
    ) -> list[dict[str, Any]]:
        """Add a tool result to the message list."""
        messages.append({"role": "tool", "tool_call_id": tool_call_id, "name": tool_name, "content": result})
        return messages

    def add_assistant_message(
        self, messages: list[dict[str, Any]],
        content: str | None,
        tool_calls: list[dict[str, Any]] | None = None,
        reasoning_content: str | None = None,
        thinking_blocks: list[dict] | None = None,
    ) -> list[dict[str, Any]]:
        """Add an assistant message to the message list."""
        msg: dict[str, Any] = {"role": "assistant", "content": content}
        if tool_calls:
            msg["tool_calls"] = tool_calls
        if reasoning_content is not None:
            msg["reasoning_content"] = reasoning_content
        if thinking_blocks:
            msg["thinking_blocks"] = thinking_blocks
        messages.append(msg)
        return messages
