# Copyright (C) 2020 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php

import sys
import socket
import concurrent
from concurrent import futures
import ipaddress
from typing import Optional

import dns
import dns.resolver

from .logging import get_logger


_logger = get_logger(__name__)

_dns_threads_executor = None  # type: Optional[concurrent.futures.Executor]


def configure_dns_depending_on_proxy(is_proxy: bool) -> None:
    # Store this somewhere so we can un-monkey-patch:
    if not hasattr(socket, "_getaddrinfo"):
        socket._getaddrinfo = socket.getaddrinfo
    if is_proxy:
        # prevent dns leaks, see http://stackoverflow.com/questions/13184205/dns-over-proxy
        def getaddrinfo(host, port, *args, **kwargs):
            if _is_force_system_dns_for_host(host):
                return socket._getaddrinfo(host, port, *args, **kwargs)
            return [(socket.AF_INET, socket.SOCK_STREAM, 6, '', (host, port))]
        socket.getaddrinfo = getaddrinfo
    else:
        if sys.platform == 'win32':
            # On Windows, socket.getaddrinfo takes a mutex, and might hold it for up to 10 seconds
            # when dns-resolving. To speed it up drastically, we resolve dns ourselves, outside that lock.
            # See https://github.com/spesmilo/electrum/issues/4421
            try:
                _prepare_windows_dns_hack()
            except Exception as e:
                _logger.exception('failed to apply windows dns hack.')
            else:
                socket.getaddrinfo = _fast_getaddrinfo
        else:
            socket.getaddrinfo = socket._getaddrinfo


def _prepare_windows_dns_hack():
    # enable dns cache
    resolver = dns.resolver.get_default_resolver()
    if resolver.cache is None:
        resolver.cache = dns.resolver.Cache()
    # ensure overall timeout for requests is long enough
    resolver.lifetime = max(resolver.lifetime or 1, 30.0)
    # prepare threads
    global _dns_threads_executor
    if _dns_threads_executor is None:
        _dns_threads_executor = concurrent.futures.ThreadPoolExecutor(max_workers=20,
                                                                      thread_name_prefix='dns_resolver')


def _is_force_system_dns_for_host(host: str) -> bool:
    return str(host) in ('localhost', 'localhost.',)


def _fast_getaddrinfo(host, *args, **kwargs):
    def needs_dns_resolving(host):
        try:
            ipaddress.ip_address(host)
            return False  # already valid IP
        except ValueError:
            pass  # not an IP
        if _is_force_system_dns_for_host(host):
            return False
        return True

    def resolve_with_dnspython(host):
        addrs = []
        expected_errors = (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
                           concurrent.futures.CancelledError, concurrent.futures.TimeoutError)
        ipv6_fut = _dns_threads_executor.submit(dns.resolver.resolve, host, dns.rdatatype.AAAA)
        ipv4_fut = _dns_threads_executor.submit(dns.resolver.resolve, host, dns.rdatatype.A)
        # try IPv6
        try:
            answers = ipv6_fut.result()
            addrs += [str(answer) for answer in answers]
        except expected_errors as e:
            pass
        except BaseException as e:
            _logger.info(f'dnspython failed to resolve dns (AAAA) for {repr(host)} with error: {repr(e)}')
        # try IPv4
        try:
            answers = ipv4_fut.result()
            addrs += [str(answer) for answer in answers]
        except expected_errors as e:
            # dns failed for some reason, e.g. dns.resolver.NXDOMAIN this is normal.
            # Simply report back failure; except if we already have some results.
            if not addrs:
                raise socket.gaierror(11001, 'getaddrinfo failed') from e
        except BaseException as e:
            # Possibly internal error in dnspython :( see #4483 and #5638
            _logger.info(f'dnspython failed to resolve dns (A) for {repr(host)} with error: {repr(e)}')
        if addrs:
            return addrs
        # Fall back to original socket.getaddrinfo to resolve dns.
        return [host]

    addrs = [host]
    if needs_dns_resolving(host):
        addrs = resolve_with_dnspython(host)
    list_of_list_of_socketinfos = [socket._getaddrinfo(addr, *args, **kwargs) for addr in addrs]
    list_of_socketinfos = [item for lst in list_of_list_of_socketinfos for item in lst]
    return list_of_socketinfos
