"""Skill registry for CleverAgents v3.

Manages registration, lookup, and validation of skill definitions.
Resolves tool references through an optional Tool Registry integration.

## Overview

- **SkillRegistry** -- In-memory registry of ``SkillDefinition`` instances,
  keyed by skill name.  Provides CRUD operations, metadata listing,
  tool resolution, definition validation, and on-demand refresh triggered
  by MCP ``notifications/tools/list_changed`` events.

## Tool Resolution

The ``resolve_tools`` method delegates to the skill's own
``SkillResolver`` to flatten tool references.  When a ``_tool_registry``
reference is configured, tool-ref names can be validated against the
Tool Registry to ensure they exist.

## Refresh

``refresh(name)`` recomputes the flattened tool set for a single skill
and validates its tool references against the Tool Registry (if one is
configured).  ``refresh_all()`` runs ``refresh`` for every registered
skill and returns an aggregated ``SkillRefreshResult``.

When no Tool Registry is configured, ``refresh`` resolves tools but
skips validation and returns the skill in ``SkillRefreshResult.refreshed``
so callers can detect the unavailability condition through the warning
logged at that point.

## Validation

The ``validate_skill`` method checks that all tool references in a skill
definition actually exist (either in the tool registry or as inline
tools), and reports any discrepancies as a list of error messages.

Based on ``docs/specification.md`` and ``implementation_plan.md`` task
C3.context.
"""

from __future__ import annotations

import logging
from typing import Any

from cleveragents.domain.models.core.skill import (
    ResolvedToolEntry,
    SkillCapabilitySummary,
    SkillResolver,
)
from cleveragents.skills.context import SkillExecutionError
from cleveragents.skills.protocol import (
    SkillDefinition,
    SkillError,
    SkillErrorType,
    SkillMetadata,
)
from cleveragents.skills.refresh import SkillRefreshResult

logger = logging.getLogger(__name__)

_TOOL_REGISTRY_UNAVAILABLE_MSG = (
    "Tool registry is not configured on SkillRegistry; tool-ref validation "
    "will be skipped during refresh. "
    "Recovery: pass a ToolRegistry instance to SkillRegistry.__init__ and "
    "call refresh() again once it is available."
)

# ---------------------------------------------------------------------------
# SkillRegistry
# ---------------------------------------------------------------------------


