"""Skill class with lazy loading support.

This module provides the Skill class that implements lazy loading -
skills are only loaded when their tools are actually invoked.
"""

from __future__ import annotations

import importlib
import importlib.util
import sys
from abc import ABC, abstractmethod
from collections.abc import Callable
from pathlib import Path
from types import ModuleType
from typing import Any

from oclawma.skills.base import (
    SkillLoadError,
    SkillMetadata,
    SkillToolError,
)
from oclawma.skills.manifest import SkillManifest


class LazySkill(ABC):
    """Abstract base class for skills with lazy loading.

    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 -> tool instance)
    """

    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] = {}

    @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
        """
        if self._loaded:
            return tool_name in self._tools

        # For unloaded skills, we can't know without loading
        # Subclasses may override to provide manifest-based checks
        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"):
                if callable(tool.execute):
                    return await tool.execute(**params)
                else:
                    raise SkillToolError(
                        f"Tool '{tool_name}' execute is not callable", skill_name=self.name
                    )
            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())
        # Return registered function names before loading
        return list(self._tool_functions.keys())

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

        Returns:
            List of tool schema dictionaries
        """
        if not self._loaded:
            self._load()

        schemas = []
        for name, tool in self._tools.items():
            if hasattr(tool, "schema"):
                schema = tool.schema
                if hasattr(schema, "to_dict"):
                    schemas.append(schema.to_dict())
                elif isinstance(schema, dict):
                    schemas.append(schema)
                else:
                    schemas.append({"name": name})
            else:
                schemas.append({"name": name})

        return schemas

    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 from metadata
            return self.metadata.estimated_tokens


class ManifestSkill(LazySkill):
    """A skill loaded from a manifest file.

    This skill type loads its implementation dynamically based on
    the entry_point specified in the manifest.
    """

    def __init__(self, manifest: SkillManifest, manifest_path: Path | None = None) -> None:
        """Initialize from a manifest.

        Args:
            manifest: The skill manifest
            manifest_path: Optional path to the manifest file
        """
        metadata = manifest.to_metadata(manifest_path)
        super().__init__(metadata)
        self._manifest = manifest
        self._manifest_path = manifest_path
        self._module: ModuleType | None = None

    @property
    def manifest(self) -> SkillManifest:
        """Get the skill manifest."""
        return self._manifest

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

        This checks the manifest without loading the skill.
        """
        # Check manifest first without loading
        for tool in self._manifest.tools:
            if tool.name == tool_name:
                return True

        # Fall back to loaded tools
        return super().has_tool(tool_name)

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

        Returns tool names from manifest without loading.
        """
        if self._loaded:
            return super().list_tools()

        # Return from manifest without loading
        return [tool.name for tool in self._manifest.tools]

    def _load(self) -> None:
        """Load the skill implementation from the entry point."""
        if self._loaded:
            return

        entry_point = self._manifest.entry_point

        if not entry_point:
            raise SkillLoadError(
                f"Skill '{self.name}' has no entry_point defined", skill_name=self.name
            )

        try:
            # Parse entry_point (format: "module.path:ClassName")
            if ":" in entry_point:
                module_path, class_name = entry_point.rsplit(":", 1)
            else:
                module_path = entry_point
                class_name = None

            # If manifest_path is set, add its directory to sys.path
            if self._manifest_path:
                skill_dir = self._manifest_path.parent
                if str(skill_dir) not in sys.path:
                    sys.path.insert(0, str(skill_dir))

            # Load the module
            self._module = importlib.import_module(module_path)

            # If a class is specified, instantiate it
            if class_name:
                skill_class = getattr(self._module, class_name)
                skill_instance = skill_class(self.metadata)

                # Copy tools from instance
                if hasattr(skill_instance, "_tools"):
                    self._tools = skill_instance._tools
                elif hasattr(skill_instance, "tools"):
                    self._tools = skill_instance.tools
                else:
                    # Try to find tools by convention
                    self._discover_tools(skill_instance)
            else:
                # Discover tools from module
                self._discover_tools_from_module(self._module)

            self._loaded = True

        except ImportError as e:
            raise SkillLoadError(
                f"Failed to import skill '{self.name}' from '{entry_point}': {e}",
                skill_name=self.name,
                cause=e,
            ) from e
        except Exception as e:
            raise SkillLoadError(
                f"Failed to load skill '{self.name}': {e}", skill_name=self.name, cause=e
            ) from e

    def _discover_tools(self, instance: Any) -> None:
        """Discover tools from a skill instance."""
        # Look for tool methods (convention: methods starting with 'tool_')
        for attr_name in dir(instance):
            if attr_name.startswith("tool_"):
                tool_name = attr_name[5:]  # Remove 'tool_' prefix
                self._tools[tool_name] = getattr(instance, attr_name)

    def _discover_tools_from_module(self, module: ModuleType) -> None:
        """Discover tools from a module."""
        # Look for exported tools
        if hasattr(module, "__all__"):
            for name in module.__all__:
                obj = getattr(module, name)
                if hasattr(obj, "is_tool") or callable(obj):
                    self._tools[name] = obj


class FunctionSkill(LazySkill):
    """A skill composed of simple functions.

    This is useful for simple skills that don't need a full module.
    """

    def __init__(
        self, metadata: SkillMetadata, tools: dict[str, Callable[..., Any]] | None = None
    ) -> None:
        """Initialize with tools.

        Args:
            metadata: Skill metadata
            tools: Dictionary of tool functions
        """
        super().__init__(metadata)
        self._tool_functions = tools or {}

    def register_tool(self, name: str, func: Callable[..., Any]) -> None:
        """Register a tool function.

        Args:
            name: Tool name
            func: Tool function
        """
        self._tool_functions[name] = func
        # If already loaded, update tools
        if self._loaded:
            self._tools[name] = func

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

        Returns tool names from registered functions without loading.
        """
        return list(self._tool_functions.keys())

    def _load(self) -> None:
        """Load the tool functions."""
        if self._loaded:
            return

        self._tools = dict(self._tool_functions)
        self._loaded = True


# Re-export for backwards compatibility
Skill = LazySkill
