"""ACMS Pipeline Scope Resolver and Context Inheritance Service.

Provides two key services for the ACMS pipeline:

- **PipelineScopeResolver** -- Resolves ACMS pipeline component overrides at
  plan > project > global scope using the ``ComponentResolver`` 3-level scope
  chain.  Supports overriding ``SkeletonCompressor``, ``PreambleGenerator``,
  ``FragmentDeduplicator``, and ``DetailDepthResolver`` at any scope level.

- **ContextInheritanceService** -- Propagates skeleton context from a parent
  plan to child subplans on subplan creation.  Uses the ``SkeletonCompressor``
  resolved for the child plan's scope to compress parent fragments into a
  skeleton suitable for the child plan's token budget.

Based on ``docs/specification.md`` Epic #935 -- pipeline scope resolution
(plan > project > global precedence) and context inheritance for hierarchical
plan decomposition.

ISSUES CLOSED: #10016
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

from cleveragents.application.services.component_resolver import (
    ComponentResolver,
    ResolutionResult,
    ScopeLevel,
)

if TYPE_CHECKING:
    from cleveragents.application.services.acms_service import (
        SkeletonCompressor,
    )
    from cleveragents.domain.models.core.context_fragment import (
        ContextBudget,
        ContextFragment,
        ContextPayload,
    )

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# PipelineScopeResolver
# ---------------------------------------------------------------------------


class PipelineScopeResolver:
    """Resolve ACMS pipeline component overrides at plan > project > global scope.

    Wraps :class:`ComponentResolver` to provide a domain-specific API for
    resolving ACMS pipeline components.  Supports overriding the following
    component types at any scope level:

    - ``SkeletonCompressor`` -- compresses parent context for child plan inheritance
    - ``PreambleGenerator`` -- generates context preambles from assembled fragments
    - ``FragmentDeduplicator`` -- removes duplicate fragments across strategy results
    - ``DetailDepthResolver`` -- resolves detail level for each fragment based on budget

    Scope precedence (highest to lowest):

    1. **Plan scope** -- overrides registered for a specific plan ID
    2. **Project scope** -- overrides registered for a specific project ID
    3. **Global scope** -- default implementations registered at startup

    Example::

        resolver = PipelineScopeResolver()
        resolver.register_global(SkeletonCompressor, DepthReductionCompressor())
        resolver.register_project("my-project", SkeletonCompressor, custom_compressor)
        resolver.register_plan("plan-001", SkeletonCompressor, plan_compressor)

        # Resolves plan-level override (highest priority)
        result = resolver.resolve_skeleton_compressor(
            plan_id="plan-001", project_id="my-project"
        )
        assert result.scope == ScopeLevel.PLAN

    Based on ``docs/specification.md`` Epic #935 -- pipeline scope resolution.
    """

    def __init__(self, resolver: ComponentResolver | None = None) -> None:
        self._resolver = resolver or ComponentResolver()

    @property
    def resolver(self) -> ComponentResolver:
        """Return the underlying :class:`ComponentResolver`."""
        return self._resolver

    # ------------------------------------------------------------------
    # Registration
    # ------------------------------------------------------------------

    def register_global(
        self,
        component_type: type[Any],
        implementation: object,
    ) -> None:
        """Register a global default implementation for a pipeline component.

        Args:
            component_type: The Protocol type to register (e.g. ``SkeletonCompressor``).
            implementation: The implementation instance.

        Raises:
            ComponentRegistrationError: If *implementation* is ``None``.
        """
        self._resolver.register_global(component_type, implementation)
        logger.debug(
            "Registered global pipeline component: %s",
            component_type.__name__,
        )

    def register_project(
        self,
        project_id: str,
        component_type: type[Any],
        implementation: object,
    ) -> None:
        """Register a project-level override for a pipeline component.

        Args:
            project_id: The project identifier.
            component_type: The Protocol type to register.
            implementation: The implementation instance.

        Raises:
            ComponentRegistrationError: If *project_id* is empty or
                *implementation* is ``None``.
        """
        self._resolver.register_project(project_id, component_type, implementation)
        logger.debug(
            "Registered project pipeline component: %s for project %s",
            component_type.__name__,
            project_id,
        )

    def register_plan(
        self,
        plan_id: str,
        component_type: type[Any],
        implementation: object,
    ) -> None:
        """Register a plan-level override for a pipeline component.

        Args:
            plan_id: The plan identifier.
            component_type: The Protocol type to register.
            implementation: The implementation instance.

        Raises:
            ComponentRegistrationError: If *plan_id* is empty or
                *implementation* is ``None``.
        """
        self._resolver.register_plan(plan_id, component_type, implementation)
        logger.debug(
            "Registered plan pipeline component: %s for plan %s",
            component_type.__name__,
            plan_id,
        )

    # ------------------------------------------------------------------
    # Resolution helpers
    # ------------------------------------------------------------------

    def _resolve(
        self,
        component_type: type[Any],
        plan_id: str | None = None,
        project_id: str | None = None,
    ) -> ResolutionResult:
        """Resolve a component through the scope chain."""
        return self._resolver.resolve(
            component_type,
            plan_id=plan_id,
            project_id=project_id,
        )

    def resolve_skeleton_compressor(
        self,
        plan_id: str | None = None,
        project_id: str | None = None,
    ) -> ResolutionResult:
        """Resolve the ``SkeletonCompressor`` for the given scope.

        Applies plan > project > global precedence.

        Args:
            plan_id: Optional plan identifier for plan-level override lookup.
            project_id: Optional project identifier for project-level override lookup.

        Returns:
            :class:`ResolutionResult` with the resolved component and scope level.

        Raises:
            ComponentNotFoundError: If no implementation is registered.
        """
        from cleveragents.application.services.acms_service import SkeletonCompressor

        return self._resolve(SkeletonCompressor, plan_id=plan_id, project_id=project_id)

    def resolve_preamble_generator(
        self,
        plan_id: str | None = None,
        project_id: str | None = None,
    ) -> ResolutionResult:
        """Resolve the ``PreambleGenerator`` for the given scope.

        Applies plan > project > global precedence.

        Args:
            plan_id: Optional plan identifier for plan-level override lookup.
            project_id: Optional project identifier for project-level override lookup.

        Returns:
            :class:`ResolutionResult` with the resolved component and scope level.

        Raises:
            ComponentNotFoundError: If no implementation is registered.
        """
        from cleveragents.application.services.acms_service import PreambleGenerator

        return self._resolve(PreambleGenerator, plan_id=plan_id, project_id=project_id)

    def resolve_deduplicator(
        self,
        plan_id: str | None = None,
        project_id: str | None = None,
    ) -> ResolutionResult:
        """Resolve the ``FragmentDeduplicator`` for the given scope.

        Applies plan > project > global precedence.

        Args:
            plan_id: Optional plan identifier for plan-level override lookup.
            project_id: Optional project identifier for project-level override lookup.

        Returns:
            :class:`ResolutionResult` with the resolved component and scope level.

        Raises:
            ComponentNotFoundError: If no implementation is registered.
        """
        from cleveragents.application.services.acms_service import FragmentDeduplicator

        return self._resolve(
            FragmentDeduplicator, plan_id=plan_id, project_id=project_id
        )

    def resolve_depth_resolver(
        self,
        plan_id: str | None = None,
        project_id: str | None = None,
    ) -> ResolutionResult:
        """Resolve the ``DetailDepthResolver`` for the given scope.

        Applies plan > project > global precedence.

        Args:
            plan_id: Optional plan identifier for plan-level override lookup.
            project_id: Optional project identifier for project-level override lookup.

        Returns:
            :class:`ResolutionResult` with the resolved component and scope level.

        Raises:
            ComponentNotFoundError: If no implementation is registered.
        """
        from cleveragents.application.services.acms_service import DetailDepthResolver

        return self._resolve(
            DetailDepthResolver, plan_id=plan_id, project_id=project_id
        )

    # ------------------------------------------------------------------
    # Scope introspection
    # ------------------------------------------------------------------

    def resolution_scope(
        self,
        component_type: type[Any],
        plan_id: str | None = None,
        project_id: str | None = None,
    ) -> ScopeLevel:
        """Return the scope level at which a component would be resolved.

        Args:
            component_type: The Protocol type to check.
            plan_id: Optional plan identifier.
            project_id: Optional project identifier.

        Returns:
            The :class:`ScopeLevel` at which the component is registered.

        Raises:
            ComponentNotFoundError: If no implementation is registered.
        """
        result = self._resolve(component_type, plan_id=plan_id, project_id=project_id)
        return result.scope


# ---------------------------------------------------------------------------
# ContextInheritanceService
# ---------------------------------------------------------------------------


class ContextInheritanceService:
    """Propagate skeleton context from parent plans to child subplans.

    When a child subplan is created, this service compresses the parent
    plan's assembled context fragments into a skeleton form suitable for
    the child plan's token budget.  The compressed skeleton fragments are
    then available for injection into the child plan's context assembly.

    The ``SkeletonCompressor`` used for compression is resolved through the
    ``PipelineScopeResolver`` at the child plan's scope (plan > project >
    global), allowing per-plan or per-project compression strategies.

    Example::

        service = ContextInheritanceService(scope_resolver)
        skeleton = service.inherit_context(
            parent_payload=parent_payload,
            child_plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
            budget=ContextBudget(max_tokens=2048),
            skeleton_ratio=0.15,
        )
        # skeleton is a tuple of ContextFragment for the child plan

    Based on ``docs/specification.md`` Epic #935 -- context inheritance for
    hierarchical plan decomposition.
    """

    def __init__(
        self,
        scope_resolver: PipelineScopeResolver,
        *,
        default_skeleton_ratio: float = 0.15,
    ) -> None:
        self._scope_resolver = scope_resolver
        self._default_skeleton_ratio = default_skeleton_ratio

    @property
    def default_skeleton_ratio(self) -> float:
        """Return the default skeleton ratio used when none is specified."""
        return self._default_skeleton_ratio

    def inherit_context(
        self,
        parent_payload: ContextPayload,
        child_plan_id: str,
        budget: ContextBudget,
        *,
        skeleton_ratio: float | None = None,
        project_id: str | None = None,
    ) -> tuple[ContextFragment, ...]:
        """Compress parent context into skeleton fragments for a child plan.

        Resolves the ``SkeletonCompressor`` at the child plan's scope and
        uses it to compress the parent payload's fragments into a skeleton
        form that fits within the skeleton budget.

        The skeleton budget is computed as::

            skeleton_budget = int(budget.available_tokens * skeleton_ratio)

        Args:
            parent_payload: The assembled context payload from the parent plan.
            child_plan_id: The identifier of the child subplan being created.
            budget: The token budget for the child plan's context assembly.
            skeleton_ratio: Fraction of the budget to allocate for skeleton
                context.  Defaults to ``default_skeleton_ratio`` (0.15).
            project_id: Optional project identifier for scope resolution.

        Returns:
            A tuple of compressed :class:`ContextFragment` instances for the
            child plan.  Empty tuple if the parent payload has no fragments or
            the skeleton budget is zero.
        """
        ratio = (
            skeleton_ratio
            if skeleton_ratio is not None
            else self._default_skeleton_ratio
        )
        skeleton_budget = int(budget.available_tokens * ratio)

        if skeleton_budget <= 0 or not parent_payload.fragments:
            logger.debug(
                "Skipping context inheritance: no budget or no parent fragments "
                "(child_plan_id=%s, skeleton_budget=%d, parent_fragment_count=%d)",
                child_plan_id,
                skeleton_budget,
                len(parent_payload.fragments),
            )
            return ()

        result = self._scope_resolver.resolve_skeleton_compressor(
            plan_id=child_plan_id,
            project_id=project_id,
        )
        compressor: SkeletonCompressor = result.component  # type: ignore[assignment]

        skeleton_fragments = compressor.compress(
            parent_payload.fragments,
            skeleton_budget,
        )

        logger.info(
            "Context inheritance complete: child_plan_id=%s, scope=%s, "
            "skeleton_budget=%d, parent_fragments=%d, skeleton_fragments=%d",
            child_plan_id,
            result.scope.value,
            skeleton_budget,
            len(parent_payload.fragments),
            len(skeleton_fragments),
        )
        return skeleton_fragments

    def inherit_from_fragments(
        self,
        parent_fragments: tuple[ContextFragment, ...],
        child_plan_id: str,
        budget: ContextBudget,
        *,
        skeleton_ratio: float | None = None,
        project_id: str | None = None,
    ) -> tuple[ContextFragment, ...]:
        """Compress parent fragments directly into skeleton for a child plan.

        Variant of :meth:`inherit_context` that accepts raw fragments instead
        of a full :class:`ContextPayload`.  Useful when the parent context is
        available as a fragment tuple rather than an assembled payload.

        Args:
            parent_fragments: Fragments from the parent plan to compress.
            child_plan_id: The identifier of the child subplan being created.
            budget: The token budget for the child plan's context assembly.
            skeleton_ratio: Fraction of the budget to allocate for skeleton
                context.  Defaults to ``default_skeleton_ratio`` (0.15).
            project_id: Optional project identifier for scope resolution.

        Returns:
            A tuple of compressed :class:`ContextFragment` instances.
        """
        ratio = (
            skeleton_ratio
            if skeleton_ratio is not None
            else self._default_skeleton_ratio
        )
        skeleton_budget = int(budget.available_tokens * ratio)

        if skeleton_budget <= 0 or not parent_fragments:
            return ()

        result = self._scope_resolver.resolve_skeleton_compressor(
            plan_id=child_plan_id,
            project_id=project_id,
        )
        compressor: SkeletonCompressor = result.component  # type: ignore[assignment]

        skeleton_fragments = compressor.compress(parent_fragments, skeleton_budget)

        logger.info(
            "Context inheritance from fragments complete: child_plan_id=%s, "
            "scope=%s, skeleton_budget=%d, parent_fragments=%d, skeleton_fragments=%d",
            child_plan_id,
            result.scope.value,
            skeleton_budget,
            len(parent_fragments),
            len(skeleton_fragments),
        )
        return skeleton_fragments


__all__: list[str] = [
    "ContextInheritanceService",
    "PipelineScopeResolver",
]
