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


def reactor():
    """
    Get system's default twisted reactor.

    Calling this function the very first time will select and initialize twisted's default reactor.
    Consecutive calls will return the very same object again.

    Note: Needed as long as damonize() functionality (as currently used in mini-buildd) breaks reactor if
    run before this import.
    """
    from twisted.internet import reactor as _reactor
    # LOG.debug(f"Twisted Reactor: {_reactor}: {id(_reactor)}")
    return _reactor


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 mini_buildd.HTTPBadRequest(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, user=None):
        user = self._user if user is None else user
        host = self.options.get("host", self.options.get("interface", mini_buildd.config.HOSTNAME_FQDN))
        port = self.options.get("port")
        return (f"{user}@" if (user is not None and with_user) else "") + host + (f":{port}" if 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, user=None, path="", query=None):
        return urllib.parse.ParseResult(scheme=self._url.scheme,
                                        netloc=self._url_netloc(with_user=with_user, user=user),
                                        path=path,
                                        params="",
                                        query=urllib.parse.urlencode({} if query is None else query, doseq=False),
                                        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(reactor(), description)  # Syntax check only for now
        super().__init__(description, protocol)

    def get_certificate(self):
        cert_key_file = self.options.get("certKey")
        if cert_key_file:
            with mini_buildd.fopen(cert_key_file) as f:
                return f.read()
        return ""


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 mini_buildd.HTTPBadRequest("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 if protocol is None else protocol
                description = typ + self._opt("host", host) + self._opt("port", default_port if port is None else 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(reactor(), 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.URIS["accounts"]["login"].join())
        next_url = self.geturl(path=mini_buildd.config.URIS["home"]["view"].join())

        # 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).")

        # 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 mini_buildd.HTTPUnauthorized(f"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, timeout=None):
        return self._opener.open(url, timeout=timeout)

    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:
        with urllib.request.urlopen(url) as _:
            pass
    except urllib.error.HTTPError as e:
        if e.code == 406 and re.findall(r"apt.cacher.ng", e.file.read().decode(mini_buildd.config.CHAR_ENCODING), re.IGNORECASE):
            return url
    except BaseException:
        pass
    return None
