"""Resource handler orchestration service.

The :class:`ResourceHandlerService` is the **orchestration bridge** that
connects the existing infrastructure pieces into a complete resource
resolution pipeline:

1. :class:`BindingResolutionService` resolves tool slots to
   :class:`BindingResult` (resource_id).
2. :class:`ResourceRegistryService` looks up the :class:`Resource` to
   get ``location`` and ``resource_type_name``.
3. :class:`ResourceRegistryService` looks up the :class:`ResourceTypeSpec`
   to get ``sandbox_strategy`` and ``handler`` reference.
4. :func:`resolve_handler` dynamically loads the handler class.
5. The handler calls :meth:`SandboxManager.get_or_create_sandbox` and
   returns a :class:`BoundResource` with ``sandbox_path`` populated.

The service also provides a convenience method that resolves ALL tool
bindings in one call, producing a ``dict[str, BoundResource]`` ready
for :class:`PlanExecutionContext`.

Based on:
    - implementation_plan.md group M1.resource-handlers (L2254-L2271)
"""

from __future__ import annotations

import logging
from collections.abc import Callable
from typing import TYPE_CHECKING, cast

from cleveragents.core.exceptions import NotFoundError
from cleveragents.domain.models.core.resource import Resource
from cleveragents.domain.models.core.resource_slot import BindingResult
from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.types import EventType
from cleveragents.infrastructure.sandbox.factory import SandboxStrategyStr
from cleveragents.infrastructure.sandbox.manager import SandboxManager
from cleveragents.resource.handlers.protocol import (
    AccessResult,
    CheckpointResult,
    Content,
    DeleteResult,
    DiffResult,
    ResourceHandler,
    RollbackResult,
    SandboxResult,
    WriteResult,
)
from cleveragents.resource.handlers.resolver import (
    HandlerResolutionError,
    resolve_handler,
)
from cleveragents.tool.context import BoundResource

if TYPE_CHECKING:
    from cleveragents.infrastructure.events.protocol import EventBus

logger = logging.getLogger(__name__)


