"""Skill 注册表 (Anthropic 标准格式)。

每个 skill 是 <root>/<name>/ 目录,内含 SKILL.md(带 frontmatter)+ 可选的
references/、scripts/、assets/。启动时只读 frontmatter 做 discovery,完整 SKILL.md
和 references 由 agent 按需加载(渐进披露)。

多来源:内置 skill(`ROOT/skills`,只读)+ 用户 skill(`user_root/.skills`,可写)。
来源按顺序扫,**后扫的同名覆盖先扫的** —— 用户 skill 排在内置之后,故"用户覆盖
内置"(user wins);覆盖关系记进 `user_overrides` 供 discovery 显式标注,不静默。
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union

import yaml


_FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n?", re.DOTALL)


def parse_frontmatter(text: str) -> Tuple[dict, str]:
    """解析 markdown 顶部的 YAML frontmatter。返回 (meta, body)。

    frontmatter YAML 非法时抛 `yaml.YAMLError`(由 `SkillRegistry._scan` 捕获记进
    load_errors —— 用户手写 skill 易踩,不能让一个坏 skill 崩掉整次扫描)。
    """
    m = _FRONTMATTER_RE.match(text)
    if not m:
        return {}, text
    meta = yaml.safe_load(m.group(1)) or {}
    if not isinstance(meta, dict):
        meta = {}
    return meta, text[m.end():]


class SkillLoadError(Exception):
    """skill 目录有 SKILL.md 但加载失败(YAML 坏 / 缺 description 等)。

    与"没有 SKILL.md(根本不是 skill 目录,静默跳过)"区分:前者要面向用户报,
    后者是正常的非 skill 子目录。
    """


@dataclass
class Skill:
    name: str
    description: str
    skill_dir: Path
    source: str = "builtin"  # 'builtin' | 'user'
    # frontmatter 可选 `model: <family.variant>`:该 skill 定向模型。load_skill 命中时
    # 由 agent_builder 注入的 switcher 热切(仅内置 skill 生效 —— 用户 skill 若照单全收
    # 等于让任何人自写 frontmatter 绕开档位门控)。空 = 不定向,跟随 task 当前模型。
    model: str = ""

    @property
    def skill_md(self) -> Path:
        return self.skill_dir / "SKILL.md"

    def full_content(self) -> str:
        return self.skill_md.read_text(encoding="utf-8")

    @classmethod
    def from_dir(cls, skill_dir: Path, source: str = "builtin") -> Optional["Skill"]:
        """加载一个 skill 目录。

        无 SKILL.md → 返回 None(静默跳过,不是 skill 目录);
        有 SKILL.md 但格式错(YAML 坏 / 缺 description) → 抛 SkillLoadError。
        """
        md = skill_dir / "SKILL.md"
        if not md.exists():
            return None  # 不是 skill 目录,静默跳过
        try:
            text = md.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError) as e:
            raise SkillLoadError(f"读不出 SKILL.md: {e}")
        try:
            meta, _ = parse_frontmatter(text)
        except yaml.YAMLError as e:
            raise SkillLoadError(f"frontmatter YAML 非法: {e}")
        name = meta.get("name") or skill_dir.name
        desc = meta.get("description") or ""
        if not desc:
            raise SkillLoadError("缺 description(frontmatter 必须有 name + description)")
        model = str(meta.get("model") or "").strip()
        return cls(name=name, description=desc, skill_dir=skill_dir, source=source, model=model)


@dataclass
class SkillSource:
    """一个 skill 搜索来源。

    container_root: docker backend 下该来源在容器内的挂载前缀
    (内置 → `/sandbox/skills`,用户 → `/workspace/.skills`);None = host backend,
    LoadSkillTool 退回 host 绝对路径。
    """
    root: Path
    source: str = "builtin"
    container_root: Optional[str] = None


SourcesArg = Union[Path, str, SkillSource, List[SkillSource]]


class SkillRegistry:
    def __init__(self, sources: SourcesArg) -> None:
        # 单个 Path/str → 包成单一 builtin 来源(向后兼容直接传目录的调用 / 测试)
        if isinstance(sources, (str, Path)):
            sources = [SkillSource(Path(sources), "builtin")]
        elif isinstance(sources, SkillSource):
            sources = [sources]
        self.sources: List[SkillSource] = list(sources)
        self.skills: Dict[str, Skill] = {}
        # 用户 skill 覆盖了内置 skill 的 name 集合 —— discovery 显式标注,覆盖不静默
        self.user_overrides: set[str] = set()
        # 加载失败的用户 skill:(目录名, 原因)。内置 skill 失败是 dev bug,不进此列
        # (不面向终端用户报),由测试 / 启动日志兜底
        self.load_errors: List[Tuple[str, str]] = []
        self._container_roots: Dict[str, Optional[str]] = {}
        self._scan()

    def _scan(self) -> None:
        for src in self.sources:
            self._container_roots[src.source] = src.container_root
            if not src.root.exists():
                continue  # 用户没有 .skills 目录 → 一次 exists() 跳过,零成本
            for child in sorted(src.root.iterdir()):
                if not child.is_dir():
                    continue
                try:
                    skill = Skill.from_dir(child, source=src.source)
                except SkillLoadError as e:
                    if src.source == "user":
                        self.load_errors.append((child.name, str(e)))
                    continue
                if skill is None:
                    continue
                prev = self.skills.get(skill.name)
                if prev is not None and prev.source != skill.source and skill.source == "user":
                    self.user_overrides.add(skill.name)  # 用户覆盖了内置
                self.skills[skill.name] = skill  # 后扫覆盖先扫 → user wins

    def discovery_block(self) -> str:
        """注入 system prompt 的 skill 列表(name + description + 来源标注)。"""
        if not self.skills and not self.load_errors:
            return ""
        lines = []
        for s in self.skills.values():
            if s.source == "user":
                tag = " [你的·已覆盖内置]" if s.name in self.user_overrides else " [你的]"
            else:
                tag = ""
            lines.append(f"- **{s.name}**{tag}: {s.description}")
        block = "\n".join(lines)
        if self.load_errors:
            errs = "; ".join(f"`{n}`({why})" for n, why in self.load_errors)
            block += (
                "\n\n> ⚠️ 你有用户 skill 因格式问题未加载,需要时提醒用户修好 frontmatter"
                f"(修好后下条消息生效):{errs}"
            )
        return block

    def container_dir(self, skill: Skill) -> Optional[str]:
        """docker 下该 skill 在容器内的目录;host backend → None(调用方退回 host 绝对路径)。"""
        root = self._container_roots.get(skill.source)
        if not root:
            return None
        return f"{root.rstrip('/')}/{skill.name}"

    def get(self, name: str) -> Optional[Skill]:
        return self.skills.get(name)
