"""LangGraph Platform RemoteGraph integration for server-side actor execution.

Per ADR-048 (Server Application Architecture), the CleverAgents server uses
LangGraph Platform with ``RemoteGraph`` for server-side actor execution.
Actor graphs (StateGraphs defined in YAML) are deployed to LangGraph Platform
as separate deployments and invoked via ``RemoteGraph``.

This module provides:

- :class:`RemoteGraphConfig` — configuration for a remote graph deployment
- :class:`RemoteGraphManager` — manages registration and invocation of remote
  actor graphs via LangGraph Platform
- :class:`RemoteGraphNotAvailableError` — raised when LangGraph Platform is
  not configured or unreachable

The implementation is a **stub** that defines the interface and raises
:class:`RemoteGraphNotAvailableError` when no LangGraph Platform URL is
configured.  When a real LangGraph Platform deployment is available, the
manager delegates execution to it via the ``RemoteGraph`` client.
"""

from __future__ import annotations

import logging
from typing import Any

from pydantic import BaseModel, ConfigDict, field_validator

_logger: logging.Logger = logging.getLogger(__name__)

_PLATFORM_NOT_CONFIGURED_MSG = (
    "LangGraph Platform is not configured"
    " — set LANGGRAPH_PLATFORM_URL to enable remote graph execution"
)


class RemoteGraphNotAvailableError(Exception):
    """Raised when LangGraph Platform is not configured or unreachable.

    This error is raised when:
    - No LangGraph Platform URL is configured
    - The platform is unreachable
    - The requested graph is not deployed

    Attributes:
        message: Human-readable error description.
        details: Optional mapping of additional context.
    """

    def __init__(
        self,
        message: str = _PLATFORM_NOT_CONFIGURED_MSG,
        *,
        details: dict[str, Any] | None = None,
    ) -> None:
        super().__init__(message)
        self.message = message
        self.details: dict[str, Any] = details or {}

    def __repr__(self) -> str:
        return f"{type(self).__name__}({self.message!r}, details={self.details!r})"


class RemoteGraphConfig(BaseModel):
    """Configuration for a remote graph deployment on LangGraph Platform.

    Attributes:
        graph_id: Unique identifier for the graph on LangGraph Platform.
        platform_url: Base URL of the LangGraph Platform deployment.
        api_key_env: Environment variable name containing the API key.
        timeout: Request timeout in seconds (default: 60).
        metadata: Optional metadata for the graph deployment.
    """

    model_config = ConfigDict(frozen=True, strict=False)

    graph_id: str
    platform_url: str
    api_key_env: str = "LANGGRAPH_API_KEY"
    timeout: float = 60.0
    metadata: dict[str, Any] = {}

    @field_validator("graph_id")
    @classmethod
    def _validate_graph_id(cls, value: str) -> str:
        if not value or not value.strip():
            raise ValueError("graph_id must not be empty")
        return value.strip()

    @field_validator("platform_url")
    @classmethod
    def _validate_platform_url(cls, value: str) -> str:
        if not value or not value.strip():
            raise ValueError("platform_url must not be empty")
        stripped = value.strip()
        if not stripped.startswith(("http://", "https://")):
            raise ValueError("platform_url must start with http:// or https://")
        return stripped

    @field_validator("timeout")
    @classmethod
    def _validate_timeout(cls, value: float) -> float:
        if value <= 0:
            raise ValueError("timeout must be positive")
        return value


