from __future__ import annotations

import logging
from dataclasses import dataclass
from http import HTTPStatus
from typing import Any

import httpx

from . import constants
from .auth import LoyaAuthenticator
from .types import ApiResponse, from_httpx

logger = logging.getLogger(__name__)


@dataclass
class LoyaClientConfig:
    base_url: str
    email: str
    password_hash: str
    apikey: str
    timeout: float = 20.0
    user_agent: str = "LoyaClient/1.0"


class LoyaClient:
    """
    Модульный асинхронный клиент для Loya API
    Полностью разделён на: HTTP, авторизацию, ошибки, утилиты
    """

    def __init__(self, config: LoyaClientConfig):
        self.config = config
        self.config.base_url = config.base_url.rstrip("/")

        limits = httpx.Limits(max_keepalive_connections=10, max_connections=50)
        self._http = httpx.AsyncClient(
            base_url=self.config.base_url,
            timeout=httpx.Timeout(self.config.timeout),
            limits=limits,
            headers={"User-Agent": self.config.user_agent, "Accept": "application/json"},
        )

        self.auth = LoyaAuthenticator(
            client=self._http,
            email=self.config.email,
            password_hash=self.config.password_hash,
            apikey=self.config.apikey,
        )

    # === Базовые HTTP-методы ===
    async def _request(
            self,
            method: str,
            path: str,
            raise_for_status: bool = False,
            **kwargs: Any
    ) -> ApiResponse:
        """
        Выполняет запрос к Loya API с авторизацией

        Если в ответе присутствует новая сессия (Set-Cookie), то обновляем
        сессию

        Если ответ имеет код 401, то повторно выполняем авторизацию и
        запрос к API
        """
        await self.auth.ensure_auth()
        response = await self._http.request(method, path, **kwargs)

        # Обновляем сессию, если пришла новая кука
        if "Set-Cookie" in response.headers:
            self.auth.update_from_set_cookie(response.headers["Set-Cookie"])

        # Авторелогин при 401
        if response.status_code == HTTPStatus.UNAUTHORIZED:
            logger.warning("сессия истекла, повторная авторизация")
            self.auth.clear()
            await self.auth.ensure_auth()
            response = await self._http.request(method, path, **kwargs)

        api_resp = from_httpx(response)

        if raise_for_status and not api_resp["success"]:
            raise httpx.HTTPStatusError(
                message=api_resp["text"] or "Loya API error",
                request=response.request,
                response=response,
            )

        return api_resp

    async def get(
            self,
            path: str,
            *,
            raise_for_status: bool = False,
            **kwargs: Any
    ) -> ApiResponse:
        """
        Отправляет GET-запрос к API
        GET /{path}

        :param path: Путь к ресурсу
        :param kwargs: дополнительные параметры для httpx.request
        :return: Ответ от API
        """
        return await self._request(
            "GET",
            path,
            raise_for_status=raise_for_status,
            **kwargs
        )

    async def post_json(
            self,
            path: str,
            data: Any = None,
            *,
            raise_for_status: bool = False,
            **kwargs: Any
    ) -> ApiResponse:
        """
        Отправляет POST-запрос к API с JSON-данными
        POST /{path}

        data - данные для отправки в формате JSON
        **kwargs - дополнительные параметры для httpx.request

        Возвращает: ответ сервера в виде объекта httpx.Response
        """
        headers = kwargs.pop("headers", {})
        headers.setdefault("Content-Type", "application/json")

        return await self._request(
            "POST",
            path,
            json=data,
            headers=headers,
            raise_for_status=raise_for_status,
            **kwargs,
        )

    async def put_json(
        self,
        path: str,
        data: Any = None,
        *,
        raise_for_status: bool = False,
        **kwargs: Any,
    ) -> httpx.Response:
        """
        PUT-запрос к API с JSON-данными
        PUT /{path}

        :param path: Путь к ресурсу
        :param data: JSON-данные для передачи
        :param kwargs: дополнительные параметры для запроса
        :return: Ответ от API
        """
        headers = kwargs.pop("headers", {})
        headers.setdefault("Content-Type", "application/json")

        return await self.request(
            "PUT",
            path,
            json=data,
            headers=headers,
            raise_for_status=raise_for_status,
            **kwargs
        )

    async def delete(
            self,
            path: str,
            *,
            raise_for_status: bool = False,
            **kwargs: Any
    ) -> httpx.Response:
        """
        DELETE-запрос к API
        DELETE /{path}

        Возвращает: ответ сервера в виде объекта httpx.Response
        """
        return await self.request(
            "DELETE",
            path,
            raise_for_status=raise_for_status,
            **kwargs
        )

    # === API методы ===
    async def get_card_by_number(
            self,
            card_number: str,
            *,
            raise_for_status: bool = False,
    ) -> ApiResponse | dict[str, Any]:
        """
        Получить информацию о карте по её номеру
        GET /api/1.0/card/number/{card_number}

        Возвращает: информацию о карте в формате словаря
        """
        if not card_number.isdigit() or len(card_number) != constants.LOYA_CARD_NUMBER_LENGTH:
            resp = ApiResponse(
                status_code=400,
                success=False,
                data=None,
                text="Некорректный номер карты",
            )
            if raise_for_status:
                raise ValueError(resp["text"])
            return resp

        resp = await self.get(
            f"/api/1.0/card/number/{card_number}",
            raise_for_status=raise_for_status
        )

        return resp["data"] if raise_for_status else resp

    async def get_client_by_phone(
            self,
            phone: str,
            *,
            raise_for_status: bool = False,
    ) -> ApiResponse | list[int]:
        """
        Поиск клиента по номеру телефона
        GET /api/1.0/clientbyphone?phone=xxxxxxxxxxx

        Возвращает: список ID клиентов (обычно 1 элемент, но может быть больше)
        Пример: [2293]
        """
        if not phone.isdigit() or len(phone) != constants.LOYA_PHONE_NUMBER_LENGTH:
            resp = ApiResponse(
                status_code=400,
                success=False,
                data=None,
                text="Некорректный номер телефона",
            )
            if raise_for_status:
                raise ValueError(resp["text"])
            return resp

        resp = await self.get(
            "/api/1.0/clientbyphone",
            params={"phone": phone},
            raise_for_status=raise_for_status
        )

        return resp["data"] if raise_for_status else resp

    async def get_client_by_id(
            self,
            client_id: int,
            *,
            raise_for_status: bool = False,
    ) -> ApiResponse | list[int]:
        """
        Получить информацию о клиенте по его ID
        GET /api/1.0/client/{client_id}

        Возвращает: информацию о клиенте в формате ApiResponse
        """
        resp = await self.get(
            f"/api/1.0/client/{client_id}"
        )

        return resp["data"] if raise_for_status else resp

    async def get_attached_clients(
            self,
            card_numbers: list[str],
            *,
            raise_for_status: bool = False,
    ) -> ApiResponse | list[dict[str, Any]]:
        """
        Получение списка клиентов по номерам карт
        POST /api/1.0/card/attached/clients

        Возвращает: список привязанных клиентов в формате списка словарей
        """
        resp = await self.post_json(
            "/api/1.0/card/attached/clients",
            data=card_numbers,
            raise_for_status=raise_for_status
        )

        return resp["data"] if raise_for_status else resp

    async def get_locations(
            self,
            *,
            raise_for_status: bool = False,
    ) -> dict[str, Any]:
        """
        Получить список объектов/локаций
        GET /api/1.0/location

        Возвращает: список объектов/локаций в формате словаря
        """
        resp = await self.get(
            "/api/1.0/location",
            raise_for_status=raise_for_status
        )

        return resp["data"] if raise_for_status else resp

    # === Жизненный цикл ===
    async def close(self) -> None:
        """
        Закрывает подчиненный HTTP-клиент
        """
        await self._http.aclose()

    async def __aenter__(self) -> LoyaClient:
        """
        Асинхронный контекст менеджера

        Returns:
            LoyaClient: self
        """
        return self

    async def __aexit__(self, exc_type, exc, tb) -> None:
        """
        Close the underlying HTTP client when exiting the async context manager
        """
        await self.close()
