"""Dora node infrastructure abstractions."""

from __future__ import annotations

import json
import time
import uuid
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Callable, Dict, Iterator, List, Optional, Protocol

import pyarrow as pa


class VoiceControlLogger:
    """Timestamped logger for voice control node."""

    def __init__(self, prefix: str = "voice_control") -> None:
        self._prefix = prefix

    def log(self, msg: str) -> None:
        """Print a timestamped log message."""
        timestamp = time.strftime("%H:%M:%S")
        print(f"[{self._prefix} {timestamp}] {msg}", flush=True)

    def warn(self, msg: str) -> None:
        """Print a timestamped warning message."""
        self.log(f"Warning: {msg}")

    def error(self, msg: str) -> None:
        """Print a timestamped error message."""
        self.log(f"Error: {msg}")


class NodePort(ABC):
    """Abstract interface for Dora node operations."""

    @abstractmethod
    def send_output(self, output_name: str, data: pa.Array, metadata: Dict[str, Any]) -> None:
        """Send output to a Dora topic."""
        pass

    @abstractmethod
    def events(self) -> Iterator[Dict[str, Any]]:
        """Iterate over incoming events."""
        pass


class DoraNodeAdapter(NodePort):
    """Real Dora node wrapper."""

    def __init__(self, node: Any) -> None:
        self._node = node

    def send_output(self, output_name: str, data: pa.Array, metadata: Dict[str, Any]) -> None:
        """Send output to a Dora topic."""
        self._node.send_output(output_name, data, metadata)

    def events(self) -> Iterator[Dict[str, Any]]:
        """Iterate over incoming events."""
        for event in self._node:
            yield event

    def send_command(self, output_name: str, action: str, payload: Dict[str, Any]) -> str:
        """Send a robot command and return the command ID."""
        command_id = str(uuid.uuid4())
        message = {"id": command_id, "action": action, "payload": payload}
        self.send_output(
            output_name,
            pa.array([json.dumps(message)]),
            {"encoding": "json", "timestamp_ns": time.time_ns()},
        )
        return command_id

    def send_json(self, output_name: str, data: Dict[str, Any]) -> None:
        """Send JSON data to a topic."""
        self.send_output(
            output_name,
            pa.array([json.dumps(data)]),
            {"encoding": "json", "timestamp_ns": time.time_ns()},
        )


class MockNodeAdapter(NodePort):
    """Mock node adapter for testing."""

    def __init__(self) -> None:
        self._outputs: list[tuple[str, pa.Array, Dict[str, Any]]] = []
        self._events: list[Dict[str, Any]] = []

    def send_output(self, output_name: str, data: pa.Array, metadata: Dict[str, Any]) -> None:
        """Record output for testing."""
        self._outputs.append((output_name, data, metadata))

    def events(self) -> Iterator[Dict[str, Any]]:
        """Return queued test events."""
        for event in self._events:
            yield event

    def queue_event(self, event: Dict[str, Any]) -> None:
        """Queue an event for testing."""
        self._events.append(event)

    def get_outputs(self) -> list[tuple[str, pa.Array, Dict[str, Any]]]:
        """Get recorded outputs."""
        return self._outputs

    def clear(self) -> None:
        """Clear recorded outputs and events."""
        self._outputs.clear()
        self._events.clear()

    def send_command(self, output_name: str, action: str, payload: Dict[str, Any]) -> str:
        """Send a robot command and return the command ID."""
        command_id = str(uuid.uuid4())
        message = {"id": command_id, "action": action, "payload": payload}
        self.send_output(
            output_name,
            pa.array([json.dumps(message)]),
            {"encoding": "json", "timestamp_ns": time.time_ns()},
        )
        return command_id

    def send_json(self, output_name: str, data: Dict[str, Any]) -> None:
        """Send JSON data to a topic."""
        self.send_output(
            output_name,
            pa.array([json.dumps(data)]),
            {"encoding": "json", "timestamp_ns": time.time_ns()},
        )


class EventHandler(Protocol):
    """Protocol for event handlers."""

    def handle(self, event: Dict[str, Any]) -> None:
        """Handle an incoming event."""
        ...


class EventDispatcher:
    """Routes Dora events to registered handlers."""

    def __init__(self, adapter: NodePort, logger: Optional[VoiceControlLogger] = None) -> None:
        self._adapter = adapter
        self._logger = logger or VoiceControlLogger()
        self._handlers: Dict[str, EventHandler] = {}
        self._tick_callbacks: List[Callable[[], None]] = []
        self._first_event_callbacks: List[Callable[[], None]] = []
        self._first_event_fired = False

    def register(self, event_id: str, handler: EventHandler) -> None:
        """Register a handler for a specific event ID."""
        self._handlers[event_id] = handler

    def on_tick(self, callback: Callable[[], None]) -> None:
        """Register a callback to run after each event."""
        self._tick_callbacks.append(callback)

    def on_first_event(self, callback: Callable[[], None]) -> None:
        """Register a callback to run on the first event."""
        self._first_event_callbacks.append(callback)

    def run(self) -> None:
        """Main event loop."""
        self._logger.log("Dora node created, waiting for events...")

        for event in self._adapter.events():
            # Fire first event callbacks
            if not self._first_event_fired:
                self._first_event_fired = True
                for callback in self._first_event_callbacks:
                    try:
                        callback()
                    except Exception as e:
                        self._logger.error(f"First event callback failed: {e}")

            # Skip non-input events
            if event.get("type") != "INPUT":
                continue

            # Dispatch to handler
            event_id = event.get("id")
            handler = self._handlers.get(event_id)
            if handler:
                try:
                    handler.handle(event)
                except Exception as e:
                    self._logger.error(f"Handler for '{event_id}' failed: {e}")

            # Run tick callbacks
            for callback in self._tick_callbacks:
                try:
                    callback()
                except Exception as e:
                    self._logger.error(f"Tick callback failed: {e}")


class OutputPort:
    """Typed output port for sending data to a Dora topic."""

    def __init__(self, adapter: DoraNodeAdapter, name: str) -> None:
        self._adapter = adapter
        self._name = name

    @property
    def name(self) -> str:
        """Get the output topic name."""
        return self._name

    def send_json(self, data: Dict[str, Any]) -> None:
        """Send JSON data to the topic."""
        self._adapter.send_json(self._name, data)

    def send_command(self, action: str, payload: Dict[str, Any]) -> str:
        """Send a robot command and return command ID."""
        return self._adapter.send_command(self._name, action, payload)


@dataclass
class NodeContext:
    """Central context for Dora node I/O.

    Bundles the adapter, logger, and typed output ports into a single
    context object that can be passed to services and handlers.
    """

    adapter: DoraNodeAdapter
    logger: VoiceControlLogger

    # Typed output ports
    robot_out: OutputPort
    scene_out: OutputPort
    voice_out: OutputPort

    @classmethod
    def create(
        cls,
        adapter: DoraNodeAdapter,
        logger: VoiceControlLogger,
        robot_output: str = "robot_cmd",
        scene_output: str = "scene_update",
        voice_output: str = "voice_out",
    ) -> "NodeContext":
        """Create a NodeContext with named output ports.

        Args:
            adapter: The Dora node adapter
            logger: The logger instance
            robot_output: Topic name for robot commands
            scene_output: Topic name for scene updates
            voice_output: Topic name for voice responses
        """
        return cls(
            adapter=adapter,
            logger=logger,
            robot_out=OutputPort(adapter, robot_output),
            scene_out=OutputPort(adapter, scene_output),
            voice_out=OutputPort(adapter, voice_output),
        )
