"""
WorkflowRunner orchestrates DAG execution for non-dialogue nodes.
Uses a stateless tick pattern to enqueue background tasks.
DialogueRunner handles all dialogue nodes separately.

This module is decoupled from app via protocols - all dependencies
are injected through constructor arguments.
"""

from typing import Any, Optional
from uuid import UUID

from swarm.config.logging import get_structured_logger
from swarm.config.protocols import (
    MetricsCollectorProtocol,
    PlanServiceProtocol,
    StreamManagerProtocol,
    TaskBackendProtocol,
    WorkflowServiceProtocol,
)
from swarm.runners.dialogue import DialogueRunner

logger = get_structured_logger(__name__)


class WorkflowRunner:
    """
    Orchestrates DAG execution for non-dialogue workflow nodes.
    Uses a stateless tick pattern to enqueue background tasks.
    DialogueRunner handles all dialogue nodes separately.

    All dependencies are injected via protocols, making this class
    fully decoupled from app-specific implementations.
    """

    def __init__(
        self,
        task_backend: TaskBackendProtocol,
        workflow_service: WorkflowServiceProtocol,
        dialogue_runner: Optional[DialogueRunner] = None,
        metrics_collector: Optional[MetricsCollectorProtocol] = None,
        plan_service: Optional[PlanServiceProtocol] = None,
    ):
        """
        Initialize the workflow runner with dependency injection.

        Args:
            task_backend: Backend for task execution (required)
            workflow_service: Service for workflow operations (required)
            dialogue_runner: Optional DialogueRunner (for compatibility)
            metrics_collector: Optional metrics collector for observability
            plan_service: Optional plan service for plan orchestration
        """
        self.task_backend = task_backend
        self.workflow_service = workflow_service
        self.dialogue_runner = dialogue_runner
        self.metrics_collector = metrics_collector
        self.plan_service = plan_service

    async def tick(
        self,
        instance_id: str,
        stream_manager: Optional[StreamManagerProtocol] = None,
    ) -> None:
        """
        Stateless tick to process ready non-dialogue nodes.

        This method:
        1. Finds nodes ready for execution (dependencies satisfied)
        2. Skips dialogue nodes (handled by DialogueRunner)
        3. Enqueues non-dialogue nodes to background tasks
        4. Returns immediately (no long-running loops)

        Called after:
        - Dialogue node completes (e.g., from create_project tool)
        - Background deterministic node completes (from execute-deterministic-node endpoint)

        Args:
            instance_id: Workflow instance ID
            stream_manager: Optional stream manager for status updates
        """
        try:
            logger.info(
                "workflow_runner.tick",
                extra={
                    "description": "Processing workflow tick",
                    "instance_id": instance_id,
                },
            )

            # Get workflow instance
            instance = await self.workflow_service.instance_service.get_instance(
                instance_id
            )
            # Use protocol behavior method instead of enum comparison
            if not instance or instance.is_completed():
                logger.info(
                    "workflow_runner.tick_skip",
                    extra={
                        "description": "Workflow already complete or not found",
                        "instance_id": instance_id,
                    },
                )
                return

            # Capture snapshot at tick start (for debugging)
            await self.workflow_service.capture_workflow_snapshot(
                instance_id,
                trigger="tick_start",
                description="Workflow tick processing started",
            )

            # Get ready nodes and resolve routing via shared resolver policy.
            (
                ready_nodes,
                decision,
            ) = await self.workflow_service.get_ready_nodes_routing_decision(
                instance_id
            )

            if not ready_nodes:
                # Check if workflow is complete by finding terminal nodes
                all_nodes = await self.workflow_service.node_instance_service.get_node_instances_by_workflow_instance_id(
                    instance_id
                )

                # Get workflow definition to access graph edges
                workflow = await self.workflow_service.get(str(instance.workflow_id))
                graph_edges = workflow.graph.edges if workflow else []

                # Find response nodes or nodes with no outgoing edges
                terminal_nodes = [
                    node
                    for node in all_nodes
                    if node.node_type == "response"
                    or not self._has_outgoing_edges(node, graph_edges)
                ]

                # If all terminal nodes are complete, workflow is done
                # Use protocol behavior method instead of enum comparison
                if terminal_nodes and all(
                    node.is_terminal() for node in terminal_nodes
                ):
                    # Mark workflow as completed
                    await self.workflow_service.complete_instance(
                        instance_id,
                        capture_snapshot=True,
                    )
                    logger.info(
                        "workflow_runner.workflow_complete",
                        extra={
                            "instance_id": instance_id,
                            "note": "Plan advancement handled by complete_workflow_node when last dialogue node completes",
                        },
                    )
                    # Plan advancement is handled by complete_workflow_node()
                    # when the last dialogue node completes (single source of truth)
                return

            if decision.dialogue_node_instance_id is not None:
                # Dialogue takes precedence: background enqueue waits for next cycle.
                dialogue_node = next(
                    (
                        node
                        for node in ready_nodes
                        if str(node.id) == decision.dialogue_node_instance_id
                    ),
                    None,
                )
                logger.info(
                    "workflow_runner.dialogue_node_ready",
                    extra={
                        "instance_id": instance_id,
                        "node_id": dialogue_node.node_id if dialogue_node else None,
                        "dialogue_node_instance_id": decision.dialogue_node_instance_id,
                        "description": "Dialogue node ready, waiting for user input",
                    },
                )
                return

            ready_nodes_by_id = {str(node.id): node for node in ready_nodes}

            for node_instance_id in decision.autonomous_node_instance_ids:
                node = ready_nodes_by_id.get(node_instance_id)
                if node is None:
                    raise RuntimeError(
                        "Routing decision referenced missing autonomous node instance"
                    )
                await self._enqueue_autonomous_agent_node(
                    instance_id, node, stream_manager
                )

            for node_instance_id in decision.deterministic_node_instance_ids:
                node = ready_nodes_by_id.get(node_instance_id)
                if node is None:
                    raise RuntimeError(
                        "Routing decision referenced missing deterministic node instance"
                    )
                await self._enqueue_non_dialogue_node(instance_id, node, stream_manager)

        except Exception as e:
            logger.error(
                "workflow_runner.tick_error",
                extra={
                    "description": "Error during workflow tick",
                    "instance_id": instance_id,
                    "error_type": type(e).__name__,
                    "error_message": str(e),
                },
                exc_info=True,
            )
            # Capture error snapshot for debugging
            await self.workflow_service.capture_workflow_snapshot(
                instance_id,
                trigger="error",
                description=f"Error captured: {type(e).__name__}: {str(e)}",
            )
            pass

    def _has_outgoing_edges(self, node: Any, graph_edges: list[Any]) -> bool:
        """
        Check if a node has outgoing edges in the workflow graph.

        Args:
            node: Node instance to check
            graph_edges: List of WorkflowEdge from the workflow definition

        Returns:
            True if node has outgoing edges, False otherwise
        """
        # Check if this node's node_id appears as from_node in any edge
        return any(edge.from_node == node.node_id for edge in graph_edges)

    async def _enqueue_non_dialogue_node(
        self,
        instance_id: str,
        node: Any,
        stream_manager: Optional[StreamManagerProtocol] = None,
    ) -> None:
        """
        Enqueue a non-dialogue node for background execution.

        Args:
            instance_id: Workflow instance ID
            node: Node to enqueue
            stream_manager: Optional stream manager for status updates
        """
        # Get retry config from node configuration
        _retry_config = (
            node.configuration.get("retry_policy", {}) if node.configuration else {}
        )

        # Use attempt count from metadata or default to 1
        current_attempt = (
            node.metadata.get("current_attempt", 1) if node.metadata else 1
        )
        idempotency_key = f"{instance_id}:{node.node_id}:{current_attempt}"

        # Check if already processed
        if node.metadata and node.metadata.get("idempotency_key") == idempotency_key:
            logger.info(
                "workflow_runner.node_already_processed",
                extra={
                    "instance_id": instance_id,
                    "node_id": node.node_id,
                    "idempotency_key": idempotency_key,
                },
            )
            return

        # Update node state to running
        await self.workflow_service.node_instance_service.start_node(str(node.id))

        # Enqueue task
        task_id = await self.task_backend.enqueue(
            task_name="execute_deterministic_node",
            payload={
                "instance_id": instance_id,
                "node_id": node.node_id,
                "node_instance_id": str(node.id),
                "node_config": (
                    node.configuration.model_dump()
                    if hasattr(node.configuration, "model_dump")
                    else node.configuration
                ),
                "idempotency_key": idempotency_key,
            },
        )

        # Store the task_id for potential cancellation
        await self.workflow_service.node_instance_service.store_task_id(
            str(node.id), task_id
        )

        logger.info(
            "workflow_runner.node_enqueued",
            extra={
                "instance_id": instance_id,
                "node_id": node.node_id,
                "task_id": task_id,
                "node_type": node.node_type,
            },
        )

    async def _enqueue_autonomous_agent_node(
        self,
        instance_id: str,
        node: Any,
        stream_manager: Optional[StreamManagerProtocol] = None,
    ) -> None:
        """
        Enqueue an autonomous agent node for batch execution.

        Agent nodes execute autonomously with DialogueRunner in a loop until:
        - agent_completed() signal is received
        - agent_paused() signal is received (for user input)
        - max_iterations is reached

        Args:
            instance_id: Workflow instance ID
            node: Agent node to enqueue
            stream_manager: Optional stream manager for status updates
        """
        # Use attempt count from metadata or default to 1
        current_attempt = (
            node.metadata.get("current_attempt", 1) if node.metadata else 1
        )
        idempotency_key = f"{instance_id}:{node.node_id}:{current_attempt}"

        # Check if already processed
        if node.metadata and node.metadata.get("idempotency_key") == idempotency_key:
            logger.info(
                "workflow_runner.agent_node_already_processed",
                extra={
                    "instance_id": instance_id,
                    "node_id": node.node_id,
                    "idempotency_key": idempotency_key,
                },
            )
            return

        # Update node state to running
        await self.workflow_service.node_instance_service.start_node(str(node.id))

        # Enqueue task for autonomous agent execution
        task_id = await self.task_backend.enqueue(
            task_name="execute_autonomous_agent",
            payload={
                "instance_id": instance_id,
                "node_id": node.node_id,
                "node_instance_id": str(node.id),
                "iteration": 0,
                "max_iterations": 100,
                "idempotency_key": idempotency_key,
            },
        )

        # Store the task_id for potential cancellation
        await self.workflow_service.node_instance_service.store_task_id(
            str(node.id), task_id
        )

        logger.info(
            "workflow_runner.agent_node_enqueued",
            extra={
                "instance_id": instance_id,
                "node_id": node.node_id,
                "task_id": task_id,
                "node_type": node.node_type,
                "max_iterations": 100,
            },
        )

    async def run(
        self,
        instance_id: UUID,
        stream_manager: StreamManagerProtocol,
    ) -> None:
        """
        Legacy run method for compatibility.
        Now just calls tick() for stateless orchestration.

        Args:
            instance_id: Workflow instance ID
            stream_manager: Stream manager for status updates
        """
        await self.tick(str(instance_id), stream_manager)