class ResourceHandlerService:
    """Orchestrates resource resolution from binding results to bound resources.

    Bridges the gap between :class:`BindingResolutionService` output
    (``BindingResult`` with ``resource_id``) and the execution context's
    ``BoundResource`` (with ``sandbox_path``).

    Parameters
    ----------
    sandbox_manager:
        The sandbox lifecycle manager for creating/reusing sandboxes.
    resource_lookup:
        Callable that takes a resource name-or-id and returns a
        :class:`Resource`.  Typically
        ``ResourceRegistryService.show_resource``.
    type_lookup:
        Callable that takes a type name and returns a
        :class:`ResourceTypeSpec`.  Typically
        ``ResourceRegistryService.show_type``.
    """

    def __init__(
        self,
        *,
        sandbox_manager: SandboxManager,
        resource_lookup: Callable[[str], Resource],
        type_lookup: Callable[[str], ResourceTypeSpec],
        event_bus: EventBus | None = None,
    ) -> None:
        self._sandbox_manager = sandbox_manager
        self._resource_lookup = resource_lookup
        self._type_lookup = type_lookup
        self._logger = logger
        self._event_bus = event_bus

    def resolve_binding(
        self,
        binding: BindingResult,
        plan_id: str,
        access: str = "read_only",
    ) -> BoundResource:
        """Resolve a single binding result into a BoundResource.

        Steps:
            1. Look up the Resource by ``binding.resource_id``.
            2. Look up the ResourceTypeSpec by ``resource.resource_type_name``.
            3. Resolve or fallback the handler.
            4. Call ``handler.resolve()`` to provision a sandbox and
               produce a BoundResource.

        Args:
            binding: A resolved binding with a ``resource_id``.
            plan_id: The plan requesting the resolution.
            access: Access mode for the bound resource.

        Returns:
            A :class:`BoundResource` with ``sandbox_path`` populated.

        Raises:
            ValueError: If the binding has no resource_id (deferred).
            NotFoundError: If the resource or type is not found.
            HandlerResolutionError: If the handler cannot be loaded.
        """
        if binding.deferred or not binding.resource_id:
            raise ValueError(
                f"Cannot resolve deferred binding for slot '{binding.slot_name}'"
            )

        # Step 1: Look up resource
        resource: Resource = self._resource_lookup(binding.resource_id)

        # Step 2: Look up type spec
        type_spec: ResourceTypeSpec = self._type_lookup(resource.resource_type_name)

        # Step 3: Resolve handler
        handler = self._resolve_handler_for_type(type_spec, resource)

        # Step 4: Delegate to handler
        bound = handler.resolve(
            resource=resource,
            plan_id=plan_id,
            slot_name=binding.slot_name,
            sandbox_manager=self._sandbox_manager,
            access=access,
        )
        # Emit RESOURCE_ACCESSED event
        if self._event_bus is not None:
            try:
                self._event_bus.emit(
                    DomainEvent(
                        event_type=EventType.RESOURCE_ACCESSED,
                        plan_id=plan_id,
                        details={
                            "resource_id": binding.resource_id,
                            "slot_name": binding.slot_name,
                            "access": access,
                        },
                    )
                )
            except Exception:
                self._logger.warning(
                    "event_bus_emit_failed: RESOURCE_ACCESSED for %s",
                    binding.resource_id,
                )
        return bound

    def resolve_bindings(
        self,
        bindings: list[BindingResult],
        plan_id: str,
        access: str = "read_only",
    ) -> dict[str, BoundResource]:
        """Resolve all non-deferred bindings into BoundResources.

        Deferred bindings (parameter mode) are silently skipped.

        Args:
            bindings: List of binding results from
                :class:`BindingResolutionService`.
            plan_id: The plan requesting the resolution.
            access: Default access mode for all bindings.

        Returns:
            A dict mapping slot names to :class:`BoundResource` objects.
        """
        result: dict[str, BoundResource] = {}
        for binding in bindings:
            if binding.deferred or not binding.resource_id:
                self._logger.debug(
                    "Skipping deferred binding for slot '%s'",
                    binding.slot_name,
                )
                continue

            try:
                bound = self.resolve_binding(
                    binding=binding,
                    plan_id=plan_id,
                    access=access,
                )
                result[binding.slot_name] = bound
                self._logger.info(
                    "Resolved binding slot='%s' -> resource='%s' sandbox='%s'",
                    binding.slot_name,
                    binding.resource_id,
                    bound.sandbox_path,
                )
            except (NotFoundError, HandlerResolutionError, ValueError) as exc:
                self._logger.warning(
                    "Failed to resolve binding for slot '%s': %s",
                    binding.slot_name,
                    exc,
                )
                raise

        return result

    def resolve_resource(
        self,
        resource: Resource,
        plan_id: str,
        slot_name: str,
        access: str = "read_only",
    ) -> BoundResource:
        """Resolve a resource directly (without going through BindingResult).

        Convenience method when you already have the Resource object.

        Args:
            resource: The resource to resolve.
            plan_id: The plan requesting the resolution.
            slot_name: Name for the binding slot.
            access: Access mode.

        Returns:
            A :class:`BoundResource` with ``sandbox_path`` populated.
        """
        type_spec = self._type_lookup(resource.resource_type_name)
        handler = self._resolve_handler_for_type(type_spec, resource)
        return handler.resolve(
            resource=resource,
            plan_id=plan_id,
            slot_name=slot_name,
            sandbox_manager=self._sandbox_manager,
            access=access,
        )

    def _resolve_handler_for_type(
        self,
        type_spec: ResourceTypeSpec,
        resource: Resource,
    ) -> ResourceHandler:
        """Resolve the handler for a resource type spec.

        If the type spec has a handler reference string, resolves it
        dynamically.  Otherwise falls back to a default handler based
        on the sandbox strategy.

        Args:
            type_spec: The resource type specification.
            resource: The resource being resolved.

        Returns:
            A handler instance satisfying :class:`ResourceHandler`.
        """
        if type_spec.handler:
            try:
                return resolve_handler(type_spec.handler)
            except HandlerResolutionError as exc:
                self._logger.debug(
                    "Handler resolution error for '%s': %s",
                    type_spec.handler,
                    exc,
                )
                self._logger.warning(
                    "Failed to resolve handler '%s' for type '%s', "
                    "falling back to default handler",
                    type_spec.handler,
                    type_spec.name,
                )

        # Fallback: use DefaultHandler which delegates to SandboxManager
        return _DefaultHandler(type_spec=type_spec)


