"""Serialization and parsing helpers for CRP messages."""

from __future__ import annotations

import hashlib
import json
from dataclasses import asdict

from .constants import CRP_VERSION
from .errors import ProtocolValidationError
from .messages import (
    ClipboardFormat,
    ClipMessage,
    DiscoverMessage,
    DiscoverReplyMessage,
    ErrorMessage,
    HeartbeatMessage,
    JoinAckMessage,
    JoinMessage,
    LeaveMessage,
)
from .types import CRPMessage, JSONObject


def hash_passphrase(passphrase: str) -> str:
    """Return SHA256(passphrase) in hex format."""
    if not isinstance(passphrase, str):
        raise ProtocolValidationError("'passphrase' must be string")
    return hashlib.sha256(passphrase.encode("utf-8")).hexdigest()


def _validate_protocol(payload: JSONObject) -> None:
    protocol = payload.get("protocol")
    if protocol is None:
        return
    if protocol != CRP_VERSION:
        raise ProtocolValidationError(
            f"Unsupported protocol '{protocol}', expected '{CRP_VERSION}'"
        )


def parse_message(payload: JSONObject) -> CRPMessage:
    """Convert a dict payload into a typed CRP message."""
    if not isinstance(payload, dict):
        raise ProtocolValidationError("Message payload must be a JSON object")

    _validate_protocol(payload)

    message_type = payload.get("type")
    if message_type == "join":
        return JoinMessage(
            session=payload["session"],
            auth=payload["auth"],
            client_name=payload["client_name"],
        )
    if message_type == "join_ack":
        return JoinAckMessage(session=payload["session"])
    if message_type == "clip":
        raw_formats = payload.get("formats", [])
        if not isinstance(raw_formats, list):
            raise ProtocolValidationError("'formats' must be a list")
        return ClipMessage(
            session=payload["session"],
            message_id=payload["message_id"],
            formats=[
                ClipboardFormat(
                    mime=item["mime"],
                    encoding=item["encoding"],
                    data=item["data"],
                )
                for item in raw_formats
            ],
        )
    if message_type == "leave":
        return LeaveMessage()
    if message_type == "heartbeat":
        return HeartbeatMessage()
    if message_type == "error":
        return ErrorMessage(message=payload["message"])
    if message_type == "discover":
        return DiscoverMessage()
    if message_type == "discover_reply":
        return DiscoverReplyMessage(
            session=payload["session"],
            requires_passphrase=payload["requires_passphrase"],
            host=payload["host"],
            port=payload["port"],
        )

    raise ProtocolValidationError(f"Unsupported message type '{message_type}'")


def message_to_dict(message: CRPMessage) -> JSONObject:
    """Convert a typed CRP message into a JSON-ready dictionary."""
    return asdict(message)


def encode_message(message: CRPMessage) -> bytes:
    """Encode message as UTF-8 newline-delimited JSON."""
    return (json.dumps(message_to_dict(message), separators=(",", ":")) + "\n").encode(
        "utf-8"
    )


def decode_message_line(line: str | bytes) -> CRPMessage:
    """Decode one newline-delimited JSON message."""
    if isinstance(line, bytes):
        line = line.decode("utf-8")
    line = line.strip()
    if not line:
        raise ProtocolValidationError("Empty message line")
    try:
        payload = json.loads(line)
    except json.JSONDecodeError as exc:
        raise ProtocolValidationError(f"Invalid JSON message: {exc}") from exc
    return parse_message(payload)
