"""
IPC Protocol for ESPal daemon.

Defines message types and serialisation for client-server communication
over Unix domain sockets.

Protocol: Newline-delimited JSON messages.
Binary data (serial bytes) is base64 encoded.
"""

import base64
import json
from dataclasses import dataclass, field, asdict
from typing import Optional, Any, Union
from enum import Enum


class SubscribeMode(Enum):
    """Subscription modes for monitor clients."""
    LIVE = "live"           # Stream from current position
    SINCE = "since"         # Replay from marker, then live
    TAIL = "tail"           # Last N entries, then live
    REPLAY = "replay"       # Historical only, disconnect after


class DataSource(Enum):
    """Source of data in the buffer."""
    SERIAL = "serial"
    STDOUT = "stdout"
    STDERR = "stderr"
    MARKER = "marker"


# ============================================================================
# Client -> Server Commands
# ============================================================================

@dataclass
class SubscribeCommand:
    """Subscribe to data stream."""
    mode: str = "live"
    marker: Optional[str] = None  # For 'since' mode
    lines: Optional[int] = None   # For 'tail' mode

    def to_dict(self) -> dict:
        return {"cmd": "subscribe", **{k: v for k, v in asdict(self).items() if v is not None}}


@dataclass
class BuildCommand:
    """Trigger a build operation."""
    args: list[str] = field(default_factory=list)
    environment: Optional[str] = None

    def to_dict(self) -> dict:
        d = {"cmd": "build", "args": self.args}
        if self.environment:
            d["environment"] = self.environment
        return d


@dataclass
class UploadCommand:
    """Trigger an upload operation."""
    args: list[str] = field(default_factory=list)
    environment: Optional[str] = None

    def to_dict(self) -> dict:
        d = {"cmd": "upload", "args": self.args}
        if self.environment:
            d["environment"] = self.environment
        return d


@dataclass
class StatusCommand:
    """Request daemon status."""

    def to_dict(self) -> dict:
        return {"cmd": "status"}


@dataclass
class ShutdownCommand:
    """Request daemon shutdown."""

    def to_dict(self) -> dict:
        return {"cmd": "shutdown"}


@dataclass
class UnsubscribeCommand:
    """Unsubscribe from data stream."""

    def to_dict(self) -> dict:
        return {"cmd": "unsubscribe"}


# ============================================================================
# Server -> Client Messages
# ============================================================================

@dataclass
class DataMessage:
    """Data from serial port or build output."""
    source: str          # "serial", "stdout", "stderr"
    timestamp: float     # Unix timestamp
    data: bytes          # Raw bytes

    def to_dict(self) -> dict:
        return {
            "type": "data",
            "source": self.source,
            "ts": self.timestamp,
            "data": base64.b64encode(self.data).decode("ascii")
        }

    @classmethod
    def from_dict(cls, d: dict) -> "DataMessage":
        return cls(
            source=d["source"],
            timestamp=d["ts"],
            data=base64.b64decode(d["data"])
        )


@dataclass
class MarkerMessage:
    """A marker in the data stream."""
    name: str
    timestamp: float

    def to_dict(self) -> dict:
        return {
            "type": "marker",
            "name": self.name,
            "ts": self.timestamp
        }

    @classmethod
    def from_dict(cls, d: dict) -> "MarkerMessage":
        return cls(name=d["name"], timestamp=d["ts"])


@dataclass
class StatusMessage:
    """Daemon status response."""
    port: Optional[str]      # Current device port
    port_state: str          # "connected", "disconnected", "released"
    clients: int             # Number of connected clients
    buffer_bytes: int        # Current buffer size
    buffer_entries: int      # Number of buffer entries
    uptime: float            # Daemon uptime in seconds
    idle_timeout: float      # Seconds until idle shutdown
    vid_pid: Optional[str]   # Device VID:PID if known

    def to_dict(self) -> dict:
        return {"type": "status", **asdict(self)}

    @classmethod
    def from_dict(cls, d: dict) -> "StatusMessage":
        d = d.copy()
        d.pop("type", None)
        return cls(**d)


@dataclass
class ErrorMessage:
    """Error response."""
    code: str
    message: str

    def to_dict(self) -> dict:
        return {"type": "error", "code": self.code, "message": self.message}

    @classmethod
    def from_dict(cls, d: dict) -> "ErrorMessage":
        return cls(code=d["code"], message=d["message"])


@dataclass
class BuildOutputMessage:
    """Build process output line."""
    stream: str   # "stdout" or "stderr"
    line: str     # Text line

    def to_dict(self) -> dict:
        return {"type": "build_output", "stream": self.stream, "line": self.line}

    @classmethod
    def from_dict(cls, d: dict) -> "BuildOutputMessage":
        return cls(stream=d["stream"], line=d["line"])


@dataclass
class BuildCompleteMessage:
    """Build process completion."""
    exit_code: int
    success: bool

    def to_dict(self) -> dict:
        return {"type": "build_complete", "exit_code": self.exit_code, "success": self.success}

    @classmethod
    def from_dict(cls, d: dict) -> "BuildCompleteMessage":
        return cls(exit_code=d["exit_code"], success=d["success"])


