# -*- coding: utf-8 -*-
"""
Knowledge 加载器模块
扫描两级目录（全局 ~/.abyss/knowledge/ + 项目 <working_dir>/.knowledge/）下的
分类子目录，解析 YAML frontmatter + 正文，支持按关键词匹配生成索引。

知识文件格式（与 SKILL.md 一致）：
- name（必填，kebab-case）：唯一标识
- description（必填）：Agent 据此判断何时加载此知识
- category（可选）：preference, bugfix, experience, tool，默认 experience
- tags（可选）：标签列表，参与关键词匹配
- level（仅供人阅读，实际由所在目录决定）
- usage_count, pinned（可选）：生命周期字段

分类子目录结构：
  preferences/  — 用户偏好（永不过期）
  bugfixes/     — bug修复记录（项目级，永不过期）
  experiences/  — 编码经验
  tools/        — 工具使用经验
根目录下的旧 .md 文件默认归类为 experience（向后兼容）。

加载策略：两级同名时项目级覆盖全局级（项目级优先）。
"""
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional

from .. import logger

# 分类子目录名 → 默认 category
CATEGORY_SUBDIRS = {
    "preferences": "preference",
    "bugfixes": "bugfix",
    "experiences": "experience",
    "tools": "tool",
}

# 索引行中的分类缩写
CATEGORY_ABBR = {
    "preference": "pref",
    "bugfix": "bug",
    "experience": "exp",
    "tool": "tool",
}


@dataclass
class Knowledge:
    """单个知识条目：元数据 + 正文 + 生命周期字段。"""
    name: str
    description: str
    content: str
    level: str  # "global" 或 "project"，由所在目录决定
    path: Path = None
    tags: List[str] = field(default_factory=list)
    category: str = "experience"  # preference, bugfix, experience, tool
    usage_count: int = 0
    last_used_at: str = None
    pinned: bool = False
    created_at: str = None
    updated_at: str = None


