# -*- coding: utf-8 -*-
"""
知识保存工具
Agent 任务后自反思时调用，原子化新增或更新知识条目（name 存在则覆盖），
# 格式：
# - name: <kebab-case-name>
#   description: <one-line-description>
#   category: <preference|bugfix|experience|tool>
#   content: |
#     <multi-line-content>

# 功能：
# 1. 新增或更新知识条目（name 存在则覆盖）
# 2. 实现知识库自进化。level + category 决定写入路径：
#  - global  -> ~/.abyss/knowledge/<category_dir>/<name>.md
#  - project -> <working_dir>/.knowledge/<category_dir>/<name>.md
# 3. bugfix 类型只允许 project 级
# 4. preference 类型自动去重（语义相似则合并更新）

构造函数接收 working_dir，因此不在 register_builtins() 中无参注册，
而在 main.py 中 _HOST 创建后注册。
"""
import re
from pathlib import Path
from typing import Any, Dict, List, Optional

from ..knowledge.loader import CATEGORY_SUBDIRS, KnowledgeLoader
from .base import Tool, coerce_list_of_str


class KnowledgeSaveTool(Tool):
    """知识保存工具：原子化新增或更新知识条目，实现知识库自进化。

    通过 YAML frontmatter + Markdown 正文写入文件；写入失败返回错误字典不抛异常。
    按类型差异化处理：bugfix 仅项目级，preference 自动去重合并。
    """

    def __init__(self, working_dir: str = "."):
        self._working_dir = working_dir

    @property
    def name(self) -> str:
        """工具名称"""
        return "knowledge_save"

    @property
    def description(self) -> str:
        """工具功能描述"""
        return (
            "新增或更新知识条目（name 存在则覆盖）。"
            "用于任务后自反思记录经验。"
            "category 决定知识类型和存储位置。"
        )

    @property
    def parameters(self) -> Dict[str, Any]:
        """工具参数 JSON Schema"""
        return {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "知识名称（kebab-case）",
                },
                "level": {
                    "type": "string",
                    "enum": ["global", "project"],
                    "description": "知识级别：global 写到 ~/.abyss/knowledge/，project 写到 <working_dir>/.knowledge/",
                },
                "description": {
                    "type": "string",
                    "description": "一行描述（用于索引注入）",
                },
                "content": {
                    "type": "string",
                    "description": "Markdown 正文",
                },
                "category": {
                    "type": "string",
                    "enum": ["preference", "bugfix", "experience", "tool"],
                    "description": "知识类型：preference=用户偏好, bugfix=bug修复, experience=编码经验, tool=工具经验",
                },
                "tags": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "可选标签列表",
                },
            },
            "required": ["name", "level", "description", "content"],
        }

    def execute(
        self,
        name: str,
        level: str,
        description: str,
        content: str,
        category: str = "experience",
        tags: List[str] = None,
    ) -> Dict[str, Any]:
        """执行知识保存，返回结果字典。

        Args:
            name: 知识名称（kebab-case）
            level: "global" 或 "project"
            description: 一行描述
            content: Markdown 正文
            category: 知识类型（preference/bugfix/experience/tool）
            tags: 可选标签列表

        Returns:
            success=True 时含 name/path/action；success=False 时含 error
        """
        # 兼容 LLM 将 tags 序列化为 JSON 字符串的情形
        tags = coerce_list_of_str(tags) if tags is not None else []

        # bugfix 类型只允许 project 级
        if category == "bugfix" and level == "global":
            return {
                "success": False,
                "error": "bugfix 类型知识只允许 project 级，不能保存到 global",
            }

        try:
            # preference 去重：查找语义重复的已有偏好
            dup_path = None
            if category == "preference":
                dup_path = self._find_duplicate_preference(
                    name, description, tags, level,
                )

            # 决定最终路径和 name
            if dup_path is not None:
                # 合并到已有条目
                save_name = dup_path.stem
                save_path = dup_path
                action = "updated"
            else:
                save_name = name
                save_path = self._resolve_path(name, level, category)
                action = "updated" if save_path.exists() else "created"

            save_path.parent.mkdir(parents=True, exist_ok=True)
            save_path.write_text(
                self._build_file_content(
                    save_name, description, tags, level, content, category,
                ),
                encoding="utf-8",
            )
        except Exception as e:
            return {"success": False, "error": str(e)}

        return {
            "success": True,
            "name": save_name,
            "path": str(save_path),
            "action": action,
        }

    def _resolve_path(self, name: str, level: str,
                      category: str = "experience") -> Path:
        """根据 level + category 计算知识文件最终路径。"""
        subdir = self._category_to_subdir(category)
        if level == "global":
            return Path.home() / ".abyss" / "knowledge" / subdir / f"{name}.md"
        return Path(self._working_dir) / ".knowledge" / subdir / f"{name}.md"

    @staticmethod
    def _category_to_subdir(category: str) -> str:
        """category 名称转子目录名（反向查找 CATEGORY_SUBDIRS）。"""
        for subdir, cat in CATEGORY_SUBDIRS.items():
            if cat == category:
                return subdir
        return "experiences"

    def _find_duplicate_preference(
        self, new_name: str, new_desc: str,
        new_tags: List[str], level: str,
    ) -> Optional[Path]:
        """查找语义重复的偏好知识。
        规则：description 关键词重叠度 > 60% 或 tags 交集 >= 2 时判定为重复。
        返回重复条目的路径，无重复返回 None。
        """
        pref_dir = self._get_category_dir(level, "preferences")
        if not pref_dir.exists():
            return None

        new_keywords = set(re.findall(r"[a-zA-Z]+|[\u4e00-\u9fa5]+", new_desc.lower()))
        new_tags_set = set(t.lower() for t in new_tags)

        for md_file in pref_dir.glob("*.md"):
            if md_file.stem == new_name:
                continue  # 同名直接覆盖，不算重复
            try:
                raw = md_file.read_text(encoding="utf-8")
            except Exception:
                continue
            fm, _ = KnowledgeLoader._split_frontmatter(raw)
            if fm is None:
                continue
            existing_desc = fm.get("description", "")
            existing_tags = fm.get("tags", []) or []
            if isinstance(existing_tags, str):
                existing_tags = [t.strip() for t in existing_tags.split(",")]

            existing_keywords = set(
                re.findall(r"[a-zA-Z]+|[\u4e00-\u9fa5]+", existing_desc.lower())
            )
            existing_tags_set = set(t.lower() for t in existing_tags)

            # 关键词重叠度 > 60%
            if new_keywords and existing_keywords:
                overlap = len(new_keywords & existing_keywords)
                ratio = overlap / max(len(new_keywords), len(existing_keywords))
                if ratio > 0.6:
                    return md_file

            # tags 交集 >= 2
            if len(new_tags_set & existing_tags_set) >= 2:
                return md_file

        return None

    def _get_category_dir(self, level: str, subdir_name: str) -> Path:
        """获取指定级别的分类子目录路径。"""
        if level == "global":
            return Path.home() / ".abyss" / "knowledge" / subdir_name
        return Path(self._working_dir) / ".knowledge" / subdir_name

    @staticmethod
    def _build_file_content(
        name: str,
        description: str,
        tags: List[str],
        level: str,
        content: str,
        category: str = "experience",
    ) -> str:
        """拼装 frontmatter + 正文，frontmatter 与 SKILL.md 格式一致。"""
        tags_inline = ", ".join(tags)
        frontmatter = (
            "---\n"
            f"name: {name}\n"
            f"description: {description}\n"
            f"category: {category}\n"
            f"tags: [{tags_inline}]\n"
            f"level: {level}\n"
            "---\n\n"
        )
        return frontmatter + content
