"""Skill management service for CleverAgents.

Provides skill registration, retrieval, listing, and removal with
optional database persistence via ``SkillRepository``.

Follows the dual-mode storage pattern from ``PlanLifecycleService``:
an in-memory dict is always maintained; when a ``SkillRepository`` is
provided, persistence calls are issued alongside the in-memory
operations **without** changing the public API.

Based on implementation_plan.md task C0.skill.cli and specification.md
"Skill Registration and Management" section.
"""

from __future__ import annotations

import logging
from collections import OrderedDict
from collections.abc import Callable
from datetime import datetime
from typing import Any

from cleveragents.domain.models.core.skill import (
    Skill,
    SkillCapabilitySummary,
    SkillResolver,
)
from cleveragents.skills.schema import SkillConfigSchema

logger = logging.getLogger(__name__)


class SkillService:
    """Skill registration service with optional DB persistence.

    Manages the lifecycle of skills: add, update, remove, list, show, and
    resolve tools.  When a ``SkillRepository`` and ``session_factory`` are
    provided at construction, every mutation is persisted to the database
    and the in-memory cache is pre-loaded from existing rows.

    Example::

        svc = SkillService()
        skill, is_new = svc.add_skill(config, update=False)
        skills = svc.list_skills(namespace="local")
        svc.remove_skill("local/my-skill")
    """

    def __init__(
        self,
        skill_repo: Any | None = None,
        session_factory: Callable[..., Any] | None = None,
    ) -> None:
        # In-memory storage (primary data store for reads)
        self._skills: OrderedDict[str, Skill] = OrderedDict()
        self._config_paths: dict[str, str] = {}
        self._created_at: dict[str, datetime] = {}
        self._updated_at: dict[str, datetime] = {}
        self._resolver = SkillResolver()

        # Optional DB persistence layer
        self._skill_repo = skill_repo
        self._session_factory = session_factory

        # Pre-load existing skills from DB when persistence is available
        if self._skill_repo is not None:
            self._load_from_db()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def add_skill(
        self,
        config: SkillConfigSchema,
        config_path: str | None = None,
        *,
        update: bool = False,
    ) -> tuple[Skill, bool]:
        """Register a skill from a validated schema config.

        Args:
            config: Validated ``SkillConfigSchema`` instance.
            config_path: Path to the YAML file (for display).
            update: If ``True``, overwrite existing skill with the same name.

        Returns:
            Tuple of (Skill, is_new).  ``is_new`` is ``True`` when the skill
            was freshly created, ``False`` when it was updated.

        Raises:
            ValueError: If the skill already exists and ``update`` is ``False``.
        """
        if config is None:
            raise ValueError("config cannot be None")

        skill_dict = self._schema_to_skill_dict(config)
        name = skill_dict["name"]

        if name in self._skills and not update:
            raise ValueError(
                f"Skill '{name}' is already registered. "
                f"To overwrite, re-run with --update"
            )

        is_new = name not in self._skills
        now = datetime.now()

        skill = Skill.from_config(skill_dict)
        self._skills[name] = skill
        if config_path:
            self._config_paths[name] = config_path
        if is_new:
            self._created_at[name] = now
        self._updated_at[name] = now

        # Persist to database when repository is available
        self._persist_skill(skill, is_new=is_new)

        return skill, is_new

    def remove_skill(self, name: str) -> Skill:
        """Remove a registered skill.

        Args:
            name: Namespaced skill name to remove.

        Returns:
            The removed ``Skill`` object.

        Raises:
            KeyError: If the skill is not found.
        """
        if not name:
            raise ValueError("Skill name cannot be empty")
        if name not in self._skills:
            raise KeyError(f"Skill '{name}' is not registered")

        skill = self._skills.pop(name)
        self._config_paths.pop(name, None)
        self._created_at.pop(name, None)
        self._updated_at.pop(name, None)

        # Remove from database when repository is available
        self._delete_skill_from_db(name)

        return skill

    def get_skill(self, name: str) -> Skill:
        """Get a registered skill by name.

        Args:
            name: Namespaced skill name.

        Returns:
            The ``Skill`` object.

        Raises:
            KeyError: If the skill is not found.
        """
        if not name:
            raise ValueError("Skill name cannot be empty")
        if name not in self._skills:
            raise KeyError(f"Skill '{name}' is not registered")
        return self._skills[name]

    def list_skills(
        self,
        namespace: str | None = None,
        source: str | None = None,
    ) -> list[Skill]:
        """List all registered skills, optionally filtered.

        Args:
            namespace: Filter by namespace prefix.
            source: Filter by tool source type (mcp, agent_skill, builtin,
                custom).

        Returns:
            List of matching ``Skill`` objects.
        """
        skills = list(self._skills.values())

        if namespace:
            skills = [s for s in skills if s.namespace == namespace]

        if source:
            skills = [s for s in skills if self._skill_has_source(s, source)]

        return skills

    def resolve_tools(self, name: str) -> tuple[Skill, list[Any]]:
        """Resolve the flattened tool set for a skill.

        Args:
            name: Namespaced skill name.

        Returns:
            Tuple of (Skill, resolved_tool_entries).

        Raises:
            KeyError: If the skill is not found.
        """
        skill = self.get_skill(name)
        entries = self._resolver.resolve_tools(skill, dict(self._skills))
        return skill, entries

    def compute_capability_summary(
        self,
        name: str,
    ) -> SkillCapabilitySummary:
        """Compute the capability summary for a skill.

        Args:
            name: Namespaced skill name.

        Returns:
            ``SkillCapabilitySummary`` with aggregated stats.
        """
        skill, entries = self.resolve_tools(name)
        return self._resolver.compute_capability_summary(skill, entries)

    def get_config_path(self, name: str) -> str | None:
        """Return the config file path for a skill, if known."""
        return self._config_paths.get(name)

    def get_created_at(self, name: str) -> datetime | None:
        """Return the creation timestamp for a skill."""
        return self._created_at.get(name)

    def get_updated_at(self, name: str) -> datetime | None:
        """Return the last-updated timestamp for a skill."""
        return self._updated_at.get(name)

    def get_source_types(self, skill: Skill) -> list[str]:
        """Return the distinct source types for a skill's tools.

        Source types: builtin, mcp, agent_skill, custom.
        """
        sources: list[str] = []
        if skill.tool_refs:
            sources.append("builtin")
        if skill.mcp_servers:
            sources.append("mcp")
        if skill.agent_skills:
            sources.append("agent_skill")
        if skill.anonymous_tools:
            sources.append("custom")
        return sources

    def get_dependents(self, name: str) -> dict[str, list[str]]:
        """Find skills and actors that reference a given skill.

        Returns:
            Dict with keys 'skills' and 'actors' listing dependents.
        """
        dependent_skills: list[str] = []
        for sname, skill in self._skills.items():
            if sname == name:
                continue
            for inc in skill.includes:
                if inc.name == name:
                    dependent_skills.append(sname)
                    break
        return {"skills": dependent_skills, "actors": []}

    def skill_count(self) -> int:
        """Return the number of registered skills."""
        return len(self._skills)

    # ------------------------------------------------------------------
    # Database persistence helpers
    # ------------------------------------------------------------------

    def _load_from_db(self) -> None:
        """Pre-load all skills from the database into the in-memory cache."""
        if self._skill_repo is None:
            return
        try:
            db_skills: list[Skill] = self._skill_repo.list_all()
            now = datetime.now()
            for skill in db_skills:
                self._skills[skill.name] = skill
                self._created_at[skill.name] = now
                self._updated_at[skill.name] = now
            logger.debug("Loaded %d skills from database", len(db_skills))
        except Exception:
            logger.warning("Failed to load skills from database", exc_info=True)

    def _persist_skill(self, skill: Skill, *, is_new: bool) -> None:
        """Persist a skill to the database (create or update)."""
        if self._skill_repo is None:
            return
        try:
            if is_new:
                self._skill_repo.create(skill)
            else:
                self._skill_repo.update(skill)
            self._commit()
        except Exception:
            logger.warning(
                "Failed to persist skill %s to database",
                skill.name,
                exc_info=True,
            )

    def _delete_skill_from_db(self, name: str) -> None:
        """Delete a skill from the database."""
        if self._skill_repo is None:
            return
        try:
            self._skill_repo.delete(name)
            self._commit()
        except Exception:
            logger.warning(
                "Failed to delete skill %s from database",
                name,
                exc_info=True,
            )

    def _commit(self) -> None:
        """Commit the current database transaction."""
        if self._session_factory is None:
            return
        try:
            session = self._session_factory()
            session.commit()
        except Exception:
            logger.warning("Failed to commit session", exc_info=True)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _schema_to_skill_dict(config: SkillConfigSchema) -> dict[str, Any]:
        """Convert a ``SkillConfigSchema`` to a dict for ``Skill.from_config``."""
        data: dict[str, Any] = {"name": config.name}
        if config.description:
            data["description"] = config.description
        else:
            data["description"] = ""

        # Tools -> tool_refs for domain model
        if config.tools:
            data["tools"] = [t.name for t in config.tools]

        # Includes
        if config.includes:
            data["includes"] = [{"name": i.name} for i in config.includes]

        # Inline tools -> anonymous_tools
        if config.inline_tools:
            data["anonymous_tools"] = []
            for it in config.inline_tools:
                tool_dict: dict[str, Any] = {
                    "description": it.description or it.name,
                    "source": it.source,
                    "code": it.code,
                }
                data["anonymous_tools"].append(tool_dict)

        # MCP servers
        if config.mcp_servers:
            data["mcp_servers"] = []
            for mcp in config.mcp_servers:
                mcp_dict: dict[str, Any] = {"server": mcp.name}
                if mcp.tool_filter and mcp.tool_filter.include:
                    mcp_dict["tools"] = list(mcp.tool_filter.include)
                data["mcp_servers"].append(mcp_dict)

        # Agent skill folders
        if config.agent_skill_folders:
            data["agent_skills"] = [
                {"path": f.path} for f in config.agent_skill_folders
            ]

        return data

    @staticmethod
    def _skill_has_source(skill: Skill, source: str) -> bool:
        """Check if a skill has tools from the given source type."""
        source_lower = source.lower()
        if source_lower == "builtin" and skill.tool_refs:
            return True
        if source_lower == "mcp" and skill.mcp_servers:
            return True
        if source_lower == "agent_skill" and skill.agent_skills:
            return True
        return bool(source_lower == "custom" and skill.anonymous_tools)
