"""Binary RouterOS API wire protocol: length encoding and sentence I/O.

The RouterOS API uses a simple TLV-like framing where every *word* is
prefixed by a variable-length integer that encodes the word's byte length:

    0x00 – 0x7F       → 1 byte   (length 0 – 127)
    0x80 – 0xBF       → 2 bytes  (length 128 – 16383)
    0xC0 – 0xDF       → 3 bytes  (length 16384 – 2097151)
    0xE0 – 0xEF       → 4 bytes  (length 2097152 – 268435455)
    0xF0              → 5 bytes  (length ≥ 268435456)

A *sentence* is a stream of words terminated by an empty word (length 0).
"""

from __future__ import annotations

import socket
import threading
from typing import List

from routeros.sentence import Sentence

# ---------------------------------------------------------------------------
# Length encoding / decoding
# ---------------------------------------------------------------------------


def encode_length(n: int) -> bytes:
    """Encode *n* as a RouterOS variable-length prefix."""
    if n < 0x80:
        return bytes([n])
    if n < 0x4000:
        return bytes([(n >> 8) | 0x80, n & 0xFF])
    if n < 0x200000:
        return bytes([(n >> 16) | 0xC0, (n >> 8) & 0xFF, n & 0xFF])
    if n < 0x10000000:
        return bytes(
            [(n >> 24) | 0xE0, (n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF]
        )
    return bytes(
        [0xF0, (n >> 24) & 0xFF, (n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF]
    )


def _recv_exact(sock: socket.socket, n: int) -> bytes:
    """Read exactly *n* bytes from *sock*, raising :class:`EOFError` on EOF."""
    buf = b""
    while len(buf) < n:
        chunk = sock.recv(n - len(buf))
        if not chunk:
            raise EOFError("connection closed by remote host")
        buf += chunk
    return buf


def decode_length(sock: socket.socket) -> int:
    """Read and decode a RouterOS variable-length integer from *sock*."""
    first = _recv_exact(sock, 1)[0]

    if first & 0x80 == 0x00:
        return first

    if first & 0xC0 == 0x80:
        rest = _recv_exact(sock, 1)[0]
        return ((first & ~0xC0) << 8) | rest

    if first & 0xE0 == 0xC0:
        rest = int.from_bytes(_recv_exact(sock, 2), "big")
        return ((first & ~0xE0) << 16) | rest

    if first & 0xF0 == 0xE0:
        rest = int.from_bytes(_recv_exact(sock, 3), "big")
        return ((first & ~0xF0) << 24) | rest

    # 0xF0 → next 4 bytes are the full length
    return int.from_bytes(_recv_exact(sock, 4), "big")


# ---------------------------------------------------------------------------
# Reader
# ---------------------------------------------------------------------------


class Reader:
    """Reads :class:`~routeros_python.sentence.Sentence` objects from a socket."""

    def __init__(self, sock: socket.socket) -> None:
        self._sock = sock

    def read_sentence(self) -> Sentence:
        """Block until a complete sentence has been received.

        Returns a fully populated :class:`~routeros_python.sentence.Sentence`.
        Raises :class:`EOFError` if the connection is closed mid-read.
        Raises :class:`ValueError` for malformed words.
        """
        words: List[str] = []
        while True:
            length = decode_length(self._sock)
            if length == 0:
                break
            raw = _recv_exact(self._sock, length).decode("utf-8", errors="replace")
            words.append(raw)
        return Sentence.from_words(words)


# ---------------------------------------------------------------------------
# Writer
# ---------------------------------------------------------------------------


class Writer:
    """Writes sentences to a socket.

    Thread-safe: a :class:`threading.Lock` serialises concurrent
    :meth:`write_sentence` calls so that sentences from different threads
    cannot interleave.
    """

    def __init__(self, sock: socket.socket) -> None:
        self._sock = sock
        self._lock = threading.Lock()

    def write_sentence(self, words: List[str]) -> None:
        """Write all *words* followed by the empty-word terminator.

        Acquires the internal lock so that concurrent callers do not
        interleave their words.
        """
        with self._lock:
            buf = b""
            for word in words:
                encoded = word.encode("utf-8")
                buf += encode_length(len(encoded)) + encoded
            # Terminating empty word.
            buf += encode_length(0)
            self._sock.sendall(buf)