class _DefaultHandler:
    """Fallback handler when no handler class is specified or resolution fails.

    Delegates directly to :class:`SandboxManager` using the type's
    default sandbox strategy (or resource override).  CRUD and
    discovery methods raise :class:`NotImplementedError`.
    """

    def __init__(self, type_spec: ResourceTypeSpec) -> None:
        self._type_spec = type_spec

    # -- CRUD stubs (required by ResourceHandler protocol) -----------------

    def read(self, *, resource: Resource, path: str = "") -> Content:
        """Not supported by the default handler."""
        raise NotImplementedError("Default handler does not support read()")

    def write(self, *, resource: Resource, path: str, data: bytes) -> WriteResult:
        """Not supported by the default handler."""
        raise NotImplementedError("Default handler does not support write()")

    def delete(self, *, resource: Resource, path: str = "") -> DeleteResult:
        """Not supported by the default handler."""
        raise NotImplementedError("Default handler does not support delete()")

    def list_children(self, *, resource: Resource) -> list[str]:
        """Not supported by the default handler."""
        raise NotImplementedError("Default handler does not support list_children()")

    def diff(self, *, resource: Resource, other_location: str) -> DiffResult:
        """Not supported by the default handler."""
        raise NotImplementedError("Default handler does not support diff()")

    def discover_children(self, *, resource: Resource) -> list[Resource]:
        """Not supported by the default handler."""
        raise NotImplementedError(
            "Default handler does not support discover_children()"
        )

    # -- Lifecycle stubs (issue #836) --------------------------------------

    def create_sandbox(
        self,
        *,
        resource: Resource,
        plan_id: str,
        sandbox_manager: SandboxManager,
    ) -> SandboxResult:
        raise NotImplementedError("Default handler does not support create_sandbox()")

    def create_checkpoint(
        self,
        *,
        resource: Resource,
        plan_id: str,
        sandbox_manager: SandboxManager,
        phase: str = "execution",
    ) -> CheckpointResult:
        raise NotImplementedError(
            "Default handler does not support create_checkpoint()"
        )

    def rollback_to(
        self,
        *,
        resource: Resource,
        plan_id: str,
        checkpoint_id: str,
        sandbox_manager: SandboxManager,
    ) -> RollbackResult:
        raise NotImplementedError("Default handler does not support rollback_to()")

    def project_access(
        self,
        *,
        resource: Resource,
        principal: str,
        action: str = "read",
        project_id: str = "",
    ) -> AccessResult:
        raise NotImplementedError("Default handler does not support project_access()")

    # -- Sandbox resolution ------------------------------------------------

    def resolve(
        self,
        *,
        resource: Resource,
        plan_id: str,
        slot_name: str,
        sandbox_manager: SandboxManager,
        access: str = "read_only",
    ) -> BoundResource:
        """Resolve using the default sandbox strategy."""
        if not resource.location:
            raise ValueError(f"Resource '{resource.resource_id}' has no location")

        # resource.sandbox_strategy may be a StrEnum or plain str (Pydantic coercion)
        if resource.sandbox_strategy:
            strategy_str = str(resource.sandbox_strategy)
        else:
            strategy_str = str(self._type_spec.sandbox_strategy)

        sandbox = sandbox_manager.get_or_create_sandbox(
            plan_id=plan_id,
            resource_id=resource.resource_id,
            original_path=resource.location,
            sandbox_strategy=cast(SandboxStrategyStr, strategy_str),
        )

        if sandbox.context is None:
            raise RuntimeError(
                f"Sandbox for resource '{resource.resource_id}' "
                f"(plan={plan_id}) was created but has no context"
            )

        return BoundResource(
            slot_name=slot_name,
            resource_id=resource.resource_id,
            resource_type=resource.resource_type_name,
            sandbox_path=sandbox.context.sandbox_path,
            access=access,
        )

    def content_hash(
        self,
        resource: Resource,
        *,
        algorithm: str = "sha256",
    ) -> str:
        """Compute content hash using the base handler default logic."""
        from cleveragents.resource.handlers._base import BaseResourceHandler

        base = BaseResourceHandler()
        base._default_strategy = self._type_spec.sandbox_strategy  # type: ignore[assignment]
        base._type_label = self._type_spec.name  # type: ignore[assignment]
        return base.content_hash(resource, algorithm=algorithm)
