"""Service layer for managing tool and validation registrations.

Orchestrates persistence through ``ToolRegistryRepository`` and
``ValidationAttachmentRepository``, providing a clean API for CLI and
higher-level orchestrators.
"""

from __future__ import annotations

from typing import Any

from cleveragents.core.exceptions import NotFoundError, ToolTypeMismatchError
from cleveragents.domain.models.core.tool import Tool, ToolType
from cleveragents.infrastructure.database.repositories import (
    ToolRegistryRepository,
    ValidationAttachmentRepository,
)


class ToolRegistryService:
    """Coordinate tool/validation registration and attachment lifecycle.

    Delegates all persistence to the repository layer.  Designed to be
    injected with a ``ToolRegistryRepository`` and a
    ``ValidationAttachmentRepository`` (both using session-factory pattern).
    """

    def __init__(
        self,
        tool_repo: ToolRegistryRepository,
        attachment_repo: ValidationAttachmentRepository,
    ) -> None:
        self._tool_repo = tool_repo
        self._attachment_repo = attachment_repo

    # -- Tool CRUD ---------------------------------------------------------

    def register_tool(self, tool: Any) -> Any:
        """Register a new tool or validation.

        Args:
            tool: A tool dict or domain object.

        Returns:
            The registered tool.

        Raises:
            DuplicateToolError: If the name already exists.
            DatabaseError: On persistence failure.
        """
        create_fn = getattr(self._tool_repo, "create", None)
        if callable(create_fn):
            return create_fn(tool)
        add_fn = getattr(self._tool_repo, "add", None)
        if callable(add_fn):
            return add_fn(tool)
        return self._tool_repo.create(tool)

    def update_tool(
        self, tool: Any, tool_config: dict[str, Any] | Tool | None = None
    ) -> Any:
        """Update an existing tool definition.

        Args:
            tool: A tool dict or domain object with updated fields.
            tool_config: Optional tool configuration for name-based update.

        Returns:
            The updated tool.

        Raises:
            DatabaseError: If the tool is not found or persistence fails.
        """
        if tool_config is None:
            return self._tool_repo.update(tool)

        if isinstance(tool_config, Tool):
            updated = tool_config
        else:
            updated = {"name": tool, **tool_config} if isinstance(tool, str) else tool

        return self._tool_repo.update(updated)

    def remove_tool(self, name: str) -> bool:
        """Remove a tool from the registry.

        Args:
            name: The namespaced name of the tool.

        Returns:
            ``True`` if the tool was removed, ``False`` if not found.

        Raises:
            ToolInUseError: If validation attachments still reference
                this tool.
        """
        delete_fn = getattr(self._tool_repo, "delete", None)
        if callable(delete_fn):
            return bool(delete_fn(name))

        remove_fn = getattr(self._tool_repo, "remove", None)
        if callable(remove_fn):
            return bool(remove_fn(name))

        return self._tool_repo.delete(name)

    def list_tools(
        self,
        namespace: str | None = None,
        tool_type: str | None = None,
        source: str | None = None,
    ) -> list[Any]:
        """List tools with optional filters.

        Args:
            namespace: Optional namespace filter.
            tool_type: Optional ``tool`` or ``validation`` filter.
            source: Optional source filter.

        Returns:
            List of tool dicts.
        """
        return self._tool_repo.list_all(
            namespace=namespace,
            tool_type=tool_type,
            source=source,
        )

    def get_tool(self, name: str) -> Any | None:
        """Retrieve a single tool by name.

        Args:
            name: The namespaced name.

        Returns:
            The tool dict, or ``None`` if not found.
        """
        return self._tool_repo.get_by_name(name)

    # -- Validation attachment lifecycle -----------------------------------

    def attach_validation(
        self,
        validation_name: str,
        resource_id: str,
        project_name: str | None = None,
        plan_id: str | None = None,
        args: dict[str, Any] | None = None,
    ) -> Any:
        """Attach a validation to a resource.

        The validation mode (``required`` or ``informational``) is read from
        the validation's registered definition — it is not an attach-time
        override.

        Args:
            validation_name: Name of the validation tool to attach.
            resource_id: Resource reference string.
            project_name: Optional project scope.
            plan_id: Optional plan scope.
            args: Optional invocation arguments.

        Returns:
            The attachment dict with generated ``attachment_id``.

        Raises:
            NotFoundError: If the validation tool does not exist.
            DatabaseError: On persistence failure.
        """
        # Verify validation exists
        existing = self._tool_repo.get_by_name(validation_name)
        if existing is None:
            raise NotFoundError(
                resource_type="validation",
                resource_id=validation_name,
            )

        # Enforce type-discriminator: only Validation entries may be attached.
        # A plain Tool has tool_type == "tool"; reject it immediately.
        if isinstance(existing, dict):
            actual_type: str = str(existing.get("tool_type", "tool"))
        else:
            raw_type = getattr(existing, "tool_type", ToolType.TOOL)
            actual_type = (
                raw_type.value if isinstance(raw_type, ToolType) else str(raw_type)
            )

        if actual_type != ToolType.VALIDATION.value:
            raise ToolTypeMismatchError(
                tool_name=validation_name,
                actual_type=actual_type,
            )

        # Read mode from the validation's registered definition
        if isinstance(existing, dict):
            mode = existing.get("mode", "required")
        else:
            mode = getattr(existing, "mode", "required")
        mode_str: str = str(mode) if mode is not None else "required"

        return self._attachment_repo.attach(
            validation_name=validation_name,
            resource_id=resource_id,
            mode=mode_str,
            project_name=project_name,
            plan_id=plan_id,
            args=args,
        )

    def detach_validation(self, attachment_id: str) -> bool:
        """Detach a validation attachment.

        Args:
            attachment_id: The ULID of the attachment.

        Returns:
            ``True`` if the attachment was removed.
        """
        return self._attachment_repo.detach(attachment_id)

    def list_validations_for_resource(
        self,
        resource_id: str,
        project_name: str | None = None,
        plan_id: str | None = None,
    ) -> list[Any]:
        """List validation attachments for a resource.

        Args:
            resource_id: The resource reference.
            project_name: Optional project scope filter.
            plan_id: Optional plan scope filter.

        Returns:
            List of attachment dicts.
        """
        return self._attachment_repo.list_for_resource(
            resource_id=resource_id,
            project_name=project_name,
            plan_id=plan_id,
        )
