"""Abstract base classes for recording adapters.
Such as TelnetAdapter, RecorderAdapter, etc.
"""

import contextlib
import importlib
import inspect
import pkgutil
import socket
import sys
import time
from abc import ABC, abstractmethod
from functools import cache
from typing import TypedDict

from lrc_core.config import SMP_IP, SMP_PW
from lrc_core.exception import LrcError

DEFINED_RECORDER_ADAPTERS = None

# Telnet protocol constants
_IAC = 0xFF  # Interpret As Command
_DONT = 0xFE
_DO = 0xFD
_WONT = 0xFC
_WILL = 0xFB


class Telnet:
    """Minimal synchronous Telnet client (replaces removed stdlib telnetlib).

    Handles IAC option negotiation (responds WONT/DONT to all requests)
    and provides the read/write interface expected by TelnetAdapter.
    """

    def __init__(self, host: str, port: int = 23, timeout: float = 10) -> None:
        self._sock = socket.create_connection((host, port), timeout=timeout)
        self._buffer = b""

    def _recv(self, timeout: float) -> bytes:
        """Read from socket, strip and negotiate IAC sequences."""
        self._sock.settimeout(timeout)
        try:
            raw = self._sock.recv(4096)
        except TimeoutError:
            return b""

        cleaned = bytearray()
        i = 0
        while i < len(raw):
            b = raw[i]
            if b == _IAC and i + 1 < len(raw):
                cmd = raw[i + 1]
                if cmd == _IAC:
                    cleaned.append(_IAC)
                    i += 2
                elif cmd in (_DO, _WILL):
                    opt = raw[i + 2] if i + 2 < len(raw) else 0
                    reply = _WONT if cmd == _DO else _DONT
                    with contextlib.suppress(OSError):
                        self._sock.send(bytes([_IAC, reply, opt]))
                    i += 3
                elif cmd in (_DONT, _WONT):
                    i += 3
                else:
                    i += 2
            else:
                cleaned.append(b)
                i += 1
        return bytes(cleaned)

    def read_until(self, match: str | bytes, timeout: float | None = None) -> bytes:
        """Read until *match* is found in the stream or *timeout* expires.

        Always returns bytes (may be shorter than match if timeout fires).
        """
        if isinstance(match, str):
            match = match.encode("ascii")

        deadline = time.monotonic() + (timeout if timeout is not None else 10)

        while True:
            idx = self._buffer.find(match)
            if idx >= 0:
                end = idx + len(match)
                result, self._buffer = self._buffer[:end], self._buffer[end:]
                return result

            remaining = deadline - time.monotonic()
            if remaining <= 0:
                result, self._buffer = self._buffer, b""
                return result

            self._buffer += self._recv(min(remaining, 0.2))

    def write(self, buffer: str | bytes) -> int:
        """Write *buffer* to the connection (str is encoded as ASCII)."""
        if isinstance(buffer, str):
            buffer = buffer.encode("ascii")
        # Escape any literal IAC bytes in user data
        buffer = buffer.replace(bytes([_IAC]), bytes([_IAC, _IAC]))
        return self._sock.send(buffer)

    def read_line(self, timeout: float = 2) -> str:
        """Read until newline; return decoded, rstripped string."""
        data = self.read_until(b"\n", timeout)
        try:
            return data.decode("ascii").rstrip()
        except UnicodeDecodeError:
            return data.decode("utf-8").rstrip()

    def read_until_non_empty_line(self) -> str | None:
        """Read lines until the first non-empty one and return it."""
        line = self.read_line()
        if line is None:
            return None
        while len(line.rstrip()) <= 0:
            line = self.read_line()
        return line

    def assert_string_in_output(
        self,
        string: str | list[str],
        timeout: int = 2,
    ) -> tuple[bool, str]:
        """Check whether *string* (or any item in a list) appears in upcoming output.

        Returns ``(found, response_str)``.
        """
        resp = ""
        strings = [string] if isinstance(string, str) else string
        for s in strings:
            raw = self.read_until(s, timeout)
            if raw is None:
                continue
            try:
                resp = raw.decode("ascii")
            except UnicodeDecodeError:
                resp = raw.decode("utf-8")
            if s in resp:
                return True, resp
        return False, resp

    def close(self) -> None:
        self._sock.close()


class TelnetAdapter(ABC):
    """
    Abstract base class for Telnet adapters.

    Attributes:
        address (str): The IP address or hostname of the device to connect to.
        tn (Telnet): The Telnet connection object.
        esc_char (str): The escape character to use for the Telnet connection.
    """

    def __init__(self, address, esc_char="W"):
        self.address = address
        self.tn = None
        self.esc_char = esc_char

    @abstractmethod
    def _login(self):
        pass

    def _run_cmd(self, cmd, _timeout=1, auto_connect=True):
        if self.tn is None and not auto_connect:
            raise LrcError("Not connected!")
        if self.tn is None:
            self._login()
        self.tn.write(cmd)
        out = self.tn.read_until_non_empty_line()
        res = out
        while out is not None and out != "":
            out = self.tn.read_until_non_empty_line()
            print(out)
            res += out
        return res

    @staticmethod
    def _get_response_str(tn_response):
        if isinstance(tn_response, bytes):
            try:
                return str(tn_response.decode("ascii").rstrip())
            except UnicodeDecodeError:
                return str(tn_response.decode("utf-8").rstrip())
        else:
            return str(tn_response).rstrip()


