"""
Static infrastructure for baltech_sdk.

This module contains base classes, protocols, and utilities that are used
by the generated API code. All classes in this module are static and do not
change between SDK versions.
"""

from __future__ import annotations

import abc
from io import BytesIO
from typing import Generic, TypeVar, Dict, Optional, List, Any, ClassVar, Type, Union, Iterable
from typing_extensions import Self

from brp_lib.err import PayloadTooShortError, PayloadTooLongError, PayloadFormatError, DeviceError, BrpError


__all__ = [
    "PayloadTooShortError",
    "PayloadTooLongError",
    "PayloadFormatError",
    "DeviceError",
    "BrpError",
    "FrameProcessor",
    "LiteralParser",
    "Command",
    "ConfigBase",
    "ConfigValue",
    "safe_read_int_from_buffer",
    "TemplateFrame",
    "TemplateBase",
    "BaltechScriptBase",
    "BaltechScriptFrame",
    "BRPFrame",
    "BRPBase",
]


# =============================================================================
# Utility Functions
# =============================================================================

def safe_read_int_from_buffer(buffer: BytesIO, field_len: int) -> int:
    """
    Read an integer from a BytesIO buffer with validation.

    Args:
        buffer: BytesIO buffer to read from
        field_len: Number of bytes to read

    Returns:
        Integer value read from buffer (big-endian)

    Raises:
        PayloadTooShortError: If buffer doesn't contain enough bytes
    """
    raw_data = buffer.read(field_len)
    if len(raw_data) != field_len:
        raise PayloadTooShortError(field_len - len(raw_data))
    else:
        return int.from_bytes(raw_data, byteorder='big')


# =============================================================================
# Frame Processing
# =============================================================================

class FrameProcessor(metaclass=abc.ABCMeta):
    """
    Abstract base class for objects that can process BRP frames.

    A frame processor handles the low-level communication with the reader,
    sending request frames and receiving response frames.
    """

    @abc.abstractmethod
    def process_frame(self, frame: bytes) -> bytes:
        """
        Process a BRP frame and return the response.

        Args:
            frame: Request frame bytes

        Returns:
            Response frame bytes

        Raises:
            BaltechApiError: On API errors
            Various protocol errors: On communication errors
        """
        ...


# =============================================================================
# Literal Parser (for Enums)
# =============================================================================

L = TypeVar("L")  # Literal type
V = TypeVar("V")  # Value type


class LiteralParser(Generic[L, V], metaclass=abc.ABCMeta):
    """
    Parser for enum-like literal values.

    Maps between string literals (e.g., "Disabled", "Enabled") and
    numeric values (e.g., 0, 1).

    Type Parameters:
        L: Literal type (usually str)
        V: Value type (usually int)
    """

    def __init__(
            self,
            name: str,
            literal_map: Dict[L, V],
            undefined_literal: Optional[L] = None
    ) -> None:
        """
        Initialize literal parser.

        Args:
            name: Name of the enum type (for error messages)
            literal_map: Mapping from literals to values
            undefined_literal: Literal to use for undefined values (optional)
        """
        self.name = name
        self.literal_map = literal_map
        self.value_map = {v: k for k, v in literal_map.items()}
        self.undefined_literal = undefined_literal

    def as_value(self, literal: L) -> V:
        """
        Convert literal to value.

        Args:
            literal: Literal to convert

        Returns:
            Corresponding value

        Raises:
            ValueError: If literal is not valid and no undefined_literal set
        """
        if literal not in self.literal_map:
            if self.undefined_literal is not None:
                literal = self.undefined_literal
            else:
                raise ValueError(f"'{ literal }' is not a valid literal for type {self.name}")
        return self.literal_map[literal]

    def as_literal(self, value: V) -> L:
        """
        Convert value to literal.

        Args:
            value: Value to convert

        Returns:
            Corresponding literal

        Raises:
            ValueError: If value is not valid and no undefined_literal set
        """
        if value not in self.value_map:
            if self.undefined_literal is not None:
                value = self.literal_map[self.undefined_literal]
            else:
                raise ValueError(f"'{ value }' is not a valid value for type {self.name}")
        return self.value_map[value]


