"""Actor registry for built-ins and custom actor management."""

from __future__ import annotations

from dataclasses import asdict
from typing import Any

from cleveragents.actor.config import ActorConfiguration
from cleveragents.application.services.actor_service import ActorService
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import ValidationError
from cleveragents.domain.models.core import Actor
from cleveragents.providers.registry import (
    ProviderCapabilities,
    ProviderInfo,
    ProviderRegistry,
)


class ActorRegistry:
    """Coordinate actor configuration parsing and built-in generation.

    Supports YAML-first actor persistence: each actor stores its original
    YAML text, a ``schema_version``, and optional ``compiled_metadata``
    alongside the canonical configuration blob.
    """

    #: Default actor config schema version applied to new entries.
    DEFAULT_SCHEMA_VERSION: str = "1.0"

    def __init__(
        self,
        *,
        actor_service: ActorService,
        provider_registry: ProviderRegistry,
        settings: Settings,
    ) -> None:
        self._actor_service = actor_service
        self._provider_registry = provider_registry
        self._settings = settings

    def _actor_name(self, provider_name: str, model_name: str) -> str:
        """Build a canonical ``namespace/identifier`` actor name.

        Provider and model names are sanitised to handle the most common
        violations (embedded slashes from providers like OpenRouter whose
        default model contains ``/``):

        * Forward slashes are replaced with ``-``.
        * The result is lowercased.

        .. note::

            This does **not** strip every character disallowed by
            ``^[a-z0-9_-]+/[a-z0-9_-]+$`` (e.g. spaces, dots, ``@``).
            In practice provider and model identifiers from configured
            providers never contain those characters.

        .. note::

            Lowercasing is a behavior change — previously mixed-case
            built-in actors (e.g. ``OpenAI/gpt-4``) were stored
            as-is.  After this fix they are superseded by lowercased
            versions on the next ``ensure_built_in_actors()`` call.
        """
        sanitized_provider = provider_name.replace("/", "-").lower()
        sanitized_model = model_name.replace("/", "-").lower()
        return f"{sanitized_provider}/{sanitized_model}"

    @staticmethod
    def _ensure_namespaced(name: str) -> str:
        """Ensure *name* follows the ``namespace/identifier`` convention.

        If the name contains no ``/`` it is prefixed with ``local/``.
        """
        if "/" not in name:
            return f"local/{name}"
        return name

    def _build_graph_descriptor(
        self,
        *,
        provider: str,
        model: str,
        source: str,
        capabilities: ProviderCapabilities | None = None,
    ) -> dict[str, Any]:
        return {
            "provider": provider,
            "model": model,
            "source": source,
            "capabilities": asdict(capabilities) if capabilities else None,
        }

    def _canonical_blob(
        self,
        config: ActorConfiguration,
        raw_blob: dict[str, Any] | None,
        *,
        source: str,
        default_graph: dict[str, Any] | None,
    ) -> dict[str, Any]:
        blob: dict[str, Any] = dict(raw_blob) if raw_blob else dict(config.options)
        blob.setdefault("provider", config.provider)
        blob.setdefault("model", config.model)
        graph = config.graph_descriptor or default_graph
        if graph is not None and "graph_descriptor" not in blob:
            blob["graph_descriptor"] = graph
        if "options" not in blob:
            if config.options:
                blob["options"] = dict(config.options)
        elif not isinstance(blob["options"], dict):
            blob["options"] = {}
        blob.setdefault("unsafe", config.unsafe)
        blob.setdefault("source", source)
        return blob

    def ensure_built_in_actors(self) -> list[Actor]:
        """Generate built-in actors from configured providers if missing."""

        configured: list[ProviderInfo] = (
            self._provider_registry.get_configured_providers()
        )
        if not configured:
            return []

        actors: list[Actor] = []
        defaults = self._settings.resolve_provider_defaults()
        for info in configured:
            provider_name = info.name or info.provider_type.value
            model_id = info.default_model
            graph_descriptor = self._build_graph_descriptor(
                provider=provider_name,
                model=model_id,
                source="provider-registry",
                capabilities=info.capabilities,
            )
            # NOTE: ``name`` is sanitised via ``_actor_name()`` (slashes
            # replaced, lowercased) while ``provider`` and ``model``
            # retain their raw values.  This is intentional: ``name`` is
            # the canonical identifier used for lookups; ``provider`` and
            # ``model`` store the original provider metadata as-is.
            actor = self._actor_service.upsert_actor(
                name=self._actor_name(provider_name, model_id),
                provider=provider_name,
                model=model_id,
                config_blob={
                    "provider": provider_name,
                    "model": model_id,
                    "capabilities": asdict(info.capabilities),
                    "unsafe": False,
                    "source": "provider-registry",
                    "graph_descriptor": graph_descriptor,
                },
                graph_descriptor=graph_descriptor,
                unsafe=False,
                set_default=False,
                is_built_in=True,
            )
            actors.append(actor)

        if not self._actor_service.get_default_actor() and actors:
            preferred = None
            for actor in actors:
                if defaults.provider and actor.provider.lower() == defaults.provider:
                    preferred = actor
                    if defaults.model is None or actor.model == defaults.model:
                        break
            if preferred is None:
                preferred = actors[0]
            self._actor_service.set_default_actor(preferred.name)

        return actors

    # ------------------------------------------------------------------
    # YAML-first add / update
    # ------------------------------------------------------------------

    def add(
        self,
        yaml_text: str,
        *,
        update: bool = False,
        schema_version: str | None = None,
        compiled_metadata: dict[str, Any] | None = None,
    ) -> Actor:
        """Add (or update) an actor from raw YAML text.

        The YAML is parsed into an ``ActorConfiguration``, validated, and
        persisted alongside the original *yaml_text* and *schema_version*.

        Args:
            yaml_text: The original actor YAML source.
            update: When ``True`` allow overwriting an existing actor.
            schema_version: Explicit schema version; defaults to
                ``DEFAULT_SCHEMA_VERSION``.
            compiled_metadata: Optional compiler-produced metadata dict.

        Returns:
            The persisted ``Actor`` domain object.

        Raises:
            ValidationError: When the YAML is invalid or the actor already
                exists and *update* is ``False``.
        """
        self.ensure_built_in_actors()

        blob = ActorConfiguration.load_yaml_text(yaml_text)
        name_raw: str = blob.get("name", "")
        if not name_raw:
            raise ValidationError("Actor YAML must include a 'name' field.")

        name = self._ensure_namespaced(name_raw)
        provider_raw = blob.get("provider") or blob.get("provider_type", "")
        model_raw = blob.get("model") or blob.get("model_id", "")

        if not provider_raw or not model_raw:
            raise ValidationError(
                "Actor YAML must include 'provider' and 'model' fields."
            )

        provider = str(provider_raw)
        model = str(model_raw)

        # Check for existing actor when not updating
        if not update:
            try:
                self._actor_service.get_actor(name)
                raise ValidationError(
                    f"Actor '{name}' already exists. Pass update=True to overwrite."
                )
            except Exception as exc:
                if "already exists" in str(exc):
                    raise
                # NotFoundError is expected for new actors

        version = schema_version or self.DEFAULT_SCHEMA_VERSION
        config_blob: dict[str, Any] = dict(blob)
        config_blob.setdefault("source", "yaml")

        return self._actor_service.upsert_actor(
            name=name,
            provider=provider,
            model=model,
            config_blob=config_blob,
            graph_descriptor=blob.get("graph_descriptor"),
            unsafe=bool(blob.get("unsafe", False)),
            set_default=False,
            is_built_in=False,
            yaml_text=yaml_text,
            schema_version=version,
            compiled_metadata=compiled_metadata,
        )

    # ------------------------------------------------------------------
    # Legacy upsert (preserved for backward compatibility)
    # ------------------------------------------------------------------

    def upsert_actor(
        self,
        *,
        name: str,
        config_blob: dict[str, Any] | None,
        provider: str | None = None,
        model: str | None = None,
        graph_descriptor: dict[str, Any] | None = None,
        unsafe: bool = False,
        set_default: bool = False,
        is_built_in: bool = False,
        allow_unsafe: bool = False,
        option_overrides: dict[str, Any] | None = None,
        yaml_text: str | None = None,
        schema_version: str | None = None,
        compiled_metadata: dict[str, Any] | None = None,
    ) -> Actor:
        """Parse, validate, and persist an actor configuration."""

        self.ensure_built_in_actors()
        config = ActorConfiguration.from_blob(
            blob=config_blob,
            name=name,
            provider=provider,
            model=model,
            graph_descriptor=graph_descriptor,
            unsafe=unsafe,
            option_overrides=option_overrides,
        )

        if config.unsafe and not (unsafe or allow_unsafe or is_built_in):
            raise ValidationError(
                "Actor configuration is marked unsafe; re-run with --unsafe to confirm."
            )

        default_graph = None
        if config.provider and config.model:
            default_graph = self._build_graph_descriptor(
                provider=config.provider,
                model=config.model,
                source=config_blob.get("source", "custom") if config_blob else "custom",
            )

        canonical_blob = self._canonical_blob(
            config,
            config_blob,
            source=config_blob.get("source", "custom") if config_blob else "custom",
            default_graph=default_graph,
        )

        return self._actor_service.upsert_actor(
            name=name,
            provider=config.provider,
            model=config.model,
            config_blob=canonical_blob,
            graph_descriptor=canonical_blob.get("graph_descriptor"),
            unsafe=config.unsafe,
            set_default=set_default,
            is_built_in=is_built_in,
            yaml_text=yaml_text,
            schema_version=schema_version or self.DEFAULT_SCHEMA_VERSION,
            compiled_metadata=compiled_metadata,
        )

    # ------------------------------------------------------------------
    # CRUD helpers
    # ------------------------------------------------------------------

    def get(self, name: str) -> Actor:
        """Retrieve an actor by its namespaced name."""
        self.ensure_built_in_actors()
        return self._actor_service.get_actor(self._ensure_namespaced(name))

    def get_actor(self, name: str) -> Actor:
        """Retrieve an actor by name (legacy alias for :meth:`get`)."""
        self.ensure_built_in_actors()
        return self._actor_service.get_actor(name)

    def list(self, namespace: str | None = None) -> list[Actor]:
        """List actors, optionally filtered by namespace prefix.

        This is a **read-only** operation and intentionally does **not**
        call :meth:`ensure_built_in_actors` — database writes must never
        be triggered by a list/query command (see bug #797).

        Args:
            namespace: When provided only actors whose name starts with
                ``namespace/`` are returned.

        Returns:
            Sorted list of ``Actor`` objects.
        """
        actors = self._actor_service.list_actors()
        if namespace is not None:
            prefix = f"{namespace}/"
            actors = [a for a in actors if a.name.startswith(prefix)]
        return actors

    def list_actors(self) -> list[Actor]:
        """Return all actors (legacy alias for :meth:`list`).

        This is a **read-only** operation and intentionally does **not**
        call :meth:`ensure_built_in_actors` — database writes must never
        be triggered by a list/query command (see bug #797).
        """
        return self._actor_service.list_actors()

    def remove(self, name: str) -> None:
        """Remove an actor by its namespaced name."""
        self.ensure_built_in_actors()
        self._actor_service.remove_actor(self._ensure_namespaced(name))

    def remove_actor(self, name: str) -> None:
        """Remove a custom actor (legacy alias for :meth:`remove`)."""
        self.ensure_built_in_actors()
        self._actor_service.remove_actor(name)

    def set_default_actor(self, name: str) -> Actor:
        self.ensure_built_in_actors()
        return self._actor_service.set_default_actor(name)

    def get_default_actor(self) -> Actor | None:
        self.ensure_built_in_actors()
        return self._actor_service.get_default_actor()
