# 中文注释：
# 文件：echobot/agent/context.py
# 说明：智能体核心逻辑，包括主循环、上下文、记忆与任务队列。

"""
上下文构建器 - 组装 Agent 的系统提示词和消息
=================================================

此模块负责构建 Agent 运行时所需的完整上下文，包括：

1. 核心身份（Identity）:
   - Agent 名称、当前时间、工作空间路径
   - 可用工具说明

2. 引导文件（Bootstrap Files）:
   - AGENTS.md: Agent 指令和行为准则
   - SOUL.md: Agent 的"灵魂"和个性
   - USER.md: 用户信息
   - TOOLS.md: 工具说明
   - IDENTITY.md: 身份定义

3. 记忆上下文:
   - 长期记忆 (MEMORY.md)
   - 今日笔记 (memory/YYYY-MM-DD.md)
   - 最近 N 天的记忆

4. 技能上下文:
   - 必加载技能（always=true）：始终包含在提示中
   - 可用技能：只显示摘要，需要时再加载完整内容

设计特点：
- 渐进式加载：只加载必要的技能，避免上下文过长
- 结构化输出：XML 格式的技能列表，便于解析
- 媒体支持：支持图片附件（Base64 编码）

主要类：
- ContextBuilder: 上下文构建器

使用示例：
    builder = ContextBuilder(workspace_path)
    system_prompt = builder.build_system_prompt(skill_names)
    messages = builder.build_messages(history, current_message, media)
"""

import base64
import mimetypes
from pathlib import Path
from typing import Any

from echobot.agent.memory import MemoryStore
from echobot.agent.skills import SkillsLoader


