"""Agent discovery surface available via ``ctx.discover``.

Agents find each other by capability/tag/skill — *never* by hardcoded URL.
The runtime attaches a :class:`DiscoveryClient`; the canonical impl
queries the platform's agent registry (control plane).
"""
from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Sequence

from .card import AgentCard


@dataclass(frozen=True)
class DiscoveredAgent:
    """A registry hit. ``url`` is what the caller hands to ``ctx.call``."""

    name: str
    url: str | None
    card: AgentCard


class DiscoveryClient(ABC):
    """Discovery surface."""

    @abstractmethod
    async def find_agents(
        self,
        *,
        tags: Sequence[str] = (),
        capability: str | None = None,
        skill: str | None = None,
        limit: int = 10,
    ) -> list[DiscoveredAgent]: ...

    @abstractmethod
    async def get_agent(self, name: str) -> DiscoveredAgent: ...


# ---------------------------------------------------------------------------
# In-memory impl: a name → AgentCard map. Used by tests + the demo.
# ---------------------------------------------------------------------------


class InMemoryDiscovery(DiscoveryClient):
    def __init__(self, agents: dict[str, DiscoveredAgent]) -> None:
        self._agents = dict(agents)

    async def find_agents(
        self,
        *,
        tags: Sequence[str] = (),
        capability: str | None = None,
        skill: str | None = None,
        limit: int = 10,
    ) -> list[DiscoveredAgent]:
        wanted = {t.lower() for t in tags}
        out: list[DiscoveredAgent] = []
        for da in self._agents.values():
            if capability and capability not in da.card.capabilities:
                continue
            if skill and not any(s.name == skill for s in da.card.skills):
                continue
            if wanted:
                skill_tags = {
                    t.lower() for s in da.card.skills for t in s.tags
                }
                if not (wanted & skill_tags):
                    continue
            out.append(da)
            if len(out) >= limit:
                break
        return out

    async def get_agent(self, name: str) -> DiscoveredAgent:
        if name not in self._agents:
            raise KeyError(f"no agent: {name!r}")
        return self._agents[name]


# ---------------------------------------------------------------------------
# Control-plane backed: queries GET /v1/agents (with optional ?tag=, ?skill=).
# ---------------------------------------------------------------------------


class ControlPlaneDiscovery(DiscoveryClient):
    """Hits the platform's agent registry (control plane)."""

    def __init__(
        self,
        api_url: str,
        *,
        token: str | None = None,
        timeout: float = 10.0,
    ) -> None:
        self.api_url = api_url.rstrip("/")
        self._token = token
        self._timeout = timeout

    def _headers(self) -> dict[str, str]:
        h = {"accept": "application/json"}
        if self._token:
            h["authorization"] = f"bearer {self._token}"
        return h

    async def find_agents(
        self,
        *,
        tags: Sequence[str] = (),
        capability: str | None = None,
        skill: str | None = None,
        limit: int = 10,
    ) -> list[DiscoveredAgent]:
        import httpx

        params: dict[str, Any] = {"limit": limit}
        if tags:
            params["tag"] = list(tags)
        if capability:
            params["capability"] = capability
        if skill:
            params["skill"] = skill
        async with httpx.AsyncClient(timeout=self._timeout) as c:
            resp = await c.get(
                f"{self.api_url}/v1/agents", headers=self._headers(), params=params
            )
            resp.raise_for_status()
            rows = resp.json()
        out: list[DiscoveredAgent] = []
        for row in rows or []:
            # Lazy: list endpoint returns AgentOut (no card). Fetch full card.
            try:
                card_url = row.get("url")
                if card_url:
                    full = await c.get(f"{card_url}/.well-known/agent-card")
                    full.raise_for_status()
                    card = AgentCard.model_validate(full.json())
                else:
                    detail = await c.get(
                        f"{self.api_url}/v1/agents/{row['name']}",
                        headers=self._headers(),
                    )
                    detail.raise_for_status()
                    card = AgentCard.model_validate(detail.json().get("card") or {})
            except Exception:  # noqa: BLE001
                continue
            out.append(
                DiscoveredAgent(
                    name=row.get("name", ""),
                    url=row.get("url"),
                    card=card,
                )
            )
        return out

    async def get_agent(self, name: str) -> DiscoveredAgent:
        import httpx

        async with httpx.AsyncClient(timeout=self._timeout) as c:
            resp = await c.get(
                f"{self.api_url}/v1/agents/{name}", headers=self._headers()
            )
            resp.raise_for_status()
            data = resp.json()
        card = AgentCard.model_validate(data.get("card") or {})
        return DiscoveredAgent(name=data["name"], url=data.get("url"), card=card)


__all__ = [
    "ControlPlaneDiscovery",
    "DiscoveredAgent",
    "DiscoveryClient",
    "InMemoryDiscovery",
]
