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

import yaml

from otto.log import get_logger

SKILLS_DIR = "skills"
SKILL_FILE = "SKILL.md"
BUILTIN_SKILLS_DIR = Path(__file__).parent / "skills" / "builtin"
NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")

log = get_logger(__name__)


@dataclass(frozen=True)
class Skill:
    name: str
    description: str
    path: Path
    license: str | None
    compatibility: str | None
    metadata: dict[str, str]


def _parse_frontmatter(content: str) -> dict[str, Any]:
    lines = content.splitlines()
    if not lines or lines[0].strip() != "---":
        raise ValueError("missing frontmatter start marker")

    end_index = next(
        (index for index, line in enumerate(lines[1:], start=1) if line.strip() == "---"), -1
    )
    if end_index == -1:
        raise ValueError("missing frontmatter end marker")

    raw_frontmatter = "\n".join(lines[1:end_index])
    parsed = yaml.safe_load(raw_frontmatter) or {}
    if not isinstance(parsed, dict):
        raise ValueError("frontmatter must be a mapping")
    return parsed


def validate_frontmatter(frontmatter: dict) -> list[str]:
    errors: list[str] = []

    name = frontmatter.get("name")
    if not isinstance(name, str):
        errors.append("name is required and must be a string")
    else:
        if not 1 <= len(name) <= 64:
            errors.append("name must be 1-64 characters")
        if "--" in name:
            errors.append("name cannot contain consecutive hyphens")
        if NAME_PATTERN.fullmatch(name) is None:
            errors.append("name must match ^[a-z0-9]+(-[a-z0-9]+)*$")

    description = frontmatter.get("description")
    if not isinstance(description, str):
        errors.append("description is required and must be a string")
    else:
        stripped = description.strip()
        if not stripped:
            errors.append("description cannot be empty")
        if len(stripped) > 1024:
            errors.append("description must be 1-1024 characters")

    license_value = frontmatter.get("license")
    if license_value is not None and not isinstance(license_value, str):
        errors.append("license must be a string when provided")

    compatibility = frontmatter.get("compatibility")
    if compatibility is not None:
        if not isinstance(compatibility, str):
            errors.append("compatibility must be a string when provided")
        elif len(compatibility) > 500:
            errors.append("compatibility must be 500 characters or fewer")

    metadata = frontmatter.get("metadata")
    if metadata is not None:
        if not isinstance(metadata, dict):
            errors.append("metadata must be a mapping when provided")
        else:
            for key, value in metadata.items():
                if not isinstance(key, str) or not isinstance(value, str):
                    errors.append("metadata keys and values must be strings")
                    break

    return errors


def discover(skills_dir: Path) -> list[Skill]:
    if not skills_dir.is_dir():
        return []

    skills: list[Skill] = []
    for skill_dir in sorted(
        (entry for entry in skills_dir.iterdir() if entry.is_dir()), key=lambda item: item.name
    ):
        skill_file = skill_dir / SKILL_FILE
        if not skill_file.is_file():
            continue
        try:
            frontmatter = _parse_frontmatter(skill_file.read_text(encoding="utf-8"))
            errors = validate_frontmatter(frontmatter)
            name = frontmatter.get("name")
            if isinstance(name, str) and name != skill_dir.name:
                errors.append("name must match directory name")
            if errors:
                raise ValueError("; ".join(errors))
            if not isinstance(name, str):
                raise ValueError("name is required and must be a string")
            description = str(frontmatter["description"]).strip()
            metadata = frontmatter.get("metadata") or {}
            skills.append(
                Skill(
                    name=name,
                    description=description,
                    path=skill_dir.resolve(),
                    license=frontmatter.get("license"),
                    compatibility=frontmatter.get("compatibility"),
                    metadata=dict(metadata),
                )
            )
        except Exception as exc:
            log.warning("Skipping invalid skill", skill=skill_dir.name, error=str(exc))
    return skills


def discover_many(skills_dirs: list[Path]) -> list[Skill]:
    """Discover skills across directories, de-duplicated by name (first match wins)."""
    seen_names: set[str] = set()
    all_skills: list[Skill] = []
    for skills_dir in skills_dirs:
        for skill in discover(skills_dir):
            if skill.name in seen_names:
                continue
            seen_names.add(skill.name)
            all_skills.append(skill)
    return all_skills


def read_skill(skills_dir: Path, name: str) -> str | None:
    skill_file = skills_dir / name / SKILL_FILE
    if not skill_file.is_file():
        return None
    return skill_file.read_text(encoding="utf-8")


def read_skill_from_dirs(skills_dirs: list[Path], name: str) -> tuple[str, Path] | None:
    """Read skill content from the first matching directory.

    Returns ``(content, skill_file_path)`` when found.
    """
    for skills_dir in skills_dirs:
        skill_file = skills_dir / name / SKILL_FILE
        if not skill_file.is_file():
            continue
        return skill_file.read_text(encoding="utf-8"), skill_file.resolve()
    return None


def _scripts_file_list(skill_path: Path) -> list[str]:
    """Return sorted filenames inside <skill>/scripts/ (empty list if absent)."""
    scripts_dir = skill_path / "scripts"
    if not scripts_dir.is_dir():
        return []
    return sorted(f.name for f in scripts_dir.iterdir() if f.is_file())


def format_skills_prompt(skills: list[Skill]) -> str:
    lines = ["<available_skills>"]
    for skill in skills:
        lines.extend(
            [
                "  <skill>",
                f"    <name>{skill.name}</name>",
                f"    <description>{skill.description}</description>",
                "  </skill>",
            ]
        )
    lines.extend(
        [
            "</available_skills>",
            "",
            "When a task matches a skill above, call use_skill to load full instructions.",
        ]
    )
    return "\n".join(lines)
