"""Static skills and OpenCode-style lazy skill discovery."""

from __future__ import annotations

from dataclasses import dataclass, field
import re
from pathlib import Path
from typing import Any

from yier_agents.src.log import logger
from yier_agents.src.tool import Tool

SKILL_NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")


@dataclass(slots=True)
class Skill:
    """A lightweight static skill bundle that can extend an agent."""

    name: str
    description: str = ""
    tools: list[Tool] = field(default_factory=list)
    system_prompt: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)


class SkillRegistry:
    """Registers and composes static skills for an agent."""

    def __init__(self, skills: list[Skill] | None = None) -> None:
        self._skills: dict[str, Skill] = {}
        for skill in skills or []:
            self.register(skill)

    def register(self, skill: Skill) -> None:
        self._skills[skill.name] = skill

    def get(self, name: str) -> Skill | None:
        return self._skills.get(name)

    def list(self) -> list[Skill]:
        return list(self._skills.values())

    def build_system_prompt(self, base_prompt: str | None) -> str | None:
        prompt_parts = [part.strip() for part in [base_prompt] if part and part.strip()]

        for skill in self.list():
            if not skill.system_prompt or not skill.system_prompt.strip():
                continue

            prompt_parts.append(
                "\n".join(
                    [
                        f"[Skill: {skill.name}]",
                        skill.system_prompt.strip(),
                    ]
                )
            )

        if not prompt_parts:
            return None

        return "\n\n".join(prompt_parts)

    def collect_tools(self, base_tools: list[Tool] | None = None) -> list[Tool]:
        tools_by_name: dict[str, Tool] = {}

        for tool in base_tools or []:
            tools_by_name[tool.name] = tool

        for skill in self.list():
            for tool in skill.tools:
                tools_by_name[tool.name] = tool

        return list(tools_by_name.values())


@dataclass(slots=True)
class SkillDocument:
    """Discovered file-based skill metadata."""

    name: str
    description: str
    location: Path
    content: str

    @property
    def directory(self) -> Path:
        return self.location.parent


class SkillCatalog:
    """Catalog of lazily loadable skills discovered from the filesystem."""

    def __init__(self, skills: list[SkillDocument] | None = None) -> None:
        self._skills: dict[str, SkillDocument] = {}
        self._dirs: set[Path] = set()

        for skill in skills or []:
            self.add(skill)

    def add(self, skill: SkillDocument) -> None:
        self._skills[skill.name] = skill
        self._dirs.add(skill.directory)

    def get(self, name: str) -> SkillDocument | None:
        return self._skills.get(name)

    def list(self) -> list[SkillDocument]:
        return sorted(self._skills.values(), key=lambda skill: skill.name)

    def dirs(self) -> list[Path]:
        return sorted(self._dirs)

    def is_empty(self) -> bool:
        return not self._skills

    def format(self, verbose: bool) -> str:
        skills = self.list()
        if not skills:
            return "No skills are currently available."

        if verbose:
            lines = ["<available_skills>"]
            for skill in skills:
                lines.extend(
                    [
                        "  <skill>",
                        f"    <name>{skill.name}</name>",
                        f"    <description>{skill.description}</description>",
                        f"    <location>{skill.location.resolve().as_uri()}</location>",
                        "  </skill>",
                    ]
                )
            lines.append("</available_skills>")
            return "\n".join(lines)

        lines = ["## Available Skills"]
        lines.extend(f"- **{skill.name}**: {skill.description}" for skill in skills)
        return "\n".join(lines)

    @classmethod
    def discover(
        cls,
        start_path: Path | None = None,
        include_project: bool = True,
        include_global: bool = False,
        extra_paths: list[Path] | None = None,
    ) -> SkillCatalog:
        start = (start_path or Path.cwd()).resolve()
        catalog = cls()

        if include_global:
            for root in cls._global_roots():
                catalog._scan_root(root)

        if include_project:
            for root in cls._project_roots(start):
                catalog._scan_root(root)

        for root in extra_paths or []:
            catalog._scan_root(root.resolve())

        return catalog

    @staticmethod
    def _global_roots() -> list[Path]:
        home = Path.home()
        return [
            home / ".opencode" / "skill",
            home / ".opencode" / "skills",
            home / ".claude" / "skills",
            home / ".agents" / "skills",
        ]

    @classmethod
    def _project_roots(cls, start: Path) -> list[Path]:
        roots: list[Path] = []
        stop_path = cls._find_git_root(start)

        current = start
        while True:
            roots.extend(
                [
                    current / ".opencode" / "skill",
                    current / ".opencode" / "skills",
                    current / ".claude" / "skills",
                    current / ".agents" / "skills",
                ]
            )

            if current == stop_path or current.parent == current:
                break
            current = current.parent

        return roots

    @staticmethod
    def _find_git_root(start: Path) -> Path:
        current = start
        while True:
            if (current / ".git").exists():
                return current
            if current.parent == current:
                return start
            current = current.parent

    def _scan_root(self, root: Path) -> None:
        if not root.is_dir():
            return

        for skill_file in sorted(root.rglob("SKILL.md")):
            document = parse_skill_document(skill_file)
            if document is None:
                continue
            self.add(document)


