# Copyright (c) 2011-2015 Denis Bilenko. See LICENSE for details.
"""
c-ares based hostname resolver.
"""
from __future__ import absolute_import, print_function, division
import os
import warnings

from _socket import gaierror
from _socket import herror
from _socket import error
from _socket import EAI_NONAME

from gevent._compat import text_type
from gevent._compat import integer_types
from gevent._compat import PY3

from gevent.hub import Waiter
from gevent.hub import get_hub

from gevent.socket import AF_UNSPEC
from gevent.socket import AF_INET
from gevent.socket import AF_INET6
from gevent.socket import SOCK_DGRAM
from gevent.socket import SOCK_STREAM
from gevent.socket import SOL_TCP
from gevent.socket import SOL_UDP


from gevent._config import config
from gevent._config import AresSettingMixin

from .cares import channel, InvalidIP # pylint:disable=import-error,no-name-in-module
from . import _lookup_port as lookup_port
from . import AbstractResolver

__all__ = ['Resolver']


class Resolver(AbstractResolver):
    """
    Implementation of the resolver API using the `c-ares`_ library.

    This implementation uses the c-ares library to handle name
    resolution. c-ares is natively asynchronous at the socket level
    and so integrates well into gevent's event loop.

    In comparison to :class:`gevent.resolver_thread.Resolver` (which
    delegates to the native system resolver), the implementation is
    much more complex. In addition, there have been reports of it not
    properly honoring certain system configurations (for example, the
    order in which IPv4 and IPv6 results are returned may not match
    the threaded resolver). However, because it does not use threads,
    it may scale better for applications that make many lookups.

    There are some known differences from the system resolver.

    - ``gethostbyname_ex`` and ``gethostbyaddr`` may return
      different for the ``aliaslist`` tuple member. (Sometimes the
      same, sometimes in a different order, sometimes a different
      alias altogether.)

    - ``gethostbyname_ex`` may return the ``ipaddrlist`` in a
      different order.

    - ``getaddrinfo`` does not return ``SOCK_RAW`` results.

    - ``getaddrinfo`` may return results in a different order.

    - Handling of ``.local`` (mDNS) names may be different, even
      if they are listed in the hosts file.

    - c-ares will not resolve ``broadcasthost``, even if listed in
      the hosts file prior to 2020-04-30.

    - This implementation may raise ``gaierror(4)`` where the
      system implementation would raise ``herror(1)`` or vice versa,
      with different error numbers. However, after 2020-04-30, this should be
      much reduced.

    - The results for ``localhost`` may be different. In
      particular, some system resolvers will return more results
      from ``getaddrinfo`` than c-ares does, such as SOCK_DGRAM
      results, and c-ares may report more ips on a multi-homed
      host.

    - The system implementation may return some names fully qualified, where
      this implementation returns only the host name. This appears to be
      the case only with entries found in ``/etc/hosts``.

    - c-ares supports a limited set of flags for ``getnameinfo`` and
      ``getaddrinfo``; unknown flags are ignored. System-specific flags
      such as ``AI_V4MAPPED_CFG`` are not supported.

    - ``getaddrinfo`` may return canonical names even without the ``AI_CANONNAME``
      being set.

    .. caution::

        This module is considered extremely experimental on PyPy, and
        due to its implementation in cython, it may be slower. It may also lead to
        interpreter crashes.

    .. versionchanged:: 1.5.0
       This version of gevent typically embeds c-ares 1.15.0 or newer. In
       that version of c-ares, domains ending in ``.onion`` `are never
       resolved <https://github.com/c-ares/c-ares/issues/196>`_ or even
       sent to the DNS server.

    .. versionchanged:: 20.5.0
       ``getaddrinfo`` is now implemented using the native c-ares function
       from c-ares 1.16 or newer.

    .. versionchanged:: 20.5.0
       Now ``herror`` and ``gaierror`` are raised more consistently with
       the standard library resolver, and have more consistent errno values.

       Handling of localhost and broadcast names is now more consistent.

    .. versionchanged:: 22.10.1
       Now has a ``__del__`` method that warns if the object is destroyed
       without being properly closed.

    .. _c-ares: http://c-ares.haxx.se
    """

    cares_class = channel

    def __init__(self, hub=None, use_environ=True, **kwargs):
        AbstractResolver.__init__(self)
        if hub is None:
            hub = get_hub()
        self.hub = hub
        if use_environ:
            for setting in config.settings.values():
                if isinstance(setting, AresSettingMixin):
                    value = setting.get()
                    if value is not None:
                        kwargs.setdefault(setting.kwarg_name, value)
        self.cares = self.cares_class(hub.loop, **kwargs)
        self.pid = os.getpid()
        self.params = kwargs
        self.fork_watcher = hub.loop.fork(ref=False) # We shouldn't keep the loop alive
        self.fork_watcher.start(self._on_fork)

    def __repr__(self):
        return '<gevent.resolver_ares.Resolver at 0x%x ares=%r>' % (id(self), self.cares)

    def _on_fork(self):
        # NOTE: See comment in gevent.hub.reinit.
        pid = os.getpid()
        if pid != self.pid:
            self.hub.loop.run_callback(self.cares.destroy)
            self.cares = self.cares_class(self.hub.loop, **self.params)
            self.pid = pid

    def close(self):
        AbstractResolver.close(self)
        if self.cares is not None:
            self.hub.loop.run_callback(self.cares.destroy)
            self.cares = None
        self.fork_watcher.stop()

    def __del__(self):
        if self.cares is not None:
            warnings.warn("cares Resolver destroyed while not closed",
                          ResourceWarning)
            self.close()

    def _gethostbyname_ex(self, hostname_bytes, family):
        while True:
            ares = self.cares
            try:
                waiter = Waiter(self.hub)
                ares.gethostbyname(waiter, hostname_bytes, family)
                result = waiter.get()
                if not result[-1]:
                    raise herror(EAI_NONAME, self.EAI_NONAME_MSG)
                return result
            except herror as ex:
                if ares is self.cares:
                    if ex.args[0] == 1:
                        # Somewhere along the line, the internal
                        # implementation of gethostbyname_ex changed to invoke
                        # getaddrinfo() as a first pass, much like we do for ``getnameinfo()``;
                        # this means it raises a different error for not-found hosts.
                        raise gaierror(EAI_NONAME, self.EAI_NONAME_MSG)
                    raise
                # "self.cares is not ares" means channel was destroyed (because we were forked)

    def _lookup_port(self, port, socktype):
        return lookup_port(port, socktype)

    def __getaddrinfo(
            self, host, port,
            family=0, socktype=0, proto=0, flags=0,
            fill_in_type_proto=True
    ):
        """
        Returns a list ``(family, socktype, proto, canonname, sockaddr)``

        :raises gaierror: If no results are found.
        """
        # pylint:disable=too-many-locals,too-many-branches
        if isinstance(host, text_type):
            host = host.encode('idna')


        if isinstance(port, text_type):
            port = port.encode('ascii')
        elif isinstance(port, integer_types):
            if port == 0:
                port = None
            else:
                port = str(port).encode('ascii')

        waiter = Waiter(self.hub)
        self.cares.getaddrinfo(
            waiter,
            host,
            port,
            family,
            socktype,
            proto,
            flags,
        )
        # Result is a list of:
        # (family, socktype, proto, canonname, sockaddr)
        # Where sockaddr depends on family; for INET it is
        # (address, port)
        # and INET6 is
        # (address, port, flow info, scope id)
        result = waiter.get()

        if not result:
            raise gaierror(EAI_NONAME, self.EAI_NONAME_MSG)

        if fill_in_type_proto:
            # c-ares 1.16 DOES NOT fill in socktype or proto in the results,
            # ever. It's at least supposed to do that if they were given as
            # hints, but it doesn't (https://github.com/c-ares/c-ares/issues/317)
            # Sigh.
            # The SOL_* constants are another (older?) name for IPPROTO_*
            if socktype:
                hard_type_proto = [
                    (socktype, SOL_TCP if socktype == SOCK_STREAM else SOL_UDP),
                ]
            elif proto:
                hard_type_proto = [
                    (SOCK_STREAM if proto == SOL_TCP else SOCK_DGRAM, proto),
                ]
            else:
                hard_type_proto = [
                    (SOCK_STREAM, SOL_TCP),
                    (SOCK_DGRAM, SOL_UDP),
                ]

            # pylint:disable=not-an-iterable,unsubscriptable-object
            result = [
                (rfamily,
                 hard_type if not rtype else rtype,
                 hard_proto if not rproto else rproto,
                 rcanon,
                 raddr)
                for rfamily, rtype, rproto, rcanon, raddr
                in result
                for hard_type, hard_proto
                in hard_type_proto
            ]
        return result

    def _getaddrinfo(self, host_bytes, port, family, socktype, proto, flags):
        while True:
            ares = self.cares
            try:
                return self.__getaddrinfo(host_bytes, port, family, socktype, proto, flags)
            except gaierror:
                if ares is self.cares:
                    raise

    def __gethostbyaddr(self, ip_address):
        waiter = Waiter(self.hub)
        try:
            self.cares.gethostbyaddr(waiter, ip_address)
            return waiter.get()
        except InvalidIP:
            result = self._getaddrinfo(ip_address, None,
                                       family=AF_UNSPEC, socktype=SOCK_DGRAM,
                                       proto=0, flags=0)
            if not result:
                raise
            # pylint:disable=unsubscriptable-object
            _ip_address = result[0][-1][0]
            if isinstance(_ip_address, text_type):
                _ip_address = _ip_address.encode('ascii')
            if _ip_address == ip_address:
                raise
            waiter.clear()
            self.cares.gethostbyaddr(waiter, _ip_address)
            return waiter.get()

    def _gethostbyaddr(self, ip_address_bytes):
        while True:
            ares = self.cares
            try:
                return self.__gethostbyaddr(ip_address_bytes)
            except herror:
                if ares is self.cares:
                    raise

    def __getnameinfo(self, hostname, port, sockaddr, flags):
        result = self.__getaddrinfo(
            hostname, port,
            family=AF_UNSPEC, socktype=SOCK_DGRAM,
            proto=0, flags=0,
            fill_in_type_proto=False)
        if len(result) != 1:
            raise error('sockaddr resolved to multiple addresses')

        family, _socktype, _proto, _name, address = result[0]

        if family == AF_INET:
            if len(sockaddr) != 2:
                raise error("IPv4 sockaddr must be 2 tuple")
        elif family == AF_INET6:
            address = address[:2] + sockaddr[2:]

        waiter = Waiter(self.hub)
        self.cares.getnameinfo(waiter, address, flags)
        node, service = waiter.get()

        if service is None and PY3:
            # ares docs: "If the query did not complete
            # successfully, or one of the values was not
            # requested, node or service will be NULL ". Python 2
            # allows that for the service, but Python 3 raises
            # an error. This is tested by test_socket in py 3.4
            err = gaierror(EAI_NONAME, self.EAI_NONAME_MSG)
            err.errno = EAI_NONAME
            raise err

        return node, service or '0'

    def _getnameinfo(self, address_bytes, port, sockaddr, flags):
        while True:
            ares = self.cares
            try:
                return self.__getnameinfo(address_bytes, port, sockaddr, flags)
            except gaierror:
                if ares is self.cares:
                    raise

    # # Things that need proper error handling
    # gethostbyaddr = AbstractResolver.convert_gaierror_to_herror(AbstractResolver.gethostbyaddr)
