"""Base classes for the skill system."""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any


class SkillError(Exception):
    """Base exception for skill errors."""

    def __init__(self, message: str, skill_name: str = "", cause: Exception | None = None) -> None:
        super().__init__(message)
        self.message = message
        self.skill_name = skill_name
        self.cause = cause


class SkillNotFoundError(SkillError):
    """Raised when a requested skill is not found."""

    pass


class SkillLoadError(SkillError):
    """Raised when a skill fails to load."""

    pass


class SkillToolError(SkillError):
    """Raised when a skill tool execution fails."""

    pass


@dataclass(frozen=True)
class ToolInfo:
    """Information about a tool provided by a skill.

    This is a lightweight representation that doesn't require
    loading the actual tool implementation.
    """

    name: str
    description: str
    parameters: list[dict[str, Any]] = field(default_factory=list)
    returns: str = "string"
    token_count: int = 0  # Estimated tokens for tool schema

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "name": self.name,
            "description": self.description,
            "parameters": self.parameters,
            "returns": self.returns,
            "token_count": self.token_count,
        }


@dataclass
class SkillMetadata:
    """Metadata about a skill.

    This is loaded from the skill manifest and doesn't require
    loading the full skill implementation.
    """

    name: str
    version: str
    description: str = ""
    author: str = ""
    category: str = "general"
    tags: list[str] = field(default_factory=list)
    requirements: list[str] = field(default_factory=list)
    entry_point: str = ""  # Python module path or file path
    manifest_path: Path | None = None

    # Token estimates (cached)
    estimated_tokens: int = 0
    tool_count: int = 0

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "name": self.name,
            "version": self.version,
            "description": self.description,
            "author": self.author,
            "category": self.category,
            "tags": self.tags,
            "requirements": self.requirements,
            "entry_point": self.entry_point,
            "estimated_tokens": self.estimated_tokens,
            "tool_count": self.tool_count,
        }


@dataclass
class SkillManifest:
    """Manifest for a skill loaded from YAML.

    This is the on-disk representation of a skill's metadata.
    """

    name: str
    version: str
    description: str = ""
    author: str = ""
    category: str = "general"
    tags: list[str] = field(default_factory=list)
    requirements: list[str] = field(default_factory=list)
    entry_point: str = ""  # Python module path or file path
    tools: list[ToolInfo] = field(default_factory=list)

    @classmethod
    def from_yaml(cls, path: Path) -> SkillManifest:
        """Load a manifest from a YAML file.

        Args:
            path: Path to the YAML manifest file

        Returns:
            Parsed SkillManifest

        Raises:
            SkillLoadError: If the manifest is invalid
        """
        import yaml

        try:
            with open(path) as f:
                data = yaml.safe_load(f)
        except Exception as e:
            raise SkillLoadError(f"Failed to load manifest from {path}: {e}", cause=e) from e

        if not isinstance(data, dict):
            raise SkillLoadError(
                f"Invalid manifest format in {path}: expected dict, got {type(data).__name__}"
            )

        # Parse tools
        tools = []
        for tool_data in data.get("tools", []):
            tools.append(
                ToolInfo(
                    name=tool_data["name"],
                    description=tool_data.get("description", ""),
                    parameters=tool_data.get("parameters", []),
                    returns=tool_data.get("returns", "string"),
                    token_count=tool_data.get("token_count", 0),
                )
            )

        return cls(
            name=data["name"],
            version=data.get("version", "0.1.0"),
            description=data.get("description", ""),
            author=data.get("author", ""),
            category=data.get("category", "general"),
            tags=data.get("tags", []),
            requirements=data.get("requirements", []),
            entry_point=data.get("entry_point", ""),
            tools=tools,
        )

    def to_yaml(self, path: Path) -> None:
        """Save the manifest to a YAML file.

        Args:
            path: Path to save the YAML file
        """
        import yaml

        data = {
            "name": self.name,
            "version": self.version,
            "description": self.description,
            "author": self.author,
            "category": self.category,
            "tags": self.tags,
            "requirements": self.requirements,
            "entry_point": self.entry_point,
            "tools": [tool.to_dict() for tool in self.tools],
        }

        with open(path, "w") as f:
            yaml.dump(data, f, default_flow_style=False, sort_keys=False)

    def to_metadata(self, manifest_path: Path | None = None) -> SkillMetadata:
        """Convert to SkillMetadata.

        Args:
            manifest_path: Optional path to the manifest file

        Returns:
            SkillMetadata with information from this manifest
        """
        # Calculate estimated tokens
        estimated_tokens = sum(tool.token_count for tool in self.tools)
        # Add overhead for skill metadata
        estimated_tokens += len(self.description) // 4  # Rough estimate

        return SkillMetadata(
            name=self.name,
            version=self.version,
            description=self.description,
            author=self.author,
            category=self.category,
            tags=self.tags,
            requirements=self.requirements,
            entry_point=self.entry_point,
            manifest_path=manifest_path,
            estimated_tokens=estimated_tokens,
            tool_count=len(self.tools),
        )