class ContextBuilder:
    """
    上下文构建器 - 组装 Agent 运行所需的完整上下文

    此类负责将各种来源的信息整合成 Agent 可以理解的完整提示词，
    包括系统提示词、会话历史、技能、记忆等。

    核心功能：
    1. build_system_prompt(): 构建完整的系统提示词
    2. build_messages(): 构建发送给 LLM 的消息列表
    3. 渐进式技能加载：必加载技能+技能摘要
    4. 媒体附件支持：图片 Base64 编码

    使用示例：
        builder = ContextBuilder(Path("~/.echobot/workspace"))
        system_prompt = builder.build_system_prompt(["coding", "debugging"])
        messages = builder.build_messages(history, "Hello!", media=["image.png"])
    """

    # 需要加载的引导文件名
    BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md", "IDENTITY.md"]

    def __init__(self, workspace: Path):
        """
        初始化上下文构建器

        Args:
            workspace: 工作空间路径（包含引导文件和记忆目录）
        """
        self.workspace = workspace
        self.memory = MemoryStore(workspace)
        self.skills = SkillsLoader(workspace)

    def build_system_prompt(
        self,
        skill_names: list[str] | None = None,
        role_content: str | None = None,
        role_skill_names: list[str] | None = None,
    ) -> str:
        """
        构建完整的系统提示词

        系统提示词由多个部分组成：
        1. 核心身份（Identity）- Agent 的基本信息和能力
        2. 角色定义（Role）- 当前角色的技能和描述
        3. 引导文件内容（Bootstrap）- AGENTS.md、SOUL.md 等
        4. 记忆上下文 - MEMORY.md 和今日笔记
        5. 必加载技能 - always=true 的技能完整内容（根据角色过滤）
        6. 可用技能列表 - 技能摘要（需要时再加载完整内容，根据角色过滤）

        Args:
            skill_names: 可选，要包含的技能名称列表
            role_content: 可选，角色定义内容（来自 RoleManager）
            role_skill_names: 可选，角色允许的技能名称列表

        Returns:
            str: 完整的系统提示词，包含多个部分用 "---" 分隔
        """
        parts = []

        # 1. 核心身份
        parts.append(self._get_identity())

        # 2. 角色定义（如果有）
        if role_content:
            parts.append(f"# Character Setting\n\n{role_content}")

        # 3. 引导文件
        bootstrap = self._load_bootstrap_files()
        if bootstrap:
            parts.append(bootstrap)

        # 4. 记忆上下文
        memory = self.memory.get_memory_context()
        if memory:
            parts.append(f"# Memory\n\n{memory}")

        # 5. 必加载技能 - 根据角色过滤
        always_skills = self.skills.get_always_skills()
        if always_skills:
            # 如果指定了角色技能列表，则过滤
            if role_skill_names is not None:
                always_skills = [s for s in always_skills if s in role_skill_names]
            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}")

        # 6. 可用技能列表 - 根据角色过滤
        skills_summary = self.skills.build_skills_summary()
        if skills_summary:
            # 如果指定了角色技能列表，则过滤摘要
            if role_skill_names is not None:
                # 简单处理：显示角色允许的技能摘要
                filtered_summary = []
                for line in skills_summary.split("\n"):
                    if line.strip().startswith("- ") or line.strip().startswith("## "):
                        skill_name = line.strip().replace("- ", "").split(":")[0].strip()
                        if skill_name in role_skill_names or "## " in line:
                            filtered_summary.append(line)
                    else:
                        filtered_summary.append(line)
                skills_summary = "\n".join(filtered_summary)

            if skills_summary.strip():
                parts.append(f"""# Skills

The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool.
If a skill has executable scripts under `skills/<name>/scripts`, use `run_skill_script` to run them.
Skills with available="false" need dependencies installed first - you can try installing them with apt/brew.

{skills_summary}""")

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

    def _get_identity(self) -> str:
        """
        获取核心身份部分

        包含：
        - Agent 名称和简介
        - 当前时间
        - 工作空间路径
        - 可用工具列表
        - 重要提示

        Returns:
            str: 格式化的身份文本
        """
        from datetime import datetime

        now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)")
        workspace_path = str(self.workspace.expanduser().resolve())

        return f"""# echobot 🐈

You are echobot, a helpful AI assistant. You have access to tools that allow you to:
- Read, write, and edit files
- Execute shell commands
- Search the web and fetch web pages
- Send messages to users on chat channels
- Create/list/remove scheduled cron jobs for future reminders or periodic tasks
- Spawn subagents for complex background tasks

## Current Time
{now}

## Workspace
Your workspace is at: {workspace_path}
- Memory files: {workspace_path}/memory/MEMORY.md
- Daily notes: {workspace_path}/memory/YYYY-MM-DD.md
- Custom skills: {workspace_path}/skills/{{skill-name}}/SKILL.md

IMPORTANT: When responding to direct questions or conversations, reply directly with your text response.
Only use the 'message' tool when you need to send a message to a specific chat channel (like Telegram).
For normal conversation, just respond with text - do not call the message tool.

IMPORTANT: If user asks for reminders/notifications at specific times, use cron tools
to create scheduled jobs instead of saying you cannot proactively notify.
You MUST call create_cron_job for these requests and then confirm the schedule.
Do not claim "I cannot proactively message you" when cron tools are available.
Example: "每天早上8点提醒我看新闻" -> call create_cron_job with cron_expr="0 8 * * *".
For "每天8点直接发今日新闻", set cron job message to "搜索今天新闻并直接发送摘要给用户".
If user asks whether a reminder/task exists or was configured, call list_cron_jobs first.
Never claim a message/search/schedule was completed unless the corresponding tool call succeeded.

Always be helpful, accurate, and concise. When using tools, explain what you're doing.
When remembering something, write to {workspace_path}/memory/MEMORY.md"""

    def _load_bootstrap_files(self) -> str:
        """
        加载所有引导文件

        引导文件是工作空间根目录下的 Markdown 文件，用于定义
        Agent 的行为准则、个性、用户信息等。

        Returns:
            str: 所有引导文件的连接内容，如果不存在则返回空字符串
        """
        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,
        role_content: str | None = None,
        role_skill_names: list[str] | None = None,
        has_image_tool: bool = False,
    ) -> list[dict[str, Any]]:
        """
        构建发送给 LLM 的完整消息列表

        消息列表结构：
        1. System Message - 系统提示词
        2. History - 对话历史
        3. User Message - 当前消息（可能包含图片附件）

        Args:
            history: 对话历史消息列表，每个消息包含 role 和 content
            current_message: 当前用户消息
            skill_names: 可选的技能名称列表
            media: 可选的图片文件路径列表（会进行 Base64 编码）
            role_content: 可选的角色定义内容
            role_skill_names: 可选的角色允许的技能名称列表

        Returns:
            list[dict[str, Any]]: 格式化的消息列表，可直接发送给 LLM API

        使用示例：
            messages = builder.build_messages(
                history=[{"role": "user", "content": "Hi"}],
                current_message="What's the weather?",
                media=["photo.jpg"]
            )
        """
        messages = []

        # 1. 系统提示词
        system_prompt = self.build_system_prompt(skill_names, role_content, role_skill_names)
        messages.append({"role": "system", "content": system_prompt})

        # 2. 对话历史
        messages.extend(history)

        # 3. 当前消息（可能包含图片）
        user_content = self._build_user_content(current_message, media, has_image_tool)
        messages.append({"role": "user", "content": user_content})

        return messages

    def _build_user_content(self, text: str, media: list[str] | None, has_image_tool: bool = False) -> str | list[dict[str, Any]]:
        """
        构建用户消息内容，支持图片附件

        如果提供了图片文件，会将其转换为 Base64 编码的图片 URL 格式，
        符合 OpenAI 多模态消息规范。

        Args:
            text: 文本内容
            media: 图片文件路径列表
            has_image_tool: 是否有 MCP 图片理解工具可用

        Returns:
            str: 如果没有图片，返回纯文本
            list: 如果有图片，返回包含图片 URL 和文本的列表
        """
        if not media:
            return text

        valid_media = []
        for path in media:
            p = Path(path)
            mime, _ = mimetypes.guess_type(path)
            # 只处理图片类型
            if not p.is_file() or not mime or not mime.startswith("image/"):
                continue
            valid_media.append(path)

        # 如果没有有效图片，返回纯文本
        if not valid_media:
            return text

        # 清理文本中的 [image: ...] 标记
        import re
        cleaned_text = re.sub(r'\[image:[^\]]*\]', '', text).strip()

        # 提示 LLM 使用 MCP 工具理解图片
        media_info = ", ".join([f"'{p}'" for p in valid_media])
        mcp_hint = f"\n\n【重要 - 必须执行】检测到用户发送了图片 {media_info}。\n你必须立即调用 MCP 工具 'understand_image' 来分析这张图片。\n调用参数：\n- image_source: 使用图片的绝对路径 {valid_media[0]}\n- prompt: 根据用户的问题'{cleaned_text}'构造一个合适的prompt来分析图片"

        # 如果有 MCP 图片工具，不发送 base64 给 LLM，只发送提示让 LLM 调用工具
        if has_image_tool:
            return cleaned_text + mcp_hint

        # 没有 MCP 工具时，使用 base64 图片（兼容性处理）
        images = []
        for path in valid_media:
            p = Path(path)
            mime, _ = mimetypes.guess_type(path)
            b64 = base64.b64encode(p.read_bytes()).decode()
            images.append({"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}})

        # 返回多模态内容：图片 + 文本 + MCP 提示
        content_text = cleaned_text + mcp_hint
        return images + [{"type": "text", "text": content_text}]

    def add_tool_result(
        self, messages: list[dict[str, Any]], tool_call_id: str, tool_name: str, result: str
    ) -> list[dict[str, Any]]:
        """
        向消息列表添加工具执行结果

        当 Agent 调用工具后，需要将执行结果添加到消息列表中，
        以便 LLM 可以看到结果并决定下一步操作。

        Args:
            messages: 当前消息列表
            tool_call_id: 工具调用的 ID（来自 LLM 的 tool_calls 响应）
            tool_name: 工具名称
            result: 工具执行结果

        Returns:
            更新后的消息列表

        使用示例：
            messages = builder.add_tool_result(
                messages, "call_123", "read_file", "File content..."
            )
        """
        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,
    ) -> list[dict[str, Any]]:
        """
        向消息列表添加助手消息

        用于添加 LLM 的响应消息，可能包含工具调用。

        Args:
            messages: 当前消息列表
            content: 助手消息内容
            tool_calls: 可选的工具调用列表

        Returns:
            更新后的消息列表
        """
        msg: dict[str, Any] = {"role": "assistant", "content": content or ""}

        if tool_calls:
            msg["tool_calls"] = tool_calls

        messages.append(msg)
        return messages