def parse_skill_document(path: Path) -> SkillDocument | None:
    """Parse one SKILL.md file.

    Only `name` and `description` are required from frontmatter.
    Invalid files are skipped to keep discovery lightweight and resilient.
    """
    text = path.read_text(encoding="utf-8")
    parsed = _split_frontmatter(text)
    if parsed is None:
        logger.warning(f"Skipping skill without valid frontmatter: {path}")
        return None

    frontmatter, body = parsed
    name = frontmatter.get("name")
    description = frontmatter.get("description")

    if not isinstance(name, str) or not isinstance(description, str):
        logger.warning(f"Skipping skill missing name/description: {path}")
        return None

    if not SKILL_NAME_PATTERN.match(name):
        logger.warning(f"Skipping skill with invalid name '{name}': {path}")
        return None

    if path.parent.name != name:
        logger.warning(
            f"Skipping skill due to directory mismatch: expected '{path.parent.name}', got '{name}' in {path}"
        )
        return None

    if not description.strip():
        logger.warning(f"Skipping skill with empty description: {path}")
        return None

    return SkillDocument(
        name=name,
        description=description.strip(),
        location=path.resolve(),
        content=body.strip(),
    )


def _split_frontmatter(text: str) -> tuple[dict[str, Any], str] | None:
    if not text.startswith("---"):
        return None

    parts = text.split("---", 2)
    if len(parts) < 3:
        return None

    _, raw_frontmatter, body = parts
    frontmatter = _parse_frontmatter(raw_frontmatter.strip("\n"))
    if not frontmatter:
        return None

    return frontmatter, body.lstrip("\n")


def _parse_frontmatter(text: str) -> dict[str, Any]:
    parsed: dict[str, Any] = {}
    active_key: str | None = None
    block_lines: list[str] = []

    def flush_block() -> None:
        nonlocal active_key, block_lines
        if active_key is None:
            return
        parsed[active_key] = "\n".join(block_lines).strip()
        active_key = None
        block_lines = []

    for raw_line in text.splitlines():
        line = raw_line.rstrip()
        stripped = line.strip()

        if not stripped or stripped.startswith("#"):
            continue

        if active_key and line.startswith((" ", "\t")):
            block_lines.append(stripped)
            continue

        flush_block()

        if ":" not in line:
            continue

        key, value = line.split(":", 1)
        key = key.strip()
        value = value.strip()

        if value in {"|", ">", "|-", ">-"}:
            active_key = key
            block_lines = []
            continue

        parsed[key] = _strip_quotes(value)

    flush_block()
    return parsed


def _strip_quotes(value: str) -> str:
    if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
        return value[1:-1]
    return value
