try:
    import requests
except ImportError:
    requests = None

try:
    import pycurl
except ImportError:
    pycurl = None

import base64
import json as json_lib
from io import BytesIO
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import Request, urlopen


class HTTPClient:
    def __init__(
        self,
        user_agent_suffix,
        lib_version,
        force_request=None,
        timeout=None,
    ):
        # Check if requests already available, default to urllib
        self.user_agent = user_agent_suffix + lib_version
        if not force_request:
            if requests:
                self.request = self._requests_request
            elif pycurl:
                self.request = self._pycurl_request
            else:
                self.request = self._urllib_request
        else:
            if force_request == "requests":
                self.request = self._requests_request
            elif force_request == "pycurl":
                self.request = self._pycurl_request
            else:
                self.request = self._urllib_request

        self.timeout = timeout

    def _pycurl_request(
        self, method, url, json=None, data=None, username="", password="", xapikey="", headers=None
    ):
        """This function will send a request with a specified method to the url endpoint using pycurl.
        Returning an AdyenResult object on 200 HTTP response.
        Either json or data has to be provided for POST/PATCH.
        If username and password are provided, basic auth will be used.
        Args:
            method (str): This is the method used to send the request to an endpoint.
            url (str): url to send the request
            json (dict, optional): Dict of the JSON to POST/PATCH
            data (dict, optional): Dict, presumed flat structure of key/value
                of request to place
            username (str, optionl): Username for basic auth. Must be included
                as part of password.
            password (str, optional): Password for basic auth. Must be included
                as part of username.
            xapikey (str, optional):    Adyen API key.  Will be used for auth
                                        if username and password are absent.
            headers (dict, optional): Key/Value pairs of headers to include
        Returns:
            str:    Raw response received
            str:    Raw request placed
            int:    HTTP status code, eg 200,404,401
            dict:   Key/Value pairs of the headers received.
        """
        if headers is None:
            headers = {}

        response_headers = {}
        stringbuffer = BytesIO()
        curl = pycurl.Curl()
        curl.setopt(curl.URL, url)
        curl.setopt(curl.WRITEDATA, stringbuffer)

        # Add User-Agent header to request so that the
        # request can be identified as coming from the Adyen Python library.
        headers["User-Agent"] = self.user_agent

        if username and password:
            curl.setopt(curl.USERPWD, f"{username}:{password}")
        elif xapikey:
            headers["X-API-KEY"] = xapikey

        # Convert the header dict to formatted array as pycurl needs.
        header_list = [f"{k}:{v}" for k, v in headers.items()]
        # Ensure proper content-type when adding headers
        if json:
            header_list.append("Content-Type:application/json")

        curl.setopt(pycurl.HTTPHEADER, header_list)

        # Return regular dict instead of JSON encoded dict for request:
        if method == "POST" or method == "PATCH":
            raw_store = json

            # Set the request body.
            raw_request = json_lib.dumps(json) if json else urlencode(data)
            curl.setopt(curl.POSTFIELDS, raw_request)
            # Needed here as POSTFIELDS sets the method to POST
            curl.setopt(curl.CUSTOMREQUEST, method)

        elif method == "GET" or method == "DELETE":
            curl.setopt(curl.CUSTOMREQUEST, method)
            raw_store = None

        curl.setopt(curl.TIMEOUT, self.timeout)
        curl.perform()

        # Grab the response content
        result = stringbuffer.getvalue()
        status_code = curl.getinfo(curl.RESPONSE_CODE)
        curl.close()

        # Return regular dict instead of JSON encoded dict for request:
        raw_request = raw_store

        return result, raw_request, status_code, response_headers

    def _requests_request(
        self, method, url, json=None, data=None, username="", password="", xapikey="", headers=None
    ):
        """This function will send a request with a specified method to the url endpoint using requests.
        Returning an AdyenResult object on 200 HTTP response.
        Either json or data has to be provided for POST/PATCH.
        If username and password are provided, basic auth will be used.
        Args:
            method (str): This is the method used to send the request to an endpoint.
            url (str): url to send the request
            json (dict, optional): Dict of the JSON to POST/PATCH
            data (dict, optional): Dict, presumed flat structure of key/value
                of request to place
            username (str, optionl): Username for basic auth. Must be included
                as part of password.
            password (str, optional): Password for basic auth. Must be included
                as part of username.
            xapikey (str, optional):    Adyen API key.  Will be used for auth
                                        if username and password are absent.
            headers (dict, optional): Key/Value pairs of headers to include
        Returns:
            str:    Raw response received
            str:    Raw request placed
            int:    HTTP status code, eg 200,404,401
            dict:   Key/Value pairs of the headers received.
        """
        if headers is None:
            headers = {}

        # Adding basic auth if username and password provided.
        auth = None
        if username and password:
            auth = requests.auth.HTTPBasicAuth(username, password)
        elif xapikey:
            headers["x-api-key"] = xapikey

        # Add User-Agent header to request so that the request
        # can be identified as coming from the Adyen Python library.
        headers["User-Agent"] = self.user_agent
        request = requests.request(
            method=method,
            url=url,
            auth=auth,
            data=data,
            json=json,
            headers=headers,
            timeout=self.timeout,
        )

        # Ensure either json or data is returned for raw request
        # Updated: Only return regular dict,
        # don't switch out formats if this is not important.
        message = json

        return request.text, message, request.status_code, request.headers

    def _urllib_request(
        self,
        method,
        url,
        json=None,
        data=None,
        username="",
        password="",
        xapikey="",
        headers=None,
    ):
        """This function will send a request with a specified method to the url endpoint using urlib2.
        Returning an AdyenResult object on 200 HTTP response.
        Either json or data has to be provided for POST/PATCH.
        If username and password are provided, basic auth will be used.
        Args:
            method (str): This is the method used to send the request to an endpoint.
            url (str): url to send the request
            json (dict, optional): Dict of the JSON to POST/PATCH
            data (dict, optional): Dict, presumed flat structure of key/value
                of request to place
            username (str, optionl): Username for basic auth. Must be included
                as part of password.
            password (str, optional): Password for basic auth. Must be included
                as part of username.
            xapikey (str, optional):    Adyen API key.  Will be used for auth
                                        if username and password are absent.
            headers (dict, optional): Key/Value pairs of headers to include
        Returns:
            str:    Raw response received
            str:    Raw request placed
            int:    HTTP status code, eg 200,404,401
            dict:   Key/Value pairs of the headers received.
        """
        if headers is None:
            headers = {}

        if method == "POST" or method == "PATCH":
            # Store regular dict to return later:
            raw_store = json
            raw_request = json_lib.dumps(json) if json else urlencode(data)
            url_request = Request(url, data=raw_request.encode("utf8"), method=method)
            raw_request = raw_store
            if json:
                url_request.add_header("Content-Type", "application/json")
            elif not data:
                raise ValueError("Please provide either a json or a data field.")

        elif method == "GET" or method == "DELETE":
            url_request = Request(url, method=method)
            raw_request = None

        # Add User-Agent header to request so that the
        # request can be identified as coming from the Adyen Python library.
        headers["User-Agent"] = self.user_agent

        # Set regular dict to return as raw_request:

        # Adding basic auth is username and password provided.
        if username and password:
            basic_authstring = (
                base64.encodebytes(f"{username}:{password}".encode()).decode().replace("\n", "")
            )

            url_request.add_header("Authorization", f"Basic {basic_authstring}")
        elif xapikey:
            headers["X-API-KEY"] = xapikey

        # Adding the headers to the request.
        for key, value in headers.items():
            url_request.add_header(key, str(value))

        # URLlib raises all non 200 responses as en error.
        try:
            response = urlopen(url_request, timeout=self.timeout)
        except HTTPError as e:
            raw_response = e.read()

            return raw_response, raw_request, e.getcode(), e.headers
        else:
            raw_response = response.read()
            response.close()

            # The dict(response.info()) is the headers of the response
            # Raw response, raw request, status code and headers returned
            return (raw_response, raw_request, response.getcode(), dict(response.info()))

    def request(
        self,
        method,
        url,
        json="",
        data="",
        username="",
        password="",
        headers=None,
    ):
        """This is overridden on module initialization. This function will make
        an HTTP method call to a given url. Either json/data will be what is posted to
        the end point. he HTTP request needs to be basicAuth when username and
        password are provided. a headers dict maybe provided,
        whatever the values are should be applied.
        Args:
            url (str):                  url to send the request
            method (str):               method to use for the endpoint
            json (dict, optional):      Dict of the JSON to POST/PATCH
            data (dict, optional):      Dict, presumed flat structure of
                                        key/value of request to place as
                                        www-form
            username (str, optional):    Username for basic auth. Must be
                                        included as part of password.
            password (str, optional):   Password for basic auth. Must be
                                        included as part of username.
            xapikey (str, optional):    Adyen API key.  Will be used for auth
                                        if username and password are absent.
            headers (dict, optional):   Key/Value pairs of headers to include
        Returns:
            str:    Raw request placed
            str:    Raw response received
            int:    HTTP status code, eg 200,404,401
            dict:   Key/Value pairs of the headers received.
        """
        raise NotImplementedError(
            "request of HTTPClient should have been "
            "overridden on initialization. "
            "Otherwise, can be overridden to "
            "supply your own post method"
        )
