"""Resolution and validation utilities for strategy actors.

Handles actor resolution from configuration, cycle detection in dependency
graphs, and actor name parsing.
"""

from __future__ import annotations

from collections import deque
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable

import structlog

from cleveragents.application.services.context_tiers import (
    ContextTierService,
)
from cleveragents.core.exceptions import PlanError
from cleveragents.providers.registry import ProviderType

if TYPE_CHECKING:
    from cleveragents.providers.registry import ProviderRegistry

logger = structlog.get_logger(__name__)

_DEFAULT_ACTOR_NAME = "openai/gpt-4"


@runtime_checkable
class LifecycleService(Protocol):
    """Minimal interface for plan/action/actor resolution."""

    def get_plan(self, plan_id: str) -> Any:
        """Get a plan by ID."""
        ...

    def get_action(self, action_name: str) -> Any:
        """Get an action by name."""
        ...

    def resolve_actor_provider_model(self, actor_name: str) -> str | None:
        """Resolve a namespaced actor name to ``provider/model`` format."""
        ...

    def resolve_actor_options(self, actor_name: str) -> dict[str, Any] | None:
        """Resolve a namespaced actor name to its opaque options dict."""
        ...


@runtime_checkable
class AcmsPipeline(Protocol):
    """Minimal interface for ACMS context retrieval."""

    def get_context_summary(self) -> str | None:
        """Get a summarized context snapshot of the project(s)."""
        ...


def validate_no_cycles(edges: list[tuple[str, str]]) -> bool:
    """Validate that the dependency graph has no circular dependencies.

    Uses Kahn's algorithm (topological sort) to detect cycles.

    Args:
        edges: List of directed edges ``(dependent_id, dependency_id)``
               meaning ``dependent_id`` depends on ``dependency_id``.
               The adjacency direction does not affect cycle detection.

    Returns:
        ``True`` if the graph is acyclic.

    Raises:
        PlanError: If a cycle is detected.
    """
    if not edges:
        return True

    # Build adjacency list and in-degree map
    nodes: set[str] = set()
    adj: dict[str, list[str]] = {}
    in_degree: dict[str, int] = {}

    for src, dst in edges:
        nodes.add(src)
        nodes.add(dst)
        if src not in adj:
            adj[src] = []
        adj[src].append(dst)
        in_degree.setdefault(src, 0)
        in_degree[dst] = in_degree.get(dst, 0) + 1

    # Kahn's algorithm — use deque for O(1) popleft
    queue: deque[str] = deque(n for n in nodes if in_degree.get(n, 0) == 0)
    visited_count = 0

    while queue:
        node = queue.popleft()
        visited_count += 1
        for neighbor in adj.get(node, []):
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    if visited_count != len(nodes):
        raise PlanError(
            "Circular dependency detected in strategy action tree. "
            f"Visited {visited_count} of {len(nodes)} nodes."
        )
    return True


def _is_known_provider(provider: str) -> bool:
    """Check whether *provider* is a known ``ProviderType`` value.

    Returns ``True`` when the string matches a registered provider
    (e.g. ``"openai"``, ``"anthropic"``).  Returns ``False`` when the
    string is something else -- typically a namespace prefix like
    ``"local"`` or ``"cleverthis"``.
    """
    return provider in ProviderType.__members__.values()


def _parse_actor_name(actor_name: str) -> tuple[str, str]:
    """Split *actor_name* into *(provider_type, model_id)*.

    When the first slash-separated segment is a known
    :class:`~cleveragents.providers.registry.ProviderType` value the
    input is treated as ``provider/model``.  Otherwise it is treated as
    a namespace/name reference (e.g. ``"local/my-strategist"``) and the
    entire string is returned as the model with the default provider.

    Callers SHOULD pre-resolve namespace/name references via the actor
    registry before calling this function.

    Falls back to the default derived from :data:`_DEFAULT_ACTOR_NAME`
    when the input is empty or contains no ``/`` separator.

    Examples:
        ``"openai/gpt-4"``        → ``("openai", "gpt-4")``
        ``"anthropic/claude-3"``  → ``("anthropic", "claude-3")``
        ``"gpt-4"``               → ``("openai", "gpt-4")``
        ``"local/my-strategist"`` → ``("openai", "local/my-strategist")``
    """
    _default_provider, _default_model = _DEFAULT_ACTOR_NAME.split("/", 1)
    if not actor_name or not actor_name.strip():
        logger.warning(
            "Empty actor name provided, defaulting to %s",
            _DEFAULT_ACTOR_NAME,
        )
        return (_default_provider, _default_model)
    parts = actor_name.split("/", 1)
    if len(parts) == 2:
        provider, model = parts[0].strip(), parts[1].strip()
        if not provider and not model:
            logger.warning(
                "Actor name '%s' has empty provider and model, defaulting to %s",
                actor_name,
                _DEFAULT_ACTOR_NAME,
            )
            return (_default_provider, _default_model)
        if _is_known_provider(provider):
            return (provider or _default_provider, model or _default_model)
        if not provider:
            return (_default_provider, model or _default_model)
        logger.debug(
            "Actor name '%s' first segment is not a known provider; "
            "treating as namespace/name.",
            actor_name,
        )
        return (_default_provider, actor_name)
    return (_default_provider, actor_name)


