"""Skills loader for agent capabilities."""

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

# Default builtin skills directory (relative to this file)
BUILTIN_SKILLS_DIR = Path(__file__).parent.parent / "skills"


class SkillsLoader:
    """
    Loader for agent skills.

    Skills are markdown files (SKILL.md) that teach the agent how to use
    specific tools or perform certain tasks.
    """

    def __init__(self, workspace: Path, builtin_skills_dir: Path | None = None):
        self.workspace = workspace
        self.workspace_skills = workspace / "skills"
        self.builtin_skills = builtin_skills_dir or BUILTIN_SKILLS_DIR
        self._skill_content_cache: dict[Path, tuple[tuple[int, int, int], str]] = {}

    def list_skills(self, filter_unavailable: bool = True) -> list[dict[str, str]]:
        """
        List all available skills.

        Args:
            filter_unavailable: If True, filter out skills with unmet requirements.

        Returns:
            List of skill info dicts with 'name', 'path', 'source'.
        """
        skills = []

        # Workspace skills (highest priority)
        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"}
                        )

        # Built-in skills
        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"}
                        )

        # Filter by requirements
        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:
        """
        Load a skill by name.

        Args:
            name: Skill name (directory name).

        Returns:
            Skill content or None if not found.
        """
        # Check workspace first
        workspace_skill = self.workspace_skills / name / "SKILL.md"
        if workspace_skill.exists():
            return self._read_skill_file(workspace_skill)

        # Check built-in
        if self.builtin_skills:
            builtin_skill = self.builtin_skills / name / "SKILL.md"
            if builtin_skill.exists():
                return self._read_skill_file(builtin_skill)

        return None

    def _read_skill_file(self, path: Path) -> str:
        stat = path.stat()
        cache_key = (stat.st_mtime_ns, stat.st_ctime_ns, stat.st_size)
        cached = self._skill_content_cache.get(path)
        if cached is not None and cached[0] == cache_key:
            return cached[1]
        content = path.read_text(encoding="utf-8")
        self._skill_content_cache[path] = (cache_key, content)
        return content

    def load_skills_for_context(self, skill_names: list[str]) -> str:
        """
        Load specific skills for inclusion in agent context.

        Args:
            skill_names: List of skill names to load.

        Returns:
            Formatted skills content.
        """
        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:
        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;")
                .replace('"', "&quot;")
            )

        lines = ["<skills>"]
        for s in all_skills:
            name = s["name"]
            skill_meta = self._get_skill_meta(name)
            available = self._check_requirements(skill_meta)
            desc = self._get_skill_description(name)
            short_desc = self._truncate_description(desc)
            source = escape_xml(s["source"])
            path = escape_xml(s["path"])
            attrs = f'path="{path}" source="{source}" available="{str(available).lower()}"'

            if not available:
                missing = self._get_missing_requirements(skill_meta)
                if missing:
                    attrs += f' requires="{escape_xml(missing)}"'

            lines.append(f"  <skill {attrs}>{escape_xml(name)} — {escape_xml(short_desc)}</skill>")
        lines.append("</skills>")

        return "\n".join(lines)

    @staticmethod
    def _truncate_description(desc: str, max_len: int = 60) -> str:
        """Truncate description to first sentence or max_len chars."""
        desc = desc.replace("\n", " ").replace("\r", " ").strip()
        for sep in (". ", "\u3002", "! ", "? "):
            idx = desc.find(sep)
            if 0 < idx < max_len:
                return desc[: idx + 1].strip()
        if len(desc) <= max_len:
            return desc
        return desc[: max_len - 1].strip() + "\u2026"

    def _get_missing_requirements(self, skill_meta: dict[str, object]) -> str:
        """Get a description of missing requirements."""
        missing = []
        requires = skill_meta.get("requires", {})
        if not isinstance(requires, dict):
            return ""

        bins = requires.get("bins", [])
        if not isinstance(bins, list):
            bins = []
        for b in bins:
            if not shutil.which(b):
                missing.append(f"CLI: {b}")

        bins_any = requires.get("bins_any", [])
        if isinstance(bins_any, list) and bins_any:
            if not any(shutil.which(b) for b in bins_any):
                missing.append(f"CLI(any): {' | '.join(bins_any)}")

        envs = requires.get("env", [])
        if not isinstance(envs, list):
            envs = []
        for env in envs:
            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_bao_metadata(self, raw: str) -> dict[str, object]:
        """Parse skill metadata JSON from frontmatter (supports `bao` and openclaw keys)."""
        try:
            data = json.loads(raw)
            return data.get("bao", data.get("openclaw", {})) if isinstance(data, dict) else {}
        except (json.JSONDecodeError, TypeError):
            return {}

    def _check_requirements(self, skill_meta: dict[str, object]) -> bool:
        """Check if skill requirements are met (bins, env vars)."""
        requires = skill_meta.get("requires", {})
        if not isinstance(requires, dict):
            return True

        bins = requires.get("bins", [])
        if not isinstance(bins, list):
            bins = []
        for b in bins:
            if not shutil.which(b):
                return False

        bins_any = requires.get("bins_any", [])
        if isinstance(bins_any, list) and bins_any:
            if not any(shutil.which(b) for b in bins_any):
                return False

        envs = requires.get("env", [])
        if not isinstance(envs, list):
            envs = []
        for env in envs:
            if not os.environ.get(env):
                return False
        return True

    def _get_skill_meta(self, name: str) -> dict[str, object]:
        """Get `bao` metadata for a skill (cached in frontmatter)."""
        meta = self.get_skill_metadata(name) or {}
        return self._parse_bao_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_bao_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[str, str] | 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
