#
#
#

from __future__ import absolute_import, division, print_function, \
    unicode_literals

from suds import WebFault

from collections import defaultdict
from .base import BaseProvider
from logging import getLogger
from ..record import Record
from transip.service.domain import DomainService
from transip.service.objects import DnsEntry


class TransipException(Exception):
    pass


class TransipConfigException(TransipException):
    pass


class TransipNewZoneException(TransipException):
    pass


class TransipProvider(BaseProvider):
    '''
    Transip DNS provider

    transip:
        class: octodns.provider.transip.TransipProvider
        # Your Transip account name (required)
        account: yourname
        # Path to a private key file (required if key is not used)
        key_file: /path/to/file
        # The api key as string (required if key_file is not used)
        key: |
            \'''
            -----BEGIN PRIVATE KEY-----
            ...
            -----END PRIVATE KEY-----
            \'''
        # if both `key_file` and `key` are presented `key_file` is used

    '''
    SUPPORTS_GEO = False
    SUPPORTS_DYNAMIC = False
    SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'SRV', 'SPF', 'TXT',
                    'SSHFP', 'CAA'))
    # unsupported by OctoDNS: 'TLSA'
    MIN_TTL = 120
    TIMEOUT = 15
    ROOT_RECORD = '@'

    def __init__(self, id, account, key=None, key_file=None,  *args, **kwargs):
        self.log = getLogger('TransipProvider[{}]'.format(id))
        self.log.debug('__init__: id=%s, account=%s, token=***', id,
                       account)
        super(TransipProvider, self).__init__(id, *args, **kwargs)

        if key_file is not None:
            self._client = DomainService(account, private_key_file=key_file)
        elif key is not None:
            self._client = DomainService(account, private_key=key)
        else:
            raise TransipConfigException(
                'Missing `key` of `key_file` parameter in config'
            )

        self.account = account
        self.key = key

        self._currentZone = {}

    def populate(self, zone, target=False, lenient=False):

        exists = False
        self._currentZone = zone
        self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
                       target, lenient)

        before = len(zone.records)
        try:
            zoneInfo = self._client.get_info(zone.name[:-1])
        except WebFault as e:
            if e.fault.faultcode == '102' and target is False:
                # Zone not found in account, and not a target so just
                # leave an empty zone.
                return exists
            elif e.fault.faultcode == '102' and target is True:
                self.log.warning('populate: Transip can\'t create new zones')
                raise TransipNewZoneException(
                    ('populate: ({}) Transip used ' +
                     'as target for non-existing zone: {}').format(
                        e.fault.faultcode, zone.name))
            else:
                self.log.error('populate: (%s) %s ', e.fault.faultcode,
                               e.fault.faultstring)
                raise e

        self.log.debug('populate: found %s records for zone %s',
                       len(zoneInfo.dnsEntries), zone.name)
        exists = True
        if zoneInfo.dnsEntries:
            values = defaultdict(lambda: defaultdict(list))
            for record in zoneInfo.dnsEntries:
                name = zone.hostname_from_fqdn(record['name'])
                if name == self.ROOT_RECORD:
                    name = ''

                if record['type'] in self.SUPPORTS:
                    values[name][record['type']].append(record)

            for name, types in values.items():
                for _type, records in types.items():
                    data_for = getattr(self, '_data_for_{}'.format(_type))
                    record = Record.new(zone, name, data_for(_type, records),
                                        source=self, lenient=lenient)
                    zone.add_record(record, lenient=lenient)
        self.log.info('populate:   found %s records, exists = %s',
                      len(zone.records) - before, exists)

        self._currentZone = {}
        return exists

    def _apply(self, plan):
        desired = plan.desired
        changes = plan.changes
        self.log.debug('apply: zone=%s, changes=%d', desired.name,
                       len(changes))

        self._currentZone = plan.desired
        try:
            self._client.get_info(plan.desired.name[:-1])
        except WebFault as e:
            self.log.exception('_apply: get_info failed')
            raise e

        _dns_entries = []
        for record in plan.desired.records:
            if record._type in self.SUPPORTS:
                entries_for = getattr(self,
                                      '_entries_for_{}'.format(record._type))

                # Root records have '@' as name
                name = record.name
                if name == '':
                    name = self.ROOT_RECORD

                _dns_entries.extend(entries_for(name, record))

        try:
            self._client.set_dns_entries(plan.desired.name[:-1], _dns_entries)
        except WebFault as e:
            self.log.warning(('_apply: Set DNS returned ' +
                              'one or more errors: {}').format(
                e.fault.faultstring))
            raise TransipException(200, e.fault.faultstring)

        self._currentZone = {}

    def _entries_for_multiple(self, name, record):
        _entries = []

        for value in record.values:
            _entries.append(DnsEntry(name, record.ttl, record._type, value))

        return _entries

    def _entries_for_single(self, name, record):

        return [DnsEntry(name, record.ttl, record._type, record.value)]

    _entries_for_A = _entries_for_multiple
    _entries_for_AAAA = _entries_for_multiple
    _entries_for_NS = _entries_for_multiple
    _entries_for_SPF = _entries_for_multiple
    _entries_for_CNAME = _entries_for_single

    def _entries_for_MX(self, name, record):
        _entries = []

        for value in record.values:
            content = "{} {}".format(value.preference, value.exchange)
            _entries.append(DnsEntry(name, record.ttl, record._type, content))

        return _entries

    def _entries_for_SRV(self, name, record):
        _entries = []

        for value in record.values:
            content = "{} {} {} {}".format(value.priority, value.weight,
                                           value.port, value.target)
            _entries.append(DnsEntry(name, record.ttl, record._type, content))

        return _entries

    def _entries_for_SSHFP(self, name, record):
        _entries = []

        for value in record.values:
            content = "{} {} {}".format(value.algorithm,
                                        value.fingerprint_type,
                                        value.fingerprint)
            _entries.append(DnsEntry(name, record.ttl, record._type, content))

        return _entries

    def _entries_for_CAA(self, name, record):
        _entries = []

        for value in record.values:
            content = "{} {} {}".format(value.flags, value.tag,
                                        value.value)
            _entries.append(DnsEntry(name, record.ttl, record._type, content))

        return _entries

    def _entries_for_TXT(self, name, record):
        _entries = []

        for value in record.values:
            value = value.replace('\\;', ';')
            _entries.append(DnsEntry(name, record.ttl, record._type, value))

        return _entries

    def _parse_to_fqdn(self, value):

        # Enforce switch from suds.sax.text.Text to string
        value = str(value)

        # TransIP allows '@' as value to alias the root record.
        # this provider won't set an '@' value, but can be an existing record
        if value == self.ROOT_RECORD:
            value = self._currentZone.name

        if value[-1] != '.':
            self.log.debug('parseToFQDN: changed %s to %s', value,
                           '{}.{}'.format(value, self._currentZone.name))
            value = '{}.{}'.format(value, self._currentZone.name)

        return value

    def _get_lowest_ttl(self, records):
        _ttl = 100000
        for record in records:
            _ttl = min(_ttl, record['expire'])
        return _ttl

    def _data_for_multiple(self, _type, records):

        _values = []
        for record in records:
            # Enforce switch from suds.sax.text.Text to string
            _values.append(str(record['content']))

        return {
            'ttl': self._get_lowest_ttl(records),
            'type': _type,
            'values': _values
        }

    _data_for_A = _data_for_multiple
    _data_for_AAAA = _data_for_multiple
    _data_for_NS = _data_for_multiple
    _data_for_SPF = _data_for_multiple

    def _data_for_CNAME(self, _type, records):
        return {
            'ttl': records[0]['expire'],
            'type': _type,
            'value': self._parse_to_fqdn(records[0]['content'])
        }

    def _data_for_MX(self, _type, records):
        _values = []
        for record in records:
            preference, exchange = record['content'].split(" ", 1)
            _values.append({
                'preference': preference,
                'exchange': self._parse_to_fqdn(exchange)
            })
        return {
            'ttl': self._get_lowest_ttl(records),
            'type': _type,
            'values': _values
        }

    def _data_for_SRV(self, _type, records):
        _values = []
        for record in records:
            priority, weight, port, target = record['content'].split(' ', 3)
            _values.append({
                'port': port,
                'priority': priority,
                'target': self._parse_to_fqdn(target),
                'weight': weight
            })

        return {
            'type': _type,
            'ttl': self._get_lowest_ttl(records),
            'values': _values
        }

    def _data_for_SSHFP(self, _type, records):
        _values = []
        for record in records:
            algorithm, fp_type, fingerprint = record['content'].split(' ', 2)
            _values.append({
                'algorithm': algorithm,
                'fingerprint': fingerprint.lower(),
                'fingerprint_type': fp_type
            })

        return {
            'type': _type,
            'ttl': self._get_lowest_ttl(records),
            'values': _values
        }

    def _data_for_CAA(self, _type, records):
        _values = []
        for record in records:
            flags, tag, value = record['content'].split(' ', 2)
            _values.append({
                'flags': flags,
                'tag': tag,
                'value': value
            })

        return {
            'type': _type,
            'ttl': self._get_lowest_ttl(records),
            'values': _values
        }

    def _data_for_TXT(self, _type, records):
        _values = []
        for record in records:
            _values.append(record['content'].replace(';', '\\;'))

        return {
            'type': _type,
            'ttl': self._get_lowest_ttl(records),
            'values': _values
        }
