import re
import traceback
import dns.reversename
import dns.inet
import dns.name
from flask import current_app
from urllib.parse import urljoin
from distutils.util import strtobool
from itertools import groupby

from .. import utils
from .base import db
from .setting import Setting
from .domain import Domain
from .domain_setting import DomainSetting


def by_record_content_pair(e):
    return e[0]['content']


class Record(object):
    """
    This is not a model, it's just an object
    which be assigned data from PowerDNS API
    """
    def __init__(self,
                 name=None,
                 type=None,
                 status=None,
                 ttl=None,
                 data=None,
                 comment_data=None):
        self.name = name
        self.type = type
        self.status = status
        self.ttl = ttl
        self.data = data
        self.comment_data = comment_data
        # PDNS configs
        self.PDNS_STATS_URL = Setting().get('pdns_api_url')
        self.PDNS_API_KEY = Setting().get('pdns_api_key')
        self.PDNS_VERSION = Setting().get('pdns_version')
        self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
        self.PRETTY_IPV6_PTR = Setting().get('pretty_ipv6_ptr')

    def get_rrsets(self, domain):
        """
        Query domain's rrsets via PDNS API
        """
        headers = {'X-API-Key': self.PDNS_API_KEY}
        try:
            jdata = utils.fetch_json(urljoin(
                self.PDNS_STATS_URL, self.API_EXTENDED_URL +
                '/servers/localhost/zones/{0}'.format(domain)),
                                     timeout=int(
                                         Setting().get('pdns_api_timeout')),
                                     headers=headers,
                                     verify=Setting().get('verify_ssl_connections'))
        except Exception as e:
            current_app.logger.error(
                "Cannot fetch domain's record data from remote powerdns api. DETAIL: {0}"
                .format(e))
            return []

        rrsets=[]
        for r in jdata['rrsets']:
            if len(r['records']) == 0:
                continue

            while len(r['comments'])<len(r['records']):
                r['comments'].append({"content": "", "account": ""})
            r['records'], r['comments'] = (list(t) for t in zip(*sorted(zip(r['records'], r['comments']), key=by_record_content_pair)))
            rrsets.append(r)

        return rrsets

    def add(self, domain_name, rrset):
        """
        Add a record to a domain (Used by auto_ptr and DynDNS)

        Args:
            domain_name(str): The zone name
            rrset(dict): The record in PDNS rrset format

        Returns:
            (dict): A dict contains status code and message
        """
        # Validate record first
        rrsets = self.get_rrsets(domain_name)
        check = list(filter(lambda check: check['name'] == self.name, rrsets))
        if check:
            r = check[0]
            if r['type'] in ('A', 'AAAA', 'CNAME'):
                return {
                    'status': 'error',
                    'msg':
                    'Record already exists with type "A", "AAAA" or "CNAME"'
                }

        # Continue if the record is ready to be added
        headers = {'X-API-Key': self.PDNS_API_KEY}

        try:
            jdata = utils.fetch_json(urljoin(
                self.PDNS_STATS_URL, self.API_EXTENDED_URL +
                '/servers/localhost/zones/{0}'.format(domain_name)),
                                     headers=headers,
                                     timeout=int(
                                         Setting().get('pdns_api_timeout')),
                                     method='PATCH',
                                     verify=Setting().get('verify_ssl_connections'),
                                     data=rrset)
            current_app.logger.debug(jdata)
            return {'status': 'ok', 'msg': 'Record was added successfully'}
        except Exception as e:
            current_app.logger.error(
                "Cannot add record to domain {}. Error: {}".format(
                    domain_name, e))
            current_app.logger.debug("Submitted record rrset: \n{}".format(
                utils.pretty_json(rrset)))
            return {
                'status': 'error',
                'msg':
                'There was something wrong, please contact administrator'
            }

    def merge_rrsets(self, rrsets):
        """
        Merge the rrsets that has same "name" and
        "type".
        Return: a new rrset which has multiple "records"
        and "comments"
        """
        if not rrsets:
            raise Exception("Empty rrsets to merge")
        elif len(rrsets) == 1:
            # It is unique rrset already
            return rrsets[0]
        else:
            # Merge rrsets into one
            rrset = rrsets[0]
            for r in rrsets[1:]:
                rrset['records'] = rrset['records'] + r['records']
                rrset['comments'] = rrset['comments'] + r['comments']
            while len(rrset['comments']) < len(rrset['records']):
                rrset['comments'].append({"content": "", "account": ""})
            zipped_list = zip(rrset['records'], rrset['comments'])
            tuples = zip(*sorted(zipped_list, key=by_record_content_pair))
            rrset['records'], rrset['comments'] = [list(t) for t in tuples]
            return rrset

    def build_rrsets(self, domain_name, submitted_records):
        """
        Build rrsets from the datatable's records

        Args:
            domain_name(str): The zone name
            submitted_records(list): List of records submitted from PDA datatable

        Returns:
            transformed_rrsets(list): List of rrsets converted from PDA datatable
        """
        rrsets = []
        for record in submitted_records:
            # Format the record name
            #
            # Translate template placeholders into proper record data
            record['record_data'] = record['record_data'].replace('[ZONE]', domain_name)
            # Translate record name into punycode (IDN) as that's the only way
            # to convey non-ascii records to the dns server
            record['record_name'] = record['record_name'].encode('idna').decode()
            #TODO: error handling
            # If the record is an alias (CNAME), we will also make sure that
            # the target domain is properly converted to punycode (IDN)
            if record["record_type"] == 'CNAME':
                record['record_data'] = record['record_data'].encode('idna').decode()
                #TODO: error handling
            # If it is ipv6 reverse zone and PRETTY_IPV6_PTR is enabled,
            # We convert ipv6 address back to reverse record format
            # before submitting to PDNS API.
            if self.PRETTY_IPV6_PTR and re.search(
                    r'ip6\.arpa', domain_name
            ) and record['record_type'] == 'PTR' and ':' in record[
                    'record_name']:
                record_name = dns.reversename.from_address(
                    record['record_name']).to_text()
            # Else, it is forward zone, then record name should be
            # in format "<name>.<domain>.". If it is root
            # domain name (name == '@' or ''), the name should
            # be in format "<domain>."
            else:
                record_name = "{}.{}.".format(
                    record["record_name"],
                    domain_name) if record["record_name"] not in [
                        '@', ''
                    ] else domain_name + '.'

            # Format the record content, it musts end
            # with a dot character if in following types
            if record["record_type"] in [
                    'MX', 'CNAME', 'SRV', 'NS', 'PTR'
            ] and record["record_data"].strip()[-1:] != '.':
                record["record_data"] += '.'

            record_content = {
                "content": record["record_data"],
                "disabled":
                False if record['record_status'] == 'Active' else True
            }

            # Format the comment
            record_comments = [{
                "content": record["record_comment"],
                "account": ""
            }] if record.get("record_comment") else [{
                "content": "",
                "account": ""
            }]

            # Add the formatted record to rrsets list
            rrsets.append({
                "name": record_name,
                "type": record["record_type"],
                "ttl": int(record["record_ttl"]),
                "records": [record_content],
                "comments": record_comments
            })

        # Group the records which has the same name and type.
        # The rrset then has multiple records inside.
        transformed_rrsets = []

        # Sort the list before using groupby
        rrsets = sorted(rrsets, key=lambda r: (r['name'], r['type']))
        groups = groupby(rrsets, key=lambda r: (r['name'], r['type']))
        for _k, v in groups:
            group = list(v)
            transformed_rrsets.append(self.merge_rrsets(group))

        return transformed_rrsets

    def compare(self, domain_name, submitted_records):
        """
        Compare the submitted records with PDNS's actual data

        Args:
            domain_name(str): The zone name
            submitted_records(list): List of records submitted from PDA datatable

        Returns:
            new_rrsets(list): List of rrsets to be added
            del_rrsets(list): List of rrsets to be deleted
        """
        # Create submitted rrsets from submitted records
        submitted_rrsets = self.build_rrsets(domain_name, submitted_records)
        current_app.logger.debug(
            "submitted_rrsets_data: \n{}".format(utils.pretty_json(submitted_rrsets)))

        # Current domain's rrsets in PDNS
        current_rrsets = self.get_rrsets(domain_name)
        current_app.logger.debug("current_rrsets_data: \n{}".format(
            utils.pretty_json(current_rrsets)))

        # Remove comment's 'modified_at' key
        # PDNS API always return the comments with modified_at
        # info, we have to remove it to be able to do the dict
        # comparison between current and submitted rrsets
        for r in current_rrsets:
            for comment in r['comments']:
                if 'modified_at' in comment:
                    del comment['modified_at']

        # List of rrsets to be added
        new_rrsets = {"rrsets": []}
        for r in submitted_rrsets:
            if r not in current_rrsets and r['type'] in Setting(
            ).get_records_allow_to_edit():
                r['changetype'] = 'REPLACE'
                new_rrsets["rrsets"].append(r)

        # List of rrsets to be removed
        del_rrsets = {"rrsets": []}
        for r in current_rrsets:
            if r not in submitted_rrsets and r['type'] in Setting(
            ).get_records_allow_to_edit() and r['type'] != 'SOA':
                r['changetype'] = 'DELETE'
                del_rrsets["rrsets"].append(r)

        current_app.logger.debug("new_rrsets: \n{}".format(utils.pretty_json(new_rrsets)))
        current_app.logger.debug("del_rrsets: \n{}".format(utils.pretty_json(del_rrsets)))

        return new_rrsets, del_rrsets

    def apply_rrsets(self, domain_name, rrsets):
        headers = {'X-API-Key': self.PDNS_API_KEY}
        jdata = utils.fetch_json(urljoin(
            self.PDNS_STATS_URL, self.API_EXTENDED_URL +
            '/servers/localhost/zones/{0}'.format(domain_name)),
                                  headers=headers,
                                  method='PATCH',
                                  verify=Setting().get('verify_ssl_connections'),
                                  data=rrsets)
        return jdata

    def apply(self, domain_name, submitted_records):
        """
        Apply record changes to a domain. This function
        will make 2 calls to the PDNS API to DELETE and
        REPLACE records (rrsets)
        """
        current_app.logger.debug(
            "submitted_records: {}".format(submitted_records))

        # Get the list of rrsets to be added and deleted
        new_rrsets, del_rrsets = self.compare(domain_name, submitted_records)

        # Remove blank comments from rrsets for compatibility with some backends
        def remove_blank_comments(rrset):
            if not rrset['comments']:
                del rrset['comments']
            elif isinstance(rrset['comments'], list):
                # Merge all non-blank comment values into a list
                merged_comments = [
                    v
                    for c in rrset['comments']
                    for v in c.values()
                    if v
                ]
                # Delete comment if all values are blank (len(merged_comments) == 0)
                if not merged_comments:
                    del rrset['comments']

        for r in new_rrsets['rrsets']:
            remove_blank_comments(r)

        for r in del_rrsets['rrsets']:
            remove_blank_comments(r)

        # Submit the changes to PDNS API
        try:
            if del_rrsets["rrsets"]:
                result = self.apply_rrsets(domain_name, del_rrsets)
                if 'error' in result.keys():
                    current_app.logger.error(
                        'Cannot apply record changes with deleting rrsets step. PDNS error: {}'
                        .format(result['error']))
                    return {
                        'status': 'error',
                        'msg': result['error'].replace("'", "")
                    }

            if new_rrsets["rrsets"]:
                result = self.apply_rrsets(domain_name, new_rrsets)
                if 'error' in result.keys():
                    current_app.logger.error(
                        'Cannot apply record changes with adding rrsets step. PDNS error: {}'
                        .format(result['error']))

                    # rollback - re-add the removed record if the adding operation is failed.
                    if del_rrsets["rrsets"]:
                        rollback_rrests = del_rrsets
                        for r in del_rrsets["rrsets"]:
                            r['changetype'] = 'REPLACE'
                        rollback = self.apply_rrsets(domain_name, rollback_rrests)
                        if 'error' in rollback.keys():
                            return dict(status='error',
                                        msg='Failed to apply changes. Cannot rollback previous failed operation: {}'
                                        .format(rollback['error'].replace("'", "")))
                        else:
                            return dict(status='error',
                                        msg='Failed to apply changes. Rolled back previous failed operation: {}'
                                        .format(result['error'].replace("'", "")))
                    else:
                        return {
                            'status': 'error',
                            'msg': result['error'].replace("'", "")
                        }

            self.auto_ptr(domain_name, new_rrsets, del_rrsets)
            self.update_db_serial(domain_name)
            current_app.logger.info('Record was applied successfully.')
            return {'status': 'ok', 'msg': 'Record was applied successfully', 'data': (new_rrsets, del_rrsets)}
        except Exception as e:
            current_app.logger.error(
                "Cannot apply record changes to domain {0}. Error: {1}".format(
                    domain_name, e))
            current_app.logger.debug(traceback.format_exc())
            return {
                'status': 'error',
                'msg':
                'There was something wrong, please contact administrator'
            }

    def auto_ptr(self, domain_name, new_rrsets, del_rrsets):
        """
        Add auto-ptr records
        """
        # Check if auto_ptr is enabled for this domain
        auto_ptr_enabled = False
        if Setting().get('auto_ptr'):
            auto_ptr_enabled = True
        else:
            domain_obj = Domain.query.filter(Domain.name == domain_name).first()
            domain_setting = DomainSetting.query.filter(
                DomainSetting.domain == domain_obj).filter(
                    DomainSetting.setting == 'auto_ptr').first()
            auto_ptr_enabled = strtobool(
                domain_setting.value) if domain_setting else False

        # If it is enabled, we create/delete the PTR records automatically
        if auto_ptr_enabled:
            try:
                RECORD_TYPE_TO_PTR = ['A', 'AAAA']
                new_rrsets = new_rrsets['rrsets']
                del_rrsets = del_rrsets['rrsets']

                if not new_rrsets and not del_rrsets:
                    msg = 'No changes detected. Skipping auto ptr...'
                    current_app.logger.info(msg)
                    return {'status': 'ok', 'msg': msg}

                new_rrsets = [
                    r for r in new_rrsets if r['type'] in RECORD_TYPE_TO_PTR
                ]
                del_rrsets = [
                    r for r in del_rrsets if r['type'] in RECORD_TYPE_TO_PTR
                ]

                d = Domain()
                for r in new_rrsets:
                    for record in r['records']:
                        # Format the reverse record name
                        # It is the reverse of forward record's content.
                        reverse_host_address = dns.reversename.from_address(
                            record['content']).to_text()

                        # Create the reverse domain name in PDNS
                        domain_reverse_name = d.get_reverse_domain_name(
                            reverse_host_address)
                        d.create_reverse_domain(domain_name,
                                                domain_reverse_name)

                        # Build the rrset for reverse zone updating
                        rrset_data = [{
                            "changetype":
                            "REPLACE",
                            "name":
                            reverse_host_address,
                            "ttl":
                            r['ttl'],
                            "type":
                            "PTR",
                            "records": [{
                                "content": r['name'],
                                "disabled": record['disabled']
                            }],
                            "comments": []
                        }]

                        # Format the rrset
                        rrset = {"rrsets": rrset_data}
                        self.add(domain_reverse_name, rrset)

                for r in del_rrsets:
                    for record in r['records']:
                        # Format the reverse record name
                        # It is the reverse of forward record's content.
                        reverse_host_address = dns.reversename.from_address(
                            record['content']).to_text()

                        # Create the reverse domain name in PDNS
                        domain_reverse_name = d.get_reverse_domain_name(
                            reverse_host_address)
                        d.create_reverse_domain(domain_name,
                                                domain_reverse_name)

                        # Delete the reverse zone
                        self.name = reverse_host_address
                        self.type = 'PTR'
                        self.data = record['content']
                        self.delete(domain_reverse_name)
                return {
                    'status': 'ok',
                    'msg': 'Auto-PTR record was updated successfully'
                }
            except Exception as e:
                current_app.logger.error(
                    "Cannot update auto-ptr record changes to domain {0}. Error: {1}"
                    .format(domain_name, e))
                current_app.logger.debug(traceback.format_exc())
                return {
                    'status':
                    'error',
                    'msg':
                    'Auto-PTR creation failed. There was something wrong, please contact administrator.'
                }

    def delete(self, domain):
        """
        Delete a record from domain
        """
        headers = {'X-API-Key': self.PDNS_API_KEY}
        data = {
            "rrsets": [{
                "name": self.name.rstrip('.') + '.',
                "type": self.type,
                "changetype": "DELETE",
                "records": []
            }]
        }
        try:
            jdata = utils.fetch_json(urljoin(
                self.PDNS_STATS_URL, self.API_EXTENDED_URL +
                '/servers/localhost/zones/{0}'.format(domain)),
                                     headers=headers,
                                     timeout=int(
                                         Setting().get('pdns_api_timeout')),
                                     method='PATCH',
                                     verify=Setting().get('verify_ssl_connections'),
                                     data=data)
            current_app.logger.debug(jdata)
            return {'status': 'ok', 'msg': 'Record was removed successfully'}
        except Exception as e:
            current_app.logger.error(
                "Cannot remove record {0}/{1}/{2} from domain {3}. DETAIL: {4}"
                .format(self.name, self.type, self.data, domain, e))
            return {
                'status': 'error',
                'msg':
                'There was something wrong, please contact administrator'
            }

    def is_allowed_edit(self):
        """
        Check if record is allowed to edit
        """
        return self.type in Setting().get_records_allow_to_edit()

    def is_allowed_delete(self):
        """
        Check if record is allowed to removed
        """
        return (self.type in Setting().get_records_allow_to_edit()
                and self.type != 'SOA')

    def exists(self, domain):
        """
        Check if record is present within domain records, and if it's present set self to found record
        """
        rrsets = self.get_rrsets(domain)
        for r in rrsets:
            if r['name'].rstrip('.') == self.name and r['type'] == self.type and r['records']:
                self.type = r['type']
                self.status = r['records'][0]['disabled']
                self.ttl = r['ttl']
                self.data = r['records'][0]['content']
                return True
        return False

    def update(self, domain, content):
        """
        Update single record
        """
        headers = {'X-API-Key': self.PDNS_API_KEY}

        data = {
            "rrsets": [{
                "name":
                self.name + '.',
                "type":
                self.type,
                "ttl":
                self.ttl,
                "changetype":
                "REPLACE",
                "records": [{
                    "content": content,
                    "disabled": self.status,
                }]
            }]
        }

        try:
            utils.fetch_json(urljoin(
                self.PDNS_STATS_URL, self.API_EXTENDED_URL +
                '/servers/localhost/zones/{0}'.format(domain)),
                             headers=headers,
                             timeout=int(Setting().get('pdns_api_timeout')),
                             method='PATCH',
                             verify=Setting().get('verify_ssl_connections'),
                             data=data)
            current_app.logger.debug("dyndns data: {0}".format(data))
            return {'status': 'ok', 'msg': 'Record was updated successfully'}
        except Exception as e:
            current_app.logger.error(
                "Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".
                format(self.name, self.type, self.data, domain, e))
            return {
                'status': 'error',
                'msg':
                'There was something wrong, please contact administrator'
            }

    def update_db_serial(self, domain):
        headers = {'X-API-Key': self.PDNS_API_KEY}
        jdata = utils.fetch_json(urljoin(
            self.PDNS_STATS_URL, self.API_EXTENDED_URL +
            '/servers/localhost/zones/{0}'.format(domain)),
                                 headers=headers,
                                 timeout=int(
                                     Setting().get('pdns_api_timeout')),
                                 method='GET',
                                 verify=Setting().get('verify_ssl_connections'))
        serial = jdata['serial']

        domain = Domain.query.filter(Domain.name == domain).first()
        if domain:
            domain.serial = serial
            db.session.commit()
            return {
                'status': True,
                'msg': 'Synced local serial for domain name {0}'.format(domain)
            }
        else:
            return {
                'status': False,
                'msg':
                'Could not find domain name {0} in local db'.format(domain)
            }
