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

"""Skills Loader - 技能加载器
=================================================

此模块负责加载和管理 Agent 的技能（Skills）。

技能是什么：
- 技能是 Markdown 文件（SKILL.md），教导 Agent 如何使用特定工具或执行特定任务
- 技能可以扩展 Agent 的能力，如编程、调试、写作等
- 技能通过 frontmatter 定义元数据（依赖、是否必加载等）

技能类型：
1. 工作空间技能：workspace/skills/<skill-name>/SKILL.md
2. 内置技能：echobot/skills/<skill-name>/SKILL.md

渐进式加载：
- 必加载技能（always=true）：始终包含在系统提示中
- 可用技能：只显示摘要，需要时再加载完整内容

技能元数据：
```yaml
---
description: 技能描述
requires:
  bins: ["command"]  # 需要的命令行工具
  env: ["API_KEY"]   # 需要的环境变量
always: false        # 是否必加载
---
```

主要类：
- SkillsLoader: 技能加载器

使用示例：
    loader = SkillsLoader(workspace)

    # 列出所有可用技能
    skills = loader.list_skills()

    # 加载特定技能
    content = loader.load_skill("coding")

    # 构建技能摘要
    summary = loader.build_skills_summary()
"""

import json
import os
import re
import shutil
from pathlib import Path

# 默认内置技能目录（相对于当前文件）
BUILTIN_SKILLS_DIR = Path(__file__).parent.parent / "skills"


