"""Binding resolution service for tool resource slots.

Resolves each :class:`~cleveragents.domain.models.core.tool.ResourceSlot`
on a tool to a concrete resource (or marks it as *deferred* for parameter
bindings).  Three binding modes are supported:

Contextual (default)
    The slot declares a ``resource_type`` requirement.  The service
    searches the plan's project for linked resources whose type matches
    (or is a compatible sub-type).  Exactly one match is auto-bound;
    multiple matches use the slot ``name`` as a hint against the
    linked-resource alias.  No match raises an error.

Static
    The slot's ``static_resource`` field names a registered resource
    directly.  Resolved by looking it up in the resource registry.

Parameter
    The slot declares ``binding=parameter``.  Resolution is deferred
    to invocation time -- the caller passes the resource reference as a
    tool argument.

Based on:

- ``docs/specification.md`` -- Binding Resolution (lines 7291-7422)
- ``implementation_plan.md`` -- Task C1.tool.binding
- ADR-011: Tool System

See Also:
    - :class:`~cleveragents.domain.models.core.tool.ResourceSlot`
    - :class:`~cleveragents.domain.models.core.resource_slot.BindingResult`
"""

from __future__ import annotations

import logging
from typing import Any

from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.domain.models.core.project import (
    LinkedResource,
    NamespacedProject,
)
from cleveragents.domain.models.core.resource import Resource
from cleveragents.domain.models.core.resource_slot import BindingResult
from cleveragents.domain.models.core.tool import (
    BindingMode,
    ResourceSlot,
    Tool,
)

logger = logging.getLogger(__name__)


