"""HTTP client implementation for MCP Proxy.

This module provides an aiohttp-based HTTP client with support for:
- Standard HTTP requests (POST, GET)
- Server-Sent Events (SSE) streaming
- Session management via MCP-Session-Id headers
- OAuth Bearer token authentication
- Connection pooling for efficient resource utilization
- Exponential backoff retry logic for connection resilience
- Graceful handling of partial message transmission
- Comprehensive error logging and diagnostics
"""

import asyncio
import logging
import random
from dataclasses import dataclass, field
from typing import Optional, AsyncIterator, Dict, Any, Callable, Set, TypeVar, List
from contextlib import asynccontextmanager
from functools import wraps

import aiohttp
from aiohttp import ClientSession, ClientTimeout, TCPConnector

from ..models import HttpMcpRequest, HttpMcpResponse
from ..exceptions import (
    ConnectionError,
    ConnectionTimeoutError,
    StreamError,
    HttpError,
    NetworkError,
)
from ..logging_config import get_logger

logger = get_logger(__name__)

# Type variable for generic retry decorator
T = TypeVar('T')


@dataclass
class RetryConfig:
    """Configuration for retry behavior with exponential backoff.
    
    Attributes:
        max_retries: Maximum number of retry attempts (default: 3)
        base_delay: Initial delay in seconds before first retry (default: 1.0)
        max_delay: Maximum delay in seconds between retries (default: 30.0)
        exponential_base: Base for exponential backoff calculation (default: 2.0)
        jitter: Whether to add random jitter to delays (default: True)
        retryable_status_codes: HTTP status codes that should trigger a retry
    """
    max_retries: int = 3
    base_delay: float = 1.0
    max_delay: float = 30.0
    exponential_base: float = 2.0
    jitter: bool = True
    retryable_status_codes: Set[int] = field(default_factory=lambda: {
        408,  # Request Timeout
        429,  # Too Many Requests
        500,  # Internal Server Error
        502,  # Bad Gateway
        503,  # Service Unavailable
        504,  # Gateway Timeout
    })

    def calculate_delay(self, attempt: int) -> float:
        """Calculate delay for a given retry attempt using exponential backoff.
        
        Args:
            attempt: The retry attempt number (0-indexed)
            
        Returns:
            Delay in seconds before the next retry
        """
        # Calculate exponential delay
        delay = self.base_delay * (self.exponential_base ** attempt)
        
        # Cap at max delay
        delay = min(delay, self.max_delay)
        
        # Add jitter if enabled (±25% randomization)
        if self.jitter:
            jitter_range = delay * 0.25
            delay = delay + random.uniform(-jitter_range, jitter_range)
            delay = max(0.1, delay)  # Ensure minimum delay
        
        return delay


# Exceptions that are considered retryable (transient failures)
RETRYABLE_EXCEPTIONS = (
    ConnectionError,
    ConnectionTimeoutError,
    asyncio.TimeoutError,
    aiohttp.ClientConnectorError,
    aiohttp.ServerDisconnectedError,
    aiohttp.ServerTimeoutError,
    aiohttp.ClientOSError,
)

# Exceptions that should NOT be retried (permanent failures)
NON_RETRYABLE_EXCEPTIONS = (
    aiohttp.InvalidURL,
    aiohttp.ClientSSLError,
    ValueError,
    TypeError,
)


def is_retryable_error(error: Exception, retry_config: RetryConfig) -> bool:
    """Determine if an error is retryable.
    
    Args:
        error: The exception to check
        retry_config: Retry configuration with retryable status codes
        
    Returns:
        True if the error is retryable, False otherwise
    """
    # Check for non-retryable exceptions first
    if isinstance(error, NON_RETRYABLE_EXCEPTIONS):
        return False
    
    # Check for retryable exceptions
    if isinstance(error, RETRYABLE_EXCEPTIONS):
        return True
    
    # Check for HTTP errors with retryable status codes
    if isinstance(error, HttpError):
        return error.status_code in retry_config.retryable_status_codes
    
    # Check for aiohttp response errors with status codes
    if isinstance(error, aiohttp.ClientResponseError):
        return error.status in retry_config.retryable_status_codes
    
    # Default: don't retry unknown errors
    return False


