r"""
Network abstraction.

# Server: HOPO compat syntax
>>> ServerEndpoint("0.0.0.0:1234").description
'tcp:interface=0.0.0.0:port=1234'
>>> ServerEndpoint(":::8066").description
'tcp6:interface=\\:\\::port=8066'

# Client: Twisted syntax
>>> ClientEndpoint("tcp:host=example.com:port=1234", protocol="http").url()
'http://example.com:1234/'
>>> ClientEndpoint("tls:host=example.com:port=1234", protocol="http").url()
'https://example.com:1234/'

# Client: URL syntax
>>> ce = ClientEndpoint("http://example.com:1234/")
>>> ce.description
'tcp:host=example.com:port=1234'
>>> ce.options.get("port")
'1234'
>>> ce.options.get("host")
'example.com'
>>> ClientEndpoint("https://example.com:1234/api?a=b&c=d").description
'tls:host=example.com:port=1234'
>>> ClientEndpoint("https://example.com:1234/api?a=b&c=d").url()
'https://example.com:1234/api?a=b&c=d'
>>> ClientEndpoint("https://foo@example.com:1234/api?a=b&c=d").url()
'https://example.com:1234/api?a=b&c=d'
>>> ClientEndpoint("https://bar@example.com:1234/api?a=b&c=d").url(with_user=True)
'https://bar@example.com:1234/api?a=b&c=d'

# Client: HOPO compat syntax
>>> ClientEndpoint("foo.example.org:1234").description
'tcp:host=foo.example.org:port=1234'
>>> ClientEndpoint("foo.example.org:1234").url()
'http://foo.example.org:1234/'
"""

import ipaddress
import re
import urllib.request
import urllib.parse
import urllib.error
import ssl
import logging
import logging.handlers

import twisted.internet.endpoints

import mini_buildd.config

LOG = logging.getLogger(__name__)


PROTOCOLS = ["http", "ftp"]


def urlopen(url, **kwargs):
    return urllib.request.urlopen(url, context=ssl.create_default_context(), **kwargs)


class TwistedEndpoint():
    @classmethod
    def _escape(cls, string):
        return string.replace(":", r"\:")

    @classmethod
    def _unescape(cls, string):
        return string.replace(r"\:", ":")

    @classmethod
    def _opt(cls, key, value):
        return f":{key}={cls._escape(value)}" if value else ""

    SUPPORTED_TYPES = ["ssl", "tls", "tcp6", "tcp", "unix"]

    def __init__(self, description):
        self.description = description

        self.params = [self._unescape(p) for p in re.split(r"(?<!\\):", description)]
        self.type = self.params[0]
        if self.type not in self.SUPPORTED_TYPES:
            raise Exception(f"Unsupported endpoint type: {self.type} (twisted types supported: {','.join(self.SUPPORTED_TYPES)})")

        self.options = {}
        for p in self.params:
            key = p.partition("=")
            if key[1]:
                self.options[key[0]] = key[2]
        LOG.debug(f"Twisted endpoint parsed: {self.description}")


class Endpoint(TwistedEndpoint):
    def __init__(self, description, protocol=None, user=None):
        super().__init__(description)
        self.protocol = protocol or "http"
        self.host = self.options.get("host", mini_buildd.config.HOSTNAME)
        self.user = user

        self.url_scheme = self.protocol
        if self.url_scheme and self.type in ["ssl", "tls"]:
            self.url_scheme += "s"

        LOG.debug(f"Endpoint initialized: {self.description}")

    def __repr__(self):
        return f"{self.url_scheme} on {self.description}"

    def hopo(self):
        return f"{self.host}:{self.options.get('port')}"

    def url(self, with_user=False):
        user_pre = f"{self.user}@" if (self.user and with_user) else ""
        port_post = f":{self.options.get('port')}" if self.options.get("port") else ""
        return f"{self.url_scheme}://{user_pre}{self.host}{port_post}/"


class ServerEndpoint(Endpoint):
    """
    Twisted-style Network Server Endpoint.

    See: https://twistedmatrix.com/documents/current/core/howto/endpoints.html#servers

    Notation Examples
    -----------------
    'tcp6:port=8066'                                     : Unencrypted, bind on all interfaces.
    'ssl:port=8066:privateKey=KEY_PATH:certKey=CERT_PATH': Encrypted, bind on all interfaces.
    'tcp6:interface=localhost:port=8066'                 : Unencrypted, bind on localhost only.
    'localhost:8066'                                     : Compatibility host:port syntax (don't use -- might eventually go away).

    """

    def __init__(self, description, protocol=None, user=None):
        if not description.startswith(tuple(t + ":" for t in self.SUPPORTED_TYPES)):
            LOG.warning(f"Deprecated 'host:port' syntax on '{description}'")

            _host, dummy, port = description.rpartition(":")
            typ = "tcp6"
            try:
                # Check if we are in explicit IPv4 IP notation (like '127.0.0.1')
                if isinstance(ipaddress.ip_address(_host), ipaddress.IPv4Address):
                    typ = "tcp"
            except ValueError:
                pass
            description = typ + self._opt("interface", _host) + self._opt("port", port)

        twisted.internet.endpoints.serverFromString(None, description)  # Syntax check only for now
        super().__init__(description, protocol, user)

    def __repr__(self):
        return f"Net server: {super().__repr__()}"

    def get_certificate(self):
        cert_key_file = self.options.get("certKey")
        return open(cert_key_file).read() if cert_key_file else ""


