import dns.message, dns.query, dns.rdtypes.ANY.TXT, dns.rdtypes.ANY.CNAME, \
    dns.rdtypes.ANY.MX, dns.rdtypes.ANY.PTR, dns.rdtypes.IN.A, dns.rdtypes.IN.AAAA

from parallels.core.logging import get_logger
from parallels.hosting_check import DNSRecord

logger = get_logger(__name__)


def get_difference(records, dns_server_ip):
    difference = []

    records_grouped = {}
    for record in records:
        if (record.rec_type, record.src) not in records_grouped:
            records_grouped[(record.rec_type, record.src)] = []
        records_grouped[(record.rec_type, record.src)].append(record)

    for (rec_type, rec_src), expected in sorted(
        records_grouped.items(),
        cmp=lambda x, y: cmp_records_src_type(x[0][0], x[0][1], y[0][0], y[0][1])
    ):
        if is_allowed_record_type(rec_type):
            logger.debug(
                u"Checking the records %s on the DNS server at %s",
                _format_list([_pretty_record_str(r) for r in expected]), dns_server_ip
            )
            actual = resolve_record(rec_type, rec_src, dns_server_ip)
            comparison_result = compare_records(expected, actual)
            if comparison_result is not None:
                difference.append(comparison_result)
        else:
            pass  # skip all records we don't know how to handle

    return difference


def cmp_records_src_type(rec_type1, rec_src1, rec_type2, rec_src2):
    if len(rec_src1) > len(rec_src2):  # first go records with minimum length of src
        return 1
    elif len(rec_src1) < len(rec_src2):
        return -1
    else:
        if rec_src1 != rec_src2:
            return cmp(rec_src1, rec_src2)  # then records are compared src
        else:
            return cmp(rec_type1, rec_type2)  # and if src is the same - compare by type


def compare_records(expected, actual):
    def sort_records(records):
        return sorted(records, key=lambda r: (r.rec_type, r.src, r.dst.lower(), r.opt))

    expected = sort_records([ComparableRec.from_rec(r) for r in expected])
    actual = sort_records([ComparableRec.from_rec(r) for r in actual])
    if expected != actual:
        if len(expected) != len(actual):
            if len(actual) != 0:
                return (
                    "expected %s record(s) %s, got %s record(s) %s" % ( 
                        len(expected), ", ".join(["'%s'" % (s,) for s in expected]),
                        len(actual), ", ".join(["'%s'" % (s,) for s in actual])
                    )
                )
            else:
                return (
                    "expected record(s) %s, got no records" % ( 
                        ", ".join(["'%s'" % (s,) for s in expected]),
                    )
                )
        else:
            return (
                "expected %s, got %s" % (
                    ", ".join(["'%s'" % (s,) for s in expected]),
                    ", ".join(["'%s'" % (s,) for s in actual])
                )
            )

    return None  # no difference


def is_allowed_record_type(rec_type):
    return rec_type in rec_type_to_rdatatype

rec_type_to_rdatatype = {
    'A': dns.rdatatype.A,
    'AAAA': dns.rdatatype.AAAA,
    'CNAME': dns.rdatatype.CNAME,
    'MX': dns.rdatatype.MX,
    'TXT': dns.rdatatype.TXT,
    'PTR': dns.rdatatype.PTR,
    'NS': dns.rdatatype.NS,
    # SRV records are not checked, because they are backed up incorrecly,
    # restored incorrectly and there are issues when creating/editing them in PPA
    # and they are not so important/frequent when migrating from other panels 
}


def resolve_record(rec_type, rec_src, dns_server_ip):
    """
    Function simplifies iteraction with dnspython library, making DNS queries easy:
    you pass DNS record type and DNS record source as a DNS query, and get list of 'Rec' objects as a query result

    Returns list of items where each item is:
    - 'Rec', in case DNS server responsed with DNS data we know how to deal with and that we know how to
    convert to 'Rec' object
    - 'UnknownDnsResponse', in case DNS responsed with something that we don't know

    Raises DnsRequestTimeoutException in case of timeout
    """
    logger.debug(u"Sending the DNS query: type='%s', src='%s', server='%s'" % (rec_type, rec_src, dns_server_ip))

    rdatatype = rec_type_to_rdatatype[rec_type]
    if rec_type == 'PTR':
        host = dns.reversename.from_address(rec_src)
    else:
        host = rec_src

    dns_request = dns.message.make_query(host, rdatatype, dns.rdataclass.IN)

    def join_dns_strings(dns_strings):
        ret_string = ''
        valid_length = len(dns_strings)-1
        for i, tmp_str in enumerate(dns_strings):
            ret_string += tmp_str
            if tmp_str.endswith(';') and i < valid_length:
                ret_string += ' '
        return ret_string

    def convert_rdata_to_record(rdata):
        if rdata.rdtype == dns.rdatatype.A:
            return DNSRecord(rec_type='A', src=rec_src, dst=str(rdata.address), opt='')
        if rdata.rdtype == dns.rdatatype.AAAA:
            return DNSRecord(rec_type='AAAA', src=rec_src, dst=str(rdata.address), opt='')
        elif rdata.rdtype == dns.rdatatype.CNAME:
            return DNSRecord(rec_type='CNAME', src=rec_src, dst=str(rdata.target), opt='')
        elif rdata.rdtype == dns.rdatatype.TXT:
            return DNSRecord(rec_type='TXT', src=rec_src, dst=join_dns_strings(rdata.strings), opt='')
        elif rdata.rdtype == dns.rdatatype.MX:
            return DNSRecord(rec_type='MX', src=rec_src, dst=str(rdata.exchange), opt=str(rdata.preference))
        elif rdata.rdtype == dns.rdatatype.NS:
            return DNSRecord(rec_type='NS', src=rec_src, dst=str(rdata.target), opt='')
        else:
            return UnknownDnsResponse(raw_data=rdata) 

    try:
        dns_response = dns.query.udp(dns_request, dns_server_ip, timeout=DNS_REQUEST_TIMEOUT)
        result = [
            convert_rdata_to_record(rdata)
            for rdset in dns_response.answer
            for rdata in rdset
        ]
        logger.debug(u"Received the DNS response: %s", result)
        return result
    except dns.exception.Timeout as e:
        logger.debug(u'Exception:', exc_info=e)
        raise DnsRequestTimeoutException(server_ip=dns_server_ip)