class SkillRegistry:
    """In-memory registry for skill definitions.

    Provides registration, lookup, listing, tool resolution, and
    validation against an optional tool registry.

    Attributes:
        _skills: Internal mapping of skill names to definitions.
        _tool_registry: Optional reference to a Tool Registry service
            for tool-ref validation.
    """

    def __init__(
        self,
        tool_registry: Any | None = None,
    ) -> None:
        self._skills: dict[str, SkillDefinition] = {}
        self._tool_registry: Any | None = tool_registry

    # -- Registration ----------------------------------------------------------

    def register(self, skill: SkillDefinition) -> None:
        """Register a skill definition.

        Args:
            skill: The ``SkillDefinition`` to register.

        Raises:
            SkillExecutionError: With ``VALIDATION_ERROR`` if a skill
                with the same name is already registered.
        """
        name = skill.skill.name
        if name in self._skills:
            raise SkillExecutionError(
                SkillError(
                    error_type=SkillErrorType.VALIDATION_ERROR,
                    message=f"Skill '{name}' is already registered",
                    skill_name=name,
                    details={"existing_skill": name},
                )
            )
        self._skills[name] = skill

    def unregister(self, name: str) -> None:
        """Remove a skill definition from the registry.

        Args:
            name: The namespaced skill name to remove.

        Raises:
            SkillExecutionError: With ``SKILL_NOT_FOUND`` if the skill
                is not registered.
        """
        if name not in self._skills:
            raise SkillExecutionError(
                SkillError(
                    error_type=SkillErrorType.SKILL_NOT_FOUND,
                    message=f"Skill '{name}' is not registered",
                    skill_name=name,
                )
            )
        del self._skills[name]

    # -- Lookup ----------------------------------------------------------------

    def get(self, name: str) -> SkillDefinition:
        """Look up a registered skill by name.

        Args:
            name: The namespaced skill name.

        Returns:
            The registered ``SkillDefinition``.

        Raises:
            SkillExecutionError: With ``SKILL_NOT_FOUND`` if the skill
                is not registered.
        """
        if name not in self._skills:
            raise SkillExecutionError(
                SkillError(
                    error_type=SkillErrorType.SKILL_NOT_FOUND,
                    message=f"Skill '{name}' not found in registry",
                    skill_name=name,
                )
            )
        return self._skills[name]

    # -- Listing ---------------------------------------------------------------

    def list_all(self) -> list[SkillMetadata]:
        """Return metadata for all registered skills.

        Returns:
            List of ``SkillMetadata`` instances, one per registered
            skill, sorted by name.
        """
        return [
            defn.metadata
            for defn in sorted(self._skills.values(), key=lambda d: d.skill.name)
        ]

    # -- Tool resolution -------------------------------------------------------

    def resolve_tools(self, skill_name: str) -> list[ResolvedToolEntry]:
        """Resolve tool references for a registered skill.

        Uses the ``SkillResolver`` to flatten the skill's tool set.
        Other registered skills are available for include resolution.

        Args:
            skill_name: The namespaced skill name.

        Returns:
            Ordered list of ``ResolvedToolEntry`` instances.

        Raises:
            SkillExecutionError: With ``SKILL_NOT_FOUND`` if the skill
                is not registered.
        """
        defn = self.get(skill_name)
        resolver = SkillResolver()

        # Build a lookup of all registered skills for include resolution
        skill_lookup = {name: d.skill for name, d in self._skills.items()}

        return resolver.resolve_tools(defn.skill, skill_lookup)

    # -- Flattened tools with summary ------------------------------------------

    def tools(
        self, skill_name: str
    ) -> tuple[list[ResolvedToolEntry], SkillCapabilitySummary]:
        """Return flattened tool list with capability summary for a skill.

        Combines ``resolve_tools()`` and ``compute_capability_summary()``
        into a single call.

        Args:
            skill_name: The namespaced skill name.

        Returns:
            A 2-tuple of (resolved entries, capability summary).

        Raises:
            SkillExecutionError: With ``SKILL_NOT_FOUND`` if the skill
                is not registered.
        """
        defn = self.get(skill_name)
        resolver = SkillResolver()
        skill_lookup = {name: d.skill for name, d in self._skills.items()}
        entries = resolver.resolve_tools(defn.skill, skill_lookup)
        summary = resolver.compute_capability_summary(defn.skill, entries)
        return entries, summary

    # -- Plan validation -------------------------------------------------------

    def validate_plan(self, plan: dict[str, Any]) -> list[str]:
        """Validate that a plan's skill references are satisfiable.

        Checks:
        - All referenced skills exist in the registry.
        - All includes are resolvable (no missing skills).
        - No cycles in include chains.
        - Tool refs are valid (if ``_tool_registry`` is configured).

        Args:
            plan: A plan dict with at minimum a ``'skills'`` key listing
                skill names.

        Returns:
            List of validation error messages (empty means valid).
        """
        errors: list[str] = []
        skill_names: list[str] = plan.get("skills", [])

        for skill_name in skill_names:
            if skill_name not in self._skills:
                errors.append(
                    f"Skill '{skill_name}' referenced in plan is not registered"
                )
                continue

            # Attempt full resolution to catch missing includes and cycles
            defn = self._skills[skill_name]
            resolver = SkillResolver()
            skill_lookup = {n: d.skill for n, d in self._skills.items()}
            try:
                resolved = resolver.resolve_tools(defn.skill, skill_lookup)
            except ValueError as exc:
                errors.append(f"Skill '{skill_name}': {exc}")
                continue

            # Validate tool refs against tool registry (if configured)
            if self._tool_registry is not None:
                for entry in resolved:
                    if not entry.is_inline and not entry.name.startswith(
                        ("mcp:", "agent_skill:")
                    ):
                        tool = self._tool_registry.get(entry.name)
                        if tool is None:
                            errors.append(
                                f"Tool '{entry.name}' in skill "
                                f"'{skill_name}' not found in tool registry"
                            )

        return errors

    # -- Refresh ---------------------------------------------------------------

    def refresh(self, name: str) -> SkillRefreshResult:
        """Recompute the flattened tool set for a single registered skill.

        Resolves tool references via ``SkillResolver`` and, when a Tool
        Registry is configured, validates that every named tool ref actually
        exists in the registry.

        When the Tool Registry is *not* configured, validation is skipped and a
        warning is emitted once with recovery steps; the skill is still counted
        as refreshed so callers are not blocked.

        Args:
            name: The namespaced skill name to refresh.

        Returns:
            ``SkillRefreshResult`` with the skill in ``refreshed``,
            ``failed``, or ``skipped`` depending on the outcome.

        Raises:
            SkillExecutionError: With ``SKILL_NOT_FOUND`` if ``name`` is not
                registered.
        """
        defn = self.get(name)

        skill_lookup = {n: d.skill for n, d in self._skills.items()}
        resolver = SkillResolver()

        try:
            entries = resolver.resolve_tools(defn.skill, skill_lookup)
        except (ValueError, RuntimeError) as exc:
            return SkillRefreshResult(failed={name: str(exc)})

        if self._tool_registry is None:
            logger.warning(_TOOL_REGISTRY_UNAVAILABLE_MSG)
            return SkillRefreshResult(refreshed=[name])

        missing: list[str] = []
        for entry in entries:
            if entry.is_inline or entry.name.startswith(("mcp:", "agent_skill:")):
                continue
            if self._tool_registry.get(entry.name) is None:
                missing.append(entry.name)

        if missing:
            error_msg = f"Tool refs not found in tool registry: {', '.join(missing)}"
            return SkillRefreshResult(failed={name: error_msg})

        return SkillRefreshResult(refreshed=[name])

    def refresh_all(self) -> SkillRefreshResult:
        """Recompute flattened tool sets for all registered skills.

        Iterates over all registered skills and calls ``refresh(name)`` for
        each, aggregating results into a single ``SkillRefreshResult``.

        Returns:
            Aggregated ``SkillRefreshResult`` across all skills.
        """
        result = SkillRefreshResult()
        for name in list(self._skills.keys()):
            result = result.merge(self.refresh(name))
        return result

    # -- Validation ------------------------------------------------------------

    def validate_skill(self, skill: SkillDefinition) -> list[str]:
        """Validate a skill definition's tool references.

        Checks that:
        1. All tool_refs refer to tools that exist in the tool registry
           (if a tool registry is configured).
        2. The skill's input/output schemas are well-formed.
        3. Inline tools have required fields.

        Args:
            skill: The ``SkillDefinition`` to validate.

        Returns:
            List of validation error messages.  Empty list means
            the skill is valid.
        """
        errors: list[str] = []

        # Validate tool_refs against tool registry
        for ref in skill.skill.tool_refs:
            if self._tool_registry is not None:
                tool = self._tool_registry.get(ref)
                if tool is None:
                    errors.append(f"Tool reference '{ref}' not found in tool registry")

        # Validate inline tools have descriptions
        for idx, inline in enumerate(skill.skill.anonymous_tools):
            if not inline.description:
                errors.append(f"Inline tool at index {idx} is missing a description")

        # Validate includes reference known skills
        for include in skill.skill.includes:
            if include.name not in self._skills:
                errors.append(f"Included skill '{include.name}' is not registered")

        return errors
