from __future__ import annotations

from dataclasses import dataclass
import asyncio
import time
from typing import Any, cast
from urllib.parse import quote

import httpx

from .config import RabbitMQConfig


_VHOST_CACHE: tuple[float, list[dict[str, Any]]] | None = None


def encode_segment(value: str) -> str:
    return quote(value, safe="")


class RabbitMQError(RuntimeError):
    pass


@dataclass
class RabbitMQClient:
    config: RabbitMQConfig
    _client: httpx.AsyncClient | None = None
    _owns_client: bool = False

    def __post_init__(self) -> None:
        if self._client is None:
            self._client = httpx.AsyncClient(
                base_url=self.config.api_base_url,
                auth=(self.config.username, self.config.password),
                verify=self.config.verify_ssl,
                timeout=self.config.timeout,
                limits=httpx.Limits(
                    max_connections=self.config.max_connections,
                    max_keepalive_connections=self.config.max_keepalive_connections,
                ),
            )
            self._owns_client = True

    async def __aenter__(self) -> "RabbitMQClient":
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: object | None,
    ) -> None:
        await self.close()

    async def close(self) -> None:
        if self._owns_client and self._client is not None:
            await self._client.aclose()

    @staticmethod
    def _normalize_path(path: str) -> str:
        normalized = path
        if normalized.startswith("/api/"):
            normalized = normalized[len("/api") :]
        elif normalized == "/api":
            normalized = ""
        if not normalized.startswith("/"):
            normalized = f"/{normalized}"
        return normalized

    async def request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json_body: dict[str, Any] | None = None,
        return_meta: bool = False,
    ) -> Any:
        if self._client is None:
            raise RabbitMQError("HTTP client not initialized")
        normalized = self._normalize_path(path)
        attempts = max(self.config.retries + 1, 1)
        for attempt in range(attempts):
            try:
                response = await self._client.request(
                    method.upper(), normalized, params=params, json=json_body
                )
                response.raise_for_status()
                if response.status_code == 204 or not response.content:
                    return (None, response.status_code) if return_meta else None
                data = response.json()
                return (data, response.status_code) if return_meta else data
            except httpx.HTTPError as exc:
                if attempt >= attempts - 1 or method.upper() not in {"GET", "HEAD"}:
                    raise RabbitMQError("RabbitMQ API request failed") from exc
                await asyncio.sleep(self.config.retry_backoff * (attempt + 1))

    async def _get(self, path: str) -> Any:
        return await self.request("GET", path)

    async def _post(self, path: str, payload: dict[str, Any]) -> Any:
        return await self.request("POST", path, json_body=payload)

    async def overview(self) -> dict[str, Any]:
        return cast(dict[str, Any], await self._get("/overview"))

    async def list_queues(self, vhost: str | None = None) -> list[dict[str, Any]]:
        if vhost is None:
            data = await self._get("/queues")
        else:
            data = await self._get(f"/queues/{encode_segment(vhost)}")
        return cast(list[dict[str, Any]], data)

    async def get_queue(self, vhost: str, name: str) -> dict[str, Any]:
        data = await self._get(
            f"/queues/{encode_segment(vhost)}/{encode_segment(name)}"
        )
        return cast(dict[str, Any], data)

    async def list_exchanges(self, vhost: str | None = None) -> list[dict[str, Any]]:
        if vhost is None:
            data = await self._get("/exchanges")
        else:
            data = await self._get(f"/exchanges/{encode_segment(vhost)}")
        return cast(list[dict[str, Any]], data)

    async def list_bindings(self, vhost: str | None = None) -> list[dict[str, Any]]:
        if vhost is None:
            data = await self._get("/bindings")
        else:
            data = await self._get(f"/bindings/{encode_segment(vhost)}")
        return cast(list[dict[str, Any]], data)

    async def list_vhosts(self) -> list[dict[str, Any]]:
        global _VHOST_CACHE
        now = time.monotonic()
        if _VHOST_CACHE and now - _VHOST_CACHE[0] < self.config.vhost_cache_ttl:
            return _VHOST_CACHE[1]
        data = cast(list[dict[str, Any]], await self._get("/vhosts"))
        _VHOST_CACHE = (now, data)
        return data

    async def list_users(self) -> list[dict[str, Any]]:
        return cast(list[dict[str, Any]], await self._get("/users"))

    async def publish(
        self,
        vhost: str,
        exchange: str,
        routing_key: str,
        payload: str,
        payload_encoding: str = "string",
        properties: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        body = {
            "properties": properties or {},
            "routing_key": routing_key,
            "payload": payload,
            "payload_encoding": payload_encoding,
        }
        path = f"/exchanges/{encode_segment(vhost)}/{encode_segment(exchange)}/publish"
        return cast(dict[str, Any], await self._post(path, body))
