"""Module contains classes used by the Apache Configurator."""
import re

from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
from certbot.plugins import common


class Addr(common.Addr):
    """Represents an Apache address."""

    def __eq__(self, other):
        """This is defined as equivalent within Apache.

        ip_addr:* == ip_addr

        """
        if isinstance(other, self.__class__):
            return ((self.tup == other.tup) or
                    (self.tup[0] == other.tup[0] and
                     self.is_wildcard() and other.is_wildcard()))
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return "certbot_apache.obj.Addr(" + repr(self.tup) + ")"

    def __hash__(self):
        # Python 3 requires explicit overridden for __hash__ if __eq__ or
        # __cmp__ is overridden. See https://bugs.python.org/issue2235
        return super(Addr, self).__hash__()

    def _addr_less_specific(self, addr):
        """Returns if addr.get_addr() is more specific than self.get_addr()."""
        # pylint: disable=protected-access
        return addr._rank_specific_addr() > self._rank_specific_addr()

    def _rank_specific_addr(self):
        """Returns numerical rank for get_addr()

        :returns: 2 - FQ, 1 - wildcard, 0 - _default_
        :rtype: int

        """
        if self.get_addr() == "_default_":
            return 0
        elif self.get_addr() == "*":
            return 1
        else:
            return 2

    def conflicts(self, addr):
        r"""Returns if address could conflict with correct function of self.

        Could addr take away service provided by self within Apache?

        .. note::IP Address is more important than wildcard.
            Connection from 127.0.0.1:80 with choices of *:80 and 127.0.0.1:*
            chooses 127.0.0.1:\*

        .. todo:: Handle domain name addrs...

        Examples:

        =========================================  =====
        ``127.0.0.1:\*.conflicts(127.0.0.1:443)``  True
        ``127.0.0.1:443.conflicts(127.0.0.1:\*)``  False
        ``\*:443.conflicts(\*:80)``                False
        ``_default_:443.conflicts(\*:443)``        True
        =========================================  =====

        """
        if self._addr_less_specific(addr):
            return True
        elif self.get_addr() == addr.get_addr():
            if self.is_wildcard() or self.get_port() == addr.get_port():
                return True
        return False

    def is_wildcard(self):
        """Returns if address has a wildcard port."""
        return self.tup[1] == "*" or not self.tup[1]

    def get_sni_addr(self, port):
        """Returns the least specific address that resolves on the port.

        Examples:

        - ``1.2.3.4:443`` -> ``1.2.3.4:<port>``
        - ``1.2.3.4:*`` -> ``1.2.3.4:*``

        :param str port: Desired port

        """
        if self.is_wildcard():
            return self

        return self.get_addr_obj(port)


class VirtualHost(object):  # pylint: disable=too-few-public-methods
    """Represents an Apache Virtualhost.

    :ivar str filep: file path of VH
    :ivar str path: Augeas path to virtual host
    :ivar set addrs: Virtual Host addresses (:class:`set` of
        :class:`common.Addr`)
    :ivar str name: ServerName of VHost
    :ivar list aliases: Server aliases of vhost
        (:class:`list` of :class:`str`)

    :ivar bool ssl: SSLEngine on in vhost
    :ivar bool enabled: Virtual host is enabled
    :ivar bool modmacro: VirtualHost is using mod_macro
    :ivar VirtualHost ancestor: A non-SSL VirtualHost this is based on

    https://httpd.apache.org/docs/2.4/vhosts/details.html

    .. todo:: Any vhost that includes the magic _default_ wildcard is given the
              same ServerName as the main server.

    """
    # ?: is used for not returning enclosed characters
    strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")

    def __init__(self, filep, path, addrs, ssl, enabled, name=None,
                 aliases=None, modmacro=False, ancestor=None):

        # pylint: disable=too-many-arguments
        """Initialize a VH."""
        self.filep = filep
        self.path = path
        self.addrs = addrs
        self.name = name
        self.aliases = aliases if aliases is not None else set()
        self.ssl = ssl
        self.enabled = enabled
        self.modmacro = modmacro
        self.ancestor = ancestor

    def get_names(self):
        """Return a set of all names."""
        all_names = set()  # type: Set[str]
        all_names.update(self.aliases)
        # Strip out any scheme:// and <port> field from servername
        if self.name is not None:
            all_names.add(VirtualHost.strip_name.findall(self.name)[0])

        return all_names

    def __str__(self):
        return (
            "File: {filename}\n"
            "Vhost path: {vhpath}\n"
            "Addresses: {addrs}\n"
            "Name: {name}\n"
            "Aliases: {aliases}\n"
            "TLS Enabled: {tls}\n"
            "Site Enabled: {active}\n"
            "mod_macro Vhost: {modmacro}".format(
                filename=self.filep,
                vhpath=self.path,
                addrs=", ".join(str(addr) for addr in self.addrs),
                name=self.name if self.name is not None else "",
                aliases=", ".join(name for name in self.aliases),
                tls="Yes" if self.ssl else "No",
                active="Yes" if self.enabled else "No",
                modmacro="Yes" if self.modmacro else "No"))

    def display_repr(self):
        """Return a representation of VHost to be used in dialog"""
        return (
            "File: {filename}\n"
            "Addresses: {addrs}\n"
            "Names: {names}\n"
            "HTTPS: {https}\n".format(
                filename=self.filep,
                addrs=", ".join(str(addr) for addr in self.addrs),
                names=", ".join(self.get_names()),
                https="Yes" if self.ssl else "No"))


    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return (self.filep == other.filep and self.path == other.path and
                    self.addrs == other.addrs and
                    self.get_names() == other.get_names() and
                    self.ssl == other.ssl and
                    self.enabled == other.enabled and
                    self.modmacro == other.modmacro)

        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash((self.filep, self.path,
                     tuple(self.addrs), tuple(self.get_names()),
                     self.ssl, self.enabled, self.modmacro))

    def conflicts(self, addrs):
        """See if vhost conflicts with any of the addrs.

        This determines whether or not these addresses would/could overwrite
        the vhost addresses.

        :param addrs: Iterable Addresses
        :type addrs: Iterable :class:~obj.Addr

        :returns: If addresses conflicts with vhost
        :rtype: bool

        """
        for pot_addr in addrs:
            for addr in self.addrs:
                if addr.conflicts(pot_addr):
                    return True
        return False

    def same_server(self, vhost, generic=False):
        """Determines if the vhost is the same 'server'.

        Used in redirection - indicates whether or not the two virtual hosts
        serve on the exact same IP combinations, but different ports.
        The generic flag indicates that that we're trying to match to a
        default or generic vhost

        .. todo:: Handle _default_

        """

        if not generic:
            if vhost.get_names() != self.get_names():
                return False

            # If equal and set is not empty... assume same server
            if self.name is not None or self.aliases:
                return True
        # If we're looking for a generic vhost,
        # don't return one with a ServerName
        elif self.name:
            return False

        # Both sets of names are empty.

        # Make conservative educated guess... this is very restrictive
        # Consider adding more safety checks.
        if len(vhost.addrs) != len(self.addrs):
            return False

        # already_found acts to keep everything very conservative.
        # Don't allow multiple ip:ports in same set.
        already_found = set()  # type: Set[str]

        for addr in vhost.addrs:
            for local_addr in self.addrs:
                if (local_addr.get_addr() == addr.get_addr() and
                        local_addr != addr and
                        local_addr.get_addr() not in already_found):

                    # This intends to make sure we aren't double counting...
                    # e.g. 127.0.0.1:* - We require same number of addrs
                    #  currently
                    already_found.add(local_addr.get_addr())
                    break
            else:
                return False

        return True
