"""Legacy HTTP + SSE transport for MCP servers.

This transport is kept for backward compatibility with older MCP servers.

It uses:
- POST `{url}/messages` for JSON-RPC requests/notifications
- GET  `{url}/sse` for server notifications/responses
"""

from __future__ import annotations

import asyncio
import json
import logging
from typing import Any

import httpx

from mcpbridge.exceptions import MCPProtocolError, TransportConnectionError, TransportDisconnectedError
from mcpbridge.transport.base import BaseTransport

logger = logging.getLogger("mcpbridge")


class HttpSseTransport(BaseTransport):
    """HTTP+SSE MCP transport implementation."""

    def __init__(self, url: str, headers: dict[str, str] | None = None, timeout: int = 30) -> None:
        self._url = url.rstrip("/")
        self._headers = headers or {}
        self._timeout = timeout
        self._client: httpx.AsyncClient | None = None

        self._connected = False
        self._sse_task: asyncio.Task[None] | None = None
        self._pending: dict[str | int, asyncio.Future[dict[str, Any]]] = {}

        self.notifications_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()

    @property
    def is_connected(self) -> bool:
        return self._connected

    async def connect(self) -> None:
        if self._connected:
            return
        self._client = httpx.AsyncClient(timeout=self._timeout)
        self._connected = True
        self._sse_task = asyncio.create_task(self._sse_listener(), name="mcpbridge-http-sse-listener")

    async def close(self) -> None:
        self._connected = False
        if self._sse_task:
            self._sse_task.cancel()
        for fut in list(self._pending.values()):
            if not fut.done():
                fut.set_exception(TransportDisconnectedError("Transport disconnected"))
        self._pending.clear()

        if self._client:
            await self._client.aclose()
        self._client = None

    async def send_notification(self, method: str, params: dict[str, Any]) -> None:
        await self._send_json({"jsonrpc": "2.0", "method": method, "params": params})

    async def send(self, message: dict[str, Any]) -> dict[str, Any]:
        if not self._client:
            raise TransportDisconnectedError("Transport not connected")
        msg_id = message.get("id")
        if msg_id is None:
            raise MCPProtocolError("send() requires a JSON-RPC `id` field")

        loop = asyncio.get_running_loop()
        fut: asyncio.Future[dict[str, Any]] = loop.create_future()
        self._pending[msg_id] = fut
        try:
            await self._send_json(message)

            resp = await asyncio.wait_for(fut, timeout=self._timeout)
            return resp
        except asyncio.TimeoutError as e:
            self._pending.pop(msg_id, None)
            raise TimeoutError("Transport timed out") from e
        finally:
            self._pending.pop(msg_id, None)

    async def _send_json(self, payload: dict[str, Any]) -> None:
        """POST JSON-RPC payload; response may be synchronous or async via SSE."""
        assert self._client is not None
        try:
            resp = await self._client.post(
                f"{self._url}/messages",
                json=payload,
                headers=self._headers,
            )
        except Exception as e:
            raise TransportConnectionError("Failed to send request") from e

        if resp.status_code in {200, 201}:
            # Synchronous response: resolve pending immediately.
            ct = resp.headers.get("content-type", "")
            if "application/json" in ct:
                data = resp.json()
                msg_id = data.get("id")
                if msg_id in self._pending and not self._pending[msg_id].done():
                    self._pending[msg_id].set_result(data)
            return

        if resp.status_code == 202:
            # Accepted; expect SSE response.
            return

        raise TransportConnectionError(f"HTTP error from MCP server: {resp.status_code} {resp.text}")

    async def _sse_listener(self) -> None:
        assert self._client is not None
        sse_url = f"{self._url}/sse"
        try:
            async with self._client.stream("GET", sse_url, headers=self._headers, timeout=self._timeout) as resp:
                if resp.status_code >= 400:
                    raise TransportConnectionError(f"SSE endpoint error: {resp.status_code}")

                event_buffer: list[str] = []
                async for line in resp.aiter_lines():
                    if line is None:
                        continue
                    line = line.strip("\r")
                    if line == "":
                        if not event_buffer:
                            continue
                        data_str = "\n".join(event_buffer)
                        event_buffer = []
                        try:
                            msg = json.loads(data_str)
                        except Exception as e:
                            logger.exception("Malformed SSE JSON: %r", data_str)
                            raise MCPProtocolError("Malformed JSON from MCP SSE") from e
                        await self._dispatch_incoming(msg)
                        continue

                    if line.startswith("data:"):
                        event_buffer.append(line[len("data:") :].lstrip())
        except asyncio.CancelledError:
            return
        except Exception as e:
            self._connected = False
            for fut in list(self._pending.values()):
                if not fut.done():
                    fut.set_exception(TransportDisconnectedError(f"Transport disconnected: {e}"))
            self._pending.clear()
            logger.error("HTTP SSE listener stopped: %s", e)

    async def _dispatch_incoming(self, msg: dict[str, Any]) -> None:
        msg_id = msg.get("id")
        if msg_id is not None and msg_id in self._pending and not self._pending[msg_id].done():
            self._pending[msg_id].set_result(msg)
            return
        method = msg.get("method")
        if msg_id is None and isinstance(method, str):
            await self.notifications_queue.put(msg)

