import asyncio
import logging
import urllib.parse
from abc import ABC
from typing import Optional, Union

import aiohttp

from maps_adv.common.helpers.timing import TimeDiff

from .exceptions import UnknownResponse

logger = logging.getLogger(__name__)


class BaseClient(ABC):
    __slots__ = "url"

    def __init__(self, url: str):
        self.url = url

    def _url(self, uri: str) -> str:
        return urllib.parse.urljoin(self.url, uri)

    async def _request(
        self, method: str, uri: str, expected_status: int, **kwargs
    ) -> Union[dict, bytes]:
        url = self._url(uri)

        class_name = self.__class__.__name__
        with TimeDiff(class_name) as time_diff:
            if class_name in {"AvatarsClient"}:
                time_diff.muted()

            async with aiohttp.ClientSession() as session:
                logger.info("Request %s, expected status_code=%s", uri, expected_status)
                async with session.request(method, url, **kwargs) as response:
                    await self._check_response(response, expected_status)
                    return await self._extract_response_content(response)

    async def _request_with_retry(
        self,
        method: str,
        uri: str,
        expected_status: int,
        retry: int = 0,
        retry_codes: Optional[Union[list, set, tuple]] = None,
        **kwargs,
    ):
        for step in range(retry + 1):
            if step:
                sleep = 2 ** (step - 1)
                logger.debug("Sleep %s seconds before retry request", sleep)
                await asyncio.sleep(sleep)
            try:
                return await self._request(method, uri, expected_status, **kwargs)
            except UnknownResponse as exception:
                logger.warning("UnknownResponse status_code=%s", exception.status_code)
                if step >= retry or (
                    retry_codes is not None and exception.status_code not in retry_codes
                ):
                    raise

    @classmethod
    async def _check_response(cls, response, expected_status: int):
        if response.status != expected_status:
            raise UnknownResponse(
                message="Response with bad status",
                host=response.host,
                status_code=response.status,
                payload=await cls._extract_response_content(response),
            )

    @staticmethod
    async def _extract_response_content(response):
        if "application/json" in response.headers.get("Content-Type", ""):
            return await response.json()
        else:
            return await response.content.read()

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args, **kwargs):
        pass
