"""Resource DAG (parent/child link) operations mixin.

Extracted from ``_resource_registry_ops.py`` to keep individual modules
under the 500-line CONTRIBUTING limit.

.. 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 cleveragents.application.services._resource_registry_data import (
    db_resource_to_domain,
    get_ancestors,
)
from cleveragents.application.services._resource_registry_ops import RegistryHost
from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.domain.models.core.resource import Resource
from cleveragents.infrastructure.database.models import (
    ResourceLinkModel,
    ResourceModel,
    ResourceTypeModel,
)
from cleveragents.resource.inheritance import is_subtype_of

__all__ = [
    "ResourceDagMixin",
]

logger = logging.getLogger(__name__)


class ResourceDagMixin:
    """Mixin providing DAG (parent/child link) operations.

    Requires the host class to satisfy :class:`RegistryHost`.
    """

    def link_child(
        self: RegistryHost, parent_name_or_id: str, child_name_or_id: str
    ) -> None:
        """Link a child resource to a parent in the DAG.

        Args:
            parent_name_or_id: Parent resource name or ULID.
            child_name_or_id: Child resource name or ULID.

        Raises:
            NotFoundError: If either resource is not found.
            ValidationError: If linking fails (cycle, type mismatch, duplicate).
        """
        parent = self.show_resource(parent_name_or_id)
        child = self.show_resource(child_name_or_id)

        session = self._session()
        try:
            # Check type compatibility
            parent_type_row = (
                session.query(ResourceTypeModel)
                .filter_by(name=parent.resource_type_name)
                .first()
            )
            if parent_type_row is not None:
                allowed_raw = getattr(parent_type_row, "allowed_child_types_json", None)
                allowed_children: list[str] = (
                    json.loads(str(allowed_raw)) if allowed_raw else []
                )
                if allowed_children:
                    registry = self._load_type_registry()
                    child_type = child.resource_type_name
                    match = any(
                        is_subtype_of(child_type, allowed, registry)
                        for allowed in allowed_children
                    )
                    if not match:
                        raise ValidationError(
                            message=(
                                f"Type '{child_type}' is not allowed "
                                f"as a child of '{parent.resource_type_name}'"
                            ),
                            details={
                                "parent_type": parent.resource_type_name,
                                "child_type": child_type,
                            },
                        )

            if parent.resource_id == child.resource_id:
                raise ValidationError(
                    message="Cannot link a resource to itself",
                    details={
                        "parent_id": parent.resource_id,
                        "child_id": child.resource_id,
                    },
                )

            # Check for duplicate link
            existing = (
                session.query(ResourceLinkModel)
                .filter_by(
                    parent_id=parent.resource_id,
                    child_id=child.resource_id,
                )
                .first()
            )
            if existing is not None:
                raise ValidationError(
                    message=(
                        f"Link already exists: "
                        f"{parent.resource_id} -> {child.resource_id}"
                    ),
                    details={
                        "parent_id": parent.resource_id,
                        "child_id": child.resource_id,
                    },
                )

            # Cycle detection: walk ancestors of parent
            ancestors = get_ancestors(session, parent.resource_id)
            if child.resource_id in ancestors:
                raise ValidationError(
                    message=(
                        f"Cycle detected: linking {child.resource_id} "
                        f"as child of {parent.resource_id} would "
                        f"create a cycle"
                    ),
                    details={
                        "parent_id": parent.resource_id,
                        "child_id": child.resource_id,
                    },
                )

            link = ResourceLinkModel(
                parent_id=parent.resource_id,
                child_id=child.resource_id,
                created_at=datetime.now(tz=UTC).isoformat(),
            )
            session.add(link)
            session.flush()
            session.commit()
        except (NotFoundError, ValidationError):
            session.rollback()
            raise
        except Exception:
            session.rollback()
            raise
        finally:
            session.close()

    def unlink_child(
        self: RegistryHost, parent_name_or_id: str, child_name_or_id: str
    ) -> None:
        """Remove a parent-child link from the DAG.

        Args:
            parent_name_or_id: Parent resource name or ULID.
            child_name_or_id: Child resource name or ULID.

        Raises:
            NotFoundError: If either resource or the link is not found.
        """
        parent = self.show_resource(parent_name_or_id)
        child = self.show_resource(child_name_or_id)

        session = self._session()
        try:
            link = (
                session.query(ResourceLinkModel)
                .filter_by(
                    parent_id=parent.resource_id,
                    child_id=child.resource_id,
                )
                .first()
            )
            if link is None:
                raise NotFoundError(
                    resource_type="resource_link",
                    resource_id=(f"{parent.resource_id} -> {child.resource_id}"),
                )
            session.delete(link)
            session.flush()
            session.commit()
        except NotFoundError:
            session.rollback()
            raise
        except Exception:
            session.rollback()
            raise
        finally:
            session.close()

    def get_children(self: RegistryHost, name_or_id: str) -> list[Resource]:
        """Get direct children of a resource.

        Args:
            name_or_id: Resource name or ULID.

        Returns:
            List of child ``Resource`` domain objects, sorted by name.
        """
        resource = self.show_resource(name_or_id)
        session = self._session()
        try:
            links = (
                session.query(ResourceLinkModel)
                .filter_by(parent_id=resource.resource_id)
                .all()
            )
            children: list[Resource] = []
            for link in links:
                child_row = (
                    session.query(ResourceModel)
                    .filter_by(resource_id=str(link.child_id))
                    .first()
                )
                if child_row is not None:
                    children.append(db_resource_to_domain(child_row))
            # Deterministic ordering: sort by name, then resource_id
            children.sort(key=lambda r: (r.name or "", r.resource_id))
            return children
        finally:
            session.close()

    def get_parents(self: RegistryHost, name_or_id: str) -> list[Resource]:
        """Get direct parents of a resource.

        Args:
            name_or_id: Resource name or ULID.

        Returns:
            List of parent ``Resource`` domain objects, sorted by name.

        Raises:
            NotFoundError: If the resource does not exist.
        """
        resource = self.show_resource(name_or_id)
        session = self._session()
        try:
            links = (
                session.query(ResourceLinkModel)
                .filter_by(child_id=resource.resource_id)
                .all()
            )
            parents: list[Resource] = []
            for link in links:
                parent_row = (
                    session.query(ResourceModel)
                    .filter_by(resource_id=str(link.parent_id))
                    .first()
                )
                if parent_row is not None:
                    parents.append(db_resource_to_domain(parent_row))
            # Deterministic ordering: sort by name, then resource_id
            parents.sort(key=lambda r: (r.name or "", r.resource_id))
            return parents
        finally:
            session.close()

    def get_resource_tree(
        self: RegistryHost,
        name_or_id: str,
        depth: int = -1,
        type_filter: str | None = None,
    ) -> list[dict[str, object]]:
        """Get resource tree as nested dicts.

        Each node is ``{"resource": Resource, "children": [...]}``.

        Args:
            name_or_id: Root resource name or ULID.
            depth: Maximum depth (-1 for unlimited).
            type_filter: Only include resources of this type.

        Returns:
            A list with a single root node dict.
        """
        root = self.show_resource(name_or_id)
        return [self._build_tree_node(root, depth, type_filter, set())]

    def _build_tree_node(
        self: RegistryHost,
        resource: Resource,
        depth: int,
        type_filter: str | None,
        visited: set[str],
    ) -> dict[str, object]:
        """Recursively build a tree node."""
        visited = visited | {resource.resource_id}
        children_nodes: list[dict[str, object]] = []

        if depth != 0:
            next_depth = depth - 1 if depth > 0 else -1
            session = self._session()
            try:
                links = (
                    session.query(ResourceLinkModel)
                    .filter_by(parent_id=resource.resource_id)
                    .all()
                )
                child_resources: list[Resource] = []
                for link in links:
                    child_row = (
                        session.query(ResourceModel)
                        .filter_by(resource_id=str(link.child_id))
                        .first()
                    )
                    if child_row is not None:
                        child_res = db_resource_to_domain(child_row)
                        if type_filter and child_res.resource_type_name != type_filter:
                            continue
                        if child_res.resource_id not in visited:
                            child_resources.append(child_res)
            finally:
                session.close()

            # Deterministic ordering
            child_resources.sort(key=lambda r: (r.name or "", r.resource_id))
            for child in child_resources:
                children_nodes.append(
                    self._build_tree_node(child, next_depth, type_filter, visited)
                )

        return {"resource": resource, "children": children_nodes}