class Skill(ABC):
    """Abstract base class for skills.

    A skill is a collection of related tools that can be loaded on-demand.
    The skill system uses lazy loading - skills are only loaded when
    one of their tools is invoked.

    Attributes:
        metadata: SkillMetadata with information about the skill
        _loaded: Whether the skill's tools have been loaded
        _tools: Dictionary of loaded tools (name -> BaseTool)
    """

    def __init__(self, metadata: SkillMetadata) -> None:
        """Initialize the skill with metadata.

        Args:
            metadata: Metadata about the skill
        """
        self.metadata = metadata
        self._loaded = False
        self._tools: dict[str, Any] = {}  # BaseTool instances

    @property
    def name(self) -> str:
        """Get the skill name."""
        return self.metadata.name

    @property
    def is_loaded(self) -> bool:
        """Check if the skill has been loaded."""
        return self._loaded

    @property
    def tools(self) -> dict[str, Any]:
        """Get the loaded tools.

        Returns:
            Dictionary mapping tool names to tool instances

        Note:
            This will trigger loading if not already loaded.
        """
        if not self._loaded:
            self._load()
        return self._tools

    def get_tool(self, tool_name: str) -> Any:
        """Get a specific tool by name.

        Args:
            tool_name: Name of the tool to get

        Returns:
            The tool instance

        Raises:
            SkillToolError: If the tool is not found
        """
        if not self._loaded:
            self._load()

        if tool_name not in self._tools:
            raise SkillToolError(
                f"Tool '{tool_name}' not found in skill '{self.name}'", skill_name=self.name
            )

        return self._tools[tool_name]

    def has_tool(self, tool_name: str) -> bool:
        """Check if the skill provides a specific tool.

        Args:
            tool_name: Name of the tool to check

        Returns:
            True if the skill provides this tool
        """
        # Check manifest without loading
        if self._loaded:
            return tool_name in self._tools

        return False

    def load(self) -> None:
        """Explicitly load the skill.

        This is called automatically when tools are accessed,
        but can be called explicitly to preload the skill.
        """
        if not self._loaded:
            self._load()

    def unload(self) -> None:
        """Unload the skill to free resources.

        After unloading, the skill will be reloaded on next access.
        """
        self._tools.clear()
        self._loaded = False
        self._on_unload()

    @abstractmethod
    def _load(self) -> None:
        """Load the skill's tools.

        This is called automatically when tools are needed.
        Implementations should populate self._tools.
        """
        ...

    def _on_unload(self) -> None:
        """Called when the skill is unloaded.

        Override this to perform cleanup.
        """
        return  # Default no-op; override in subclasses

    async def execute_tool(self, tool_name: str, **params: Any) -> Any:
        """Execute a tool by name.

        Args:
            tool_name: Name of the tool to execute
            **params: Parameters to pass to the tool

        Returns:
            Tool execution result

        Raises:
            SkillToolError: If the tool doesn't exist or fails
        """
        tool = self.get_tool(tool_name)

        try:
            # Assuming tool has an async execute method
            if hasattr(tool, "execute"):
                return await tool.execute(**params)
            else:
                raise SkillToolError(
                    f"Tool '{tool_name}' does not have an execute method", skill_name=self.name
                )
        except Exception as e:
            if isinstance(e, SkillToolError):
                raise
            raise SkillToolError(
                f"Tool '{tool_name}' execution failed: {e}", skill_name=self.name, cause=e
            ) from e

    def list_tools(self) -> list[str]:
        """List the names of tools provided by this skill.

        Returns:
            List of tool names
        """
        if self._loaded:
            return list(self._tools.keys())
        else:
            # Return from metadata without loading
            return [tool.name for tool in self.metadata.tools]

    def get_tool_schemas(self) -> list[dict[str, Any]]:
        """Get schemas for all tools in this skill.

        Returns:
            List of tool schema dictionaries
        """
        if self._loaded:
            return [
                tool.to_dict() if hasattr(tool, "to_dict") else {"name": name}
                for name, tool in self._tools.items()
            ]
        else:
            # Return from metadata without loading
            return [tool.to_dict() for tool in self.metadata.tools]

    def estimate_tokens(self) -> int:
        """Estimate the token count for this skill.

        Returns:
            Estimated token count
        """
        if self._loaded:
            # Calculate from actual tools
            total = 0
            for tool in self._tools.values():
                if hasattr(tool, "schema"):
                    # Rough estimation: 1 token per 4 characters
                    schema_str = str(tool.schema)
                    total += len(schema_str) // 4
            return total
        else:
            # Use cached estimate
            return self.metadata.estimated_tokens