class ClientEndpoint(Endpoint):
    """
    Twisted-style Network Client Endpoint.

    See: https://twistedmatrix.com/documents/current/core/howto/endpoints.html#clients

    Notation Examples
    -----------------
    'tcp:host=localhost:port=8066': Connect to localhost, unencrypted.
    'tls:host=localhost:port=8066': Connect to localhost, encrypted.
    'http://localhost:8066'       : Extra URL-style syntax (always prefer twisted-style if possible, though).
    'localhost:8066'              : Compatibility host:port syntax (don't use -- might eventually go away).
    """

    def __init__(self, description, protocol=None, user=None):
        self._parsed_url = None
        if not description.startswith(tuple(t + ":" for t in self.SUPPORTED_TYPES)):
            if re.match(r".*://.*", description):
                self._parsed_url = urllib.parse.urlsplit(description)
                if self._parsed_url.password:
                    raise Exception("Endpoint from URL: We don't allow to give password in URL")

                typ, default_port, _protocol = {"http": ("tcp", "80", "http"),
                                                "https": ("tls", "443", "http"),
                                                "ftp": ("tcp", "21", "ftp"),
                                                "ftps": ("tls", "990", "ftp")}[self._parsed_url.scheme]
                _user_host, dummy, port = self._parsed_url.netloc.partition(":")
                host = _user_host.rpartition("@")[2]

                protocol = protocol or _protocol
                user = user or self._parsed_url.username
                description = typ + self._opt("host", host) + self._opt("port", port or default_port)
            else:
                LOG.warning(f"Deprecated 'host:port' syntax on '{description}'")

                host, dummy, port = description.rpartition(":")
                description = "tcp" + self._opt("host", host) + self._opt("port", port)

        twisted.internet.endpoints.clientFromString(None, description)  # Syntax check only for now
        super().__init__(description, protocol, user)

    def __repr__(self):
        return f"Net client: {super().__repr__()}"

    def url(self, with_user=False):
        if self._parsed_url:
            if with_user:
                return self._parsed_url.geturl()
            return self._parsed_url._replace(netloc=f"{self._parsed_url.netloc.rpartition('@')[2]}").geturl()
        return super().url(with_user=with_user)

    def get_certificate(self):
        return ssl.get_server_certificate((self.options.get("host"), self.options.get("port")))

    def web_login(self,
                  keyring,
                  login_loc="accounts/login/",
                  next_loc="mini_buildd/"):
        try:
            password, new = keyring.get(self.url())
            login_url = urllib.parse.urljoin(self.url(), login_loc)
            next_url = urllib.parse.urljoin(self.url(), next_loc)

            # Create cookie-enabled opener
            cookie_handler = urllib.request.HTTPCookieProcessor()
            opener = urllib.request.build_opener(cookie_handler)

            # Retrieve login page
            opener.open(login_url)
            opener.addheaders = [("Referer", self.url())]

            # Find "csrftoken" in cookiejar
            csrf_cookies = [c for c in cookie_handler.cookiejar if c.name == "csrftoken"]
            if len(csrf_cookies) != 1:
                raise Exception(f"{len(csrf_cookies)} csrftoken cookies found in login pages (need exactly 1).")
            LOG.debug(f"csrftoken={csrf_cookies[0].value}")

            # Login via POST request
            response = opener.open(
                login_url,
                bytes(urllib.parse.urlencode({"username": self.user,
                                              "password": password,
                                              "csrfmiddlewaretoken": csrf_cookies[0].value,
                                              "this_is_the_login_form": "1",
                                              "next": next_url}),
                      encoding=mini_buildd.config.CHAR_ENCODING))

            # If successful, next url of the response must match
            if canonize_url(response.geturl()) != canonize_url(next_url):
                raise Exception("Wrong credentials: Please try again")

            # Logged in: Install opener, save credentials
            LOG.info(f"User logged in: {self.url()}")
            urllib.request.install_opener(opener)

            if new:
                keyring.set(self.url(), password)

            # We need to use this very opener object to stay logged in (for some reason, standard urlopen() just worked fine for http).
            return opener
        except Exception as e:
            raise Exception(f"Login failed: {self.url()}: {e}")


def detect_apt_cacher_ng(url="http://localhost:3142"):
    """Little heuristic helper for the "local archives" wizard."""
    try:
        urlopen(url)
    except urllib.error.HTTPError as e:
        if e.code == 406 and re.findall(r"apt.cacher.ng", e.file.read().decode("UTF-8"), re.IGNORECASE):
            return url
    except Exception:  # pylint: disable=broad-except
        pass
    return None


def canonize_url(url):
    """Poor man's URL canonizer: Always include the port (currently only works for 'http' and 'https' default ports)."""
    default_scheme2port = {"http": ":80", "https": ":443"}

    parsed = urllib.parse.urlsplit(url)
    netloc = parsed.netloc
    if parsed.port is None:
        netloc = parsed.hostname + default_scheme2port.get(parsed.scheme, "")
    return urllib.parse.urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, ""))