@dataclass
class SubscribedMessage:
    """Confirmation of subscription."""
    mode: str
    position: int  # Starting sequence number

    def to_dict(self) -> dict:
        return {"type": "subscribed", "mode": self.mode, "position": self.position}

    @classmethod
    def from_dict(cls, d: dict) -> "SubscribedMessage":
        return cls(mode=d["mode"], position=d["position"])


@dataclass
class ShutdownNotice:
    """Notification that daemon is shutting down."""
    reason: str

    def to_dict(self) -> dict:
        return {"type": "shutdown", "reason": self.reason}

    @classmethod
    def from_dict(cls, d: dict) -> "ShutdownNotice":
        return cls(reason=d["reason"])


# ============================================================================
# Message Parsing
# ============================================================================

# Command type registry
COMMAND_TYPES = {
    "subscribe": SubscribeCommand,
    "build": BuildCommand,
    "upload": UploadCommand,
    "status": StatusCommand,
    "shutdown": ShutdownCommand,
    "unsubscribe": UnsubscribeCommand,
}

# Response type registry
RESPONSE_TYPES = {
    "data": DataMessage,
    "marker": MarkerMessage,
    "status": StatusMessage,
    "error": ErrorMessage,
    "build_output": BuildOutputMessage,
    "build_complete": BuildCompleteMessage,
    "subscribed": SubscribedMessage,
    "shutdown": ShutdownNotice,
}

CommandType = Union[SubscribeCommand, BuildCommand, UploadCommand, StatusCommand, ShutdownCommand, UnsubscribeCommand]
ResponseType = Union[DataMessage, MarkerMessage, StatusMessage, ErrorMessage, BuildOutputMessage, BuildCompleteMessage, SubscribedMessage, ShutdownNotice]


class ProtocolError(Exception):
    """Error in protocol parsing or validation."""
    pass


def serialize(msg: Union[CommandType, ResponseType]) -> bytes:
    """
    Serialize a message to newline-delimited JSON bytes.

    Args:
        msg: Message object to serialize

    Returns:
        JSON bytes with trailing newline
    """
    return (json.dumps(msg.to_dict()) + "\n").encode("utf-8")


def parse_command(data: dict) -> CommandType:
    """
    Parse a command from a dictionary.

    Args:
        data: Parsed JSON dictionary

    Returns:
        Command object

    Raises:
        ProtocolError: If command is invalid
    """
    cmd = data.get("cmd")
    if not cmd:
        raise ProtocolError("Missing 'cmd' field")

    if cmd not in COMMAND_TYPES:
        raise ProtocolError(f"Unknown command: {cmd}")

    cls = COMMAND_TYPES[cmd]

    try:
        # Extract fields for the command
        if cls == SubscribeCommand:
            return SubscribeCommand(
                mode=data.get("mode", "live"),
                marker=data.get("marker"),
                lines=data.get("lines")
            )
        elif cls == BuildCommand:
            return BuildCommand(
                args=data.get("args", []),
                environment=data.get("environment")
            )
        elif cls == UploadCommand:
            return UploadCommand(
                args=data.get("args", []),
                environment=data.get("environment")
            )
        elif cls == StatusCommand:
            return StatusCommand()
        elif cls == ShutdownCommand:
            return ShutdownCommand()
        elif cls == UnsubscribeCommand:
            return UnsubscribeCommand()
        else:
            raise ProtocolError(f"Unhandled command type: {cmd}")
    except (KeyError, TypeError) as e:
        raise ProtocolError(f"Invalid command format: {e}")


def parse_response(data: dict) -> ResponseType:
    """
    Parse a response from a dictionary.

    Args:
        data: Parsed JSON dictionary

    Returns:
        Response object

    Raises:
        ProtocolError: If response is invalid
    """
    msg_type = data.get("type")
    if not msg_type:
        raise ProtocolError("Missing 'type' field")

    if msg_type not in RESPONSE_TYPES:
        raise ProtocolError(f"Unknown message type: {msg_type}")

    cls = RESPONSE_TYPES[msg_type]

    try:
        return cls.from_dict(data)
    except (KeyError, TypeError) as e:
        raise ProtocolError(f"Invalid message format: {e}")


def parse_json_line(line: bytes) -> dict:
    """
    Parse a JSON line.

    Args:
        line: Raw bytes (may include trailing newline)

    Returns:
        Parsed dictionary

    Raises:
        ProtocolError: If JSON is invalid
    """
    try:
        text = line.decode("utf-8").strip()
        if not text:
            raise ProtocolError("Empty message")
        return json.loads(text)
    except json.JSONDecodeError as e:
        raise ProtocolError(f"Invalid JSON: {e}")
    except UnicodeDecodeError as e:
        raise ProtocolError(f"Invalid UTF-8: {e}")


class MessageFramer:
    """
    Handles framing of newline-delimited JSON messages.

    Accumulates incoming bytes and yields complete messages.
    """

    def __init__(self):
        self._buffer = b""

    def feed(self, data: bytes) -> list[bytes]:
        """
        Feed data and return any complete messages.

        Args:
            data: Incoming bytes

        Returns:
            List of complete message lines (without newlines)
        """
        self._buffer += data
        messages = []

        while b"\n" in self._buffer:
            line, self._buffer = self._buffer.split(b"\n", 1)
            if line:  # Skip empty lines
                messages.append(line)

        return messages

    def clear(self) -> None:
        """Clear the internal buffer."""
        self._buffer = b""
