# -*- coding: utf-8 -*-
"""A2A Client Manager: manage A2A 1.0 connections without SDK dependency.

Implements the A2A 1.0 JSON-RPC + SSE protocol directly using httpx
and httpx-sse, providing a unified interface for both generic A2A
and intelligent gateway scenarios.
"""

from __future__ import annotations

import json
import logging
import time
from collections.abc import AsyncIterator
from dataclasses import dataclass, field
from uuid import uuid4

import httpx
import httpx_sse

from .auth_interceptor import get_auth_headers
from .gateway_adapter import (
    normalize_gateway_card,
    patch_card_url,
)
from .gateway_token import GatewayTokenProvider

logger = logging.getLogger("qwenpaw").getChild(
    __name__.replace("plugin_cloudpaw.", ""),
)

_CARD_CACHE_TTL = 3600  # 1 hour
_DEFAULT_TIMEOUT = 60.0
_A2A_VERSION = "1.0.0"


@dataclass
class _AgentEntry:
    """Internal state for a connected agent."""

    url: str
    auth_type: str
    auth_token: str = ""
    card: dict | None = None
    card_fetched_at: float = 0
    rpc_url: str = ""
    token_provider: GatewayTokenProvider | None = None
    gateway_config: dict = field(default_factory=dict)


class A2AClientManager:
    """Manage A2A connections for multiple remote agents.

    Usage::

        manager = A2AClientManager()
        info = await manager.connect("https://agent.example.com")
        async for event in manager.send_message(
            "https://agent.example.com",
            "hello",
        ):
            print(event)
        await manager.close()
    """

    def __init__(self) -> None:
        self._agents: dict[str, _AgentEntry] = {}
        self._http: httpx.AsyncClient | None = None

    async def connect(
        self,
        agent_url: str,
        auth_type: str = "",
        auth_token: str = "",
        gateway_config: dict | None = None,
    ) -> dict:
        """Discover Agent Card and establish authenticated connection.

        Args:
            agent_url: Remote A2A Agent base URL.
            auth_type: "bearer", "api_key", "gateway", or "" (none).
            auth_token: Token/key value (not needed for gateway).
            gateway_config: Optional gateway-specific config overrides.

        Returns:
            dict with agent card summary and connection status.
        """
        entry = self._agents.get(agent_url)
        if entry and entry.card and not self._card_expired(entry):
            logger.info("Reusing cached connection for %s", agent_url)
            return self._card_summary(entry)

        entry = _AgentEntry(
            url=agent_url,
            auth_type=auth_type,
            auth_token=auth_token,
            gateway_config=gateway_config or {},
        )

        card = await self._fetch_agent_card(agent_url, auth_type)
        entry.card = card
        entry.card_fetched_at = time.time()

        if auth_type == "gateway":
            card = patch_card_url(card)
            entry.card = card
            gw_cfg = gateway_config or {}
            entry.token_provider = GatewayTokenProvider(
                client_id=gw_cfg.get("client_id", "4081417976505782102"),
                scope=gw_cfg.get("scope", "/internal/agenthub"),
                endpoint=gw_cfg.get("endpoint", "ramoauth.aliyuncs.com"),
            )
            logger.info("Applied gateway URL patch for %s", agent_url)

        entry.rpc_url = card.get("url", agent_url)

        self._agents[agent_url] = entry
        logger.info("Connected to agent: %s (auth=%s)", agent_url, auth_type)
        return self._card_summary(entry)

    async def send_message(
        self,
        agent_url: str,
        message: str,
        context_id: str = "",
        streaming: bool = True,
    ) -> AsyncIterator[dict]:
        """Send a message to a remote agent and yield response events.

        Args:
            agent_url: Agent URL (must have been connected first).
            message: Text message to send.
            context_id: Optional conversation context ID.
            streaming: Whether to use streaming (SSE) mode.

        Yields:
            dict with event data (type, task, statusUpdate, etc.)
        """
        entry = await self._ensure_connected(agent_url)

        msg_id = str(uuid4())
        parts = [{"text": message}]
        msg_obj: dict = {
            "messageId": msg_id,
            "role": "ROLE_USER",
            "parts": parts,
        }
        if context_id:
            msg_obj["contextId"] = context_id

        method = "SendStreamingMessage" if streaming else "SendMessage"
        payload = self._build_jsonrpc(method, {"message": msg_obj})

        auth_headers = await get_auth_headers(
            entry.auth_type,
            entry.auth_token,
            entry.token_provider,
        )
        headers = {
            "Content-Type": "application/json",
            "A2A-Version": _A2A_VERSION,
            **auth_headers,
        }

        http = self._get_http(entry)

        if streaming:
            async for event_data in self._stream_sse(
                http,
                entry.rpc_url,
                payload,
                headers,
            ):
                yield self._classify_event(event_data)
        else:
            resp = await http.post(
                entry.rpc_url,
                json=payload,
                headers=headers,
            )
            resp.raise_for_status()
            body = resp.json()
            if "error" in body:
                raise RuntimeError(
                    f"A2A error {body['error'].get('code')}: "
                    f"{body['error'].get('message')}",
                )
            result = body.get("result", {})
            yield self._classify_event(result)

    async def get_card_info(self, agent_url: str) -> dict | None:
        """Get cached card info for a connected agent."""
        entry = self._agents.get(agent_url)
        if entry and entry.card:
            return self._card_summary(entry)
        return None

    async def close(self) -> None:
        """Close all connections."""
        if self._http:
            await self._http.aclose()
            self._http = None
        self._agents.clear()
        logger.info("A2AClientManager closed all connections")

    async def disconnect(self, agent_url: str) -> None:
        """Disconnect and remove a specific agent."""
        self._agents.pop(agent_url, None)

    def list_connected(self) -> list[dict]:
        """List all connected agents with their card summaries."""
        return [
            self._card_summary(entry)
            for entry in self._agents.values()
            if entry.card
        ]

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    async def _ensure_connected(self, agent_url: str) -> _AgentEntry:
        entry = self._agents.get(agent_url)
        if entry and entry.card and not self._card_expired(entry):
            return entry
        await self.connect(agent_url)
        entry = self._agents.get(agent_url)
        if not entry or not entry.card:
            raise RuntimeError(f"Failed to connect to {agent_url}")
        return entry

    async def _fetch_agent_card(
        self,
        agent_url: str,
        auth_type: str,
    ) -> dict:
        """Fetch and parse Agent Card JSON from remote endpoint."""
        is_gateway = auth_type == "gateway"
        verify_ssl = not is_gateway

        card_url = f"{agent_url.rstrip('/')}/.well-known/agent-card.json"
        async with httpx.AsyncClient(
            verify=verify_ssl,
            timeout=30.0,
        ) as http:
            resp = await http.get(
                card_url,
                headers={"A2A-Version": _A2A_VERSION},
            )
            resp.raise_for_status()
            card = resp.json()

        if is_gateway:
            card = normalize_gateway_card(card)

        if not card.get("url"):
            card["url"] = agent_url

        logger.info(
            "Agent Card fetched for %s: %s",
            agent_url,
            card.get("name"),
        )
        return card

    def _get_http(self, entry: _AgentEntry) -> httpx.AsyncClient:
        """Get or create a shared httpx client."""
        if self._http is None or self._http.is_closed:
            verify = entry.auth_type != "gateway"
            self._http = httpx.AsyncClient(
                verify=verify,
                timeout=_DEFAULT_TIMEOUT,
            )
        return self._http

    async def _stream_sse(
        self,
        http: httpx.AsyncClient,
        url: str,
        payload: dict,
        headers: dict,
    ) -> AsyncIterator[dict]:
        """Send JSON-RPC request and yield parsed SSE event data.

        Some A2A servers (e.g. the Alibaba Cloud gateway) may respond
        with Content-Type: application/json even for streaming requests.
        We handle both SSE and plain JSON responses gracefully.
        """
        resp = await http.send(
            http.build_request("POST", url, json=payload, headers=headers),
            stream=True,
        )
        try:
            resp.raise_for_status()
            content_type = resp.headers.get("content-type", "")

            if "text/event-stream" in content_type:
                event_source = httpx_sse.EventSource(resp)
                async for sse_event in event_source.aiter_sse():
                    if sse_event.data:
                        try:
                            data = json.loads(sse_event.data)
                            if "error" in data:
                                err = data["error"]
                                logger.warning(
                                    "SSE error: %s - %s",
                                    err.get("code"),
                                    err.get("message"),
                                )
                            result = data.get("result", data)
                            yield result
                        except json.JSONDecodeError:
                            logger.warning(
                                "Non-JSON SSE event: %s",
                                sse_event.data[:200],
                            )
            else:
                body = await resp.aread()
                data = json.loads(body)
                if "error" in data:
                    err = data["error"]
                    raise RuntimeError(
                        f"A2A error {err.get('code')}: {err.get('message')}",
                    )
                result = data.get("result", data)
                yield result
        finally:
            await resp.aclose()

    @staticmethod
    def _build_jsonrpc(method: str, params: dict) -> dict:
        return {
            "jsonrpc": "2.0",
            "id": 1,
            "method": method,
            "params": params,
        }

    @staticmethod
    def _classify_event(data: dict) -> dict:
        """Add a 'type' field to an event dict for downstream processing."""
        if "task" in data:
            return {"type": "task", **data}
        if "statusUpdate" in data:
            return {"type": "status_update", **data}
        if "artifactUpdate" in data:
            return {"type": "artifact_update", **data}
        if "message" in data:
            return {"type": "message", **data}
        return {"type": "unknown", **data}

    @staticmethod
    def _card_expired(entry: _AgentEntry) -> bool:
        return (time.time() - entry.card_fetched_at) > _CARD_CACHE_TTL

    @staticmethod
    def _card_summary(entry: _AgentEntry) -> dict:
        card = entry.card
        if not card:
            return {"url": entry.url, "status": "not_connected"}

        skills = []
        for s in card.get("skills", []):
            skill_info = {"name": s.get("name", "")}
            desc = s.get("description")
            if desc:
                skill_info["description"] = desc
            skills.append(skill_info)

        interfaces = []
        for iface in card.get("additionalInterfaces", []):
            interfaces.append(
                {
                    "url": iface.get("url", ""),
                    "protocol_binding": iface.get("preferredTransport", ""),
                    "protocol_version": iface.get("protocolVersion", ""),
                },
            )

        caps = card.get("capabilities", {})
        return {
            "url": entry.url,
            "status": "connected",
            "auth_type": entry.auth_type,
            "name": card.get("name", ""),
            "description": card.get("description", ""),
            "version": card.get("version", ""),
            "skills": skills,
            "interfaces": interfaces,
            "capabilities": {
                "streaming": caps.get("streaming", False),
                "push_notifications": bool(
                    caps.get("pushNotifications")
                    or caps.get("pushNotificationConfig"),
                ),
            },
        }


_manager_instance: A2AClientManager | None = None


def get_a2a_manager() -> A2AClientManager:
    """Get or create the singleton A2AClientManager instance."""
    global _manager_instance
    if _manager_instance is None:
        _manager_instance = A2AClientManager()
    return _manager_instance


async def shutdown_a2a_manager() -> None:
    """Shutdown the singleton manager if it exists."""
    global _manager_instance
    if _manager_instance is not None:
        await _manager_instance.close()
        _manager_instance = None
