import io
import json
import re

import requests
from requests.adapters import HTTPAdapter
from six.moves.urllib.parse import urlencode
from urllib3 import Retry


class RESTResponse(io.IOBase):

    def __init__(self, resp):
        self.status = resp.status_code
        self.reason = resp.reason
        self.resp = resp
        self.headers = resp.headers

    def getheaders(self):
        return self.headers


class RESTClientObject(object):
    def __init__(self, connection=None):
        self.connection = connection or requests.Session()
        retry_strategy = Retry(
            total=3,
            backoff_factor=2,
            status_forcelist=[429, 500, 502, 503, 504],
            allowed_methods=[
                "HEAD",
                "GET",
                "OPTIONS",
                "DELETE",
            ],  # all the methods that are supposed to be idempotent
        )
        self.connection.mount("https://", HTTPAdapter(max_retries=retry_strategy))
        self.connection.mount("http://", HTTPAdapter(max_retries=retry_strategy))

    def request(
        self,
        method,
        url,
        query_params=None,
        headers=None,
        body=None,
        post_params=None,
        _preload_content=True,
        _request_timeout=None,
    ):
        """Perform requests.

        :param method: http request method
        :param url: http request url
        :param query_params: query parameters in the url
        :param headers: http request headers
        :param body: request json body, for `application/json`
        :param post_params: request post parameters,
                            `application/x-www-form-urlencoded`
                            and `multipart/form-data`
        :param _preload_content: if False, the urllib3.HTTPResponse object will
                                 be returned without reading/decoding response
                                 data. Default is True.
        :param _request_timeout: timeout setting for this request. If one
                                 number provided, it will be total request
                                 timeout. It can also be a pair (tuple) of
                                 (connection, read) timeouts.
        """
        method = method.upper()
        assert method in ["GET", "HEAD", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"]

        if post_params and body:
            raise ValueError(
                "body parameter cannot be used with post_params parameter."
            )

        post_params = post_params or {}
        headers = headers or {}

        timeout = _request_timeout if _request_timeout is not None else (120, 120)

        if "Content-Type" not in headers:
            headers["Content-Type"] = "application/json"

        try:
            # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
            if method in ["POST", "PUT", "PATCH", "OPTIONS", "DELETE"]:
                if query_params:
                    url += "?" + urlencode(query_params)
                if re.search(
                    "json", headers["Content-Type"], re.IGNORECASE
                ) or isinstance(body, str):
                    request_body = "{}"
                    if body is not None:
                        request_body = json.dumps(body)
                        if isinstance(body, str):
                            request_body = request_body.strip('"')
                    r = self.connection.request(
                        method, url, data=request_body, timeout=timeout, headers=headers
                    )
                else:
                    # Cannot generate the request from given parameters
                    msg = """Cannot prepare a request message for provided
                             arguments. Please check that your arguments match
                             declared content type."""
                    raise ApiException(status=0, reason=msg)
            # For `GET`, `HEAD`
            else:
                r = self.connection.request(
                    method, url, params=query_params, timeout=timeout, headers=headers
                )
        except Exception as e:
            msg = "{0}\n{1}".format(type(e).__name__, str(e))
            raise ApiException(status=0, reason=msg)

        if _preload_content:
            r = RESTResponse(r)

        if r.status == 401 or r.status == 403:
            raise AuthorizationException(http_resp=r)

        if not 200 <= r.status <= 299:
            raise ApiException(http_resp=r)

        return r

    def GET(
        self,
        url,
        headers=None,
        query_params=None,
        _preload_content=True,
        _request_timeout=None,
    ):
        return self.request(
            "GET",
            url,
            headers=headers,
            _preload_content=_preload_content,
            _request_timeout=_request_timeout,
            query_params=query_params,
        )

    def HEAD(
        self,
        url,
        headers=None,
        query_params=None,
        _preload_content=True,
        _request_timeout=None,
    ):
        return self.request(
            "HEAD",
            url,
            headers=headers,
            _preload_content=_preload_content,
            _request_timeout=_request_timeout,
            query_params=query_params,
        )

    def OPTIONS(
        self,
        url,
        headers=None,
        query_params=None,
        post_params=None,
        body=None,
        _preload_content=True,
        _request_timeout=None,
    ):
        return self.request(
            "OPTIONS",
            url,
            headers=headers,
            query_params=query_params,
            post_params=post_params,
            _preload_content=_preload_content,
            _request_timeout=_request_timeout,
            body=body,
        )

    def DELETE(
        self,
        url,
        headers=None,
        query_params=None,
        body=None,
        _preload_content=True,
        _request_timeout=None,
    ):
        return self.request(
            "DELETE",
            url,
            headers=headers,
            query_params=query_params,
            _preload_content=_preload_content,
            _request_timeout=_request_timeout,
            body=body,
        )

    def POST(
        self,
        url,
        headers=None,
        query_params=None,
        post_params=None,
        body=None,
        _preload_content=True,
        _request_timeout=None,
    ):
        return self.request(
            "POST",
            url,
            headers=headers,
            query_params=query_params,
            post_params=post_params,
            _preload_content=_preload_content,
            _request_timeout=_request_timeout,
            body=body,
        )

    def PUT(
        self,
        url,
        headers=None,
        query_params=None,
        post_params=None,
        body=None,
        _preload_content=True,
        _request_timeout=None,
    ):
        return self.request(
            "PUT",
            url,
            headers=headers,
            query_params=query_params,
            post_params=post_params,
            _preload_content=_preload_content,
            _request_timeout=_request_timeout,
            body=body,
        )

    def PATCH(
        self,
        url,
        headers=None,
        query_params=None,
        post_params=None,
        body=None,
        _preload_content=True,
        _request_timeout=None,
    ):
        return self.request(
            "PATCH",
            url,
            headers=headers,
            query_params=query_params,
            post_params=post_params,
            _preload_content=_preload_content,
            _request_timeout=_request_timeout,
            body=body,
        )


class ApiException(Exception):

    def __init__(self, status=None, reason=None, http_resp=None, body=None):
        if http_resp:
            self.status = http_resp.status
            self.code = http_resp.status
            self.reason = http_resp.reason
            self.body = http_resp.resp.text
            try:
                if http_resp.resp.text:
                    error = json.loads(http_resp.resp.text)
                    self.message = error["message"]
                else:
                    self.message = http_resp.resp.text
            except Exception as e:
                self.message = http_resp.resp.text
            self.headers = http_resp.getheaders()
        else:
            self.status = status
            self.code = status
            self.reason = reason
            self.body = body
            self.message = body
            self.headers = None

    def __str__(self):
        """Custom error messages for exception"""
        error_message = "({0})\n" "Reason: {1}\n".format(self.status, self.reason)
        if self.headers:
            error_message += "HTTP response headers: {0}\n".format(self.headers)

        if self.body:
            error_message += "HTTP response body: {0}\n".format(self.body)

        return error_message

    def is_not_found(self) -> bool:
        return self.code == 404


class AuthorizationException(ApiException):
    def __init__(self, status=None, reason=None, http_resp=None, body=None):
        try:
            data = json.loads(http_resp.resp.text)
            if "error" in data:
                self._error_code = data["error"]
            else:
                self._error_code = ""
        except Exception:
            self._error_code = ""
        super().__init__(status, reason, http_resp, body)

    @property
    def error_code(self):
        return self._error_code

    @property
    def status_code(self):
        return self.status

    @property
    def token_expired(self) -> bool:
        return self._error_code == "EXPIRED_TOKEN"

    @property
    def invalid_token(self) -> bool:
        return self._error_code == "INVALID_TOKEN"

    def __str__(self):
        """Custom error messages for exception"""
        error_message = f"authorization error: {self._error_code}.  status_code: {self.status}, reason: {self.reason}"

        if self.headers:
            error_message += f", headers: {self.headers}"

        if self.body:
            error_message += f", response: {self.body}"

        return error_message
