"""
Main dispatcher for processing events from workers.
"""

import logging
import queue
import threading
import time
from typing import TYPE_CHECKING, final

from dify_graph.graph_events import (
    GraphNodeEventBase,
    NodeRunExceptionEvent,
    NodeRunFailedEvent,
    NodeRunSucceededEvent,
)

from ..event_management import EventManager
from .execution_coordinator import ExecutionCoordinator

if TYPE_CHECKING:
    from ..event_management import EventHandler

logger = logging.getLogger(__name__)


@final
class Dispatcher:
    """
    Main dispatcher that processes events from the event queue.

    This runs in a separate thread and coordinates event processing
    with timeout and completion detection.
    """

    _COMMAND_TRIGGER_EVENTS = (
        NodeRunSucceededEvent,
        NodeRunFailedEvent,
        NodeRunExceptionEvent,
    )

    def __init__(
        self,
        event_queue: queue.Queue[GraphNodeEventBase],
        event_handler: "EventHandler",
        execution_coordinator: ExecutionCoordinator,
        event_emitter: EventManager | None = None,
    ) -> None:
        """
        Initialize the dispatcher.

        Args:
            event_queue: Queue of events from workers
            event_handler: Event handler registry for processing events
            execution_coordinator: Coordinator for execution flow
            event_emitter: Optional event manager to signal completion
        """
        self._event_queue = event_queue
        self._event_handler = event_handler
        self._execution_coordinator = execution_coordinator
        self._event_emitter = event_emitter

        self._thread: threading.Thread | None = None
        self._stop_event = threading.Event()
        self._start_time: float | None = None

    def start(self) -> None:
        """Start the dispatcher thread."""
        if self._thread and self._thread.is_alive():
            return

        self._stop_event.clear()
        self._start_time = time.time()
        self._thread = threading.Thread(target=self._dispatcher_loop, name="GraphDispatcher", daemon=True)
        self._thread.start()

    def stop(self) -> None:
        """Stop the dispatcher thread."""
        self._stop_event.set()
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=2.0)

    def _dispatcher_loop(self) -> None:
        """Main dispatcher loop."""
        try:
            self._process_commands()
            paused = False
            while not self._stop_event.is_set():
                if self._execution_coordinator.aborted or self._execution_coordinator.execution_complete:
                    break
                if self._execution_coordinator.paused:
                    paused = True
                    break

                self._execution_coordinator.check_scaling()
                try:
                    event = self._event_queue.get(timeout=0.1)
                    self._event_handler.dispatch(event)
                    self._event_queue.task_done()
                    self._process_commands(event)
                except queue.Empty:
                    time.sleep(0.1)

            self._process_commands()
            if paused:
                self._drain_events_until_idle()
            else:
                self._drain_event_queue()

        except Exception as e:
            logger.exception("Dispatcher error")
            self._execution_coordinator.mark_failed(e)

        finally:
            self._execution_coordinator.mark_complete()
            # Signal the event emitter that execution is complete
            if self._event_emitter:
                self._event_emitter.mark_complete()

    def _process_commands(self, event: GraphNodeEventBase | None = None):
        if event is None or isinstance(event, self._COMMAND_TRIGGER_EVENTS):
            self._execution_coordinator.process_commands()

    def _drain_event_queue(self) -> None:
        while True:
            try:
                event = self._event_queue.get(block=False)
                self._event_handler.dispatch(event)
                self._event_queue.task_done()
            except queue.Empty:
                break

    def _drain_events_until_idle(self) -> None:
        while not self._stop_event.is_set():
            try:
                event = self._event_queue.get(timeout=0.1)
                self._event_handler.dispatch(event)
                self._event_queue.task_done()
                self._process_commands(event)
            except queue.Empty:
                if not self._execution_coordinator.has_executing_nodes():
                    break
        self._drain_event_queue()
