"""
coco Agent — 独立智能体

核心循环:
  1. 组装上下文 (身份 + 工作记忆 + 情景记忆 + 语义记忆)
  2. 调用 LLM 生成回复 (支持工具调用)
  3. 记录对话到情景记忆
  4. 会话结束时整理记忆 (摘要 + 事实提取)

语音对话:
  - 语音输入 → STT → Agent 回复 → TTS 播放
"""

import json
import re
import time
import threading
from pathlib import Path
from typing import Optional, Generator

from .config import AgentConfig
from .memory import MemoryEngine
from .runtime import (
    active_user_model,
    global_api_key,
    migrate_legacy_agent_memory,
)
from .voice import SpeechToText, TextToSpeech
from .tools import TOOLS, ToolExecutor


class Agent:
    """独立智能体"""

    MAX_TOOL_ROUNDS = 5  # 工具调用最多 5 轮，防止死循环

    def __init__(self, agent_dir: str, auto_load: bool = True, account_id: str = "local"):
        self.agent_dir = Path(agent_dir).resolve()
        self.agent_dir.mkdir(parents=True, exist_ok=True)
        self.account_id = account_id or "local"

        config_path = self.agent_dir / "config.yaml"
        self.config = AgentConfig.from_yaml(config_path)

        identity_path = self.agent_dir / "IDENTITY.md"
        self.identity = ""
        if identity_path.exists():
            self.identity = identity_path.read_text(encoding="utf-8")

        relationship_memory = migrate_legacy_agent_memory(
            self.agent_dir.name,
            self.agent_dir / "memory",
            account_id=self.account_id,
        )
        self.memory = MemoryEngine(
            self.agent_dir,
            memory_dir=relationship_memory,
            llm_call=self._llm_simple_call,
            raw_retention_days=self.config.memory.summary_threshold_days,
            summary_retention_days=self.config.memory.summary_retention_days,
            semantic_decay_days=self.config.memory.semantic_decay_days,
            semantic_forget_days=self.config.memory.semantic_forget_days,
        )

        # 工具系统
        self._tools = ToolExecutor(self.agent_dir / "workspace")

        # 语音模块
        self._stt: Optional[SpeechToText] = None
        self._tts: Optional[TextToSpeech] = None

        # 对话历史
        self._messages: list[dict] = []
        self._chat_lock = threading.RLock()

        if auto_load:
            self._init_session()

    # ─── 会话管理 ────────────────────────────────────────

    def _init_session(self):
        self._messages = [{"role": "system", "content": self._build_system_message()}]

    def _build_system_message(self, query: str = "") -> str:
        user_name, owner_settings = self._current_agent_relationship()
        current_model = active_user_model(account_id=self.account_id)
        model_name = str(current_model.get("name") or current_model.get("model") or "").strip()

        context = self.memory.assemble_context(
            token_budget=self.config.memory.max_context_tokens,
            query=query,
        )
        context = self._render_user_profile_text(context, user_name, owner_settings)
        system = self._render_user_profile_text(
            self.config.system_prompt or self.identity or "你是一个智能助手。",
            user_name,
            owner_settings,
        )

        user_profile_prompt = (
            f"当前主人称呼: {user_name}\n"
            f"- 与用户对话时，把用户称呼为“{user_name}”。\n"
            "- 智能体设置里的“主人”和“关系”是当前主人关系设定，优先于智能体身份和记忆里的旧描述。\n"
        )
        if owner_settings:
            user_profile_prompt += f"- 主人关系: {owner_settings}\n"
        if model_name:
            user_profile_prompt += f"- 当前大模型: {model_name}\n"
            user_profile_prompt += f"- 用户问你是什么模型时，直接回答“{model_name}”，不要说其它模型名。\n"

        tool_desc = "你可以使用工具来完成实际任务：\n"
        for t in TOOLS:
            fn = t["function"]
            params = ", ".join(fn["parameters"].get("required", []))
            tool_desc += f"  • {fn['name']}({params}) — {fn['description']}\n"
        tool_desc += "当需要执行实际操作时，直接调用这些工具。\n"

        style_guard = (
            "回答风格要求:\n"
            "- 默认像正常人对话一样简短直接，优先 1 句，最多 3 句。\n"
            "- 语音对话默认不超过 30 个中文字；能一句说完就别说第二句。\n"
            "- 不要铺垫、不要复述问题、不要自我解释。\n"
            f"- 不要说'好的{user_name}我来...'这类寒暄，直接给结论。\n"
            f"- 只有在{user_name}要求方案、步骤、代码或复杂分析时才展开。\n"
            "- 不要使用 Markdown 加粗符号 **。\n"
            "- 不要输出括号中的动作描写，例如（点头）（微笑）。\n"
            "- 语音对话尤其要短。"
        )

        return f"{system}\n\n{context}\n\n{user_profile_prompt}\n{tool_desc}\n\n{style_guard}"

    def _current_agent_relationship(self) -> tuple[str, str]:
        user_name = (getattr(self.config, "owner_name", "") or "公子").strip() or "公子"
        owner_settings = (getattr(self.config, "relationship", "") or "").strip()
        return user_name, owner_settings

    def _render_user_profile_text(self, text: str, user_name: str, owner_settings: str = "") -> str:
        replacements = [
            ("{{user_name}}", user_name),
            ("{user_name}", user_name),
            ("${user_name}", user_name),
            ("{{用户称呼}}", user_name),
            ("{用户称呼}", user_name),
            ("{{主人称呼}}", user_name),
            ("{主人称呼}", user_name),
            ("{{owner_settings}}", owner_settings),
            ("{owner_settings}", owner_settings),
            ("${owner_settings}", owner_settings),
            ("{{master_profile}}", owner_settings),
            ("{master_profile}", owner_settings),
            ("{{personal_settings}}", owner_settings),
            ("{personal_settings}", owner_settings),
            ("{{user_notes}}", owner_settings),
            ("{user_notes}", owner_settings),
            ("{{用户设定}}", owner_settings),
            ("{用户设定}", owner_settings),
            ("{{主人设定}}", owner_settings),
            ("{主人设定}", owner_settings),
            ("{{主人的设定}}", owner_settings),
            ("{主人的设定}", owner_settings),
        ]
        for key, value in replacements:
            text = text.replace(key, value)
        return text

    def _refresh_memory_context(self, query: str, system_overlay: str = ""):
        system_message = self._build_system_message(query=query)
        overlay = str(system_overlay or "").strip()
        if overlay:
            system_message = f"{system_message}\n\n{overlay}"
        if self._messages and self._messages[0].get("role") == "system":
            self._messages[0]["content"] = system_message
        else:
            self._messages.insert(0, {"role": "system", "content": system_message})

    def reset_session(self):
        self.memory.end_session()
        self._init_session()

    # ─── 对话接口 ────────────────────────────────────────

    def chat(self, user_input: str, save: bool = True, system_overlay: str = "") -> str:
        """同步对话（支持工具调用）"""
        with self._chat_lock:
            self._refresh_memory_context(user_input, system_overlay=system_overlay)
            self._messages.append({"role": "user", "content": user_input})

            response = self._run_tool_loop()

            self._messages.append({"role": "assistant", "content": response})

            if save:
                self.memory.record_conversation(user_input, response)

            return response

    def chat_stream(self, user_input: str, save: bool = True, system_overlay: str = "") -> Generator[str, None, None]:
        """
        流式对话

        注：流式模式下工具调用检测有延迟。如需要精确的工具调用，
        建议用 chat() 或在工具调用轮次收集结果后一次性输出。
        """
        with self._chat_lock:
            self._refresh_memory_context(user_input, system_overlay=system_overlay)
            self._messages.append({"role": "user", "content": user_input})

            full_response = self._run_tool_loop()

            self._messages.append({"role": "assistant", "content": full_response})

            if save:
                self.memory.record_conversation(user_input, full_response)

        # 逐 chunk yield（模拟流式）
        chunk_size = 4
        for i in range(0, len(full_response), chunk_size):
            yield full_response[i:i + chunk_size]

    # ─── 工具调用循环 ─────────────────────────────────

    def _run_tool_loop(self) -> str:
        """
        Agent-Critic 循环：
        1. 调用 LLM（带 tools 定义）
        2. 如果 LLM 返回 tool_call → 执行 → 把结果加入 messages → 回到 1
        3. 如果 LLM 返回纯文本 → 返回
        """
        for _ in range(self.MAX_TOOL_ROUNDS):
            self._repair_tool_message_sequence()
            msg = self._llm_chat_with_tools(self._messages)

            if msg.get("content") and not msg.get("tool_calls"):
                # 纯文本回复
                return msg["content"] or "（无内容）"

            if msg.get("tool_calls"):
                # 执行工具调用
                self._messages.append({
                    "role": "assistant",
                    "content": msg.get("content") or "",
                    "tool_calls": msg["tool_calls"],
                })

                for tc in msg["tool_calls"]:
                    self._messages.append(self._execute_tool_call(tc))

            else:
                # 没有 tool_calls 也没有 content，奇怪的响应
                return "（无法理解，请重新描述一下？）"

        return "（重复调用工具太多次，已停止）"

    def _execute_tool_call(self, tool_call: dict) -> dict:
        tool_call_id = tool_call.get("id", "")
        fn = tool_call.get("function", {})
        fn_name = fn.get("name", "")
        raw_args = fn.get("arguments") or "{}"

        try:
            fn_args = json.loads(raw_args)
            if not isinstance(fn_args, dict):
                raise ValueError("工具参数必须是 JSON 对象")
            print(f"  [工具] {fn_name}({fn_args})")
            result = self._tools.execute(fn_name, fn_args)
        except Exception as e:
            result = f"[工具错误] {fn_name or 'unknown'}: {e}"

        return {
            "role": "tool",
            "tool_call_id": tool_call_id,
            "content": str(result),
        }

    def _repair_tool_message_sequence(self):
        """确保每个 assistant.tool_calls 后面紧跟对应 tool 消息。"""
        repaired = []
        i = 0

        while i < len(self._messages):
            msg = self._messages[i]
            if msg.get("role") == "tool":
                i += 1
                continue

            repaired.append(msg)
            tool_calls = msg.get("tool_calls") or []
            if msg.get("role") != "assistant" or not tool_calls:
                i += 1
                continue

            expected_ids = [
                tc.get("id")
                for tc in tool_calls
                if tc.get("id")
            ]
            seen = set()
            i += 1

            while i < len(self._messages) and self._messages[i].get("role") == "tool":
                tool_msg = self._messages[i]
                tool_call_id = tool_msg.get("tool_call_id")
                if tool_call_id in expected_ids and tool_call_id not in seen:
                    repaired.append(tool_msg)
                    seen.add(tool_call_id)
                i += 1

            for tool_call_id in expected_ids:
                if tool_call_id not in seen:
                    repaired.append({
                        "role": "tool",
                        "tool_call_id": tool_call_id,
                        "content": "[工具错误] 上一次工具调用被中断，已补齐占位结果。",
                    })

        self._messages = repaired

    # ─── 语音模块 ────────────────────────────────────────

    def _init_voice(self):
        if self._stt is None:
            self._stt = SpeechToText(
                region=getattr(self.config.voice, "stt_region", "ap-guangzhou"),
                engine_model_type=getattr(self.config.voice, "stt_engine_model_type", "16k_zh"),
                hotword_list=getattr(self.config.voice, "stt_hotword_list", ""),
                hotword_id=getattr(self.config.voice, "stt_hotword_id", ""),
                customization_id=getattr(self.config.voice, "stt_customization_id", ""),
                filter_dirty=getattr(self.config.voice, "stt_filter_dirty", 0),
                filter_modal=getattr(self.config.voice, "stt_filter_modal", 0),
                filter_punc=getattr(self.config.voice, "stt_filter_punc", 0),
                convert_num_mode=getattr(self.config.voice, "stt_convert_num_mode", 1),
            )
        if self._tts is None:
            self._tts = TextToSpeech(
                voice=self.config.voice.tts_voice,
                rate=getattr(self.config.voice, "tts_rate", "+0%"),
            )

    def listen(self) -> str:
        """按键录音：Enter 开始，说完再 Enter 结束。返回 /QUIT 表示退出。"""
        self._init_voice()
        try:
            text = self._stt.transcribe_mic_ptt(language=self.config.voice.language)
            return text.strip()
        except Exception as e:
            print(f"听不见了: {e}")
            return ""

    def speak(self, text: str):
        self._init_voice()
        try:
            spoken = self._prepare_spoken_text(text)
            if spoken:
                self._tts.speak(spoken, block=True)
        except Exception as e:
            print(f"说不了: {e}")

    def _prepare_spoken_text(self, text: str) -> str:
        """去掉不适合朗读的舞台动作/旁白。"""
        text = re.sub(r"（[^）]*）", "", text)
        text = re.sub(r"\([^)]*\)", "", text)
        text = re.sub(r"【[^】]*】", "", text)
        text = re.sub(r"\[[^\]]*\]", "", text)
        text = re.sub(r"\*\*(.*?)\*\*", r"\1", text)
        text = re.sub(r"__(.*?)__", r"\1", text)
        text = re.sub(r"`([^`]*)`", r"\1", text)
        text = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"\1", text)
        text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
        text = re.sub(r"(?m)^\s{0,3}#{1,6}\s*", "", text)
        text = re.sub(r"(?m)^\s*[-*+]\s+", "", text)
        text = text.replace("*", "")
        text = text.replace("_", "")
        text = text.replace("~", "")
        text = re.sub(r"\s+", " ", text)
        return text.strip()

    def chat_voice(self) -> Optional[str]:
        """语音对话(一轮): 按键录音 → 识别 → 回复 → 朗读。返回 '/QUIT' 表示用户要退出。"""
        self._init_voice()
        user_text = self.listen()
        if not user_text:
            return None

        if user_text == "/QUIT":
            return "/QUIT"

        user_name, _ = self._current_agent_relationship()
        print(f"{user_name}: {user_text}")

        print(f"\n{self.config.name}: ", end="", flush=True)
        response = self.chat(user_text)
        print(response)

        if response and "错误" not in response:
            self.speak(response)

        return response

    # ─── 记忆操作 ────────────────────────────────────────

    def remember_this(self, fact: str, tags: list = None):
        self.memory.semantic.add(fact, tags=tags)
        return f"已记住: {fact}"

    def forget(self, query: str):
        results = self.memory.search(query, limit=10)
        if not results:
            return "未找到相关记忆"
        lines = ["找到以下相关记忆:"]
        for i, r in enumerate(results):
            lines.append(f"  [{i}] (score={r['score']}) {r.get('content', r.get('name', ''))[:80]}")
        return "\n".join(lines)

    def load_memory(self, module_id: str) -> str:
        if self.memory.load_module(module_id):
            return f"已加载记忆模块: {module_id}"
        return f"加载失败: {module_id}"

    def unload_memory(self, module_id: str) -> str:
        if self.memory.unload_module(module_id):
            return f"已卸载记忆模块: {module_id}"
        return f"卸载失败: {module_id} (可能是核心模块或不存在)"

    def list_memories(self, memory_type: str = None) -> str:
        modules = self.memory.list_modules(memory_type)
        if not modules:
            return "暂无记忆模块"
        lines = ["记忆模块:"]
        for m in modules:
            status = "●" if m["loaded"] else "○"
            lines.append(f"  {status} {m['id']} [{m['type']}] - {m.get('summary', '(无摘要)')[:50]}")
        return "\n".join(lines)

    def memory_stats(self) -> str:
        stats = self.memory.get_stats()
        return (
            f"记忆统计:\n"
            f"  活跃模块: {stats['active_modules']} / {stats['total_modules']}\n"
            f"  情景记忆: {stats['episodic_entries']} 条\n"
            f"  对话摘要: {stats.get('summaries', 0)} 条\n"
            f"  语义事实: {stats['semantic_facts']} 条\n"
            f"  程序技能: {stats['procedural_skills']} 条\n"
            f"  上下文估算: ~{stats['estimated_context_tokens']} tokens\n"
            f"  关系记忆目录: {stats.get('memory_dir', '')}"
        )

    def search_memory(self, query: str) -> str:
        results = self.memory.search(query, limit=5)
        if not results:
            return f"未找到与 '{query}' 相关的记忆"
        lines = [f"搜索 '{query}' 结果:"]
        for r in results:
            content = r.get("content", r.get("name", ""))[:100]
            lines.append(f"  - [{r['score']}] {content}")
        return "\n".join(lines)

    # ─── LLM 调用 ────────────────────────────────────────

    def _llm_chat_with_tools(self, messages: list[dict]) -> dict:
        """带工具调用的 LLM 请求，返回完整 message 对象"""
        import requests

        model = self._active_model_config()
        payload = {
            "model": model["model"],
            "messages": messages,
            "max_tokens": self.config.max_response_tokens,
            "tools": TOOLS,
            "tool_choice": "auto",
        }

        resp = requests.post(
            self._chat_completions_url(model["api_base"]),
            headers={
                "Authorization": f"Bearer {self._get_api_key(model)}",
                "Content-Type": "application/json",
            },
            json=payload,
            timeout=120,
        )

        if resp.status_code != 200:
            raise RuntimeError(f"API 错误 ({resp.status_code}): {resp.text[:200]}")

        choice = resp.json()["choices"][0]
        return choice["message"]

    def _llm_chat(self, messages: list[dict]) -> str:
        msg = self._llm_chat_with_tools(messages)
        return msg.get("content", "")

    def _llm_chat_stream(self, messages: list[dict]) -> Generator[str, None, None]:
        import requests

        model = self._active_model_config()
        payload = {
            "model": model["model"],
            "messages": messages,
            "max_tokens": self.config.max_response_tokens,
            "stream": True,
        }

        resp = requests.post(
            self._chat_completions_url(model["api_base"]),
            headers={
                "Authorization": f"Bearer {self._get_api_key(model)}",
                "Content-Type": "application/json",
            },
            json=payload,
            stream=True,
            timeout=120,
        )

        if resp.status_code != 200:
            raise RuntimeError(f"API 错误 ({resp.status_code}): {resp.text[:200]}")

        for line in resp.iter_lines():
            if not line:
                continue
            line = line.decode("utf-8")
            if line.startswith("data: "):
                data = line[6:]
                if data == "[DONE]":
                    break
                try:
                    chunk = json.loads(data)
                    delta = chunk["choices"][0].get("delta", {})
                    content = delta.get("content", "")
                    if content:
                        yield content
                except json.JSONDecodeError:
                    continue

    def _chat_completions_url(self, api_base: str) -> str:
        value = str(api_base or "").strip().rstrip("/")
        if value.endswith("/chat/completions"):
            return value
        return f"{value}/chat/completions"

    def _llm_simple_call(self, prompt: str, max_tokens: int = 500) -> str:
        try:
            messages = [{"role": "user", "content": prompt}]
            return self._llm_chat(messages)
        except Exception:
            return ""

    def _active_model_config(self) -> dict:
        model = active_user_model(account_id=self.account_id)
        if not model.get("configured") and self.config.api_base:
            model["api_base"] = self.config.api_base
        if not model.get("api_key") and self.config.api_key:
            model["api_key"] = self.config.api_key
        return model

    def _get_api_key(self, model: dict | None = None) -> str:
        model = model or self._active_model_config()
        return model.get("api_key", "") or global_api_key() or self.config.api_key

    # ─── 生命周期 ────────────────────────────────────────

    def close(self):
        self.memory.end_session()
        self.config.to_yaml(self.agent_dir / "config.yaml")

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    def __del__(self):
        try:
            self.close()
        except Exception:
            pass