# =============================================================================
# Command Infrastructure
# =============================================================================

class Command:
    """
    Base class for all BRP command classes.

    Each command class inherits from this and implements build_frame() and
    __call__() methods for executing the command.
    """

    def __init__(self, frame_processor: FrameProcessor) -> None:
        """
        Initialize command with a frame processor.

        Args:
            frame_processor: Processor for sending/receiving frames
        """
        self.frame_processor = frame_processor

    def process_frame(self, frame: bytes) -> bytes:
        """
        Process a frame through the frame processor.

        Args:
            frame: Request frame bytes

        Returns:
            Response frame bytes
        """
        return self.frame_processor.process_frame(frame)


# =============================================================================
# Configuration Infrastructure
# =============================================================================


class ConfigBase(metaclass=abc.ABCMeta):
    """
    Base class for all configuration operations.

    Provides frame processing for configuration get/set/delete operations.
    """

    MasterKey: ClassVar[int]
    SubKey: ClassVar[Optional[int]] = None
    ValueKey: ClassVar[Optional[int]] = None
    MasterKeyCount: ClassVar[int] = 1
    SubKeyCount: ClassVar[int] = 1
    ValueKeyCount: ClassVar[int] = 1

    def __init__(self, frame_processor: FrameProcessor) -> None:
        """
        Initialize configuration with a frame processor.

        Args:
            frame_processor: Processor for sending/receiving frames
        """
        self.frame_processor = frame_processor

    def process_frame(self, frame: bytes) -> bytes:
        """
        Process a configuration frame.

        Args:
            frame: Configuration frame (mode + keys + content)

        Returns:
            Response frame bytes
        """
        return self.frame_processor.process_frame(frame)


