"""Resource instance operation mixin for the Resource Registry.

Contains ``ResourceInstanceMixin`` which is mixed into
``ResourceRegistryService`` to keep each module under the 500-line
CONTRIBUTING limit.  DAG operations live in ``_resource_registry_dag``.

.. note::

    This is an **internal** module (``_``-prefixed).  External code
    should import from ``resource_registry_service`` instead.
"""

from __future__ import annotations

import json
import logging
from datetime import UTC, datetime
from typing import Any, Protocol

from ulid import ULID

from cleveragents.application.services._resource_registry_data import (
    db_resource_to_domain,
)
from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.domain.models.core.resource import PhysVirt, Resource
from cleveragents.infrastructure.database.models import (
    ResourceModel,
    ResourceTypeModel,
)
from cleveragents.resource.inheritance import find_subtypes

__all__ = [
    "RegistryHost",
    "ResourceInstanceMixin",
]


class RegistryHost(Protocol):
    """Protocol declaring the interface that mixin host classes must provide.

    This is an **internal** protocol used solely by the mixin classes
    in ``_resource_registry_ops`` and ``_resource_registry_dag``.
    """

    def _session(self) -> Any: ...

    def _load_type_registry(self) -> dict[str, dict[str, Any]]: ...

    def show_resource(self, name_or_id: str) -> Resource: ...

    def _build_tree_node(
        self,
        resource: Resource,
        depth: int,
        type_filter: str | None,
        visited: set[str],
    ) -> dict[str, object]: ...


logger = logging.getLogger(__name__)


class ResourceInstanceMixin:
    """Mixin providing resource instance CRUD operations.

    Requires the host class to provide:

    - ``_session() -> Any``
    - ``_load_type_registry() -> dict[str, dict[str, Any]]``
    """

    def register_resource(
        self: RegistryHost,
        type_name: str,
        name: str | None = None,
        *,
        location: str | None = None,
        description: str | None = None,
        read_only: bool = False,
        properties: dict[str, Any] | None = None,
    ) -> Resource:
        """Create and register a resource instance.

        Args:
            type_name: Name of the resource type.
            name: Optional namespaced name for the resource.
            location: Physical location (for physical resources).
            description: Human-readable description.
            read_only: Whether the resource is read-only.
            properties: Type-specific properties dict.

        Returns:
            The created ``Resource`` domain object.

        Raises:
            NotFoundError: If the resource type does not exist.
            ValidationError: If the resource type is not user-addable,
                or if the resource name already exists.
        """
        session = self._session()
        try:
            type_row = (
                session.query(ResourceTypeModel).filter_by(name=type_name).first()
            )
            if type_row is None:
                raise NotFoundError(
                    resource_type="resource_type",
                    resource_id=type_name,
                )

            if not type_row.user_addable:
                raise ValidationError(
                    f"Resource type '{type_name}' is not user-addable. "
                    "Only types with user_addable=true can be instantiated.",
                )

            resource_kind_str: str = str(type_row.resource_kind)
            resource_id = str(ULID())

            # Determine namespace from the resource name
            namespace: str | None = None
            if name is not None:
                parts = name.split("/", 1)
                if len(parts) == 2:
                    namespace = parts[0]

            now_iso = datetime.now(tz=UTC).isoformat()
            properties_json: str | None = None
            if properties:
                properties_json = json.dumps(properties)

            db_resource = ResourceModel(
                resource_id=resource_id,
                namespaced_name=name,
                namespace=namespace,
                type_name=type_name,
                resource_kind=resource_kind_str,
                location=location,
                description=description,
                read_only=read_only,
                auto_discovered=False,
                sandbox_strategy=None,
                content_hash=None,
                properties_json=properties_json,
                metadata_json=None,
                created_at=now_iso,
                updated_at=now_iso,
            )

            session.add(db_resource)
            session.flush()
            session.commit()

            return Resource(
                resource_id=resource_id,
                name=name,
                resource_type_name=type_name,
                classification=PhysVirt(resource_kind_str),
                description=description,
                properties=properties or {},
                location=location,
                content_hash=None,
                sandbox_strategy=None,
                created_at=datetime.fromisoformat(now_iso),
                updated_at=datetime.fromisoformat(now_iso),
            )
        except (NotFoundError, ValidationError):
            session.rollback()
            raise
        except Exception:
            session.rollback()
            raise
        finally:
            session.close()

    def list_resources(
        self: RegistryHost,
        type_name: str | None = None,
        *,
        exact: bool = False,
        include_auto_discovered: bool = True,
    ) -> list[Resource]:
        """List registered resources.

        Args:
            type_name: Optional type name filter.
            exact: When ``False`` (default) and *type_name* is given,
                include resources whose type is a **subtype** of
                *type_name* (ADR-042 polymorphic listing).  When
                ``True``, match only the exact type name.
            include_auto_discovered: When ``False``, exclude resources
                with ``auto_discovered=True`` (default shows all).

        Returns:
            List of ``Resource`` domain objects.
        """
        session = self._session()
        try:
            query = session.query(ResourceModel)
            if type_name is not None:
                if exact:
                    query = query.filter_by(type_name=type_name)
                else:
                    # ADR-042: polymorphic listing — include subtypes
                    registry = self._load_type_registry()
                    subtypes = find_subtypes(type_name, registry)
                    all_types = [type_name, *subtypes]
                    query = query.filter(ResourceModel.type_name.in_(all_types))
            if not include_auto_discovered:
                query = query.filter(ResourceModel.auto_discovered == False)  # noqa: E712
            query = query.order_by(ResourceModel.created_at)
            rows = query.all()
            return [db_resource_to_domain(row) for row in rows]
        finally:
            session.close()

    def show_resource(self: RegistryHost, name_or_id: str) -> Resource:
        """Show details of a registered resource by name or ULID.

        Args:
            name_or_id: Namespaced name or ULID of the resource.

        Returns:
            ``Resource`` domain object.

        Raises:
            NotFoundError: If the resource is not found.
        """
        session = self._session()
        try:
            # Try by namespaced name first, then by ULID
            row = (
                session.query(ResourceModel)
                .filter_by(namespaced_name=name_or_id)
                .first()
            )
            if row is None:
                row = (
                    session.query(ResourceModel)
                    .filter_by(resource_id=name_or_id)
                    .first()
                )
            if row is None:
                raise NotFoundError(
                    resource_type="resource",
                    resource_id=name_or_id,
                )
            return db_resource_to_domain(row)
        finally:
            session.close()
