"""The roster — which agents an edge runs: ours preconfigured, the customer's plugged in.

The owner's product direction (2026-08-04): *our AI agents arrive preconfigured, and the
customer can add their own.* This module is that sentence as code, and nothing more:

  * `PRECONFIGURED` is the roster we ship. It is data, not a code path — adding one of
    ours is adding a line, exactly the adapters' rule.
  * `AgentRegistry.add(...)` is the customer's slot. What it checks is the SEAM, not the
    agent's quality: the class must be an `EdgeAgent`, carry a distinct name, and answer
    `capabilities()` — because consent to run an agent is worth little if nobody can say
    what it answers before switching it on.
  * A name collision is refused, never silently resolved. A customer who wants to replace
    one of ours does it loudly, with `replace=True`, and the registry remembers that it
    happened (`overridden`) — an audit question deserves a stored answer.

The seam's law is inherited, not restated: every agent here gets a READ view through
`AgentContext` and answers questions. Nothing in this file can record, seal or send.
"""

from __future__ import annotations

from .base import AgentContext, EdgeAgent
from .structured_source import StructuredSourceAgent

PRECONFIGURED: dict[str, type[EdgeAgent]] = {
    StructuredSourceAgent.name: StructuredSourceAgent,
}


class RosterError(Exception):
    """A class that does not fit the seam, or a name that is already taken."""


class AgentRegistry:
    """One edge's agent roster: ours preconfigured, the customer's added at run time."""

    def __init__(self, preconfigured: dict[str, type[EdgeAgent]] | None = None):
        self._classes: dict[str, type[EdgeAgent]] = dict(
            PRECONFIGURED if preconfigured is None else preconfigured
        )
        self.overridden: list[str] = []

    def add(self, agent_class: type[EdgeAgent], *, replace: bool = False) -> None:
        """The customer's slot. Checks the seam, never the model behind it."""
        if not (isinstance(agent_class, type) and issubclass(agent_class, EdgeAgent)):
            raise RosterError("an agent is an EdgeAgent subclass — the seam is a class, "
                              "not a callable, so its surface is inspectable before it runs")
        name = getattr(agent_class, "name", "")
        if not name or name == EdgeAgent.name:
            raise RosterError("an agent needs its own name — 'agent' is the base class, "
                              "not an identity")
        if name in self._classes and not replace:
            raise RosterError(f"agent name {name!r} is taken; pass replace=True to override "
                              "it deliberately — a silent replacement is how an audit loses "
                              "track of what ran")
        if name in self._classes:
            self.overridden.append(name)
        self._classes[name] = agent_class

    def available(self) -> list[dict]:
        """What can be switched on, and what each one answers — BEFORE it runs."""
        out = []
        for name in sorted(self._classes):
            cls = self._classes[name]
            out.append({"name": name, "description": getattr(cls, "description", ""),
                        "preconfigured": PRECONFIGURED.get(name) is cls})
        return out

    def start(self, name: str, context: AgentContext) -> EdgeAgent:
        cls = self._classes.get(name)
        if cls is None:
            raise RosterError(f"no agent named {name!r} — available: {sorted(self._classes)}")
        return cls(context)

    def __len__(self) -> int:
        return len(self._classes)