class ConfigValue(ConfigBase, metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def parse_frame(self, payload: bytes) -> Any:
        """
        Parse configuration value from payload bytes.

        Args:
            payload: Raw payload bytes from configuration response

        Returns:
            Parsed configuration value (type depends on configuration)
        """
        ...


# =============================================================================
# Protocols
# =============================================================================
class ProtocolFrame(metaclass=abc.ABCMeta):
    """Abstract base class for protocol frame types (Template, BaltechScript)."""

    @classmethod
    @abc.abstractmethod
    def read_from(cls, buffer: BytesIO) -> Self:
        """
        Read a frame from a BytesIO buffer.

        Args:
            buffer: BytesIO buffer to read from

        Returns:
            Parsed frame instance
        """
        ...

    @abc.abstractmethod
    def __bytes__(self) -> bytes:
        """
        Serialize frame to bytes.

        Returns:
            Frame as bytes
        """
        ...


F = TypeVar("F", bound=ProtocolFrame)


class ProtocolBase(Generic[F], metaclass=abc.ABCMeta):
    """
    Abstract base class for protocol implementations (Template, BaltechScript).

    Type Parameters:
        F: Frame type (must be a ProtocolFrame subclass)
    """

    @classmethod
    @abc.abstractmethod
    def parse_frames(cls, data: bytes) -> List[F]:
        """
        Parse multiple frames from bytes.

        Args:
            data: Raw bytes containing one or more frames

        Returns:
            List of parsed frames
        """
        ...

    def __init__(self, frames: Optional[Union[Iterable[F], bytes]] = None) -> None:
        """
        Initialize protocol with frames.

        Args:
            frames: Can be:
                - None: Create empty protocol
                - bytes: Parse frames from bytes
                - Iterable[F]: Use provided frames
        """
        if frames is None:
            self.frames: List[F] = []
        elif isinstance(frames, bytes):
            self.frames = self.parse_frames(frames)
        else:
            self.frames = list(frames)

    @abc.abstractmethod
    def frame_header(self, frame: F) -> bytes:
        """
        Generate header bytes for a frame.

        Args:
            frame: Frame to generate header for

        Returns:
            Header bytes
        """
        ...

    def __bytes__(self) -> bytes:
        """Serialize all frames to bytes with headers."""
        return b''.join(map(lambda f: self.frame_header(f) + bytes(f), self.frames))

    def __add__(self, other: Self) -> Self:
        """Concatenate two protocol instances."""
        return type(self)([*self.frames, *other.frames])

    def __iadd__(self, other: Self) -> Self:
        """In-place concatenation."""
        self.frames += other.frames
        return self

    def _as_string(self, *, sep: str) -> str:
        """Format frames as string with given separator."""
        constructor = f"{ type(self).__name__ }()"
        if not self.frames:
            return constructor
        parts = [constructor, *map(repr, self.frames)]
        return f"{sep.join(parts)}"

    def __repr__(self) -> str:
        """String representation for REPL."""
        return self._as_string(sep=".")

    def __str__(self) -> str:
        return repr(self)


class TemplateFrame(ProtocolFrame, metaclass=abc.ABCMeta):
    Code: Optional[int] = None


class Static(TemplateFrame):
    def __init__(self, Data: bytes) -> None:
        self.Data = Data

    @classmethod
    def read_from(cls, frame: BytesIO) -> Self:
        data = b''
        while True:
            next_byte = frame.read(1)
            if not next_byte:
                break
            if next_byte == b"\x1B":
                frame.seek(frame.tell( ) -1)
                break
            data += next_byte
        return cls(data)

    def __bytes__(self) -> bytes:
        return self.Data

    def __repr__(self) -> str:
        return f"Static(Data={ repr(self.Data) })"


class TemplateBase(ProtocolBase[TemplateFrame]):
    CodeMap: Dict[int, Type[TemplateFrame]] = {}

    def Static(self, Data: bytes) -> Self:
        self.frames.append(Static(Data))
        return self

    def frame_header(self, frame: TemplateFrame) -> bytes:
        if frame.Code is not None:
            return frame.Code.to_bytes(length=2, byteorder='big')
        return b''

    @classmethod
    def parse_frames(cls, data: bytes) -> List[TemplateFrame]:
        buffer = BytesIO(data)
        frames: List[TemplateFrame] = []
        while True:
            next_byte = buffer.read(1)
            if not next_byte:
                break
            if next_byte != b'\x1B':
                buffer.seek(buffer.tell() - 1)
                frames.append(Static.read_from(buffer))
            else:
                code = int.from_bytes(next_byte + buffer.read(1), byteorder="big")
                frame_class = cls.CodeMap.get(code)
                if frame_class is None:
                    raise PayloadFormatError(f'Invalid Template code 0x{code:04X}')
                frames.append(frame_class.read_from(buffer))

        return frames



class BaltechScriptFrame(ProtocolFrame, metaclass=abc.ABCMeta):
    Code: int


class BaltechScriptBase(ProtocolBase[BaltechScriptFrame]):
    CodeMap: Dict[int, Type[BaltechScriptFrame]] = {}

    def frame_header(self, frame: BaltechScriptFrame) -> bytes:
        return frame.Code.to_bytes(length=1, byteorder='big')

    @classmethod
    def parse_frames(cls, data: bytes) -> List[BaltechScriptFrame]:
        buffer = BytesIO(data)
        frames: List[BaltechScriptFrame] = []
        while True:
            code_byte = buffer.read(1)
            if not code_byte:
                break
            else:
                code = int.from_bytes(code_byte, byteorder="big")
                frame_class = cls.CodeMap.get(code)
                if frame_class is None:
                    raise PayloadFormatError(f'Invalid BaltechScript code 0x{code:02X}')
                frames.append(frame_class.read_from(buffer))

        return frames


class BRPFrame(ProtocolFrame, metaclass=abc.ABCMeta):
    pass


class BRPBase(ProtocolBase[BRPFrame]):
    def frame_header(self, frame: BRPFrame) -> bytes:
        raise NotImplementedError()

    @classmethod
    def parse_frames(cls, data: bytes) -> List[BRPFrame]:
        raise NotImplementedError()