class BindingResolutionService:
    """Resolve resource-slot bindings for a tool activation.

    Requires access to a *resource registry* (for resource and type
    look-ups) and the *project* that owns the plan being activated.

    The registry is injected as a protocol-compatible object exposing
    ``show_resource`` and ``is_subtype_of`` methods (matching
    :class:`~cleveragents.application.services.resource_registry_service.ResourceRegistryService`).
    """

    def __init__(self, resource_registry: Any) -> None:
        """Initialise with a resource registry dependency.

        Args:
            resource_registry: Object with ``show_resource(name_or_id)``
                and ``is_subtype_of(type_name, ancestor_name)`` methods.
        """
        self._registry = resource_registry

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

    def resolve(
        self,
        tool: Tool,
        project: NamespacedProject,
        *,
        invocation_params: dict[str, str] | None = None,
    ) -> list[BindingResult]:
        """Resolve all resource slots on *tool* within *project*.

        Args:
            tool: The tool whose slots need resolution.
            project: The project providing linked resources for
                contextual binding.
            invocation_params: Optional mapping of parameter names to
                resource references (used by ``parameter`` bindings).

        Returns:
            A :class:`BindingResult` for each slot.

        Raises:
            ValidationError: When a required slot cannot be resolved.
        """
        results: list[BindingResult] = []
        for slot in tool.resource_slots:
            result = self._resolve_slot(
                slot,
                project,
                invocation_params=invocation_params,
            )
            results.append(result)
        return results

    # ------------------------------------------------------------------
    # Internal resolution dispatch
    # ------------------------------------------------------------------

    def _resolve_slot(
        self,
        slot: ResourceSlot,
        project: NamespacedProject,
        *,
        invocation_params: dict[str, str] | None = None,
    ) -> BindingResult:
        """Dispatch to the appropriate resolver based on binding mode."""
        if slot.binding == BindingMode.STATIC:
            return self._resolve_static(slot)
        if slot.binding == BindingMode.PARAMETER:
            return self._resolve_parameter(
                slot,
                invocation_params=invocation_params,
            )
        return self._resolve_contextual(slot, project)

    # ------------------------------------------------------------------
    # Static binding
    # ------------------------------------------------------------------

    def _resolve_static(self, slot: ResourceSlot) -> BindingResult:
        """Resolve a statically-bound slot.

        The resource is identified by the ``static_resource`` field.
        """
        assert slot.static_resource is not None  # enforced by model
        try:
            resource: Resource = self._registry.show_resource(
                slot.static_resource,
            )
        except NotFoundError as exc:
            raise ValidationError(
                message=(
                    f"Static binding for slot '{slot.name}' "
                    f"references unknown resource "
                    f"'{slot.static_resource}'"
                ),
                details={
                    "slot": slot.name,
                    "resource": slot.static_resource,
                },
            ) from exc

        self._check_type_compatibility(
            slot,
            resource.resource_type_name,
        )

        return BindingResult(
            slot_name=slot.name,
            resource_id=resource.resource_id,
            resource_name=resource.name,
            binding_mode="static",
            deferred=False,
        )

    # ------------------------------------------------------------------
    # Parameter binding
    # ------------------------------------------------------------------

    def _resolve_parameter(
        self,
        slot: ResourceSlot,
        *,
        invocation_params: dict[str, str] | None = None,
    ) -> BindingResult:
        """Resolve a parameter-bound slot.

        If *invocation_params* contains the slot name, the resource is
        eagerly resolved.  Otherwise the result is marked as deferred.
        """
        ref = (invocation_params or {}).get(slot.name)
        if ref is None:
            return BindingResult(
                slot_name=slot.name,
                resource_id=None,
                resource_name=None,
                binding_mode="parameter",
                deferred=True,
            )

        try:
            resource: Resource = self._registry.show_resource(ref)
        except NotFoundError as exc:
            raise ValidationError(
                message=(
                    f"Parameter binding for slot '{slot.name}' "
                    f"references unknown resource '{ref}'"
                ),
                details={"slot": slot.name, "resource": ref},
            ) from exc

        self._check_type_compatibility(
            slot,
            resource.resource_type_name,
        )

        return BindingResult(
            slot_name=slot.name,
            resource_id=resource.resource_id,
            resource_name=resource.name,
            binding_mode="parameter",
            deferred=False,
        )

    # ------------------------------------------------------------------
    # Contextual binding
    # ------------------------------------------------------------------

    def _resolve_contextual(
        self,
        slot: ResourceSlot,
        project: NamespacedProject,
    ) -> BindingResult:
        """Resolve a contextual slot from project linked resources.

        Resolution rules:
        1. Collect all linked resources whose type matches the slot's
           ``resource_type`` (including sub-types).
        2. If exactly one match: auto-bind.
        3. If multiple matches: use ``slot.name`` as alias hint.
        4. If no match and ``slot.required``: raise an error.
        """
        candidates = self._find_matching_resources(
            slot,
            project.linked_resources,
        )

        if len(candidates) == 1:
            resource, _link = candidates[0]
            return BindingResult(
                slot_name=slot.name,
                resource_id=resource.resource_id,
                resource_name=resource.name,
                binding_mode="contextual",
                deferred=False,
            )

        if len(candidates) > 1:
            return self._disambiguate(slot, candidates)

        # No matches
        if slot.required:
            raise ValidationError(
                message=(
                    f"No resource of type '{slot.resource_type}' "
                    f"found in project "
                    f"'{project.namespaced_name}' for "
                    f"required slot '{slot.name}'"
                ),
                details={
                    "slot": slot.name,
                    "resource_type": slot.resource_type,
                    "project": project.namespaced_name,
                },
            )

        return BindingResult(
            slot_name=slot.name,
            resource_id=None,
            resource_name=None,
            binding_mode="contextual",
            deferred=False,
        )

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _find_matching_resources(
        self,
        slot: ResourceSlot,
        linked_resources: list[LinkedResource],
    ) -> list[tuple[Resource, LinkedResource]]:
        """Return linked resources whose type is compatible."""
        matches: list[tuple[Resource, LinkedResource]] = []
        for link in linked_resources:
            try:
                resource = self._registry.show_resource(
                    link.resource_id,
                )
            except NotFoundError:
                continue

            if self._is_type_compatible(
                slot.resource_type,
                resource.resource_type_name,
            ):
                matches.append((resource, link))
        return matches

    def _disambiguate(
        self,
        slot: ResourceSlot,
        candidates: list[tuple[Resource, LinkedResource]],
    ) -> BindingResult:
        """Pick the best candidate using the slot name as alias hint."""
        for resource, link in candidates:
            if link.alias == slot.name:
                return BindingResult(
                    slot_name=slot.name,
                    resource_id=resource.resource_id,
                    resource_name=resource.name,
                    binding_mode="contextual",
                    deferred=False,
                )

        # Also try matching slot name against resource name suffix
        for resource, _link in candidates:
            if resource.name is not None:
                short = resource.name.rsplit("/", 1)[-1]
                if short == slot.name:
                    return BindingResult(
                        slot_name=slot.name,
                        resource_id=resource.resource_id,
                        resource_name=resource.name,
                        binding_mode="contextual",
                        deferred=False,
                    )

        names = [r.name or r.resource_id for r, _ in candidates]
        raise ValidationError(
            message=(
                f"Ambiguous contextual binding for slot "
                f"'{slot.name}': multiple resources of type "
                f"'{slot.resource_type}' found and no alias "
                f"hint matched. Candidates: {names}"
            ),
            details={
                "slot": slot.name,
                "candidates": names,
            },
        )

    def _is_type_compatible(
        self,
        required_type: str,
        actual_type: str,
    ) -> bool:
        """Check if *actual_type* satisfies *required_type*.

        Exact match always passes.  Uses the full inheritance chain via
        is_subtype_of() on the registry so that multi-level
        inheritance (e.g. devcontainer-instance → container-instance)
        is correctly resolved (ADR-042 §Tool Binding Polymorphism).
        """
        return self._registry.is_subtype_of(actual_type, required_type)

    def _check_type_compatibility(
        self,
        slot: ResourceSlot,
        actual_type: str,
    ) -> None:
        """Raise ``ValidationError`` if types are incompatible."""
        if not self._is_type_compatible(
            slot.resource_type,
            actual_type,
        ):
            raise ValidationError(
                message=(
                    f"Type mismatch for slot '{slot.name}': "
                    f"expected '{slot.resource_type}', "
                    f"got '{actual_type}'"
                ),
                details={
                    "slot": slot.name,
                    "expected_type": slot.resource_type,
                    "actual_type": actual_type,
                },
            )
