"""Role registry — pluggable agent-role catalog.

OSS roles are pre-registered at framework boot from
``decepticon_core.contracts.slots.SLOTS_PER_ROLE``. Plugins register
custom roles via the new ``decepticon.roles`` entry-point group;
this closes gap §8 #5 (a vendor ``apt`` previously had to abuse
``default_role="decepticon"`` in ``LLMFactory``).

Per spec §16.4 #3 ``register()`` is idempotent on identical parameters:
multi-process workers each register on startup without conflict.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Literal

from decepticon_core.contracts.slots import MiddlewareSlot

AgentPurpose = Literal["execution", "planning", "observation"]


def default_role_purpose(name: str) -> AgentPurpose:
    """Unknown roles require execution authorization unless explicitly registered."""
    if name == "soundwave":
        return "planning"
    if name == "blue_cell":
        return "observation"
    return "execution"


@dataclass(frozen=True)
class RoleSpec:
    """Frozen description of one registered role.

    Stored in ``RoleRegistry``; used by the framework's middleware
    assembler and LLM factory at boot.
    """

    name: str
    slots: frozenset[MiddlewareSlot]
    skill_sources: tuple[str, ...] = ()
    llm_role_fallback: str | None = None
    purpose: AgentPurpose = "execution"


class RoleRegistry:
    """Process-wide singleton role catalog.

    Stays inside the contract layer so plugin authors can register
    roles without importing the framework. The framework's
    ``LLMFactory`` and middleware assembler consume this registry at
    boot (wired in Phase 2).
    """

    _entries: dict[str, RoleSpec] = {}

    @classmethod
    def register(
        cls,
        name: str,
        *,
        slots: frozenset[MiddlewareSlot],
        skill_sources: tuple[str, ...] = (),
        llm_role_fallback: str | None = None,
        purpose: AgentPurpose | None = None,
    ) -> None:
        """Register a role (idempotent on identical parameters).

        Re-registering with different parameters raises ``ValueError`` —
        callers should explicitly call ``unregister()`` first if they
        intend to replace.

        Idempotency check (spec §16.4 #3): if ``name`` already exists
        and the proposed ``RoleSpec`` is equal to the stored one, the
        call returns silently. Multi-process workers all calling
        ``register()`` on startup is therefore safe.
        """
        proposed = RoleSpec(
            name=name,
            slots=slots,
            skill_sources=skill_sources,
            llm_role_fallback=llm_role_fallback,
            purpose=purpose if purpose is not None else default_role_purpose(name),
        )
        existing = cls._entries.get(name)
        if existing is not None:
            if existing == proposed:
                return
            raise ValueError(
                f"role {name!r} already registered with different parameters; "
                f"call RoleRegistry.unregister({name!r}) first if intentional"
            )
        cls._entries[name] = proposed

    @classmethod
    def get(cls, name: str) -> RoleSpec | None:
        """Return the ``RoleSpec`` for ``name``, or ``None`` if unknown."""
        return cls._entries.get(name)

    @classmethod
    def list(cls) -> tuple[RoleSpec, ...]:
        """Return all registered specs in name-sorted order."""
        return tuple(cls._entries[name] for name in sorted(cls._entries))

    @classmethod
    def unregister(cls, name: str) -> None:
        """Remove ``name`` from the registry. No-op if not registered."""
        cls._entries.pop(name, None)
