"""Explicit registry for deployable object-detection model artifacts."""

from __future__ import annotations

from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass
from importlib import metadata
from typing import Any, cast

from .base import DetectionModel


class DetectionModelRegistryError(RuntimeError):
    """Base class for detection model registration and lookup failures."""


class DuplicateDetectionModelError(DetectionModelRegistryError):
    """Raised when a model identifier is registered more than once."""


class UnknownDetectionModelError(DetectionModelRegistryError):
    """Raised when a requested model identifier has not been registered."""


DetectionModelFactory = Callable[[Mapping[str, Any]], DetectionModel]


@dataclass(frozen=True, slots=True)
class DetectionModelSpec:
    """Static metadata and factory for one versioned model artifact."""

    model_id: str
    name: str
    supported_labels: tuple[str, ...]
    factory: DetectionModelFactory
    backend: str
    description: str = ""

    def __post_init__(self) -> None:
        if not isinstance(self.model_id, str) or "@" not in self.model_id:
            raise ValueError(
                "model id must be versioned (for example detector-name@1): "
                f"{self.model_id}"
            )
        identifier, version = self.model_id.rsplit("@", maxsplit=1)
        if not identifier or not version.isdigit() or int(version) < 1:
            raise ValueError(
                "model id must end in a positive numeric version: " f"{self.model_id}"
            )

        if not isinstance(self.name, str) or not self.name.strip():
            raise ValueError("model name must be a non-empty string")
        if not isinstance(self.backend, str) or not self.backend.strip():
            raise ValueError("model backend must be a non-empty string")
        if not isinstance(self.description, str):
            raise TypeError("model description must be a string")
        if not callable(self.factory):
            raise TypeError("detection model factory must be callable")

        labels_value = self.supported_labels
        if not isinstance(labels_value, Sequence) or isinstance(
            labels_value, (str, bytes, bytearray)
        ):
            raise TypeError("supported_labels must be an ordered collection")
        labels = tuple(labels_value)
        if not labels:
            raise ValueError("supported_labels must not be empty")

        invalid = [
            repr(label)
            for label in labels
            if not isinstance(label, str) or not label.strip()
        ]
        if invalid:
            raise ValueError(
                "supported_labels must contain non-empty strings: " + ", ".join(invalid)
            )
        seen: set[str] = set()
        duplicates: list[str] = []
        for label in labels:
            if label in seen and label not in duplicates:
                duplicates.append(label)
            seen.add(label)
        if duplicates:
            raise ValueError(
                "supported_labels must be unique; duplicate(s): "
                + ", ".join(duplicates)
            )

        object.__setattr__(self, "name", self.name.strip())
        object.__setattr__(self, "backend", self.backend.strip())
        object.__setattr__(self, "supported_labels", labels)


class DetectionModelRegistry:
    """In-memory model registry with optional Python entry-point discovery."""

    ENTRY_POINT_GROUP = "cmvr_edge_ai.detection_models"

    def __init__(self) -> None:
        self._models: dict[str, DetectionModelSpec] = {}

    def register(self, spec: DetectionModelSpec) -> DetectionModelSpec:
        if not isinstance(spec, DetectionModelSpec):
            raise TypeError("detection model registry accepts DetectionModelSpec")
        if spec.model_id in self._models:
            raise DuplicateDetectionModelError(
                f"detection model already registered: {spec.model_id}"
            )
        self._models[spec.model_id] = spec
        return spec

    def resolve(self, model_id: str) -> DetectionModelSpec:
        try:
            return self._models[model_id]
        except KeyError as exc:
            available = ", ".join(sorted(self._models)) or "<none>"
            raise UnknownDetectionModelError(
                f"unknown detection model {model_id!r}; available models: {available}"
            ) from exc

    def specs(self) -> tuple[DetectionModelSpec, ...]:
        return tuple(self._models[key] for key in sorted(self._models))

    def load_entry_points(self) -> None:
        """Load model specs or registration callbacks from installed packages."""

        discovered = cast(Any, metadata.entry_points())
        selected = (
            discovered.select(group=self.ENTRY_POINT_GROUP)
            if hasattr(discovered, "select")
            else discovered.get(self.ENTRY_POINT_GROUP, ())
        )
        for entry_point in selected:
            loaded = entry_point.load()
            if isinstance(loaded, DetectionModelSpec):
                self.register(loaded)
            elif callable(loaded):
                loaded(self)
            else:
                raise DetectionModelRegistryError(
                    f"entry point {entry_point.name!r} must expose "
                    "DetectionModelSpec or a registry callback"
                )


__all__ = [
    "DetectionModelFactory",
    "DetectionModelRegistry",
    "DetectionModelRegistryError",
    "DetectionModelSpec",
    "DuplicateDetectionModelError",
    "UnknownDetectionModelError",
]
