r"""
Network abstraction.

Doctest Examples::

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

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

  # Client: URL syntax
  >>> ce = ClientEndpoint("http://example.com:1234/")
  >>> ce.description
  'tcp:host=example.com:port=1234'
  >>> ClientEndpoint("https://example.com:1234").description
  'tls:host=example.com:port=1234'
  >>> ClientEndpoint("https://example.com:1234").geturl(with_user=True, path="api", query={"a": "b", "c": "d"})
  'https://example.com:1234/api?a=b&c=d'
  >>> ClientEndpoint("https://foo@example.com:1234").geturl(with_user=False, path="api", query={"a": "b", "c": "d"})
  'https://example.com:1234/api?a=b&c=d'
  >>> ClientEndpoint("https://bar@example.com:1234").geturl(with_user=True, path="api", query={"a": "b", "c": "d"})
  '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").geturl()
  'http://foo.example.org:1234'
"""

import ipaddress
import re
import http.client
import urllib.request
import urllib.parse
import urllib.error
import ssl
import logging

import twisted.internet.endpoints

import mini_buildd.config

LOG = logging.getLogger(__name__)


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

        params = [self._unescape(p) for p in re.split(r"(?<!\\):", description)]
        self._type = 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 params:
            key = p.partition("=")
            if key[1]:
                self.options[key[0]] = key[2]
        LOG.debug(f"Twisted endpoint parsed: {self.description}")

    def is_ssl(self):
        return self._type in ["ssl", "tls"]


class Endpoint(TwistedEndpoint):
    def _url_netloc(self, with_user=True):
        return (f"{self._user}@" if (self._user and with_user) else "") + self.options.get("host", self.options.get("interface", mini_buildd.config.HOSTNAME)) + (f":{self.options.get('port')}" if self.options.get("port") else "")

    def __init__(self, description, protocol):
        super().__init__(description)
        self._user = None

        self._url = urllib.parse.ParseResult(scheme=protocol + ("s" if self.is_ssl() else ""),
                                             netloc=self._url_netloc(),
                                             path="", params="", query="", fragment="")
        LOG.debug(f"Endpoint initialized: {self.description}")

    def __str__(self):
        return self.geturl()

    def get_user(self):
        return self._user

    def set_user(self, user):
        self._user = user

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

    def geturl(self, with_user=False, path="", query=None):
        return urllib.parse.ParseResult(scheme=self._url.scheme,
                                        netloc=self._url_netloc(with_user=with_user),
                                        path=path,
                                        params="",
                                        query=urllib.parse.urlencode(query or {}),
                                        fragment="").geturl()


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):
        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)

    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 (prefer twisted-style in saved configs, though).
      'localhost:8066'              : Compatibility host:port syntax (don't use -- might eventually go away).
    """

    def __init__(self, description, protocol="http"):
        user = None
        if not description.startswith(tuple(t + ":" for t in self.SUPPORTED_TYPES)):
            if re.match(r".*://.*", description):
                parsed_url = urllib.parse.urlsplit(description)
                if 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")}[parsed_url.scheme]
                _user_host, dummy, port = parsed_url.netloc.partition(":")
                host = _user_host.rpartition("@")[2]

                protocol = protocol or _protocol
                description = typ + self._opt("host", host) + self._opt("port", port or default_port)
                user = parsed_url.username
            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)
        self.set_user(user)

        # Login/out handling
        self.set_user(user)
        self._anon_opener = urllib.request.build_opener()
        self._opener = self._anon_opener

    def __str__(self):
        return f"{self.geturl(with_user=True)}({'logged in' if self.logged_in() else 'logged out'})"

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

    def login(self, password):
        login_url = self.geturl(path=mini_buildd.config.HTTP_PATH_LOGIN)
        next_url = self.geturl(path=mini_buildd.config.HTTP_PATH_LOGIN_NEXT)

        # 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.geturl())]

        # 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"{self}: 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 response.geturl() != next_url:
            raise Exception(f"{self}: Login for user '{self._user}' failed.")

        # Logged in: Install opener
        self._opener = opener

    def logout(self):
        self._opener = self._anon_opener

    def logged_in(self):
        return self._opener != self._anon_opener

    def urlopen(self, url):
        return self._opener.open(url)

    def http_connect(self):
        return (http.client.HTTPSConnection if self.is_ssl() else http.client.HTTPConnection)(self.options.get("host"), port=self.options.get("port"))


def detect_apt_cacher_ng(url="http://localhost:3142"):
    """Little heuristic helper for the "local archives" wizard."""
    try:
        urllib.request.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
