"""Low-level async client for the Dexster Bot API.

Handles HTTP and WebSocket transport with structured exception handling.
Used internally by :class:`~dexfather.bot.DexFatherBot`.
"""

from __future__ import annotations

import asyncio
import json
import uuid
import warnings
from typing import Any, Optional

import httpx

from ._logging import get_logger
from .exceptions import (
    APIError,
    NetworkError,
    TimeoutError as DexFatherTimeoutError,
    _map_error,
)
from ._retry import DEFAULT_RETRY
from .types import ApiResponse

logger = get_logger("dexfather")

# Default Dexster API base URL
DEFAULT_API_BASE = "https://dexster.io/api/bot"


class DexFatherClient:
    """Low-level async client for the Dexster Bot API.

    Handles HTTP and WebSocket transport with structured exceptions.
    Used internally by DexFatherBot.

    Args:
        token: Bot API token (``dxt_...`` format).
        api_base: Base URL for the API (default: ``https://dexster.io/api/bot``).
        timeout: HTTP request timeout in seconds (default: 30).
    """

    # Methods that use GET (no request body)
    _GET_METHODS = frozenset({
        "getMe", "getMyName", "getMyDescription", "getMyShortDescription",
        "getUpdates", "getWebhookInfo", "getMyCommands",
    })

    def __init__(
        self,
        token: str,
        api_base: str = DEFAULT_API_BASE,
        timeout: float = 30.0,
    ) -> None:
        """Initialize the low-level API client.

        Args:
            token: Bot API token in ``dxt_...`` format.
            api_base: Base URL for the API (default: ``https://dexster.io/api/bot``).
            timeout: HTTP request timeout in seconds (default: 30).
        """
        self.token = token
        self.api_base = api_base.rstrip("/")
        from . import __version__
        self._client = httpx.AsyncClient(
            headers={
                "Authorization": f"Bot {token}",
                "User-Agent": f"dexfather-python/{__version__}",
            },
            timeout=timeout,
        )
        self._ws: Any = None  # WebSocket connection (when in WS mode)
        self._pending_responses: dict[str, asyncio.Future[Any]] = {}

        # Warn if using plaintext HTTP for a non-localhost API base (VULN-SDK-002)
        if not api_base.startswith('https://') and 'localhost' not in api_base and '127.0.0.1' not in api_base:
            warnings.warn(
                f"Bot API URL uses plaintext HTTP ({api_base}). "
                "Bot tokens will be transmitted unencrypted. Use HTTPS in production.",
                SecurityWarning,
                stacklevel=2,
            )

    @property
    def ws(self) -> Any:
        """Get the current WebSocket connection, or ``None``."""
        return self._ws

    @ws.setter
    def ws(self, value: Any) -> None:
        """Set the WebSocket connection."""
        self._ws = value

    @DEFAULT_RETRY
    async def _request(self, method: str, **kwargs: Any) -> ApiResponse:
        """Make an API request -- via WebSocket if connected, else HTTP.

        Args:
            method: Bot API method name (e.g. ``"sendMessage"``).
            **kwargs: Method parameters. ``None`` values are filtered out.

        Returns:
            The parsed :class:`ApiResponse`.

        Raises:
            BadRequest: On 400 responses.
            Unauthorized: On 401 responses.
            Forbidden: On 403 responses.
            NotFound: On 404 responses.
            RateLimited: On 429 responses.
            APIError: On other non-ok API responses.
            NetworkError: On connection failures.
            TimeoutError: On request timeouts.
        """
        # Remove None values
        data = {k: v for k, v in kwargs.items() if v is not None}

        # Serialize Pydantic models in reply_markup
        if "reply_markup" in data and hasattr(data["reply_markup"], "to_dict"):
            data["reply_markup"] = data["reply_markup"].to_dict()

        # WebSocket fast path -- send API call over WS, await response
        if self._ws is not None:
            req_id = str(uuid.uuid4())[:8]
            loop = asyncio.get_event_loop()
            future: asyncio.Future[Any] = loop.create_future()
            self._pending_responses[req_id] = future

            try:
                await self._ws.send(json.dumps({
                    "type": "bot:api_call",
                    "id": req_id,
                    "method": method,
                    "params": data,
                }))

                # Wait for response with timeout
                resp_msg = await asyncio.wait_for(future, timeout=10)
                return ApiResponse(
                    ok=resp_msg.get("ok", False),
                    result=resp_msg.get("result"),
                )
            except asyncio.TimeoutError:
                logger.warning(
                    "WS API call timeout for %s, falling back to HTTP", method,
                )
            except Exception as e:
                logger.warning(
                    "WS API call failed for %s: %s, falling back to HTTP",
                    method, e,
                )
            finally:
                self._pending_responses.pop(req_id, None)

        # HTTP path with structured exception wrapping
        url = f"{self.api_base}/{method}"

        try:
            # Use GET for read-only endpoints, POST for everything else
            if method in self._GET_METHODS:
                resp = await self._client.get(url, params=data if data else None)
            else:
                resp = await self._client.post(url, json=data)
        except httpx.ConnectError as e:
            raise NetworkError(f"Connection failed: {e}") from e
        except httpx.TimeoutException as e:
            raise DexFatherTimeoutError(f"Request timed out: {e}") from e
        except httpx.HTTPError as e:
            raise NetworkError(f"HTTP error: {e}") from e

        # Handle non-JSON or non-standard error responses
        try:
            body = resp.json()
        except Exception:
            raise _map_error(resp.status_code, resp.text[:200])

        # Handle rate limit or error responses without 'ok' field
        if "ok" not in body:
            error_msg = body.get("error", body.get("message", "Unknown error"))
            raise _map_error(resp.status_code, error_msg)

        result = ApiResponse.model_validate(body)

        if not result.ok:
            raise _map_error(
                result.error_code or resp.status_code,
                result.description or "Unknown error",
            )
        return result

    async def aclose(self) -> None:
        """Close the underlying HTTP client.

        Should be called when the client is no longer needed to release
        connection resources.
        """
        await self._client.aclose()