def build_actor_resolver(
    actor_service: Any,
) -> Callable[[str], str | None]:
    """Build a namespace/name → provider/model resolver closure.

    Returns a callable ``(actor_name: str) -> str | None`` that:
    - returns ``None`` when the name is already in ``provider/model`` format
      (checked via :func:`_is_known_provider`)
    - looks up the actor via *actor_service* and returns
      ``"<provider>/<model>"`` on success
    - returns ``None`` when the actor is unknown or lookup fails

    Used by both CLI session commands and the A2A facade to provide
    ``SessionWorkflow`` with namespace/name resolution capability.
    """

    def resolve(actor_name: str) -> str | None:
        if not actor_name:
            return None
        parts = actor_name.split("/", 1)
        if len(parts) == 2 and _is_known_provider(parts[0].strip()):
            return None  # already in provider/model format
        try:
            actor = actor_service.get_actor(actor_name)
        except Exception:
            return None
        return f"{actor.provider}/{actor.model}"

    return resolve


def build_actor_options_resolver(
    actor_service: Any,
) -> Callable[[str], dict[str, Any] | None]:
    """Build a namespace/name → options resolver closure.

    Returns a callable ``(actor_name: str) -> dict[str, Any] | None`` that
    looks up an actor by its namespace/name reference and returns its
    ``options`` dict from the stored config blob, or ``None`` when the
    actor is unknown or has no options configured.
    """

    def resolve_options(actor_name: str) -> dict[str, Any] | None:
        if not actor_name:
            return None
        parts = actor_name.split("/", 1)
        if len(parts) == 2 and _is_known_provider(parts[0].strip()):
            return None
        try:
            actor = actor_service.get_actor(actor_name)
        except Exception:
            return None
        blob = getattr(actor, "config_blob", None) or {}
        options = blob.get("options") if isinstance(blob, dict) else None
        return dict(options) if isinstance(options, dict) else None

    return resolve_options


def resolve_strategy_actor(
    provider_registry: ProviderRegistry | None = None,
    lifecycle_service: LifecycleService | None = None,
    acms_pipeline: AcmsPipeline | None = None,
    tier_service: ContextTierService | None = None,
    config_value: str | None = None,
) -> Any:  # Returns StrategyActor | None to avoid circular import
    """Resolve the strategy actor based on configuration.

    When ``config_value`` is ``"llm"`` or a provider registry is
    available, returns a :class:`StrategyActor` with LLM capability.
    Otherwise returns ``None`` (caller should use the default
    ``StrategizeStubActor``).

    This function is the integration point for the
    ``actor.default.strategy`` configuration key.

    Args:
        provider_registry: Optional provider registry.
        lifecycle_service: Optional lifecycle service.
        acms_pipeline: Optional ACMS pipeline.
        tier_service: Optional context tier service.
        config_value: Value of the ``actor.default.strategy`` config key.

    Returns:
        A ``StrategyActor`` instance or ``None``.
    """
    # Import here to avoid circular dependency
    from cleveragents.application.services.strategy_actor import StrategyActor

    if config_value == "stub":
        return None

    if config_value == "llm" or provider_registry is not None:
        if config_value == "llm" and provider_registry is None:
            logger.warning(
                "Strategy actor config is 'llm' but no provider registry is "
                "available; actor will operate in stub mode",
            )
        return StrategyActor(
            provider_registry=provider_registry,
            lifecycle_service=lifecycle_service,
            acms_pipeline=acms_pipeline,
            tier_service=tier_service,
        )

    if config_value is not None:
        logger.warning(
            "Unrecognised strategy actor config value '%s'; "
            "returning None (caller should use stub)",
            config_value,
        )
    return None
