"""
Boring-OS SDK agents client.

Provides the AgentsClient class for agent operations:
run, stream, session management, and agent CRUD.
Accessed via `client.agents` on the main BoringOS client.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, AsyncIterator

if TYPE_CHECKING:
    from boring_os.client import BoringOS

from boring_os.agent_models import (
    AgentDefinition,
    AgentListResponse,
    AgentResponse,
    AgentStreamChunk,
)
from boring_os.sessions import AgentSession


class AgentsClient:
    """
    Client for agent operations.

    Accessed via `client.agents` on the main BoringOS client.
    """

    def __init__(self, client: "BoringOS"):
        self._client = client

    # -------------------------------------------------------------------------
    # Agent Execution
    # -------------------------------------------------------------------------

    def run(
        self,
        agent: str,
        message: str,
        session_id: str | None = None,
        timeout: int = 120,
    ) -> AgentResponse:
        """
        Execute a single-turn agent call (synchronous).

        Sends a message to the agent and waits for the full response.
        The server holds the connection open until the agent loop completes.

        Args:
            agent: Agent name to invoke
            message: User message text
            session_id: Optional session ID for multi-turn context
            timeout: Max seconds to wait for the response

        Returns:
            AgentResponse with text, tool_calls, token_usage, etc.

        Raises:
            AgentNotFoundError: If the agent doesn't exist
            AgentTimeoutError: If execution exceeds timeout

        Example:
            response = boring.agents.run("billing_agent", message="Why overcharged?")
            print(response.text)
        """
        body: dict[str, Any] = {"agent": agent, "message": message}
        if session_id:
            body["session_id"] = session_id

        response = self._client._request(
            "POST",
            "/agents/run",
            json=body,
            timeout=timeout + 5,
        )
        return AgentResponse.model_validate(response)

    async def run_async(
        self,
        agent: str,
        message: str,
        session_id: str | None = None,
        timeout: int = 120,
    ) -> AgentResponse:
        """
        Async version of run().

        Args:
            agent: Agent name to invoke
            message: User message text
            session_id: Optional session ID
            timeout: Max seconds to wait

        Returns:
            AgentResponse
        """
        body: dict[str, Any] = {"agent": agent, "message": message}
        if session_id:
            body["session_id"] = session_id

        response = await self._client._request_async(
            "POST",
            "/agents/run",
            json=body,
            timeout=timeout + 5,
        )
        return AgentResponse.model_validate(response)

    async def stream(
        self,
        agent: str,
        message: str,
        session_id: str | None = None,
    ) -> AsyncIterator[AgentStreamChunk]:
        """
        Stream an agent response as it is generated.

        Connects via SSE and yields chunks as the model generates tokens.

        Args:
            agent: Agent name to invoke
            message: User message text
            session_id: Optional session ID

        Yields:
            AgentStreamChunk objects; final chunk has done=True

        Example:
            async for chunk in boring.agents.stream("billing_agent", message="..."):
                if chunk.text:
                    print(chunk.text, end="")
                if chunk.done:
                    print(f"\\nTokens: {chunk.token_usage}")
        """
        body: dict[str, Any] = {"agent": agent, "message": message}
        if session_id:
            body["session_id"] = session_id

        async for chunk_data in self._client._stream_request(
            "POST",
            "/agents/stream",
            json=body,
        ):
            yield AgentStreamChunk.model_validate(chunk_data)

    # -------------------------------------------------------------------------
    # Session Management
    # -------------------------------------------------------------------------

    def create_session(self, agent_name: str) -> AgentSession:
        """
        Create a persistent session for multi-turn conversation.

        The session maintains conversation memory across send() calls.

        Args:
            agent_name: Agent to associate with the session

        Returns:
            AgentSession instance (usable as context manager)

        Example:
            session = boring.agents.create_session("billing_agent")
            with session:
                r1 = session.send("Why was I overcharged?")
                r2 = session.send("Break down the roaming charges")
        """
        response = self._client._request(
            "POST",
            "/agents/sessions",
            json={"agent_name": agent_name},
        )
        session_id = response.get("session_id", "")
        return AgentSession(
            client=self._client,
            session_id=session_id,
            agent_name=agent_name,
        )

    async def create_session_async(self, agent_name: str) -> AgentSession:
        """Async version of create_session()."""
        response = await self._client._request_async(
            "POST",
            "/agents/sessions",
            json={"agent_name": agent_name},
        )
        session_id = response.get("session_id", "")
        return AgentSession(
            client=self._client,
            session_id=session_id,
            agent_name=agent_name,
        )

    # -------------------------------------------------------------------------
    # Async Submit / Poll
    # -------------------------------------------------------------------------

    def submit(self, agent: str, message: str) -> str:
        """
        Submit an agent call asynchronously (fire-and-forget).

        Returns a request_id for polling. Useful for long-running agents.

        Args:
            agent: Agent name
            message: User message

        Returns:
            request_id string for use with get_result()
        """
        response = self._client._request(
            "POST",
            "/agents/run",
            json={"agent": agent, "message": message, "async": True},
        )
        return response.get("request_id", "")

    def get_result(self, request_id: str, timeout: int = 30) -> AgentResponse:
        """
        Get the result of an async agent call.

        Args:
            request_id: Request ID from submit()
            timeout: Max seconds to wait (long polling)

        Returns:
            AgentResponse
        """
        response = self._client._request(
            "GET",
            f"/agents/result/{request_id}",
            timeout=timeout + 5,
        )
        return AgentResponse.model_validate(response)

    # -------------------------------------------------------------------------
    # Agent CRUD (Management)
    # -------------------------------------------------------------------------

    def list(self) -> AgentListResponse:
        """
        List all registered agents.

        Returns:
            AgentListResponse with agent summaries and total count
        """
        response = self._client._request("GET", "/agents")
        return AgentListResponse.model_validate(response)

    def get(self, agent_name: str) -> AgentDefinition:
        """
        Get details for a specific agent.

        Args:
            agent_name: Name of the agent

        Returns:
            Full AgentDefinition

        Raises:
            AgentNotFoundError: If the agent doesn't exist
        """
        response = self._client._request("GET", f"/agents/{agent_name}")
        return AgentDefinition.model_validate(response)

    def register(
        self,
        name: str,
        model: str,
        tools: list[str] | None = None,
        description: str = "",
        prompt_id: str | None = None,
        knowledge_sources: list[str] | None = None,
        max_iterations: int = 25,
        max_total_tokens: int = 100_000,
        **model_config: Any,
    ) -> AgentDefinition:
        """
        Register a new agent programmatically.

        Args:
            name: Unique agent name
            model: LLM model name (e.g., "claude-sonnet-4")
            tools: List of tool names the agent can use
            description: Human-readable description
            prompt_id: Prompt registry ID for system prompt
            knowledge_sources: Knowledge source IDs for RAG
            max_iterations: Max model loop iterations
            max_total_tokens: Max cumulative tokens
            **model_config: Additional model parameters (temperature, etc.)

        Returns:
            Created AgentDefinition

        Example:
            boring.agents.register(
                name="roaming_agent",
                model="claude-sonnet-4",
                tools=["get_roaming_usage", "get_customer_plan"],
                description="Handles roaming inquiries",
            )
        """
        body: dict[str, Any] = {
            "name": name,
            "model": model,
            "description": description,
            "tools": tools or [],
            "max_iterations": max_iterations,
            "max_total_tokens": max_total_tokens,
        }
        if prompt_id:
            body["prompt_id"] = prompt_id
        if knowledge_sources:
            body["knowledge_sources"] = knowledge_sources
        if model_config:
            body["model_config"] = model_config

        response = self._client._request("POST", "/agents", json=body)
        return AgentDefinition.model_validate(response)

    def update(self, name: str, **changes: Any) -> AgentDefinition:
        """
        Update an existing agent's configuration.

        Args:
            name: Agent name to update
            **changes: Fields to update (model, tools, max_iterations, etc.)

        Returns:
            Updated AgentDefinition

        Example:
            boring.agents.update("billing_agent", model="claude-sonnet-4-5")
        """
        response = self._client._request(
            "PUT",
            f"/agents/{name}",
            json=changes,
        )
        return AgentDefinition.model_validate(response)

    def deprecate(self, name: str) -> None:
        """
        Deprecate an agent.

        Args:
            name: Agent name to deprecate
        """
        self._client._request("POST", f"/agents/{name}/deprecate")