class RemoteGraphManager:
    """Manages registration and invocation of remote actor graphs.

    Per ADR-048, the server delegates actor graph execution to LangGraph
    Platform via RemoteGraph.  This manager:

    - Registers actor graphs by their graph ID
    - Invokes registered graphs with input state
    - Raises :class:`RemoteGraphNotAvailableError` when the platform is
      not configured or a graph is not registered

    When ``platform_url`` is ``None`` (no LangGraph Platform configured),
    all invocation methods raise :class:`RemoteGraphNotAvailableError`.
    This allows the server to start and report its unavailability gracefully
    rather than failing at startup.

    Example::

        manager = RemoteGraphManager(platform_url="https://langgraph.example.com")
        manager.register_graph(RemoteGraphConfig(
            graph_id="strategy-actor",
            platform_url="https://langgraph.example.com",
        ))
        result = manager.invoke("strategy-actor", {"messages": []})
    """

    def __init__(self, platform_url: str | None = None) -> None:
        """Initialise the manager.

        Args:
            platform_url: Base URL of the LangGraph Platform deployment.
                When ``None``, all invocation methods raise
                :class:`RemoteGraphNotAvailableError`.
        """
        self._platform_url = platform_url
        self._graphs: dict[str, RemoteGraphConfig] = {}
        _logger.debug(
            "remote_graph_manager.init",
            extra={
                "platform_url": platform_url,
                "available": platform_url is not None,
            },
        )

    @property
    def platform_url(self) -> str | None:
        """Base URL of the LangGraph Platform deployment, or ``None``."""
        return self._platform_url

    @property
    def is_available(self) -> bool:
        """``True`` when a LangGraph Platform URL is configured."""
        return self._platform_url is not None

    def register_graph(self, config: RemoteGraphConfig) -> None:
        """Register an actor graph for remote execution.

        Args:
            config: Configuration for the remote graph deployment.

        Raises:
            RemoteGraphNotAvailableError: When no platform URL is configured.
        """
        if not self.is_available:
            raise RemoteGraphNotAvailableError(
                _PLATFORM_NOT_CONFIGURED_MSG,
                details={"graph_id": config.graph_id},
            )
        self._graphs[config.graph_id] = config
        _logger.info(
            "remote_graph_manager.graph_registered",
            extra={
                "graph_id": config.graph_id,
                "platform_url": config.platform_url,
            },
        )

    def unregister_graph(self, graph_id: str) -> None:
        """Unregister a previously registered actor graph.

        Args:
            graph_id: Identifier of the graph to unregister.

        Raises:
            KeyError: When the graph is not registered.
            RemoteGraphNotAvailableError: When no platform URL is configured.
        """
        if not self.is_available:
            raise RemoteGraphNotAvailableError(
                _PLATFORM_NOT_CONFIGURED_MSG,
                details={"graph_id": graph_id},
            )
        if graph_id not in self._graphs:
            raise KeyError(f"Graph {graph_id!r} is not registered")
        del self._graphs[graph_id]
        _logger.info(
            "remote_graph_manager.graph_unregistered",
            extra={"graph_id": graph_id},
        )

    def get_graph_config(self, graph_id: str) -> RemoteGraphConfig:
        """Return the configuration for a registered graph.

        Args:
            graph_id: Identifier of the graph.

        Returns:
            The :class:`RemoteGraphConfig` for the graph.

        Raises:
            KeyError: When the graph is not registered.
            RemoteGraphNotAvailableError: When no platform URL is configured.
        """
        if not self.is_available:
            raise RemoteGraphNotAvailableError(
                _PLATFORM_NOT_CONFIGURED_MSG,
                details={"graph_id": graph_id},
            )
        if graph_id not in self._graphs:
            raise KeyError(f"Graph {graph_id!r} is not registered")
        return self._graphs[graph_id]

    def list_graphs(self) -> list[str]:
        """Return the IDs of all registered graphs.

        Returns:
            Sorted list of registered graph IDs.

        Raises:
            RemoteGraphNotAvailableError: When no platform URL is configured.
        """
        if not self.is_available:
            raise RemoteGraphNotAvailableError(_PLATFORM_NOT_CONFIGURED_MSG)
        return sorted(self._graphs.keys())

    def invoke(
        self,
        graph_id: str,
        input_state: dict[str, Any],
        *,
        thread_id: str | None = None,
        config: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Invoke a registered actor graph on LangGraph Platform.

        This method is a **stub** that raises
        :class:`RemoteGraphNotAvailableError` in all cases.  When a real
        LangGraph Platform deployment is available, this method will delegate
        to the ``RemoteGraph`` client.

        Args:
            graph_id: Identifier of the graph to invoke.
            input_state: Input state dictionary for the graph.
            thread_id: Optional thread ID for stateful execution.
            config: Optional LangGraph run configuration overrides.

        Returns:
            Output state dictionary from the graph execution.

        Raises:
            RemoteGraphNotAvailableError: Always — LangGraph Platform
                execution is not yet implemented in this stub.
            KeyError: When the graph is not registered (checked before
                raising the not-available error).
        """
        if not self.is_available:
            raise RemoteGraphNotAvailableError(
                _PLATFORM_NOT_CONFIGURED_MSG,
                details={"graph_id": graph_id},
            )
        if graph_id not in self._graphs:
            raise KeyError(f"Graph {graph_id!r} is not registered")

        graph_config = self._graphs[graph_id]
        _logger.warning(
            "remote_graph_manager.invoke_stub",
            extra={
                "graph_id": graph_id,
                "platform_url": graph_config.platform_url,
                "thread_id": thread_id,
            },
        )
        # Stub: real implementation would call RemoteGraph.invoke() here.
        raise RemoteGraphNotAvailableError(
            "LangGraph Platform RemoteGraph invocation is not yet implemented"
            " — deploy actor graphs to LangGraph Platform to enable remote execution",
            details={
                "graph_id": graph_id,
                "platform_url": graph_config.platform_url,
                "thread_id": thread_id,
            },
        )

    def health_check(self) -> bool:
        """Check whether LangGraph Platform is reachable.

        Returns:
            ``False`` when no platform URL is configured (stub behaviour).
            A real implementation would perform an HTTP health check.
        """
        if not self.is_available:
            _logger.debug("remote_graph_manager.health_check.not_configured")
            return False
        # Stub: real implementation would perform an HTTP health check.
        _logger.warning(
            "remote_graph_manager.health_check_stub",
            extra={"platform_url": self._platform_url},
        )
        return False


__all__ = [
    "RemoteGraphConfig",
    "RemoteGraphManager",
    "RemoteGraphNotAvailableError",
]
