"""Shared skills management for AI agents."""

from __future__ import annotations

import importlib.metadata

from dataclasses import dataclass, field
from pathlib import Path

import yaml

from ai_rules.utils import is_managed_target


@dataclass
class SkillMetadata:
    """Parsed SKILL.md metadata."""

    name: str
    description: str
    version: str | None = None
    disabled: bool = False


@dataclass
class SkillItem:
    """Status of a single skill."""

    actual_source: Path | None
    expected_source: Path | None
    is_broken: bool


@dataclass
class SkillStatus:
    """Status of skills for an agent."""

    managed_installed: dict[str, SkillItem] = field(default_factory=dict)
    managed_pending: dict[str, SkillItem] = field(default_factory=dict)
    managed_wrong_target: dict[str, SkillItem] = field(default_factory=dict)
    unmanaged: dict[str, SkillItem] = field(default_factory=dict)


class SkillManager:
    """Manages skills for any AI agent."""

    def __init__(
        self,
        config_dir: Path,
        agent_id: str,
        user_skills_dirs: list[Path] | None = None,
    ):
        """Initialize skill manager.

        Args:
            config_dir: ai-rules config directory
            agent_id: Agent identifier (e.g., 'claude', 'goose', '' for shared)
            user_skills_dirs: Directories where skills are installed
        """
        self.config_dir = config_dir
        self.agent_id = agent_id
        if user_skills_dirs:
            self.user_skills_dirs = [p.expanduser() for p in user_skills_dirs]
        else:
            self.user_skills_dirs = []

    @staticmethod
    def parse_skill_md(skill_dir: Path) -> SkillMetadata | None:
        """Parse SKILL.md file and extract metadata.

        Args:
            skill_dir: Path to skill directory containing SKILL.md

        Returns:
            SkillMetadata or None if parsing fails
        """
        skill_file = skill_dir / "SKILL.md"
        if not skill_file.exists():
            return None

        content = skill_file.read_text(encoding="utf-8")

        if content.startswith("---"):
            parts = content.split("---", 2)
            if len(parts) >= 3:
                try:
                    frontmatter = yaml.safe_load(parts[1])
                    if not isinstance(frontmatter, dict):
                        return SkillMetadata(name=skill_dir.name, description="")
                    raw_version = frontmatter.get("version")
                    return SkillMetadata(
                        name=frontmatter.get("name", skill_dir.name),
                        description=frontmatter.get("description", ""),
                        version=str(raw_version) if raw_version is not None else None,
                        disabled=frontmatter.get("disabled", False) is True,
                    )
                except yaml.YAMLError:
                    pass

        return SkillMetadata(name=skill_dir.name, description="")

    @staticmethod
    def is_skill_disabled(skill_dir: Path) -> bool:
        """Check if a skill has disabled: true in its SKILL.md frontmatter."""
        meta = SkillManager.parse_skill_md(skill_dir)
        return meta is not None and meta.disabled

    def _get_managed_skills(self) -> dict[str, Path]:
        """Get all managed skills from config_dir."""
        if self.agent_id:
            source_dir = self.config_dir / self.agent_id / "skills"
        else:
            source_dir = self.config_dir / "skills"

        if not source_dir.exists():
            return {}

        result = {}
        for item in sorted(source_dir.glob("*")):
            if item.is_dir() and not self.is_skill_disabled(item):
                result[item.name] = item
        return result

    def get_orphaned_skills(self) -> dict[str, list[Path]]:
        """Get skill symlinks that point to ai-rules but source no longer exists.

        Returns:
            dict mapping skill name -> list of orphaned symlink paths
        """
        orphaned: dict[str, list[Path]] = {}

        for user_dir in self.user_skills_dirs:
            if not user_dir.exists():
                continue

            for item in user_dir.glob("*"):
                if not item.is_symlink() or not item.is_dir():
                    continue

                try:
                    target = item.resolve()
                    if (
                        is_managed_target(target, self.config_dir)
                        and not target.exists()
                    ):
                        if item.name not in orphaned:
                            orphaned[item.name] = []
                        orphaned[item.name].append(item)
                except OSError, RuntimeError:
                    try:
                        raw_target = item.readlink()
                        if is_managed_target(raw_target, self.config_dir):
                            if item.name not in orphaned:
                                orphaned[item.name] = []
                            orphaned[item.name].append(item)
                    except OSError, RuntimeError:
                        pass

        return orphaned

    def _scan_installed_skills(self) -> dict[str, list[tuple[Path, Path | None, bool]]]:
        """Scan user directories for installed skills.

        Returns:
            dict mapping name -> list of (target_path, symlink_source_or_none, is_broken) per directory
        """
        result: dict[str, list[tuple[Path, Path | None, bool]]] = {}

        for user_dir in self.user_skills_dirs:
            if not user_dir.exists():
                continue

            for item in sorted(user_dir.glob("*")):
                if not item.is_dir() and not item.is_symlink():
                    continue

                name = item.name

                if item.is_symlink():
                    try:
                        source = item.resolve()
                        is_broken = not source.exists()
                    except OSError, RuntimeError:
                        source = None
                        is_broken = True
                    entry = (item, source, is_broken)
                else:
                    entry = (item, None, False)

                if name not in result:
                    result[name] = []
                result[name].append(entry)

        return result

    def get_status(self) -> SkillStatus:
        """Get comprehensive status of skills."""
        status = SkillStatus()
        managed_sources = self._get_managed_skills()
        installed = self._scan_installed_skills()

        for name, expected_source in managed_sources.items():
            if name in installed:
                installations = installed[name]
                expected_count = len(self.user_skills_dirs)
                actual_count = len(installations)

                synced_count = sum(
                    1
                    for _, actual_source, is_broken in installations
                    if not is_broken
                    and actual_source
                    and actual_source == expected_source.resolve()
                )

                has_issues = any(
                    is_broken
                    or (actual_source and actual_source != expected_source.resolve())
                    for _, actual_source, is_broken in installations
                )

                if synced_count == expected_count and not has_issues:
                    _, actual_source, _ = installations[0]
                    status.managed_installed[name] = SkillItem(
                        actual_source=actual_source,
                        expected_source=expected_source,
                        is_broken=False,
                    )
                elif has_issues or actual_count < expected_count:
                    _, actual_source, _ = installations[0]
                    status.managed_wrong_target[name] = SkillItem(
                        actual_source=actual_source,
                        expected_source=expected_source,
                        is_broken=any(ib for _, _, ib in installations),
                    )
            else:
                status.managed_pending[name] = SkillItem(
                    actual_source=None,
                    expected_source=expected_source,
                    is_broken=False,
                )

        for name, installations in installed.items():
            if name not in managed_sources:
                _, actual_source, is_broken = installations[0]
                status.unmanaged[name] = SkillItem(
                    actual_source=actual_source,
                    expected_source=None,
                    is_broken=is_broken,
                )

        return status

    def list_bundled_skills(
        self, *, include_disabled: bool = False
    ) -> list[SkillMetadata]:
        """List bundled skills with their metadata."""
        if self.agent_id:
            source_dir = self.config_dir / self.agent_id / "skills"
        else:
            source_dir = self.config_dir / "skills"

        if not source_dir.exists():
            return []

        results = []
        for item in sorted(source_dir.glob("*")):
            if not item.is_dir():
                continue
            metadata = self.parse_skill_md(item)
            if metadata is None:
                continue
            if metadata.disabled and not include_disabled:
                continue
            results.append(metadata)
        return results

    def get_skill_content(self, name: str) -> str | None:
        """Get the raw SKILL.md content for a bundled skill.

        Returns:
            Raw file content, or None if skill doesn't exist.
        """
        managed = self._get_managed_skills()
        if name not in managed:
            return None
        skill_file = managed[name] / "SKILL.md"
        if not skill_file.exists():
            return None
        return skill_file.read_text(encoding="utf-8")

    @staticmethod
    def _get_repo_url() -> str | None:
        try:
            dist = importlib.metadata.distribution("ai-agent-rules")
        except importlib.metadata.PackageNotFoundError:
            return None

        for url_entry in dist.metadata.get_all("Project-URL") or []:
            parts = url_entry.split(",", 1)
            if len(parts) < 2:
                continue
            label, url = parts
            if label.strip().lower() == "repository":
                return str(url.strip().rstrip("/"))

        return None

    @staticmethod
    def get_skill_url(name: str) -> str | None:
        """Construct a versioned GitHub URL for a bundled skill."""
        repo_url = SkillManager._get_repo_url()
        if not repo_url:
            return None
        return f"{repo_url}/blob/main/src/ai_rules/config/skills/{name}/SKILL.md"

    @staticmethod
    def get_download_url(name: str | None = None) -> str | None:
        """Construct a download-directory URL for skills.

        Args:
            name: Skill name for a single skill, or None for all skills.
        """
        repo_url = SkillManager._get_repo_url()
        if not repo_url:
            return None
        skills_path = "src/ai_rules/config/skills"
        if name is not None:
            skills_path = f"{skills_path}/{name}"
        return f"https://download-directory.github.io/?url={repo_url}/tree/main/{skills_path}"