class PartialMessageBuffer:
    """Buffer for handling partial message transmission.
    
    This class accumulates partial data chunks and provides methods
    to extract complete messages when they are fully received.
    """
    
    def __init__(self, max_buffer_size: int = 10 * 1024 * 1024):  # 10MB default
        """Initialize the partial message buffer.
        
        Args:
            max_buffer_size: Maximum buffer size in bytes
        """
        self._buffer: bytes = b""
        self._max_buffer_size = max_buffer_size
    
    def append(self, data: bytes) -> None:
        """Append data to the buffer.
        
        Args:
            data: Data bytes to append
            
        Raises:
            StreamError: If buffer exceeds maximum size
        """
        if len(self._buffer) + len(data) > self._max_buffer_size:
            raise StreamError(
                f"Buffer overflow: exceeded maximum size of {self._max_buffer_size} bytes"
            )
        self._buffer += data
    
    def extract_complete_messages(self, delimiter: bytes = b"\n\n") -> List[bytes]:
        """Extract complete messages from the buffer.
        
        Args:
            delimiter: Message delimiter (default: double newline for SSE)
            
        Returns:
            List of complete messages
        """
        messages = []
        while delimiter in self._buffer:
            message, self._buffer = self._buffer.split(delimiter, 1)
            if message:
                messages.append(message)
        return messages
    
    def get_remaining(self) -> bytes:
        """Get any remaining data in the buffer.
        
        Returns:
            Remaining buffer contents
        """
        remaining = self._buffer
        self._buffer = b""
        return remaining
    
    def clear(self) -> None:
        """Clear the buffer."""
        self._buffer = b""
    
    @property
    def size(self) -> int:
        """Get current buffer size in bytes."""
        return len(self._buffer)


@dataclass
class SSEEvent:
    """Represents a Server-Sent Event."""
    event: str = "message"
    data: str = ""
    id: Optional[str] = None
    retry: Optional[int] = None


