"""Base HTTP client with retries, timeout, and error handling."""

from __future__ import annotations

from typing import Any, Dict, Optional, Union
import urllib.parse

import httpx

from sd_api.config import settings
from sd_api.exceptions import APIRequestError, APIResponseError
from sd_api.logging import get_logger
from sd_api.client.health import HealthMixin
from sd_api.client.realtime import RealtimeMixin
from sd_api.client.history import HistoryMixin
from sd_api.client.kline import KlineMixin
from sd_api.client.products import ProductsMixin
from sd_api.client.commands import CommandsMixin

logger = get_logger("client")


class BaseClient:
    """Low-level HTTP client wrapping httpx.

    Provides:
    - Automatic base URL joining
    - Timeout handling
    - Configurable retries on transient errors
    - Consistent JSON error raising
    """

    def __init__(
        self,
        base_url: str,
        *,
        timeout: float | None = None,
        max_retries: int | None = None,
    ) -> None:
        if not base_url:
            raise ValueError("base_url is required")
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout or settings.default_timeout
        self.max_retries = max_retries if max_retries is not None else settings.max_retries
        self._client: Optional[httpx.Client] = None

    # ── httpx client management ────────────────────────────────────────────

    @property
    def http(self) -> httpx.Client:
        if self._client is None:
            self._client = httpx.Client(
                base_url=self.base_url,
                timeout=self.timeout,
                follow_redirects=True,
            )
        return self._client

    def close(self) -> None:
        if self._client is not None:
            self._client.close()
            self._client = None

    def __enter__(self) -> "BaseClient":
        return self

    def __exit__(self, *_: Any) -> None:
        self.close()

    # ── HTTP verbs ─────────────────────────────────────────────────────────

    def _url(self, path: str, params: Optional[Dict[str, Any]] = None) -> str:
        if params:
            q = urllib.parse.urlencode(params)
            return f"{self.base_url}{path}?{q}"
        return f"{self.base_url}{path}"

    def get(
        self,
        path: str,
        *,
        params: Optional[Dict[str, Any]] = None,
        retries: int | None = None,
    ) -> Dict[str, Any]:
        """Send a GET request with retry logic."""
        retries = retries if retries is not None else self.max_retries
        url = self._url(path, params)
        for attempt in range(retries + 1):
            try:
                resp = self.http.get(url)
                return self._handle_response(resp, url)
            except APIRequestError as exc:
                if attempt < retries and self._is_retryable(exc):
                    logger.warning(f"Retryable error on {url} (attempt {attempt + 1}): {exc}")
                    continue
                raise
            except Exception as exc:
                if attempt < retries:
                    logger.warning(f"Unexpected error on {url} (attempt {attempt + 1}): {exc}")
                    continue
                raise

    def post(
        self,
        path: str,
        *,
        json: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None,
        retries: int | None = None,
    ) -> Dict[str, Any]:
        """Send a POST request with retry logic."""
        retries = retries if retries is not None else self.max_retries
        url = self._url(path, params)
        for attempt in range(retries + 1):
            try:
                resp = self.http.post(url, json=json)
                return self._handle_response(resp, url)
            except APIRequestError as exc:
                if attempt < retries and self._is_retryable(exc):
                    logger.warning(f"Retryable POST error on {url} (attempt {attempt + 1}): {exc}")
                    continue
                raise
            except Exception as exc:
                if attempt < retries:
                    logger.warning(f"Unexpected POST error on {url} (attempt {attempt + 1}): {exc}")
                    continue
                raise

    # ── Response handling ──────────────────────────────────────────────────

    @staticmethod
    def _is_retryable(exc: APIRequestError) -> bool:
        """Return True for transient errors worth retrying."""
        if exc.status_code is not None:
            return exc.status_code in (429, 500, 502, 503, 504)
        msg = str(exc).lower()
        return any(kw in msg for kw in ("timeout", "connection", "network"))

    def _handle_response(self, resp: httpx.Response, url: str) -> Dict[str, Any]:
        if resp.status_code >= 500:
            raise APIResponseError(
                f"Server error {resp.status_code}",
                url=url,
                response_body=resp.text[:500],
            )
        if resp.status_code >= 400:
            raise APIResponseError(
                f"Client error {resp.status_code}",
                url=url,
                response_body=resp.text[:500],
            )
        try:
            payload = resp.json()
        except Exception as e:
            if resp.status_code != 204:
                raise APIResponseError(
                    f"Failed to parse JSON: {e}",
                    url=url,
                    response_body=resp.text[:500],
                )
            return {}
        if isinstance(payload, dict) and payload.get("error"):
            raise APIResponseError(
                str(payload["error"]),
                url=url,
                response_body=resp.text[:500],
            )
        return payload


class SDClient(BaseClient, HealthMixin, RealtimeMixin, HistoryMixin, KlineMixin, ProductsMixin, CommandsMixin):
    """Unified API client.

    Exposes all REST endpoints through logical sub-namespaces::

        client = SDClient("http://127.0.0.1:5000")

        client.system.health()
        client.system.products()

        client.quote.live("XAUUSD")
        client.quote.latest("XAUUSD")

        client.kline.get("XAUUSD", tf="5min")

        client.hist.quote_history("XAUUSD", limit=500)

    All network I/O is lazy (only on method call). The client is safe to
    use as a context manager (``with SDClient(...) as client:``) which ensures
    the underlying httpx connection is properly closed.
    """

    def __init__(
        self,
        base_url: str,
        *,
        timeout: float | None = None,
        max_retries: int | None = None,
    ) -> None:
        super().__init__(base_url, timeout=timeout, max_retries=max_retries)
        # Initialise mixins — they hold a reference to self._base
        HealthMixin.__init__(self, self)
        RealtimeMixin.__init__(self, self)
        HistoryMixin.__init__(self, self)
        KlineMixin.__init__(self, self)
        ProductsMixin.__init__(self, self)
        CommandsMixin.__init__(self, self)
