import logging
from urllib.parse import urlencode

import requests
from requests import HTTPError
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

logger = logging.getLogger(__name__)


class RestAPI:
    response = None

    def __init__(
        self,
        url=None,
        username=None,
        password=None,
        token=None,
        timeout=75,
        api_root=None,
        api_version=None,
        verify_ssl=True,
        session=None,
        raw_response=None,
        proxies=None,
        cert=None,
    ):
        self.url = url
        self.timeout = int(timeout)
        self.verify_ssl = verify_ssl
        self.api_root = api_root
        self.api_version = api_version
        self.raw_response = raw_response
        self._session = None
        self._auth_headers = None
        self.proxies = proxies
        self.cert = cert

        if session is None:
            self._session = RestAPI.default_session()
        else:
            self._session = session

        if username and password:
            self._create_basic_session(username, password)
        elif token is not None:
            self._create_token_session(token)

    def __enter__(self):
        return self

    def __exit__(self, *_):
        self.close()

    def _create_basic_session(self, username, password):
        self._session.auth = (username, password)

    def _create_token_session(self, token):
        self._update_header("Authorization", f"Bearer {token}")

    @staticmethod
    def default_session():
        session = requests.Session()
        retries = Retry(
            total=10,
            connect=6,
            read=4,
            backoff_factor=1,
            status_forcelist=[
                500,
                502,
                503,
                504,
                520,
                521,
                522,
                523,
                524,
                525,
                527,
                530,
            ],
        )
        session.mount("http://", HTTPAdapter(max_retries=retries))
        session.mount("https://", HTTPAdapter(max_retries=retries))
        return session

    def _update_header(self, key, value):
        """
        Update header for existing session
        :param key:
        :param value:
        :return:
        """
        self._session.headers.update({key: value})

    @staticmethod
    def _response_handler(response):
        try:
            return response.json()
        except ValueError:
            logger.debug("Received response with no content")
            return None
        except Exception as ex:
            logger.error("<%s>: %s", type(ex).__name__, ex, exc_info=False)
            return None

    def resource_url(self, resource, api_root=None, api_version=None):
        if api_root is None:
            api_root = self.api_root
        if api_version is None:
            api_version = self.api_version

        return "/".join(
            s.strip("/") for s in [api_root, api_version, resource] if s is not None
        )

    @staticmethod
    def url_joiner(*path, trailing=None):
        url_link = "/".join(str(s).strip("/") for s in [*path] if s is not None)
        if trailing:
            url_link += "/"
        return url_link

    def close(self):
        return self._session.close()

    def request(
        self,
        method="GET",
        path="/",
        data=None,
        json=None,
        flags=None,
        params=None,
        headers=None,
        stream=False,
        files=None,
        trailing=None,
        absolute=False,
        raw_response=False,
    ):
        """

        :param method:
        :param path:
        :param data:
        :param json:
        :param flags:
        :param params:
        :param headers:
        :param stream:
        :param files:
        :param trailing: bool
        :param absolute: OPTIONAL: Do not prefix url, url is absolute
        :param raw_response: bool, OPTIONAL: Return the raw response
        :return:
        """

        url = RestAPI.url_joiner(None if absolute else self.url, path, trailing)
        params_already_in_url = "?" in url
        if params or flags:
            if params_already_in_url:
                url += "&"
            else:
                url += "?"
        if params:
            url += urlencode(params or {})
        if flags:
            url += ("&" if params or params_already_in_url else "") + "&".join(
                flags or []
            )

        response = self._session.request(
            method=method,
            url=url,
            headers=headers,
            data=data,
            json=json,
            stream=stream,
            timeout=self.timeout,
            verify=self.verify_ssl,
            files=files,
            proxies=self.proxies,
            cert=self.cert,
        )
        response.encoding = "utf-8"

        logger.debug(
            "HTTP: %s %s for %s %s", response.status_code, response.reason, method, path
        )
        if self.raw_response or raw_response:
            return response

        RestAPI.raise_for_status(response)
        return response

    def get(
        self,
        path,
        data=None,
        flags=None,
        params=None,
        headers=None,
        stream=False,
        trailing=None,
        absolute=False,
        raw_response=False,
    ):
        """
        Get request based on the python-requests module.
        :param path:
        :param data:
        :param flags:
        :param params:
        :param headers:
        :param stream:
        :param trailing: Adds a '/' at the end of string
        :param absolute: Do not prefix url, url is ABSOLUTE
        :param raw_response: Return the raw response
        :return:
        """

        response = self.request(
            "GET",
            path=path,
            flags=flags,
            params=params,
            data=data,
            headers=headers,
            stream=stream,
            trailing=trailing,
            absolute=absolute,
            raw_response=raw_response,
        )
        if self.raw_response or raw_response:
            return response
        if not response.text:
            return None
        try:
            return response.json()
        except Exception as ex:
            logger.exception("<%s>: %s", type(ex).__name__, ex)
            return response.text

    def post(
        self,
        path,
        data=None,
        json=None,
        headers=None,
        files=None,
        params=None,
        trailing=None,
        absolute=False,
        raw_response=False,
    ):
        """
        :param path:
        :param data:
        :param json:
        :param headers:
        :param files:
        :param params:
        :param files:
        :param trailing:
        :param absolute:
        :param raw_response: bool, OPTIONAL: Return the raw response
        :return: if raw_response is not set - returns dictionary.
        If it is set - returns raw response.
        """

        response = self.request(
            "POST",
            path=path,
            data=data,
            json=json,
            headers=headers,
            files=files,
            params=params,
            trailing=trailing,
            absolute=absolute,
        )
        if self.raw_response or raw_response:
            return response
        return RestAPI._response_handler(response)

    def put(
        self,
        path,
        data=None,
        json=None,
        headers=None,
        files=None,
        trailing=None,
        params=None,
        absolute=False,
        raw_response=False,
    ):
        """
        :param path:
        :param data:
        :param json:
        :param headers:
        :param files:
        :param trailing:
        :param files:
        :param params:
        :param absolute:
        :param raw_response: bool, OPTIONAL: Return the raw response
        :return: if raw_response is not set - returns dictionary.
        If it is set - returns raw response.
        """

        response = self.request(
            "PUT",
            path=path,
            data=data,
            json=json,
            headers=headers,
            files=files,
            params=params,
            trailing=trailing,
            absolute=absolute,
        )
        if self.raw_response or raw_response:
            return response
        return RestAPI._response_handler(response)

    def patch(
        self,
        path,
        data=None,
        json=None,
        headers=None,
        files=None,
        trailing=None,
        params=None,
        absolute=False,
        raw_response=False,
    ):
        """
        :param path: path of request
        :param data:
        :param json:
        :param headers: adjusted headers, usually it's default
        :param files:
        :param trailing:
        :param params:
        :param absolute:
        :param raw_response: bool, OPTIONAL: Return the raw response
        :return: if raw_response is not set - returns dictionary.
        If it is set - returns raw response.
        """

        response = self.request(
            "PATCH",
            path=path,
            data=data,
            json=json,
            headers=headers,
            files=files,
            params=params,
            trailing=trailing,
            absolute=absolute,
        )
        if self.raw_response or raw_response:
            return response
        return RestAPI._response_handler(response)

    def delete(
        self,
        path,
        data=None,
        headers=None,
        params=None,
        trailing=None,
        absolute=False,
        raw_response=False,
    ):
        """
        Deletes resources at given paths.
        :param path:
        :param data:
        :param headers:
        :param params:
        :param trailing:
        :param absolute:
        :param raw_response:
        :return:
        """

        response = self.request(
            "DELETE",
            path=path,
            data=data,
            headers=headers,
            params=params,
            trailing=trailing,
            absolute=absolute,
        )
        if self.raw_response or raw_response:
            return response
        return RestAPI._response_handler(response)

    def _get_streamed_raw_response(self, url):
        """
        Gets a raw streamed response object for the given absolute URL.
        :param url:
        :return:
        """

        return self.get(url, absolute=True, stream=True, raw_response=True)

    def download_file(self, url, file_path, chunk_size=10485760):
        """
        Downloads a file from the specified URL and saves it to the given file path.
        :param url: The URL of the file to download.
        :param file_path: The file path where the downloaded file will be saved.
        :param chunk_size: The size of each chunk to download (default: 10 MB).
        :return:
        """

        response = self._get_streamed_raw_response(url)
        if response:
            with open(file_path, "wb") as file_write:
                for chunk in response.iter_content(chunk_size=chunk_size):
                    file_write.write(chunk)

    @staticmethod
    def raise_for_status(response):
        """
        Raises `HTTPError` if one occurred.
        Checks the response for an error status and raises an exception with the error
        message provided by the server.
        :param response:
        :return:
        """

        if 400 <= response.status_code < 600:
            if isinstance(response.reason, bytes):
                try:
                    reason = response.reason.decode("utf-8")
                except UnicodeDecodeError:
                    reason = response.reason.decode("iso-8859-1")
            else:
                reason = response.reason

            raise HTTPError(
                f"{response.status_code} {reason} -> ({response.text}) for url: {response.url}",
                response=response,
            )

    @property
    def session(self):
        return self._session