class HttpClientImpl:
    """HTTP client for communicating with backend MCP servers.
    
    This client supports:
    - Standard HTTP requests with automatic retries
    - Server-Sent Events (SSE) streaming for real-time updates
    - Session management via MCP-Session-Id headers
    - OAuth Bearer token authentication
    - Connection pooling and resource management
    - Exponential backoff retry logic for connection resilience
    - Graceful handling of partial message transmission
    
    Attributes:
        base_url: Base URL for the HTTP MCP server
        timeout: Request timeout in seconds
        max_connections: Maximum number of concurrent connections
        auth_token: OAuth Bearer token for authentication
        session_id: Current MCP session ID
        retry_config: Configuration for retry behavior
    """

    MCP_SESSION_HEADER = "Mcp-Session-Id"
    AUTHORIZATION_HEADER = "Authorization"
    CONTENT_TYPE_HEADER = "Content-Type"
    ACCEPT_HEADER = "Accept"
    
    SSE_CONTENT_TYPE = "text/event-stream"
    JSON_CONTENT_TYPE = "application/json"

    def __init__(
        self,
        base_url: str,
        timeout: int = 30,
        max_connections: int = 10,
        max_connections_per_host: int = 5,
        retry_config: Optional[RetryConfig] = None,
    ):
        """Initialize the HTTP client.
        
        Args:
            base_url: Base URL for the HTTP MCP server
            timeout: Request timeout in seconds (default: 30)
            max_connections: Maximum total connections in pool (default: 10)
            max_connections_per_host: Maximum connections per host (default: 5)
            retry_config: Configuration for retry behavior (default: RetryConfig())
        """
        self._base_url = base_url.rstrip("/")
        self._timeout = ClientTimeout(total=timeout)
        self._max_connections = max_connections
        self._max_connections_per_host = max_connections_per_host
        self._retry_config = retry_config or RetryConfig()
        
        self._auth_token: Optional[str] = None
        self._session_id: Optional[str] = None
        self._session: Optional[ClientSession] = None
        self._connector: Optional[TCPConnector] = None
        self._is_closed = False
        
        # Connection failure tracking for diagnostics
        self._connection_failures: List[Dict[str, Any]] = []
        self._last_successful_request: Optional[float] = None
        
        logger.info(
            "HTTP client initialized",
            base_url=self._base_url,
            timeout=timeout,
            max_connections=max_connections,
            max_connections_per_host=max_connections_per_host,
            max_retries=self._retry_config.max_retries,
        )

    @property
    def base_url(self) -> str:
        """Get the base URL."""
        return self._base_url

    @property
    def session_id(self) -> Optional[str]:
        """Get the current session ID."""
        return self._session_id

    @session_id.setter
    def session_id(self, value: Optional[str]) -> None:
        """Set the session ID."""
        self._session_id = value
        logger.debug("Session ID set to: %s", value)

    @property
    def auth_token(self) -> Optional[str]:
        """Get the current auth token."""
        return self._auth_token

    @property
    def retry_config(self) -> RetryConfig:
        """Get the retry configuration."""
        return self._retry_config

    @retry_config.setter
    def retry_config(self, value: RetryConfig) -> None:
        """Set the retry configuration."""
        self._retry_config = value
        logger.debug("Retry config updated: max_retries=%d", value.max_retries)

    @property
    def is_connected(self) -> bool:
        """Check if the client has an active session."""
        return self._session is not None and not self._session.closed

    async def _ensure_session(self) -> ClientSession:
        """Ensure an aiohttp session exists and return it.
        
        Creates a new session with connection pooling if one doesn't exist.
        
        Returns:
            Active ClientSession instance
            
        Raises:
            ConnectionError: If client has been closed
        """
        if self._is_closed:
            raise ConnectionError("HTTP client has been closed")
            
        if self._session is None or self._session.closed:
            self._connector = TCPConnector(
                limit=self._max_connections,
                limit_per_host=self._max_connections_per_host,
                enable_cleanup_closed=True,
            )
            self._session = ClientSession(
                timeout=self._timeout,
                connector=self._connector,
            )
            logger.debug("Created new aiohttp session with connection pooling")
            
        return self._session

    def set_auth_token(self, token: str) -> None:
        """Set the OAuth Bearer token for authentication.
        
        Args:
            token: OAuth access token
        """
        self._auth_token = token
        logger.debug("Auth token updated")

    def clear_auth_token(self) -> None:
        """Clear the current auth token."""
        self._auth_token = None
        logger.debug("Auth token cleared")

    def _build_headers(
        self,
        additional_headers: Optional[Dict[str, str]] = None,
        include_session: bool = True,
        content_type: Optional[str] = None,
        accept: Optional[str] = None,
    ) -> Dict[str, str]:
        """Build request headers with authentication and session info.
        
        Args:
            additional_headers: Extra headers to include
            include_session: Whether to include session ID header
            content_type: Content-Type header value
            accept: Accept header value
            
        Returns:
            Dictionary of headers
        """
        headers: Dict[str, str] = {}
        
        # Add authentication header
        if self._auth_token:
            headers[self.AUTHORIZATION_HEADER] = f"Bearer {self._auth_token}"
        
        # Add session ID header
        if include_session and self._session_id:
            headers[self.MCP_SESSION_HEADER] = self._session_id
        
        # Add content type
        if content_type:
            headers[self.CONTENT_TYPE_HEADER] = content_type
        
        # Add accept header
        if accept:
            headers[self.ACCEPT_HEADER] = accept
        
        # Merge additional headers
        if additional_headers:
            headers.update(additional_headers)
        
        return headers

    def _extract_session_id(self, response_headers: Dict[str, str]) -> None:
        """Extract and store session ID from response headers.
        
        Args:
            response_headers: Response headers dictionary
        """
        # Check for session ID in response (case-insensitive)
        for key, value in response_headers.items():
            if key.lower() == self.MCP_SESSION_HEADER.lower():
                if value != self._session_id:
                    self._session_id = value
                    logger.info("Session ID updated from response: %s", value)
                break

    async def send_request(self, request: HttpMcpRequest) -> HttpMcpResponse:
        """Send an HTTP request to the MCP server with retry logic.
        
        Implements exponential backoff retry for transient failures.
        
        Args:
            request: HTTP MCP request to send
            
        Returns:
            HTTP MCP response
            
        Raises:
            ConnectionError: If connection fails after all retries
            ConnectionTimeoutError: If request times out after all retries
            HttpError: If HTTP error occurs and is not retryable
        """
        return await self._send_request_with_retry(request)

    async def _send_request_with_retry(
        self,
        request: HttpMcpRequest,
        retry_config: Optional[RetryConfig] = None,
    ) -> HttpMcpResponse:
        """Send request with exponential backoff retry logic.
        
        Args:
            request: HTTP MCP request to send
            retry_config: Optional override for retry configuration
            
        Returns:
            HTTP MCP response
            
        Raises:
            ConnectionError: If connection fails after all retries
            ConnectionTimeoutError: If request times out after all retries
            HttpError: If HTTP error occurs and is not retryable
        """
        config = retry_config or self._retry_config
        last_error: Optional[Exception] = None
        
        for attempt in range(config.max_retries + 1):
            try:
                response = await self._send_request_once(request)
                
                # Record successful request
                import time
                self._last_successful_request = time.time()
                
                return response
                
            except Exception as e:
                last_error = e
                
                # Record failure for diagnostics
                self._record_connection_failure(e, request, attempt)
                
                # Check if we should retry
                if attempt >= config.max_retries:
                    logger.error(
                        "Request failed after all retry attempts",
                        attempts=attempt + 1,
                        error=str(e),
                        error_type=type(e).__name__,
                        url=request.url,
                        method=request.method,
                    )
                    raise
                
                if not is_retryable_error(e, config):
                    logger.error(
                        "Non-retryable error encountered",
                        error=str(e),
                        error_type=type(e).__name__,
                        url=request.url,
                        method=request.method,
                    )
                    raise
                
                # Calculate delay and wait
                delay = config.calculate_delay(attempt)
                logger.warning(
                    "Request failed, retrying",
                    attempt=attempt + 1,
                    max_attempts=config.max_retries + 1,
                    retry_delay=delay,
                    error=str(e),
                    error_type=type(e).__name__,
                    url=request.url,
                    method=request.method,
                )
                await asyncio.sleep(delay)
        
        # This should not be reached, but just in case
        if last_error:
            raise last_error
        raise ConnectionError("Request failed with unknown error")

    def _record_connection_failure(
        self,
        error: Exception,
        request: HttpMcpRequest,
        attempt: int
    ) -> None:
        """Record a connection failure for diagnostic tracking.
        
        Args:
            error: The exception that occurred
            request: The request that failed
            attempt: The attempt number
        """
        import time
        
        failure_record = {
            "timestamp": time.time(),
            "error_type": type(error).__name__,
            "error_message": str(error),
            "url": request.url,
            "method": request.method,
            "attempt": attempt,
        }
        self._connection_failures.append(failure_record)
        
        # Keep only recent failures (last 100)
        if len(self._connection_failures) > 100:
            self._connection_failures = self._connection_failures[-100:]
        
        # Log diagnostic info if many recent failures
        if len(self._connection_failures) >= 10:
            recent_failures = [
                f for f in self._connection_failures
                if time.time() - f["timestamp"] < 300  # Last 5 minutes
            ]
            if len(recent_failures) >= 10:
                self._log_connection_diagnostics(recent_failures)

    async def _send_request_once(self, request: HttpMcpRequest) -> HttpMcpResponse:
        """Send a single HTTP request without retry logic.
        
        Args:
            request: HTTP MCP request to send
            
        Returns:
            HTTP MCP response
            
        Raises:
            ConnectionError: If connection fails
            ConnectionTimeoutError: If request times out
            HttpError: If HTTP error occurs
        """
        session = await self._ensure_session()
        
        # Build full URL - handle "/" specially to avoid trailing slash issues
        if request.url.startswith("http"):
            url = request.url
        elif request.url == "/" or request.url == "":
            # For root path, use base URL directly (no trailing slash)
            url = self._base_url
        else:
            # For other paths, join properly
            url = f"{self._base_url}{request.url}"
        
        # Use session ID from request if provided, otherwise use stored one
        if request.session_id:
            self._session_id = request.session_id
        
        # Build headers - MCP requires Accept header for both JSON and SSE
        headers = self._build_headers(
            additional_headers=request.headers,
            include_session=True,
            content_type=self.JSON_CONTENT_TYPE if request.body else None,
            accept="application/json, text/event-stream",  # MCP requires both
        )
        
        logger.debug(
            "Sending %s request to %s with session_id=%s",
            request.method, url, self._session_id
        )
        
        try:
            async with session.request(
                method=request.method,
                url=url,
                headers=headers,
                data=request.body,
            ) as response:
                # Extract session ID from response
                self._extract_session_id(dict(response.headers))
                
                # Handle partial message transmission gracefully
                body = await self._read_response_body_safely(response)
                
                http_response = HttpMcpResponse(
                    status=response.status,
                    headers=dict(response.headers),
                    body=body,
                    content_type=response.content_type or self.JSON_CONTENT_TYPE,
                )
                
                logger.debug(
                    "Received response: status=%d, content_type=%s, body_length=%d",
                    response.status, response.content_type, len(body)
                )
                
                # Check for retryable HTTP status codes
                if response.status in self._retry_config.retryable_status_codes:
                    raise HttpError(
                        response.status,
                        f"Server returned retryable status code: {response.status}",
                        details={"body": body[:500] if body else None}
                    )
                
                return http_response
                
        except asyncio.TimeoutError as e:
            logger.error("Request timeout for %s %s", request.method, url)
            raise ConnectionTimeoutError(f"Request timed out: {url}") from e
        except aiohttp.ClientConnectorError as e:
            logger.error("Connection error for %s %s: %s", request.method, url, e)
            raise ConnectionError(f"Failed to connect to {url}: {e}") from e
        except aiohttp.ServerDisconnectedError as e:
            logger.error("Server disconnected for %s %s: %s", request.method, url, e)
            raise ConnectionError(f"Server disconnected: {url}") from e
        except aiohttp.ClientError as e:
            logger.error("HTTP client error for %s %s: %s", request.method, url, e)
            raise NetworkError(f"HTTP request failed: {e}") from e

    async def _read_response_body_safely(
        self,
        response: aiohttp.ClientResponse,
        max_size: int = 10 * 1024 * 1024,  # 10MB default
    ) -> str:
        """Read response body with graceful handling of partial transmission.
        
        Args:
            response: aiohttp response object
            max_size: Maximum body size to read in bytes
            
        Returns:
            Response body as string
            
        Raises:
            StreamError: If body exceeds max size or cannot be decoded
        """
        buffer = PartialMessageBuffer(max_buffer_size=max_size)
        
        try:
            async for chunk in response.content.iter_any():
                if chunk:
                    buffer.append(chunk)
            
            # Get all accumulated data
            data = buffer.get_remaining()
            
            # Try to decode as UTF-8
            try:
                return data.decode("utf-8")
            except UnicodeDecodeError:
                # Try with error handling
                return data.decode("utf-8", errors="replace")
                
        except asyncio.TimeoutError:
            # Return partial data on timeout
            logger.warning("Timeout while reading response body, returning partial data")
            data = buffer.get_remaining()
            if data:
                try:
                    return data.decode("utf-8", errors="replace")
                except Exception:
                    return ""
            return ""
        except aiohttp.ClientPayloadError as e:
            # Handle incomplete payload gracefully
            logger.warning("Incomplete payload received: %s", e)
            data = buffer.get_remaining()
            if data:
                try:
                    return data.decode("utf-8", errors="replace")
                except Exception:
                    return ""
            return ""

    async def post(
        self,
        path: str,
        body: str,
        headers: Optional[Dict[str, str]] = None,
    ) -> HttpMcpResponse:
        """Send a POST request to the MCP server.
        
        Convenience method for sending POST requests.
        
        Args:
            path: URL path (will be appended to base URL)
            body: Request body (JSON string)
            headers: Additional headers
            
        Returns:
            HTTP MCP response
        """
        request = HttpMcpRequest(
            method="POST",
            url=path,
            headers=headers or {},
            body=body,
            session_id=self._session_id,
        )
        return await self.send_request(request)

    async def get(
        self,
        path: str,
        headers: Optional[Dict[str, str]] = None,
    ) -> HttpMcpResponse:
        """Send a GET request to the MCP server.
        
        Convenience method for sending GET requests.
        
        Args:
            path: URL path (will be appended to base URL)
            headers: Additional headers
            
        Returns:
            HTTP MCP response
        """
        request = HttpMcpRequest(
            method="GET",
            url=path,
            headers=headers or {},
            session_id=self._session_id,
        )
        return await self.send_request(request)

    @staticmethod
    def _parse_sse_event(raw_event: str) -> SSEEvent:
        """Parse a raw SSE event string into an SSEEvent object.
        
        Args:
            raw_event: Raw event string from SSE stream
            
        Returns:
            Parsed SSEEvent object
        """
        event = SSEEvent()
        data_lines = []
        
        for line in raw_event.split("\n"):
            line = line.strip()
            if not line:
                continue
                
            if line.startswith("event:"):
                event.event = line[6:].strip()
            elif line.startswith("data:"):
                data_lines.append(line[5:].strip())
            elif line.startswith("id:"):
                event.id = line[3:].strip()
            elif line.startswith("retry:"):
                try:
                    event.retry = int(line[6:].strip())
                except ValueError:
                    pass
        
        event.data = "\n".join(data_lines)
        return event

    async def open_sse_stream(
        self,
        path: str = "",
        headers: Optional[Dict[str, str]] = None,
    ) -> AsyncIterator[SSEEvent]:
        """Open a Server-Sent Events stream.
        
        Opens a persistent connection to receive server-sent events.
        The stream will yield SSEEvent objects as they arrive.
        
        Args:
            path: URL path for SSE endpoint (default: base URL)
            headers: Additional headers
            
        Yields:
            SSEEvent objects as they arrive from the server
            
        Raises:
            ConnectionError: If connection fails
            StreamError: If stream encounters an error
        """
        session = await self._ensure_session()
        
        # Build full URL - handle empty/root path specially to avoid trailing slash issues
        if path.startswith("http"):
            url = path
        elif path == "/" or path == "":
            # For root path, use base URL directly (no trailing slash)
            url = self._base_url
        else:
            # For other paths, join properly
            url = f"{self._base_url}{path}"
        
        # Build headers for SSE
        request_headers = self._build_headers(
            additional_headers=headers,
            include_session=True,
            accept=self.SSE_CONTENT_TYPE,
        )
        
        logger.info("Opening SSE stream to %s with session_id=%s", url, self._session_id)
        
        try:
            async with session.get(
                url,
                headers=request_headers,
            ) as response:
                # Verify we got an SSE response
                content_type = response.content_type or ""
                if not content_type.startswith(self.SSE_CONTENT_TYPE):
                    raise StreamError(
                        f"Expected SSE content type, got: {content_type}"
                    )
                
                # Extract session ID from response
                self._extract_session_id(dict(response.headers))
                
                logger.debug("SSE stream opened successfully")
                
                # Buffer for accumulating event data
                buffer = ""
                
                async for chunk in response.content.iter_any():
                    if not chunk:
                        continue
                    
                    # Decode chunk and add to buffer
                    try:
                        text = chunk.decode("utf-8")
                    except UnicodeDecodeError:
                        logger.warning("Failed to decode SSE chunk, skipping")
                        continue
                    
                    buffer += text
                    
                    # Process complete events (separated by double newlines)
                    while "\n\n" in buffer:
                        event_str, buffer = buffer.split("\n\n", 1)
                        if event_str.strip():
                            sse_event = self._parse_sse_event(event_str)
                            logger.debug(
                                "Received SSE event: type=%s, id=%s, data_length=%d",
                                sse_event.event, sse_event.id, len(sse_event.data)
                            )
                            yield sse_event
                
                # Process any remaining data in buffer
                if buffer.strip():
                    sse_event = self._parse_sse_event(buffer)
                    if sse_event.data:
                        yield sse_event
                        
        except asyncio.TimeoutError as e:
            logger.error("SSE stream timeout for %s", url)
            raise ConnectionTimeoutError(f"SSE stream timed out: {url}") from e
        except aiohttp.ClientConnectorError as e:
            logger.error("SSE connection error for %s: %s", url, e)
            raise ConnectionError(f"Failed to connect to SSE stream: {url}") from e
        except aiohttp.ClientError as e:
            logger.error("SSE client error for %s: %s", url, e)
            raise StreamError(f"SSE stream error: {e}") from e

    @asynccontextmanager
    async def sse_stream_context(
        self,
        path: str = "",
        headers: Optional[Dict[str, str]] = None,
    ):
        """Context manager for SSE streams.
        
        Provides a context manager interface for SSE streams with
        automatic cleanup on exit.
        
        Args:
            path: URL path for SSE endpoint
            headers: Additional headers
            
        Yields:
            AsyncIterator of SSEEvent objects
            
        Example:
            async with client.sse_stream_context("/events") as stream:
                async for event in stream:
                    process(event)
        """
        stream = self.open_sse_stream(path, headers)
        try:
            yield stream
        finally:
            # Cleanup is handled by the async generator
            pass

    async def close(self) -> None:
        """Close the HTTP client and release resources.
        
        Closes the aiohttp session and connector, releasing all
        pooled connections.
        """
        self._is_closed = True
        
        if self._session and not self._session.closed:
            await self._session.close()
            logger.debug("Closed aiohttp session")
        
        if self._connector and not self._connector.closed:
            self._connector.close()
            logger.debug("Closed TCP connector")
        
        self._session = None
        self._connector = None
        
        logger.info(
            "HTTP client closed",
            total_connection_failures=len(self._connection_failures),
        )

    def _log_connection_diagnostics(self, recent_failures: List[Dict[str, Any]]) -> None:
        """Log diagnostic information for repeated connection failures.
        
        Args:
            recent_failures: List of recent failure records
        """
        # Analyze failure patterns
        error_types = {}
        urls = {}
        for failure in recent_failures:
            error_type = failure["error_type"]
            url = failure["url"]
            error_types[error_type] = error_types.get(error_type, 0) + 1
            urls[url] = urls.get(url, 0) + 1
        
        # Get most recent failure
        most_recent = recent_failures[-1] if recent_failures else None
        
        import time
        time_since_success = None
        if self._last_successful_request:
            time_since_success = time.time() - self._last_successful_request
        
        logger.error(
            "DIAGNOSTIC: Repeated connection failures detected",
            failure_count=len(recent_failures),
            failure_window_seconds=300,
            error_types=error_types,
            affected_urls=urls,
            most_recent_error=most_recent["error_message"] if most_recent else None,
            most_recent_url=most_recent["url"] if most_recent else None,
            base_url=self._base_url,
            timeout=self._timeout.total,
            max_connections=self._max_connections,
            time_since_last_success=time_since_success,
            diagnostic_suggestions=[
                "Check network connectivity to the MCP server",
                "Verify the MCP server URL is correct and accessible",
                "Check if the MCP server is running and accepting connections",
                "Review firewall rules and network policies",
                "Check DNS resolution for the server hostname",
                "Verify SSL/TLS certificates if using HTTPS",
                "Consider increasing timeout or retry settings",
            ],
        )

    async def __aenter__(self) -> "HttpClientImpl":
        """Async context manager entry."""
        await self._ensure_session()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        """Async context manager exit."""
        await self.close()
