import logging
import socket
from threading import Thread
import time

from plesk_mail_migrator.providers.imap.exceptions import ImapAutodetectFailedException
from plesk_mail_migrator.providers.imap.server_detect.methods.autoconfig_xml_dir import \
    AutoconfigXmlDirImapServerDetectMethod
from plesk_mail_migrator.providers.imap.server_detect.methods.autoconfig_xml_domain import \
    AutoconfigXmlDomainImapServerDetectMethod
from plesk_mail_migrator.providers.imap.server_detect.methods.domain_name import DomainNameServerDetectMethod
from plesk_mail_migrator.providers.imap.server_detect.methods.imap_prefix import ImapPrefixImapServerDetectMethod
from plesk_mail_migrator.providers.imap.server_detect.methods.mail_prefix import MailPrefixImapServerDetectMethod
from plesk_mail_migrator.providers.imap.server_detect.methods.mozilla_database import \
    MozillaDatabaseImapServerDetectMethod
from plesk_mail_migrator.providers.imap.server_detect.methods.mx_record_value import MxRecordValueImapServerDetectMethod
from plesk_mail_migrator.providers.imap.server_detect.methods.mx_record_with_mozilla_database import \
    MxRecordWithMozillaDatabaseImapServerDetectMethod
from plesk_mail_migrator.providers.imap.server_detect.methods.srv_record import \
    SrvRecordWithMozillaDatabaseImapServerDetectMethod
from plesk_mail_migrator.utils.list_utils import unique_list
from plesk_mail_migrator.utils.string_utils import safe_utf8_decode

logger = logging.getLogger(__name__)


# Method identifier which means that IMAP host was not detected automatically but rather specified manually
IMAP_HOST_DETECTION_METHOD_MANUAL = 'manual'


def create_detection_methods(dns_server, ca_bundle):
    """Get list of detection methods ordered by priority.

    :param str | unicode dns_server: DNS server IP or hostname
    :param str | unicode ca_bundle: Path to CA certificates bundle
    :rtype: list[plesk_mail_migrator.providers.imap.server_detect.methods.base.ImapServerDetectMethod]
    """
    return [
        AutoconfigXmlDomainImapServerDetectMethod(ca_bundle),
        AutoconfigXmlDirImapServerDetectMethod(ca_bundle),
        MozillaDatabaseImapServerDetectMethod(),
        MxRecordWithMozillaDatabaseImapServerDetectMethod(dns_server),
        SrvRecordWithMozillaDatabaseImapServerDetectMethod(dns_server),
        ImapPrefixImapServerDetectMethod(),
        MailPrefixImapServerDetectMethod(),
        MxRecordValueImapServerDetectMethod(dns_server),
        DomainNameServerDetectMethod()
    ]


def detect_imap_server(domain_name, dns_server, ca_bundle):
    """Detect IMAP servers for a domain name using different methods: autoconfig XML, Thunderbird's database, etc.

    This function is a generator, it does not return a list of detected IMAP servers but rather returns an
    iterator. Also, each detection method which could take long time is executed in a separate thread.
    That is necessary to speed up IMAP server detection - you get each next IMAP server hostname
    as soon as it is ready, you don't wait for all the rest IMAP server detection methods to work.

    All detection methods have strict ordering by priority: you won't get results of the 2nd method until
    the 1st method returned result or reached timeout (see note below).

    If you request result of a method in more than 15 seconds and result is not ready yet, the method is skipped.
    That is necessary to keep detection fast and avoid hangs caused by issues like network timeouts.

    The list of IMAP hosts is unique - e.g. if some IMAP hostname was detected and returned by the 1st method,
    it won't be returned anymore even if 2nd, 3rd, and so on methods return the same hostname.

    :type domain_name: str | unicode
    :param str | unicode dns_server: DNS server IP or hostname
    :param str | unicode ca_bundle: Path to CA certificates bundle
    :rtype: list[str | unicode
    """
    threads = {}
    method_results = {}

    domain_name = safe_utf8_decode(domain_name).encode('idna')

    detection_methods = create_detection_methods(dns_server, ca_bundle)

    for method in detection_methods:
        if method.is_fast():
            method_results[method] = method.detect(domain_name)
        else:
            def run_method(thread_method=method):
                try:
                    method_results[thread_method] = thread_method.detect(domain_name)
                except Exception as e:
                    logger.debug(
                        "Exception (usually not an actual error, but just details why detection has failed): ",
                        exc_info=True
                    )
                    logger.debug(
                        "Detection of IMAP host by method '%s' has failed: %s", thread_method.get_method_id(), str(e)
                    )

            threads[method] = Thread(target=run_method)
            threads[method].daemon = True
            threads[method].start()

    start_time = time.time()
    max_time = 15

    detected_hostnames = set()

    for method in detection_methods:
        if not method.is_fast():
            current_time = time.time()
            time_elapsed = current_time - start_time
            if time_elapsed < max_time:
                threads[method].join(max_time - time_elapsed)

        results = method_results.get(method)
        if results is not None:
            for hostname in results:
                if hostname not in detected_hostnames:
                    yield method, hostname
                    detected_hostnames.add(hostname)


def detect_alive_imap_server(domain_name, tcp_ports, tcp_timeout, dns_server, ca_bundle):
    """Detect IMAP servers using different methods and return the 1st one for which connection succeeded

    Detection of IMAP hostnames works as described in "detect_imap_server" function.
    Then, for each detected hostname this function tries to connect to the server, and returns the 1st
    hostname to which TCP connection succeeded.

    The function returns tuple (hostname, port, socket, method_id), where:
    - hostname is an IMAP server hostname, as string
    - port is a IMAP server TCP port, as integer
    - socket is a socket object connected to IMAP server and ready for actual
    - method_id is a string which identifies method which has detected hostname
    communications (SSL negotiation, IMAP commands)

    When detection failed (e.g. no hosts were returned by autodetection, or TCP connections to all of them failed),
    then ImapAutodetectFailedException exception is thrown.

    :type domain_name: str | unicode
    :type tcp_ports: list[int]
    :type tcp_timeout: int | None
    :param str | unicode dns_server: DNS server IP or hostname
    :param str | unicode ca_bundle: Path to CA certificates bundle
    :rtype: tuple
    :raises: plesk_mail_migrator.providers.imap.exceptions.ImapAutodetectFailedException
    """
    for method, hostname in detect_imap_server(domain_name, dns_server, ca_bundle):
        for tcp_port in unique_list(tcp_ports):
            logger.debug(
                "Try to connect to %s (detected by '%s' method) to port %s",
                hostname, method.get_method_id(), tcp_port
            )
            try:
                if tcp_timeout is not None:
                    imap_socket = socket.create_connection((hostname, tcp_port), timeout=tcp_timeout)
                else:
                    imap_socket = socket.create_connection((hostname, tcp_port))
                logger.debug(
                    "Successfully connected to '%s' (detected by '%s' method) to port '%s'",
                    hostname, method.get_method_id(), tcp_port
                )
                return hostname, tcp_port, imap_socket, method.get_method_id()
            except Exception as e:
                logger.debug(
                    "Exception (usually not an actual error, but just details why connection has failed): ",
                    exc_info=True
                )
                logger.debug(
                    "Connection to '%s' (detected by '%s' method) to port '%s' has failed: %s",
                    hostname, method.get_method_id(), tcp_port, str(e)
                )

    logger.debug("No more IMAP hostnames to try to connect to, consider that detection failed")
    raise ImapAutodetectFailedException("Failed to detect IMAP host automatically, please specify it manually")
