import errno
import socket

# Python 2/3 compatibility
try:
    from urlparse import urlparse, urlunparse
except ImportError:
    from urllib.parse import urlparse, urlunparse
try:
    from urllib2 import build_opener, HTTPSHandler, URLError, Request, HTTPErrorProcessor
except ImportError:
    from urllib.request import build_opener, HTTPSHandler, Request, HTTPErrorProcessor
    from urllib.error import URLError
try:
    from httplib import HTTPSConnection, HTTPConnection
except ImportError:
    from http.client import HTTPSConnection, HTTPConnection


from parallels.core.logging import get_logger

logger = get_logger(__name__)

#
# We use NSS due to missing SSL SNI support in OpenSSL library on CentOS/RHEL 5.
# It's required to successfully check windows 2012 IIS domains from CentOS/RHEL 5 PPA MN.
#
nss_imported = False
try:
    import nss.io as io
    import nss.nss as nss
    import nss.ssl as ssl
    nss_imported = True
except ImportError:
    logger.debug(u'Unable to import NSS. We will use OpenSSL')
    import ssl

nss_is_initialized = False


class HttpClient(object):
    def __init__(self, website_availability_check_timeout):
        self.website_availability_check_timeout = website_availability_check_timeout

    def get(self, url, server_ip=None):
        """Make an HTTP GET request.

        Args:
            url: request URL
            server_ip: Web server IP, for which the request will be sent. Host
            name is passed in HTTP 'Host' request header.

        :rtype: parallels.hosting_check.utils.http_client.HttpResponse
        """
        response = self._request(url, server_ip)

        self._log_request_result(response)

        return response

    @staticmethod
    def _log_request_result(response):
        """Log response info

        :type response: parallels.hosting_check.utils.http_client.HttpResponse
        :rtype: None
        """
        if response is None:
            logger.debug("No response received")
            return

        logger.debug("Response received with code: %s", response.code)
        if response.code in (301, 302):
            redirect_location = response.header('location')
            logger.debug("Redirect location is: %s", redirect_location)

    def _request(self, url, server_ip):
        """Make an HTTP request using the given server_ip."""
        request = self._get_request_object(url, server_ip)

        socket.setdefaulttimeout(int(self.website_availability_check_timeout))
        if nss_imported:
            opener = build_opener(NoRedirection, HTTPSHandler)
            response = opener.open(request)
        else:
            if hasattr(ssl, "_create_unverified_context"):
                # Special handling for Python >= 2.7.9, which got enabled SSL certificate verification by default.
                # Checking for attribute is required, because old versions of Python does not have such
                # internal method and don't know about contexts, and don't have such check by default.

                # As site's certificates are often not valid, we should not check them.
                # Just do HTTPS request ignoring all certificate validity issues.
                # noinspection PyProtectedMember
                unverified_context = ssl._create_unverified_context()

                # Custom HTTPS connection and handler are in-place patch to make SNI requests to
                # Apache work. We replace hostname specified in SNI request here. Otherwise, Apache will respond
                # with 400 HTTP code, which means "Host" header and SNI server hostname mismatch.
                _, server_hostname = self._get_vhost_request(url, server_ip)
                server_hostname = server_hostname.lower()

                class CustomHTTPSConnection(HTTPSConnection):
                    def connect(self):
                        HTTPConnection.connect(self)
                        self.sock = self._context.wrap_socket(self.sock, server_hostname=server_hostname)

                class CustomHTTPSHandler(HTTPSHandler):
                    def https_open(self, req):
                        return self.do_open(CustomHTTPSConnection, req, context=self._context)

                opener = build_opener(
                    NoRedirection,
                    CustomHTTPSHandler(context=unverified_context)
                )
            else:
                opener = build_opener(NoRedirection, HTTPSHandler())

            try:
                response = opener.open(request)
            except URLError:
                # Workaround for https://jira.sw.ru/browse/PMT-147 (
                # Unable to check HTTPS on site hosting on Windows 2012 if
                # migration checker run on Windows)
                url = request.get_full_url()
                if self._get_protocol(url) == 'https':
                    logger.debug(u'Exception:', exc_info=True)
                    logger.warning(u'Unable to make HTTPS request "%s".' % url)
                    return HttpResponse.dummy(code=200, body='')
                else:
                    raise
        return HttpResponse.parse(response, remote_ip=server_ip)

    def _get_request_object(self, url, server_ip):
        """Construct urllib2.Request object."""
        if server_ip:
            (url, header_host) = self._get_vhost_request(url, server_ip)
            request = Request(url)
            request.add_header('Host', header_host)
            request.add_header(
                'User-Agent',
                'Mozilla/5.0 (Windows NT 6.3; WOW64)'
                ' AppleWebKit/537.36 (KHTML, like Gecko)'
                ' Chrome/54.0.2840.71 Safari/537.36'
            )
            logger.debug("HTTP request: curl -H 'Host: %s' '%s'" % (header_host, url))
        else:
            request = Request(url)
            logger.debug("HTTP request: curl '%s'" % (url,))

        return request

    def _get_vhost_request(self, url, server_ip):
        """Construct HTTP request for a name-based virtual host.

        Replace domain name in the URL with server_ip; specify domain name in
        HTTP header 'Host'.
        """
        if server_ip is not None and self._is_ipv6(server_ip):
            request_host = '[%s]' % server_ip
        else:
            request_host = server_ip
        parsed_url = list(urlparse(url))
        header_host = parsed_url[1].encode('idna')
        hostport = parsed_url[1].split(':')  # example.com:80 -> ['example.com', 80]
        if len(hostport) > 1:
            # use port number specified in URL
            parsed_url[1] = "%s:%s" % (request_host, hostport[1])
        else:
            parsed_url[1] = request_host
        url = urlunparse(parsed_url)
        return url, header_host

    @staticmethod
    def _get_protocol(url):
        """Return protocol part of the URL."""
        urlparts = urlparse(url)
        return urlparts[0].lower() 

    @staticmethod
    def _is_ipv6(string):
        # We cannot use socket.inet_pton because it is not available on Windows
        # and may be some Unix OSes
        # https://docs.python.org/2/library/socket.html#socket.inet_pton
        # Availability: Unix (maybe not all platforms).
        return ':' in string


