import errno
import os
import ssl
import sys

import parallels

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

from collections import OrderedDict
from parallels.core import messages
from parallels.core.logging import get_logger
from parallels.core.registry import Registry
from parallels.core.utils.common import default, is_run_on_windows, safe_format
from parallels.core.utils.common.string_utils import safe_utf8_decode
from parallels.core.utils.entity import Entity
from parallels.core.utils.migrator_utils import get_package_extras_file_path

logger = get_logger(__name__)
_https_protocol_cache = {}


def perform_simple_http_request(server_ip, hostname, path):
    """Perform simple HTTP request

    Simple means the following:
    1. Only HTTP is supported, HTTPS is not supported.
    2. Function always returns string, even for non-200 HTTP response codes all the other errors.
    If some other issue occurred, for example connection time out, then empty string is returned.
    3. Request is performed to specified IP address, specifying hostname in headers.
    DNS system is completely excluded.

    To request http://example.com/test.php at 192.168.1.1, call the function in the following way:
      simple_http_request('192.168.1.1', 'example.com', 'test.php')

    :type server_ip: str | unicode
    :type hostname: str | unicode
    :type path: str | unicode
    :rtype: str | unicode
    """
    url = "http://{server_ip}/{path}".format(server_ip=server_ip, path=path)
    log_url = "http://{hostname}/{path}".format(hostname=hostname, path=path)

    try:
        request = Request(url)
        # specify user agent to avoid requests rejection by web server
        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'
        )
        if sys.version_info.major >= 3:
            header = str(hostname.encode('idna'))
        else:
            header = hostname.encode('idna')
        request.add_header('Host', header)
        result = urlopen(request, timeout=30)
        return result.read()
    except HTTPError as e:
        logger.fdebug(messages.FAILED_TO_PERFORM_HTTP_REQUEST, url=log_url, ip=server_ip, reason=e)
        content = e.fp.read() if e.fp is not None else None
        return content
    except Exception as e:
        logger.exception()
        logger.fdebug(messages.FAILED_TO_PERFORM_HTTP_REQUEST, url=log_url, ip=server_ip, reason=e)
        return ''


def perform_http_request_response_text(
        url, data=None, headers=None, hostname=None, ip_address=None, verify_certificates=True
):
    """
    :type url: str | unicode
    :type data: str | unicode | None
    :type headers: dict | None
    :type hostname: str | unicode | None
    :type ip_address: str | unicode | None
    :type verify_certificates: bool
    :rtype: str | unicode
    """
    response = perform_http_request_response(url, data, headers, hostname, ip_address, verify_certificates)
    return response.response_text


class HTTPResponse(Entity):
    def __init__(self, response_text, final_url):
        self._response_text = response_text
        self._final_url = final_url

    @property
    def response_text(self):
        """
        :rtype: str | unicode
        """
        return self._response_text

    @property
    def final_url(self):
        """Final URL we requested after all HTTP redirects.

        :rtype: str | unicode
        """
        return self._final_url


def perform_http_request_response(
        url, data=None, headers=None, hostname=None, ip_address=None, verify_certificates=True
):
    """
    :type url: str | unicode
    :type data: str | unicode | None
    :type headers: dict | None
    :type hostname: str | unicode | None
    :type ip_address: str | unicode | None
    :type verify_certificates: bool
    :rtype: parallels.core.utils.http_client.HTTPResponse
    """
    response = perform_http_request_response_raw(
        url, data=data, headers=headers, hostname=hostname, ip_address=ip_address,
        verify_certificates=verify_certificates
    )
    return HTTPResponse(response_text=response.read(), final_url=response.geturl())