RecorderModel = TypedDict(
    "RecorderModel",
    {
        "id": str,
        "name": str,
        "commands": dict[str, dict[str, str]],
        "path": str | None,  # __file__ kann in seltenen Fällen None sein
        "requires_user": bool,
        "requires_password": bool,
        "class": type,  # Typ-Referenz für die Adapter-Klasse
    },
    total=False,
)


class RecorderAdapter:
    """
    Abstract base class for recorder adapters.

    Attributes:
        address (str): The address of the recorder.
        user (str): The username for the recorder.
        password (str): The password for the recorder.
    """

    def __init__(self, address: str, user: str, password: str):
        self.address = address
        self.user = user
        self.password = password

    @classmethod
    @abstractmethod
    def get_recorder_params(cls) -> dict:
        """
        Returns a dictionary of parameters for the recorder.

        Returns:
            dict: A dictionary of parameters for the recorder.
        """

    @abstractmethod
    def _get_name(self) -> str:
        """
        Returns the name of the recorder.

        Returns:
            str: The name of the recorder.
        """

    @abstractmethod
    def _get_version(self) -> str:
        """
        Returns the version of the recorder.

        Returns:
            str: The version of the recorder.
        """

    @abstractmethod
    def is_recording(self) -> bool:
        """
        Returns True if the recorder is currently recording, False otherwise.

        Returns:
            bool: True if the recorder is currently recording, False otherwise.
        """

    @abstractmethod
    def get_recording_status(self) -> str:
        """
        Returns the recording status of the recorder.

        Returns:
            str: The recording status of the recorder.
        """


def get_defined_recorder_adapters() -> list[RecorderModel]:
    """
    Returns a list of recorder adapter models defined in the recorder_adapters package.

    Each model is represented as a dictionary with the following keys:
    - id: The unique identifier of the model.
    - name: The name of the model.
    - commands: A dictionary of commands supported by the model, where each key
        is the name of the command and the value is a dictionary of parameter names and types.
    - path: The file path of the module defining the model.
    - requires_user: (Optional) A boolean indicating whether the model requires
        a user to be authenticated.
    - requires_password: (Optional) A boolean indicating whether the model requires
        a password to be authenticated.
    """
    rec_adapters_module = importlib.import_module(
        ".recorder_adapters",
        package="backend",
    )
    rec_adapter_class = (
        rec_adapters_module.RecorderAdapter
    )  # needed, otherwise subclass check may fail
    models = []
    found_packages = list(pkgutil.iter_modules(rec_adapters_module.__path__))
    for f_p in found_packages:
        full_module_name = f"{rec_adapters_module.__name__}.{f_p.name}"

        # Importiere das Modul sauber über importlib
        rec_model_module = importlib.import_module(full_module_name)

        rec_model: RecorderModel = {
            "id": f_p.name,
            "name": f_p.name,
            "commands": {},
            "path": rec_model_module.__file__,
        }
        if hasattr(rec_model_module, "RECORDER_MODEL_NAME"):
            rec_model["name"] = rec_model_module.RECORDER_MODEL_NAME
        if hasattr(rec_model_module, "REQUIRES_USER"):
            rec_model["requires_user"] = rec_model_module.REQUIRES_USER
        if hasattr(rec_model_module, "REQUIRES_PW"):
            rec_model["requires_password"] = rec_model_module.REQUIRES_PW
        for name, obj in inspect.getmembers(rec_model_module, inspect.isclass):
            if issubclass(obj, rec_adapter_class) and name != "RecorderAdapter":
                rec_model["id"] = rec_model["id"] + "." + obj.__name__
                rec_model["class"] = obj
                commands = {}
                for method_name, method in inspect.getmembers(
                    obj,
                    predicate=inspect.isfunction,
                ):
                    if len(method_name) > 0 and method_name[0] == "_":
                        continue
                    signature = inspect.signature(method)
                    parameters: dict[str, str] = {}
                    for params in signature.parameters:
                        if params == "self":
                            continue
                        param_type = signature.parameters[params].annotation.__name__
                        param_type = (
                            "_unknown_type" if param_type == "_empty" else param_type
                        )
                        parameters[signature.parameters[params].name] = param_type
                    commands[method_name] = parameters
                rec_model["commands"] = commands
        models.append(rec_model)
    return models


@cache
def get_cached_recorder_adapters() -> list[RecorderModel]:
    """Wrapper, der das Ergebnis beim ersten Aufruf für immer zwischengespeichert."""
    return get_defined_recorder_adapters()


def get_recorder_adapter_by_id(rec_id: str, **kwargs):
    """
    Returns a recorder adapter instance based on the given ID.
    ...
    """
    adapters = get_cached_recorder_adapters()

    for rec_adapter in adapters:
        if rec_id in rec_adapter.get("id", "").split("."):
            return rec_adapter["class"](**kwargs)

    return None


if __name__ == "__main__":
    print(get_defined_recorder_adapters())
    get_recorder_adapter_by_id("SMP35x", address=SMP_IP, password=SMP_PW)
    sys.exit()