class SkillsLoader:
    """
    技能加载器 - 管理 Agent 的技能集合

    技能是教导 Agent 如何执行特定任务的 Markdown 文件。

    主要功能：
    1. 列出可用技能
    2. 加载技能内容
    3. 检查依赖是否满足
    4. 构建技能摘要（XML 格式）

    技能来源：
    - 工作空间技能（优先）：workspace/skills/<name>/SKILL.md
    - 内置技能：echobot/skills/<name>/SKILL.md

    使用示例：
        loader = SkillsLoader(Path("~/.echobot/workspace"))

        # 列出技能
        all_skills = loader.list_skills()

        # 加载技能内容
        coding_skill = loader.load_skill("coding")

        # 构建摘要
        summary = loader.build_skills_summary()
    """

    def __init__(self, workspace: Path, builtin_skills_dir: Path | None = None):
        """
        初始化技能加载器

        Args:
            workspace: 工作空间路径
            builtin_skills_dir: 可选的内置技能目录
        """
        self.workspace = workspace
        self.workspace_skills = workspace / "skills"
        self.builtin_skills = builtin_skills_dir or BUILTIN_SKILLS_DIR

    def list_skills(self, filter_unavailable: bool = True) -> list[dict[str, str]]:
        """
        列出所有可用技能
        
        技能来源优先级：
        1. 工作空间技能（优先）
        2. 内置技能
        
        Args:
            filter_unavailable: 是否过滤掉依赖未满足的技能
        
        Returns:
            list[dict]: 技能信息列表，每个包含 name、path、source
        """
        skills = []
        
        # 工作空间技能（最高优先级）
        if self.workspace_skills.exists():
            for skill_dir in self.workspace_skills.iterdir():
                if skill_dir.is_dir():
                    skill_file = skill_dir / "SKILL.md"
                    if skill_file.exists():
                        skills.append({"name": skill_dir.name, "path": str(skill_file), "source": "workspace"})
        
        # 内置技能
        if self.builtin_skills and self.builtin_skills.exists():
            for skill_dir in self.builtin_skills.iterdir():
                if skill_dir.is_dir():
                    skill_file = skill_dir / "SKILL.md"
                    if skill_file.exists() and not any(s["name"] == skill_dir.name for s in skills):
                        skills.append({"name": skill_dir.name, "path": str(skill_file), "source": "builtin"})
        
        # 按依赖过滤
        if filter_unavailable:
            return [s for s in skills if self._check_requirements(self._get_skill_meta(s["name"]))]
        return skills
    
    def load_skill(self, name: str) -> str | None:
        """
        加载指定名称的技能内容
        
        Args:
            name: 技能名称（目录名）
        
        Returns:
            str: 技能文件内容，如果未找到则返回 None
        """
        # 优先检查工作空间
        workspace_skill = self.workspace_skills / name / "SKILL.md"
        if workspace_skill.exists():
            return workspace_skill.read_text(encoding="utf-8")
        
        # 检查内置技能
        if self.builtin_skills:
            builtin_skill = self.builtin_skills / name / "SKILL.md"
            if builtin_skill.exists():
                return builtin_skill.read_text(encoding="utf-8")
        
        return None
    
    def load_skills_for_context(self, skill_names: list[str]) -> str:
        """
        加载指定技能列表的完整内容
        
        用于渐进式加载，将多个技能内容连接成单一字符串。
        
        Args:
            skill_names: 要加载的技能名称列表
        
        Returns:
            str: 格式化的技能内容（用 "---" 分隔）
        """
        parts = []
        for name in skill_names:
            content = self.load_skill(name)
            if content:
                content = self._strip_frontmatter(content)
                parts.append(f"### Skill: {name}\n\n{content}")
        
        return "\n\n---\n\n".join(parts) if parts else ""
    
    def build_skills_summary(self) -> str:
        """
        构建所有技能的摘要（XML 格式）
        
        摘要包含每个技能的名称、描述、路径和可用性。
        用于渐进式加载，Agent 可以看到技能列表但只加载需要的技能。
        
        Returns:
            str: XML 格式的技能摘要，如果无技能则返回空字符串
        """
        all_skills = self.list_skills(filter_unavailable=False)
        if not all_skills:
            return ""
        
        def escape_xml(s: str) -> str:
            return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
        
        lines = ["<skills>"]
        for s in all_skills:
            name = escape_xml(s["name"])
            path = s["path"]
            desc = escape_xml(self._get_skill_description(s["name"]))
            skill_meta = self._get_skill_meta(s["name"])
            available = self._check_requirements(skill_meta)
            
            lines.append(f"  <skill available=\"{str(available).lower()}\">")
            lines.append(f"    <name>{name}</name>")
            lines.append(f"    <description>{desc}</description>")
            lines.append(f"    <location>{path}</location>")
            
            # 对于不可用技能，显示缺失的依赖
            if not available:
                missing = self._get_missing_requirements(skill_meta)
                if missing:
                    lines.append(f"    <requires>{escape_xml(missing)}</requires>")
            
            lines.append(f"  </skill>")
        lines.append("</skills>")
        
        return "\n".join(lines)

    def _get_missing_requirements(self, skill_meta: dict) -> str:
        """Get a description of missing requirements."""
        missing = []
        requires = skill_meta.get("requires", {})
        for b in requires.get("bins", []):
            if not shutil.which(b):
                missing.append(f"CLI: {b}")
        for env in requires.get("env", []):
            if not os.environ.get(env):
                missing.append(f"ENV: {env}")
        return ", ".join(missing)

    def _get_skill_description(self, name: str) -> str:
        """Get the description of a skill from its frontmatter."""
        meta = self.get_skill_metadata(name)
        if meta and meta.get("description"):
            return meta["description"]
        return name  # Fallback to skill name

    def _strip_frontmatter(self, content: str) -> str:
        """Remove YAML frontmatter from markdown content."""
        if content.startswith("---"):
            match = re.match(r"^---\n.*?\n---\n", content, re.DOTALL)
            if match:
                return content[match.end():].strip()
        return content

    def _parse_echobot_metadata(self, raw: str) -> dict:
        """Parse echobot metadata JSON from frontmatter."""
        try:
            data = json.loads(raw)
            return data.get("echobot", {}) if isinstance(data, dict) else {}
        except (json.JSONDecodeError, TypeError):
            return {}

    def _check_requirements(self, skill_meta: dict) -> bool:
        """Check if skill requirements are met (bins, env vars)."""
        requires = skill_meta.get("requires", {})
        for b in requires.get("bins", []):
            if not shutil.which(b):
                return False
        for env in requires.get("env", []):
            if not os.environ.get(env):
                return False
        return True

    def _get_skill_meta(self, name: str) -> dict:
        """Get echobot metadata for a skill (cached in frontmatter)."""
        meta = self.get_skill_metadata(name) or {}
        return self._parse_echobot_metadata(meta.get("metadata", ""))

    def get_always_skills(self) -> list[str]:
        """Get skills marked as always=true that meet requirements."""
        result = []
        for s in self.list_skills(filter_unavailable=True):
            meta = self.get_skill_metadata(s["name"]) or {}
            skill_meta = self._parse_echobot_metadata(meta.get("metadata", ""))
            if skill_meta.get("always") or meta.get("always"):
                result.append(s["name"])
        return result

    def get_skill_metadata(self, name: str) -> dict | None:
        """
        Get metadata from a skill's frontmatter.

        Args:
            name: Skill name.

        Returns:
            Metadata dict or None.
        """
        content = self.load_skill(name)
        if not content:
            return None

        if content.startswith("---"):
            match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
            if match:
                # Simple YAML parsing
                metadata = {}
                for line in match.group(1).split("\n"):
                    if ":" in line:
                        key, value = line.split(":", 1)
                        metadata[key.strip()] = value.strip().strip('"\'')
                return metadata

        return None
