"""SkillInstallTool - Agent 可调用的技能安装工具"""

import asyncio
import logging
import os
import re
from typing import Callable, Awaitable

from agentkit.tools.base import Tool

logger = logging.getLogger(__name__)

# Skill names are used to construct filesystem paths — reject anything
# that could escape the skills directory (path traversal).
_SAFE_NAME_RE = re.compile(r"^[a-zA-Z0-9_\-]{1,64}$")


class SkillInstallTool(Tool):
    """技能安装工具

    让 Agent 可以通过正确的命令安装技能，而不是用 shell 执行 npm install。
    使用 `npx skills install <owner/repo@skill>` 安装技能，然后注册到 skill_registry。

    Usage:
        tool = SkillInstallTool()
        result = await tool.execute(name="find-skills", source="vercel-labs/skills@find-skills")
    """

    def __init__(
        self,
        name: str = "skill_install",
        description: str = (
            "安装 Agent 技能包。使用 npx skills install 安装指定技能，不要用 npm install。"
            "重要：安装前应先用 skill_search 工具搜索确认技能名称和来源(source)。"
            "如果用户只提供了模糊名称，先用 skill_search 搜索，再根据搜索结果安装。"
        ),
        input_schema: dict[str, object] | None = None,
        output_schema: dict[str, object] | None = None,
        version: str = "1.0.0",
        tags: list[str] | None = None,
        confirm_callback: Callable[[str], Awaitable[bool]] | None = None,
        skill_registry=None,
        tool_registry=None,
    ):
        schema = input_schema or {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "description": "技能名称，如 find-skills",
                },
                "source": {
                    "type": "string",
                    "description": "技能来源，格式为 owner/repo@skill，如 vercel-labs/skills@find-skills。如果不提供则用 name 搜索",
                },
            },
            "required": ["name"],
        }
        super().__init__(
            name=name,
            description=description,
            input_schema=schema,
            output_schema=output_schema,
            version=version,
            tags=tags,
        )
        self._confirm_callback = confirm_callback
        self._skill_registry = skill_registry
        self._tool_registry = tool_registry

    async def execute(self, **kwargs) -> dict:
        name = kwargs.get("name", "").strip()
        source = kwargs.get("source", "").strip()

        if not name:
            return {
                "output": "错误: 必须提供 name 参数",
                "exit_code": 1,
                "is_error": True,
            }

        # Validate name — it's used to construct filesystem paths in
        # _try_register_skill, so reject anything that could escape the
        # skills directory (e.g. ``../../etc/passwd``).
        if not _SAFE_NAME_RE.match(name):
            return {
                "output": f"错误: 技能名称包含非法字符: {name}（仅允许字母、数字、下划线、连字符）",
                "exit_code": 1,
                "is_error": True,
            }

        # Build the install command
        if source:
            install_target = source
        else:
            install_target = name

        # Request confirmation before installing
        if self._confirm_callback:
            confirmed = await self._confirm_callback(f"npx skills install {install_target}")
            if not confirmed:
                return {
                    "output": f"技能安装被用户拒绝: {install_target}",
                    "exit_code": 126,
                    "is_error": True,
                }

        try:
            # ``--yes`` is an npx flag (NOT a skills flag) that auto-accepts
            # the "Need to install the following packages: skills@x.y.z.
            # Ok to proceed?" prompt. Without it, npx blocks waiting for
            # stdin in the non-interactive subprocess and the install fails
            # with "npm error canceled". The trailing ``-y`` is the skills
            # CLI's own confirm flag.
            proc = await asyncio.create_subprocess_exec(
                "npx",
                "--yes",
                "skills@latest",
                "install",
                install_target,
                "-y",
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
            output = stdout.decode("utf-8", errors="replace")
            error_output = stderr.decode("utf-8", errors="replace")

            if proc.returncode == 0:
                # Try to register the skill into skill_registry
                registration_msg = ""
                if self._skill_registry:
                    registration_msg = self._try_register_skill(name)

                return {
                    "output": f"技能 {name} 安装成功\n{output}\n{registration_msg}",
                    "exit_code": 0,
                    "is_error": False,
                }
            else:
                return {
                    "output": f"技能 {name} 安装失败 (exit={proc.returncode})\n{error_output}\n{output}",
                    "exit_code": proc.returncode,
                    "is_error": True,
                    "suggestions": [
                        "检查技能名称是否正确",
                        "使用 source 参数指定完整路径，如 vercel-labs/skills@find-skills",
                        "运行 npx skills search <name> 搜索可用技能",
                    ],
                }
        except asyncio.TimeoutError:
            return {
                "output": f"技能 {name} 安装超时（120s）",
                "exit_code": -1,
                "is_error": True,
            }
        except Exception as e:
            return {
                "output": f"技能 {name} 安装异常: {e}",
                "exit_code": -1,
                "is_error": True,
            }

    def _try_register_skill(self, name: str) -> str:
        """Try to find and register the installed skill YAML/SKILL.md into skill_registry.

        ``npx skills add`` installs skills as ``{skills_dir}/{name}/SKILL.md``
        (a directory containing a markdown-frontmatter file), NOT as a
        flat ``{name}.yaml``. This method therefore checks both layouts:

        1. Flat YAML: ``{skills_dir}/{name}.yaml``  (legacy / hand-authored)
        2. Directory with SKILL.md: ``{skills_dir}/{name}/SKILL.md``  (npx skills)
        3. Directory with any .yaml/.yml: ``{skills_dir}/{name}/*.yaml``  (fallback)
        """
        try:
            from agentkit.skills.loader import SkillLoader

            search_dirs = [
                os.path.join(os.getcwd(), ".agents", "skills"),
                os.path.join(os.path.expanduser("~"), ".agents", "skills"),
                os.path.join(os.getcwd(), "configs", "skills"),
            ]

            for search_dir in search_dirs:
                # Layout 1: flat {name}.yaml
                yaml_path = os.path.join(search_dir, f"{name}.yaml")
                if os.path.exists(yaml_path):
                    loader = SkillLoader(
                        skill_registry=self._skill_registry,
                        tool_registry=self._tool_registry,
                    )
                    loader.load_from_file(yaml_path)
                    return f"技能已注册到系统（来源: {yaml_path}）"

            # Layout 2 & 3: directory-based skills ({name}/SKILL.md or {name}/*.yaml)
            for search_dir in search_dirs:
                skill_dir = os.path.join(search_dir, name)
                if not os.path.isdir(skill_dir):
                    continue
                # Prefer SKILL.md (the format npx skills actually produces)
                md_path = os.path.join(skill_dir, "SKILL.md")
                if os.path.isfile(md_path):
                    loader = SkillLoader(
                        skill_registry=self._skill_registry,
                        tool_registry=self._tool_registry,
                    )
                    loader.load_from_skill_md(md_path)
                    return f"技能已注册到系统（来源: {md_path}）"
                # Fallback: any YAML file inside the directory
                for fname in sorted(os.listdir(skill_dir)):
                    if fname.endswith((".yaml", ".yml")):
                        yaml_path = os.path.join(skill_dir, fname)
                        loader = SkillLoader(
                            skill_registry=self._skill_registry,
                            tool_registry=self._tool_registry,
                        )
                        loader.load_from_file(yaml_path)
                        return f"技能已注册到系统（来源: {yaml_path}）"

            return "技能文件已下载，但未找到 YAML/SKILL.md 配置文件进行注册。可能需要重启服务。"
        except Exception as e:
            logger.warning(f"Failed to register skill {name}: {e}")
            return f"技能文件已下载，但注册失败: {e}"