def get_difference_between_dns_servers(records, expected_dns_server_ip, actual_dns_server_ip):
    difference = []

    records_grouped = dict()
    for record in records:
        key = (record.rec_type, record.src)
        if key not in records_grouped:
            records_grouped[key] = list()
        records_grouped[key].append(record)

    for (rec_type, rec_src), expected in sorted(
        records_grouped.items(), cmp=lambda x, y: cmp_records_src_type(x[0][0], x[0][1], y[0][0], y[0][1])
    ):
        if is_allowed_record_type(rec_type):
            logger.debug(
                u"Checking the record '%s %s' on the DNS server at %s and "
                u"comparing it to the response of the DNS server at %s",
                rec_type, rec_src, actual_dns_server_ip, expected_dns_server_ip
            )
            actual = resolve_record(rec_type, rec_src, actual_dns_server_ip)
            expected = resolve_record(rec_type, rec_src, expected_dns_server_ip)
            comparison_result = compare_records(expected, actual)
            if comparison_result is not None:
                difference.append(comparison_result)
        else:
            pass  # skip all records we don't know how to handle

    return difference


class DnsServerInfo(object):
    def __init__(self, hostname, ips):
        self.hostname = hostname
        self.ips = ips


def get_authoritative_dns_servers(domain, dns_server_ip):
    dns_servers = []
    for ns_rec in resolve_record('NS', '%s.' % (domain,), dns_server_ip):
        dns_server = DnsServerInfo(hostname=ns_rec.dst, ips=[])
        dns_servers.append(dns_server)
        for a_rec in resolve_record('A', dns_server.hostname, dns_server_ip):
            if a_rec.rec_type == 'A':
                dns_server.ips.append(a_rec.dst)
    return dns_servers


DNS_REQUEST_TIMEOUT = 60


class DnsRequestTimeoutException(Exception):
    def __init__(self, server_ip):
        self.server_ip = server_ip


class UnknownDnsResponse(object):
    def __init__(self, raw_data):
        self.raw_data = raw_data

    def __str__(self):
        return str(self.raw_data)


class ComparableRec(object):
    def __init__(self, rec_type, src, dst, opt):
        self.rec_type = rec_type
        self.src = src
        self.dst = dst
        self.opt = opt

    @classmethod
    def from_rec(cls, rec):
        return cls(rec_type=rec.rec_type, src=rec.src, dst=rec.dst, opt=rec.opt)

    def __str__(self):
        return _pretty_record_str(self)

    def __eq__(self, rec):
        return (
            self.rec_type == rec.rec_type and
            _safe_idn_encode(self.src) == _safe_idn_encode(rec.src) and
            (
                # Fix for domainkey TXT records, which may have an additional ";"
                # symbol at the end of the record after migration
                _safe_idn_encode(self.dst).lower() == _safe_idn_encode(rec.dst).lower() or
                (self.rec_type == 'TXT' and self.dst.rstrip(';') == rec.dst.rstrip(';'))
            ) and 
            self.opt == rec.opt
        )

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


def _safe_idn_encode(s):
    """Try to convert string to IDN form, on failure return unchanged string

    :type s: str | unicode
    :rtype: str | unicode
    """
    try:
        return s.encode('idna')
    except UnicodeError:
        return s


def _format_list(lst):
    return ", ".join(["'%s'" % (elem,) for elem in lst])


def _pretty_record_str(rec):
    if rec.opt is not None and rec.opt != "":
        opt_str = u" %s" % (rec.opt,) 
    else:
        opt_str = ""

    return u"%s %s%s %s" % (rec.src, rec.rec_type, opt_str, rec.dst)
