"""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 typing import TYPE_CHECKING, Any, Protocol, runtime_checkable

import structlog

from cleveragents.core.exceptions import PlanError

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 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."""
        ...


@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 _parse_actor_name(actor_name: str) -> tuple[str, str]:
    """Split ``provider/model`` into *(provider_type, model_id)*.

    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")``
        ``gpt-4``            -> ``("openai", "gpt-4")``  (default provider)
    """
    _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:
            # Both segments empty (e.g. "/") — full default.
            logger.warning(
                "Actor name '%s' has empty provider and model, defaulting to %s",
                actor_name,
                _DEFAULT_ACTOR_NAME,
            )
            return (_default_provider, _default_model)
        # Preserve whichever segment the caller provided, filling
        # in the default for the missing half.
        return (provider or _default_provider, model or _default_model)
    return (_default_provider, actor_name)


def resolve_strategy_actor(
    provider_registry: ProviderRegistry | None = None,
    lifecycle_service: LifecycleService | None = None,
    acms_pipeline: AcmsPipeline | 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.
        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,
        )

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