class KnowledgeLoader:
    """扫描并加载两级知识库，支持按关键词匹配生成索引。

    两级目录：global（全局）+ project（项目）。项目级覆盖全局级同名知识。
    """

    def __init__(self, knowledge_dirs: Dict[str, Path]):
        """接收 {"global": <path>, "project": <path>} 字典。
        project 为 None 则只扫全局。
        """
        self.knowledge_dirs = knowledge_dirs
        self._knowledge: Dict[str, Knowledge] = {}

    def discover(self) -> List[Knowledge]:
        """扫描两级目录下分类子目录及根目录的 .md 文件并解析。
        两级同名时项目级覆盖全局级。解析失败的文件跳过并告警，不中断。
        根目录下的旧 .md 文件默认归类为 experience（向后兼容）。
        返回已加载的 Knowledge 列表。
        """
        self._knowledge.clear()
        # 先扫 global，再扫 project，使项目级覆盖全局级
        for level in ("global", "project"):
            base = self.knowledge_dirs.get(level)
            if base is None or not base.exists() or not base.is_dir():
                continue
            # 扫描分类子目录
            for subdir_name, default_cat in CATEGORY_SUBDIRS.items():
                cat_dir = base / subdir_name
                if cat_dir.exists() and cat_dir.is_dir():
                    for md_file in cat_dir.glob("*.md"):
                        k = self._parse_knowledge(md_file, level, default_cat)
                        if k is not None:
                            self._knowledge[k.name] = k
            # 扫描根目录（向后兼容旧知识，默认 experience）
            for md_file in base.glob("*.md"):
                k = self._parse_knowledge(md_file, level, "experience")
                if k is not None:
                    self._knowledge[k.name] = k
        return list(self._knowledge.values())

    def _parse_knowledge(self, path: Path, level: str,
                         default_category: str = "experience") -> Optional[Knowledge]:
        """解析单个 .md：拆分 frontmatter 和正文，校验必填字段。
        level 由目录决定，覆盖 frontmatter 里的 level 字段。
        category 优先取 frontmatter，缺失则用 default_category（由子目录推断）。
        name 和 description 必填，缺失返回 None。
        """
        try:
            raw = path.read_text(encoding="utf-8")
        except Exception as e:
            logger.warning(f"读取知识文件失败 path={path} err={e}")
            return None

        frontmatter, body = self._split_frontmatter(raw)
        if frontmatter is None:
            logger.warning(f"知识文件无 frontmatter，跳过 path={path}")
            return None

        name = frontmatter.get("name", "").strip()
        description = frontmatter.get("description", "").strip()
        if not name or not description:
            logger.warning(f"知识文件缺 name/description，跳过 path={path}")
            return None

        tags = frontmatter.get("tags", []) or []
        if isinstance(tags, str):
            tags = [t.strip() for t in tags.split(",") if t.strip()]

        # category 优先取 frontmatter，缺失用目录推断的默认值
        category = frontmatter.get("category", "").strip() or default_category

        # 生命周期字段（可选）
        usage_count = int(frontmatter.get("usage_count", 0) or 0)
        pinned = frontmatter.get("pinned", "").strip().lower() == "true"
        last_used_at = frontmatter.get("last_used_at", "").strip() or None
        created_at = frontmatter.get("created_at", "").strip() or None
        updated_at = frontmatter.get("updated_at", "").strip() or None

        return Knowledge(
            name=name,
            description=description,
            content=body.strip(),
            level=level,
            path=path,
            tags=tags,
            category=category,
            usage_count=usage_count,
            last_used_at=last_used_at,
            pinned=pinned,
            created_at=created_at,
            updated_at=updated_at,
        )

    @staticmethod
    def _split_frontmatter(raw: str):
        """拆分 YAML frontmatter 和正文。返回 (dict, str)，无 frontmatter 返回 (None, raw)"""
        if not raw.startswith("---"):
            return None, raw
        # 找第二个 --- 作为 frontmatter 结束
        end_idx = raw.find("\n---", 3)
        if end_idx == -1:
            return None, raw
        fm_text = raw[3:end_idx].strip()
        body = raw[end_idx + 4:].lstrip("\n")
        # 简单 YAML 解析（仅支持 key: value 和列表）
        fm = {}
        for line in fm_text.splitlines():
            line = line.rstrip()
            if not line or line.startswith("#"):
                continue
            if line.startswith("  - "):
                # 列表项，归到最近一个 key
                if fm:
                    last_key = list(fm.keys())[-1]
                    if not isinstance(fm[last_key], list):
                        fm[last_key] = []
                    fm[last_key].append(line[4:].strip())
                continue
            if ":" in line:
                key, _, value = line.partition(":")
                key = key.strip()
                value = value.strip()
                # 处理内联列表 [a, b, c]
                if value.startswith("[") and value.endswith("]"):
                    inner = value[1:-1].strip()
                    if inner:
                        fm[key] = [v.strip() for v in inner.split(",") if v.strip()]
                    else:
                        fm[key] = []
                elif value:
                    fm[key] = value
                else:
                    fm[key] = []  # 可能是列表开头
        return fm, body

    def get(self, name: str) -> Optional[Knowledge]:
        """按名获取已加载的 Knowledge，找不到返回 None"""
        return self._knowledge.get(name)

    def list_all(self) -> List[Knowledge]:
        """返回所有已加载知识"""
        return list(self._knowledge.values())

    def match_by_keyword(self, user_input: str, limit: int = 5,
                         category: str = None) -> List[Knowledge]:
        """根据用户输入关键词匹配 Knowledge 的 description + tags。
        description 命中权重 ×2，tags 命中权重 ×1。
        可选 category 过滤，只返回指定类型的知识。
        按总分降序排列。
        """
        matched = []
        user_lower = user_input.lower()
        for k in self._knowledge.values():
            # category 过滤
            if category is not None and k.category != category:
                continue
            # description 匹配（权重 ×2）
            desc_lower = k.description.lower()
            desc_keywords = re.findall(r"[a-zA-Z]+|[\u4e00-\u9fa5]+", desc_lower)
            desc_hits = sum(1 for kw in desc_keywords
                           if len(kw) >= 2 and kw in user_lower)
            # tags 匹配（权重 ×1）
            tag_hits = sum(1 for tag in k.tags
                          if len(tag) >= 2 and tag.lower() in user_lower)
            total_score = desc_hits * 2 + tag_hits
            if total_score > 0:
                matched.append((total_score, k))
        # 按总分降序
        matched.sort(key=lambda x: x[0], reverse=True)
        return [k for _, k in matched[:limit]]

    def build_index_lines(self, matched: List[Knowledge]) -> str:
        """生成索引文本，每条一行：[G][pref] <name>: <description>。
        分类前缀：pref=偏好, bug=修复, exp=经验, tool=工具。
        多行用 \\n 连接。空列表返回空字符串。
        """
        if not matched:
            return ""
        level_prefix = {"global": "G", "project": "P"}
        lines = []
        for k in matched:
            lp = level_prefix.get(k.level, "G")
            cat_abbr = CATEGORY_ABBR.get(k.category, "exp")
            lines.append(f"[{lp}][{cat_abbr}] {k.name}: {k.description}")
        return "\n".join(lines)


def create_default_loader(working_dir: str) -> KnowledgeLoader:
    """创建指向 ~/.abyss/knowledge/ + <working_dir>/.knowledge/ 的默认加载器"""
    return KnowledgeLoader({
        "global": Path.home() / ".abyss" / "knowledge",
        "project": Path(working_dir) / ".knowledge",
    })
