from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any, Callable
from ._exceptions import CodecError

@dataclass(frozen=True, slots=True)
class CodecInfo:
    name: str
    available: bool


class BinaryCodecRegistry:
    def __init__(self) -> None:
        self._codecs: dict[str, tuple[Callable[[Any], bytes], Callable[[bytes], Any]]] = {}
        self._register_builtin_codecs()

    def _register_builtin_codecs(self) -> None:
        self.register("json", lambda obj: json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8"), lambda raw: json.loads(raw.decode("utf-8")))
        try:
            import msgpack
            self.register("msgpack", lambda obj: msgpack.packb(obj, use_bin_type=True), lambda raw: msgpack.unpackb(raw, raw=False))
        except Exception:
            pass
        try:
            import cbor2
            self.register("cbor", lambda obj: cbor2.dumps(obj), lambda raw: cbor2.loads(raw))
        except Exception:
            pass

    def register(self, name: str, encoder: Callable[[Any], bytes], decoder: Callable[[bytes], Any]) -> None:
        if not isinstance(name, str) or not name:
            raise ValueError("name must be a non-empty string")
        if not callable(encoder) or not callable(decoder):
            raise TypeError("encoder and decoder must be callable")
        self._codecs[name] = (encoder, decoder)

    def unregister(self, name: str) -> bool:
        if name == "json":
            raise CodecError("cannot unregister built-in json codec")
        return self._codecs.pop(name, None) is not None

    def available_codecs(self) -> list[CodecInfo]:
        names = ["json", "msgpack", "cbor"]
        out = [CodecInfo(name=name, available=name in self._codecs) for name in names]
        for name in sorted(self._codecs):
            if name not in names:
                out.append(CodecInfo(name=name, available=True))
        return out

    def encode(self, obj: Any, *, codec: str = "json") -> bytes:
        try:
            encoder, _ = self._codecs[codec]
        except KeyError as e:
            raise CodecError(f"Unknown or unavailable codec: {codec}") from e
        try:
            data = encoder(obj)
        except Exception as e:
            raise CodecError(f"Failed to encode with codec '{codec}': {e}") from e
        if not isinstance(data, (bytes, bytearray, memoryview)):
            raise CodecError(f"Codec '{codec}' encoder did not return bytes-like data")
        return bytes(data)

    def decode(self, raw: bytes | bytearray | memoryview, *, codec: str = "json") -> Any:
        try:
            _, decoder = self._codecs[codec]
        except KeyError as e:
            raise CodecError(f"Unknown or unavailable codec: {codec}") from e
        if not isinstance(raw, (bytes, bytearray, memoryview)):
            raise TypeError("raw must be bytes-like")
        try:
            return decoder(bytes(raw))
        except Exception as e:
            raise CodecError(f"Failed to decode with codec '{codec}': {e}") from e