"""Reply types for the RouterOS Python client library."""

from __future__ import annotations

import queue
import threading
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Iterator, List, Optional

from routeros.errors import DeviceError, UnknownReplyError
from routeros.sentence import Sentence

if TYPE_CHECKING:
    from routeros.client import Client

# Sentence type words.
_RE = "!re"
_DONE = "!done"
_TRAP = "!trap"
_FATAL = "!fatal"
_EMPTY = "!empty"


@dataclass
class Reply:
    """Aggregated response to a single RouterOS command.

    Attributes:
        re:   List of ``!re`` sentences (the result rows).
        done: The final ``!done`` sentence (always present on success).
    """

    re: List[Sentence] = field(default_factory=list)
    done: Optional[Sentence] = None

    # ------------------------------------------------------------------
    # Internal sentence processing (state machine)
    # ------------------------------------------------------------------

    def process_sentence(self, sen: Sentence) -> tuple[bool, Optional[Exception]]:
        """Process one incoming sentence.

        Returns ``(is_done, error)``:

        * ``is_done=True`` means the reply is complete and no more
          sentences should be fed.
        * ``error`` is non-``None`` for ``!trap``/``!fatal`` or unknown
          words.

        This method mirrors ``reply.go``'s ``processSentence``.
        """
        if sen.word == _RE:
            self.re.append(sen)
            return False, None
        if sen.word == _DONE:
            self.done = sen
            return True, None
        if sen.word in (_TRAP, _FATAL):
            return sen.word == _FATAL, DeviceError(sen)
        if sen.word in ("", _EMPTY):
            # RouterOS 7.18+ can send !empty; API spec says ignore.
            return False, None
        return True, UnknownReplyError(sen)

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def __str__(self) -> str:
        lines = [str(s) for s in self.re]
        if self.done is not None:
            lines.append(str(self.done))
        return "\n".join(lines)


# ---------------------------------------------------------------------------
# ListenReply – streaming reply backed by a Queue
# ---------------------------------------------------------------------------


class ListenReply:
    """Streaming reply for ``listen`` commands.

    Sentences arriving with ``!re`` are placed on an internal
    :class:`queue.Queue`.  The caller consumes them via :meth:`chan` (the
    ``Queue`` object directly) or :meth:`__iter__`.

    The stream ends when:
    * The device sends ``!done`` (normal completion / cancel).
    * The device sends ``!trap`` (error).
    * The device sends ``!fatal`` (fatal error, connection broken).
    * :meth:`cancel` is called.

    After the stream ends, ``done`` is set to the terminal sentence and
    :attr:`err` contains any error.
    """

    def __init__(self, client: "Client", tag: str, queue_size: int = 0) -> None:
        self._client = client
        self._tag = tag
        self._queue: queue.Queue[Optional[Sentence]] = queue.Queue(
            maxsize=queue_size
        )
        self._done_sentence: Optional[Sentence] = None
        self._err: Optional[Exception] = None
        self._closed = threading.Event()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def chan(self) -> "queue.Queue[Optional[Sentence]]":
        """Return the underlying :class:`~queue.Queue`.

        Each item is a ``!re`` :class:`~routeros_python.sentence.Sentence`.
        A ``None`` sentinel is placed on the queue when the stream ends.
        """
        return self._queue

    def __iter__(self) -> Iterator[Sentence]:
        """Iterate over ``!re`` sentences until the stream ends."""
        while True:
            item = self._queue.get()
            if item is None:
                return
            yield item

    def cancel(self) -> Reply:
        """Send ``/cancel`` to the device and wait for the stream to close."""
        return self._client.run_args(["/cancel", f"=tag={self._tag}"])

    @property
    def tag(self) -> str:
        return self._tag

    @property
    def done(self) -> Optional[Sentence]:
        """The terminal sentence (``!done`` or ``!trap``) once received."""
        return self._done_sentence

    @property
    def err(self) -> Optional[Exception]:
        """The first error that occurred, or ``None``."""
        return self._err

    # ------------------------------------------------------------------
    # Internal – called by the async loop
    # ------------------------------------------------------------------

    def process_sentence(self, sen: Sentence) -> tuple[bool, Optional[Exception]]:
        """Route an incoming sentence to the queue or terminal state."""
        if sen.word == _RE:
            self._queue.put(sen)
            return False, None
        if sen.word == _DONE:
            self._done_sentence = sen
            return True, None
        if sen.word == _TRAP:
            # category 2 = "execution of command interrupted" (normal cancel)
            if sen.map.get("category") == "2":
                self._done_sentence = sen
                return True, None
            return True, DeviceError(sen)
        if sen.word == _FATAL:
            return True, DeviceError(sen)
        if sen.word in ("", _EMPTY):
            return False, None
        return True, UnknownReplyError(sen)

    def close(self, err: Optional[Exception] = None) -> None:
        """Signal end-of-stream by placing ``None`` sentinel on the queue."""
        if self._err is None and err is not None:
            self._err = err
        self._closed.set()
        self._queue.put(None)