def perform_http_request_response_raw(
        url, data=None, headers=None, hostname=None, ip_address=None, verify_certificates=True
):
    """
    :type url: str | unicode
    :type data: str | unicode | None
    :type headers: dict | None
    :type hostname: str | unicode | None
    :type ip_address: str | unicode | None
    :type verify_certificates: bool
    """
    headers = default(headers, {})
    if hostname:
        headers['Host'] = hostname
    if 'User-Agent' not in headers:
        # specify user agent to avoid requests rejection by web server
        headers['User-Agent'] = (
            'Mozilla/5.0 (Windows NT 6.3; WOW64)'
            ' AppleWebKit/537.36 (KHTML, like Gecko)'
            ' Chrome/54.0.2840.71 Safari/537.36'
        )

    ssl_protocols = _list_supported_ssl_protocols()

    url_data = urlparse(url)
    netloc_idna = safe_utf8_decode(url_data.netloc).encode('idna')
    if sys.version_info.major >= 3:
        netloc_idna = netloc_idna.decode("utf-8")
    url = urlunparse((url_data.scheme, netloc_idna, url_data.path, url_data.params, url_data.query, url_data.fragment))

    url_hostname = url_data.hostname
    if url_hostname not in _https_protocol_cache:
        https_protocols = ssl_protocols.keys()
    else:
        https_protocols = [_https_protocol_cache[url_hostname]]
    for https_protocol in https_protocols:
        logger.fdebug(messages.DEBUG_SSL_PROTOCOL_NAME, protocol_name=ssl_protocols[https_protocol])
        if verify_certificates:
            certfile = get_package_extras_file_path(parallels.core, os.path.join("cacert", "cacert.pem"))
            context = ssl._create_unverified_context(
                https_protocol, cert_reqs=ssl.CERT_REQUIRED, cafile=certfile, check_hostname=True
            )
        else:
            context = ssl._create_unverified_context(https_protocol)
        https_handler = HTTPSHandlerWithContext(hostname=hostname, ip_address=ip_address, context=context)

        request = Request(url, data, headers)
        opener = build_opener(https_handler)

        try:
            response = opener.open(request)
            _https_protocol_cache[url_hostname] = https_protocol
            return response
        except HTTPError as e:
            content = e.fp.read() if e.fp is not None else None
            raise HttpClientError(e, content, url, e.code)
        except URLError:
                logger.exception()
                if https_protocol == https_protocols[-1]:
                    raise


class HTTPSHandlerNoContext(HTTPSHandler):
    def __init__(self, ip_address, **kwargs):
        HTTPSHandler.__init__(self, **kwargs)
        self.ip_address = ip_address

    def https_open(self, req):

        def http_class(*args, **kwargs):
            host = urlparse(req.get_full_url()).hostname
            if self.ip_address is not None:
                ip_address = self.ip_address
            elif host == '127.0.0.1' and is_run_on_windows():
                ip_address = '127.0.0.1'
            else:
                ip_address = Registry.get_instance().get_context().conn.target.main_node_ip

            return HTTPSConnection(source_address=(ip_address, 0), *args, **kwargs)

        return self.do_open(http_class, req)


class HTTPSHandlerWithContext(HTTPSHandler):
    def __init__(self, hostname, ip_address, **kwargs):
        HTTPSHandler.__init__(self, **kwargs)
        self.hostname = hostname
        self.ip_address = ip_address

    def https_open(self, req):
        host = urlparse(req.get_full_url()).hostname
        if self.ip_address is not None:
            ip_address = self.ip_address
        elif host == '127.0.0.1' and is_run_on_windows():
            ip_address = '127.0.0.1'
        else:
            ip_address = Registry.get_instance().get_context().conn.target.main_node_ip

        if self.hostname is not None:
            server_hostname = self.hostname

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

            def http_class(*args, **kwargs):
                return HTTPSConnectionWithHostname(source_address=(ip_address, 0), *args, **kwargs)
        else:
            def http_class(*args, **kwargs):
                return HTTPSConnection(source_address=(ip_address, 0), *args, **kwargs)

        return self.do_open(http_class, req, context=self._context)


class HttpClientError(Exception):
    def __init__(self, cause, content, url=None, code=None):
        self.cause = cause
        self.content = content
        self.url = url
        self.code = code

    def __str__(self):
        return safe_format(
            messages.HTTP_CLIENT_ERROR, url=self.url, code=self.code, response=self.content
        )

    def __repr__(self):
        return "HttpClientError(%s, %r)" % (self.cause, self.content)


def _list_supported_ssl_protocols():
    """Get list of available SSL protocols with their titles, in order of usage priority.
    The first protocol in the ordered dictionary should be used first.
    SSLv3 and SSLv2 at the end are necessary for CentOS 5.

    :rtype: OrderedDict
    """
    protocols = [
        ('TLSv1_2', 'TLSv1.2'),
        ('TLSv1_1', 'TLSv1.1'),
        ('TLSv1', 'TLSv1'),
        ('SSLv3', 'SSLv3'),
        ('SSLv2', 'SSLv2'),
        ('TLS', 'autodetect')
    ]

    ssl_protocols = OrderedDict()
    for protocol_id, protocol_title in protocols:
        protocol_var_name = 'PROTOCOL_%s' % protocol_id
        # Not all protocols could be built into Python. Check existence before adding.
        if hasattr(ssl, protocol_var_name):
            ssl_protocols[getattr(ssl, protocol_var_name)] = protocol_title

    return ssl_protocols