class HTTPSHandler(HTTPSHandler):
    def https_open(self, req):
        return self.do_open(NSSConnection, req)


# noinspection PyClassHasNoInit
class NoRedirection(HTTPErrorProcessor):
    def http_response(self, request, response):
        return response
    https_response = http_response


class HttpResponse(object):
    """HTTP server response that includes HTTP headers and data.

    This is an interface to response object returned by urllib2
    HTTP request.
    """
    def __init__(self, data, body=None, remote_ip=None):
        """Construct HttpResponse object from an existing HTTP response.

        Args:
            data: Response object, as returned by urllib2.
            body: Response body
            remote_ip: IP address of the server
        """
        self.data = data
        self.body = body
        self.remote_ip = remote_ip

    @staticmethod
    def parse(response, remote_ip=None):
        """Create a response object based on 'urlopen()' returned value."""
        return HttpResponse(
            response, body=response.read(), remote_ip=remote_ip
        ) 

    @staticmethod
    def dummy(code=200, body=None, headers=None):
        """Create an empty HttpResponse with status code 200."""
        class DummyResponseData(object):
            code = None
        response = DummyResponseData()
        response.code = code
        response.headers = headers
        http_response = HttpResponse(response)
        if body is not None:
            http_response.body = body
        else:
            http_response.body = ''
        return http_response

    def header(self, header):
        """Return content of response header."""
        if self.data:
            return self.data.headers.getheader(header)
        else:
            return None

    @property
    def code(self):
        if self.data:
            return self.data.code
        else:
            return None

    @property
    def code_class(self):
        """Return the first digit of HTTP status code (1xx .. 5xx)."""
        if self.code:
            return self.code // 100


class NSSConnection(HTTPSConnection):
    NSS_CERT_DB = '/etc/pki/nssdb'
    SOCKET_TIMEOUT = 30

    def __init__(self, host, port=None, strict=None, db_dir=NSS_CERT_DB, **kwargs):
        HTTPSConnection.__init__(self, host, port, strict, **kwargs)

        if not db_dir:
            raise RuntimeError('db_dir is required')

        if not nss_is_initialized:
            nss.nss_init(db_dir)

        self.sock = None
        self._hostname = self.host
        ssl.set_domestic_policy()

    def connect(self):
        addr_info = io.AddrInfo(self.host)
        for net_addr in addr_info:
            net_addr.port = self.port
            self._create_socket(net_addr.family)
            try:
                self.sock.connect(net_addr, timeout=io.seconds_to_interval(self.SOCKET_TIMEOUT))
                return
            except Exception as e:
                logger.debug(u'Exception:', exc_info=e)

        raise IOError(errno.ENOTCONN, u'Could not connect to %s at port %d' % (self.host, self.port))

    def _create_socket(self, family):
        # We accept any server's SSL certificate
        def auth_certificate_callback(sock, check_sig, is_server, certdb):
            return True

        # We do not use client SSL certificates
        def client_auth_data_callback(ca_names, chosen_nickname, password, certdb):
            return False

        self.sock = ssl.SSLSocket(family)
        self.sock.set_ssl_option(ssl.SSL_SECURITY, True)
        self.sock.set_ssl_option(ssl.SSL_HANDSHAKE_AS_CLIENT, True)
        self.sock.set_hostname(self._hostname)

        self.sock.set_auth_certificate_callback(auth_certificate_callback, nss.get_default_certdb())
        self.sock.set_client_auth_data_callback(client_auth_data_callback, '', '', nss.get_default_certdb())

    def _send_request(self, method, url, body, headers):
        host_header_value = [v for k, v in headers.items() if k.lower() == 'host']
        if host_header_value:
            self._hostname = host_header_value[0]

        HTTPSConnection._send_request(self, method, url, body, headers)
