from django.core.exceptions import ValidationError
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from orchestra.core import services
from orchestra.core.validators import (validate_ipv4_address, validate_ipv6_address,
    validate_hostname, validate_ascii)

from . import settings, validators, utils


class Domain(models.Model):
    name = models.CharField(_("name"), max_length=256, unique=True,
            validators=[validate_hostname, validators.validate_allowed_domain])
    account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
            related_name='domains', blank=True)
    top = models.ForeignKey('domains.Domain', null=True, related_name='subdomains')
    serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial,
            help_text=_("Serial number"))
    
    def __unicode__(self):
        return self.name
    
    @cached_property
    def origin(self):
        return self.top or self
    
    @cached_property
    def is_top(self):
        return not bool(self.top)
    
    def get_records(self):
        """ proxy method, needed for input validation """
        return self.records.all()
    
    def get_topsubdomains(self):
        """ proxy method, needed for input validation """
        return self.origin.subdomains.all()
    
    def get_subdomains(self):
        return self.get_topsubdomains().filter(name__regex=r'.%s$' % self.name)
    
    def render_zone(self):
        origin = self.origin
        zone = origin.render_records()
        for subdomain in origin.get_topsubdomains():
            zone += subdomain.render_records()
        return zone
    
    def refresh_serial(self):
        """ Increases the domain serial number by one """
        serial = utils.generate_zone_serial()
        if serial <= self.serial:
            num = int(str(self.serial)[8:]) + 1
            if num >= 99:
                raise ValueError('No more serial numbers for today')
            serial = str(self.serial)[:8] + '%.2d' % num
            serial = int(serial)
        self.serial = serial
        self.save()
    
    def render_records(self):
        types = {}
        records = []
        for record in self.get_records():
            types[record.type] = True
            if record.type == record.SOA:
                # Update serial and insert at 0
                value = record.value.split()
                value[2] = str(self.serial)
                records.insert(0, (record.SOA, ' '.join(value)))
            else:
                records.append((record.type, record.value))
        if not self.top:
            if Record.NS not in types:
                for ns in settings.DOMAINS_DEFAULT_NS:
                    records.append((Record.NS, ns))
            if Record.SOA not in types:
                soa = [
                    "%s." % settings.DOMAINS_DEFAULT_NAME_SERVER,
                    utils.format_hostmaster(settings.DOMAINS_DEFAULT_HOSTMASTER),
                    str(self.serial),
                    settings.DOMAINS_DEFAULT_REFRESH,
                    settings.DOMAINS_DEFAULT_RETRY,
                    settings.DOMAINS_DEFAULT_EXPIRATION,
                    settings.DOMAINS_DEFAULT_MIN_CACHING_TIME
                ]
                records.insert(0, (Record.SOA, ' '.join(soa)))
        no_cname = Record.CNAME not in types
        if Record.MX not in types and no_cname:
            for mx in settings.DOMAINS_DEFAULT_MX:
                records.append((Record.MX, mx))
        if (Record.A not in types and Record.AAAA not in types) and no_cname:
            records.append((Record.A, settings.DOMAINS_DEFAULT_A))
        result = ''
        for type, value in records:
            name = '%s.%s' % (self.name, ' '*(37-len(self.name)))
            type = '%s %s' % (type, ' '*(7-len(type)))
            result += '%s IN %s %s\n' % (name, type, value)
        return result
    
    def save(self, *args, **kwargs):
        """ create top relation """
        update = False
        if not self.pk:
            top = self.get_top()
            if top:
                self.top = top
            else:
                update = True
        super(Domain, self).save(*args, **kwargs)
        if update:
            domains = Domain.objects.exclude(pk=self.pk)
            for domain in domains.filter(name__endswith=self.name):
                domain.top = self
                domain.save()
        self.get_subdomains().update(account=self.account)
    
    def get_top(self):
        split = self.name.split('.')
        top = None
        for i in range(1, len(split)-1):
            name = '.'.join(split[i:])
            domain = Domain.objects.filter(name=name)
            if domain:
                top = domain.get()
        return top


class Record(models.Model):
    """ Represents a domain resource record  """
    MX = 'MX'
    NS = 'NS'
    CNAME = 'CNAME'
    A = 'A'
    AAAA = 'AAAA'
    SRV = 'SRV'
    TXT = 'TXT'
    SOA = 'SOA'
    
    TYPE_CHOICES = (
        (MX, "MX"),
        (NS, "NS"),
        (CNAME, "CNAME"),
        (A, _("A (IPv4 address)")),
        (AAAA, _("AAAA (IPv6 address)")),
        (SRV, "SRV"),
        (TXT, "TXT"),
        (SOA, "SOA"),
    )
    
    # TODO TTL
    domain = models.ForeignKey(Domain, verbose_name=_("domain"), related_name='records')
    type = models.CharField(max_length=32, choices=TYPE_CHOICES)
    value = models.CharField(max_length=256)
    
    def __unicode__(self):
        return "%s IN %s %s" % (self.domain, self.type, self.value)
    
    def clean(self):
        """ validates record value based on its type """
        # validate value
        mapp = {
            self.MX: validators.validate_mx_record,
            self.NS: validators.validate_zone_label,
            self.A: validate_ipv4_address,
            self.AAAA: validate_ipv6_address,
            self.CNAME: validators.validate_zone_label,
            self.TXT: validate_ascii,
            self.SRV: validators.validate_srv_record,
            self.SOA: validators.validate_soa_record,
        }
        mapp[self.type](self.value)


services.register(Domain)