# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017  Gabriel Détraz
# Copyright © 2017  Goulven Kermarec
# Copyright © 2017  Augustin Lemesle
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

from django.db import models
from django.db.models.signals import post_save, pre_delete, post_delete
from django.dispatch import receiver
from django.forms import ValidationError
from django.utils.functional import cached_property
from django.utils import timezone
from macaddress.fields import MACAddressField
from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress
from django.core.validators import MinValueValidator,MaxValueValidator
import re
from reversion import revisions as reversion
from datetime import timedelta


class Machine(models.Model):
    """ Class définissant une machine, object parent user, objets fils interfaces"""
    PRETTY_NAME = "Machine"
    
    user = models.ForeignKey('users.User', on_delete=models.PROTECT)
    name = models.CharField(max_length=255, help_text="Optionnel", blank=True, null=True)
    active = models.BooleanField(default=True)

    def __str__(self):
        return str(self.user) + ' - ' + str(self.id) + ' - ' +  str(self.name)

class MachineType(models.Model):
    """ Type de machine, relié à un type d'ip, affecté aux interfaces"""
    PRETTY_NAME = "Type de machine"

    type = models.CharField(max_length=255)
    ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True)

    def all_interfaces(self):
        return Interface.objects.filter(type=self)

    def __str__(self):
         return self.type

class IpType(models.Model):
    """ Type d'ip, définissant un range d'ip, affecté aux machine types"""
    PRETTY_NAME = "Type d'ip"

    type = models.CharField(max_length=255)
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
    need_infra = models.BooleanField(default=False)
    domaine_ip_start = models.GenericIPAddressField(protocol='IPv4')
    domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4')
    vlan = models.ForeignKey('Vlan', on_delete=models.PROTECT, blank=True, null=True)

    @cached_property
    def ip_range(self):
        return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop)

    @cached_property
    def ip_set(self):
        return IPSet(self.ip_range)

    @cached_property
    def ip_set_as_str(self):
        return [str(x) for x in self.ip_set]

    def ip_objects(self):
        return IpList.objects.filter(ip_type=self)

    def free_ip(self):
        return IpList.objects.filter(interface__isnull=True).filter(ip_type=self)

    def gen_ip_range(self):
        # Creation du range d'ip dans les objets iplist
        networks = []
        for net in self.ip_range.cidrs():
            networks += net.iter_hosts()
        ip_obj = [IpList(ip_type=self, ipv4=str(ip)) for ip in networks]
        listes_ip = IpList.objects.filter(ipv4__in=[str(ip) for ip in networks])
        # Si il n'y a pas d'ip, on les crée
        if not listes_ip:
            IpList.objects.bulk_create(ip_obj)
        # Sinon on update l'ip_type
        else:
            listes_ip.update(ip_type=self)
        return

    def del_ip_range(self):
        """ Methode dépréciée, IpList est en mode cascade et supprimé automatiquement"""
        if Interface.objects.filter(ipv4__in=self.ip_objects()):
            raise ValidationError("Une ou plusieurs ip du range sont affectées, impossible de supprimer le range")
        for ip in self.ip_objects():
            ip.delete()

    def clean(self):
        if IPAddress(self.domaine_ip_start) > IPAddress(self.domaine_ip_stop):
            raise ValidationError("Domaine end doit être après start...")
        # On ne crée pas plus grand qu'un /16
        if self.ip_range.size > 65536:
            raise ValidationError("Le range est trop gros, vous ne devez pas créer plus grand qu'un /16")
        # On check que les / ne se recoupent pas
        for element in IpType.objects.all().exclude(pk=self.pk):
            if not self.ip_set.isdisjoint(element.ip_set):
                raise ValidationError("Le range indiqué n'est pas disjoint des ranges existants")
        return

    def save(self, *args, **kwargs):
        self.clean()
        super(IpType, self).save(*args, **kwargs)

    def __str__(self):
        return self.type

class Vlan(models.Model):
    PRETTY_NAME = "Vlans"

    vlan_id = models.IntegerField()
    name = models.CharField(max_length=256)
    comment = models.CharField(max_length=256, blank=True)

    def __str__(self):
        return self.name

class Extension(models.Model):
    PRETTY_NAME = "Extensions dns"

    name = models.CharField(max_length=255, unique=True)
    need_infra = models.BooleanField(default=False)
    origin = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True)

    @cached_property
    def dns_entry(self):
        return  "@   IN  A " + str(self.origin)

    def __str__(self):
        return self.name

class Mx(models.Model):
    PRETTY_NAME = "Enregistrements MX"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
    priority = models.IntegerField(unique=True)
    name = models.OneToOneField('Domain', on_delete=models.PROTECT)

    @cached_property
    def dns_entry(self):
        return  "@   IN  MX " + str(self.priority) + " " + str(self.name)

    def __str__(self):
        return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name)

class Ns(models.Model):
    PRETTY_NAME = "Enregistrements NS"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
    ns = models.OneToOneField('Domain', on_delete=models.PROTECT)

    @cached_property
    def dns_entry(self):
        return  "@ IN NS " + str(self.ns)

    def __str__(self):
        return str(self.zone) + ' ' + str(self.ns)

class Text(models.Model):
    PRETTY_NAME = "Enregistrement text"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
    field1 = models.CharField(max_length=255)
    field2 = models.CharField(max_length=255)
  
    def __str__(self):
        return str(self.zone) + " : " + str(self.field1) + " " + str(self.field2)

    @cached_property
    def dns_entry(self):
        return str(self.field1) + " IN TXT " + str(self.field2)

class Interface(models.Model):
    PRETTY_NAME = "Interface"

    ipv4 = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True)
    #ipv6 = models.GenericIPAddressField(protocol='IPv6', null=True)
    mac_address = MACAddressField(integer=False, unique=True)
    machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
    type = models.ForeignKey('MachineType', on_delete=models.PROTECT)
    details = models.CharField(max_length=255, blank=True)

    @cached_property
    def is_active(self):
        """ Renvoie si une interface doit avoir accès ou non """
        machine = self.machine
        user = self.machine.user
        return machine.active and user.has_access()

    def mac_bare(self):
        return str(EUI(self.mac_address, dialect=mac_bare)).lower()

    def clean(self, *args, **kwargs):
        self.mac_address = str(EUI(self.mac_address)) or None
        if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
            self.assign_ipv4()
        super(Interface, self).clean(*args, **kwargs)

    def assign_ipv4(self):
        """ Assigne une ip à l'interface """
        free_ips = self.type.ip_type.free_ip()
        if free_ips:
            self.ipv4 = free_ips[0]
        else:
            raise ValidationError("Il n'y a plus d'ip disponibles dans le slash")
        return

    def unassign_ipv4(self):
        self.ipv4 = None

    def update_type(self):
        """ Lorsque le machinetype est changé de type d'ip, on réassigne"""
        self.clean()
        self.save()

    def save(self, *args, **kwargs):
        self.mac_address = str(EUI(self.mac_address)) or None
        # On verifie la cohérence en forçant l'extension par la méthode
        if self.type.ip_type != self.ipv4.ip_type:
             raise ValidationError("L'ipv4 et le type de la machine ne correspondent pas")
        super(Interface, self).save(*args, **kwargs)

    def __str__(self):
        try:
            domain = self.domain
        except:
            domain = None
        return str(domain)

class Domain(models.Model):
    PRETTY_NAME = "Domaine dns"

    interface_parent = models.OneToOneField('Interface', on_delete=models.CASCADE, blank=True, null=True)
    name = models.CharField(help_text="Obligatoire et unique, ne doit pas comporter de points", max_length=255)
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
    cname = models.ForeignKey('self', null=True, blank=True, related_name='related_domain')

    class Meta:
        unique_together = (("name", "extension"),)

    def get_extension(self):
        if self.interface_parent:
            return self.interface_parent.type.ip_type.extension
        elif hasattr(self,'extension'):
            return self.extension
        else:
            return None

    def clean(self):
        if self.get_extension():
            self.extension=self.get_extension()
        """ Validation du nom de domaine, extensions dans type de machine, prefixe pas plus long que 63 caractères """
        if self.interface_parent and self.cname:
            raise ValidationError("On ne peut créer à la fois A et CNAME")
        if self.cname==self:
            raise ValidationError("On ne peut créer un cname sur lui même")
        HOSTNAME_LABEL_PATTERN = re.compile("(?!-)[A-Z\d-]+(?<!-)$", re.IGNORECASE)
        dns = self.name.lower()
        if len(dns) > 63:
            raise ValidationError("Le nom de domaine %s est trop long (maximum de 63 caractères)." % dns)
        if not HOSTNAME_LABEL_PATTERN.match(dns):
            raise ValidationError("Ce nom de domaine %s contient des carractères interdits." % dns)
        self.validate_unique()
        super(Domain, self).clean()

    @cached_property
    def dns_entry(self):
        if self.cname:
            return  str(self.name) + " IN CNAME " + str(self.cname) + "."

    def save(self, *args, **kwargs):
        if not self.get_extension():
            raise ValidationError("Extension invalide")
        self.full_clean()
        super(Domain, self).save(*args, **kwargs)

    def __str__(self):
        return str(self.name) + str(self.extension)

class IpList(models.Model):
    PRETTY_NAME = "Addresses ipv4"

    ipv4 = models.GenericIPAddressField(protocol='IPv4', unique=True)
    ip_type = models.ForeignKey('IpType', on_delete=models.CASCADE)

    @cached_property
    def need_infra(self):
        return self.ip_type.need_infra

    def clean(self):
        if not str(self.ipv4) in self.ip_type.ip_set_as_str:
            raise ValidationError("L'ipv4 et le range de l'iptype ne correspondent pas!")
        return

    def save(self, *args, **kwargs):
        self.clean()
        super(IpList, self).save(*args, **kwargs)

    def __str__(self):
        return self.ipv4

class Service(models.Model):
    """ Definition d'un service (dhcp, dns, etc)"""
    service_type = models.CharField(max_length=255, blank=True, unique=True)
    min_time_regen = models.DurationField(default=timedelta(minutes=1), help_text="Temps minimal avant nouvelle génération du service")
    regular_time_regen = models.DurationField(default=timedelta(hours=1), help_text="Temps maximal avant nouvelle génération du service") 
    servers = models.ManyToManyField('Interface', through='Service_link')

    def ask_regen(self):
        """ Marque à True la demande de régénération pour un service x """
        Service_link.objects.filter(service=self).exclude(asked_regen=True).update(asked_regen=True)
        return

    def process_link(self, servers):
        """ Django ne peut créer lui meme les relations manytomany avec table intermediaire explicite"""
        for serv in servers.exclude(pk__in=Interface.objects.filter(service=self)):
            link = Service_link(service=self, server=serv)
            link.save()
        Service_link.objects.filter(service=self).exclude(server__in=servers).delete()
        return

    def save(self, *args, **kwargs):
        super(Service, self).save(*args, **kwargs)

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

def regen(service):
    """ Fonction externe pour régérération d'un service, prend un objet service en arg"""
    obj = Service.objects.filter(service_type=service)
    if obj:
        obj[0].ask_regen()
    return

class Service_link(models.Model):
    """ Definition du lien entre serveurs et services"""
    service = models.ForeignKey('Service', on_delete=models.CASCADE)
    server = models.ForeignKey('Interface', on_delete=models.CASCADE)
    last_regen = models.DateTimeField(auto_now_add=True)
    asked_regen = models.BooleanField(default=False)

    def done_regen(self):
        """ Appellé lorsqu'un serveur a regénéré son service"""
        self.last_regen = timezone.now()
        self.asked_regen = False
        self.save()

    def need_regen(self):
        """ Décide si le temps minimal écoulé est suffisant pour provoquer une régénération de service"""
        if (self.asked_regen and (self.last_regen + self.service.min_time_regen) < timezone.now()) or (self.last_regen + self.service.regular_time_regen) < timezone.now():
            return True
        else:
            return False

    def __str__(self):
        return str(self.server) + " " + str(self.service)

@receiver(post_save, sender=Machine)
def machine_post_save(sender, **kwargs):
    user = kwargs['instance'].user
    user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
    regen('dhcp')
    regen('mac_ip_list')

@receiver(post_delete, sender=Machine)
def machine_post_delete(sender, **kwargs):
    machine = kwargs['instance']
    user = machine.user
    user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
    regen('dhcp')
    regen('mac_ip_list')

@receiver(post_save, sender=Interface)
def interface_post_save(sender, **kwargs):
    interface = kwargs['instance']
    user = interface.machine.user
    user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
    regen('dhcp')
    regen('mac_ip_list')

@receiver(post_delete, sender=Interface)
def interface_post_delete(sender, **kwargs):
    interface = kwargs['instance']
    user = interface.machine.user
    user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)

@receiver(post_save, sender=IpType)
def iptype_post_save(sender, **kwargs):
    iptype = kwargs['instance']
    iptype.gen_ip_range()

@receiver(post_save, sender=MachineType)
def machine_post_save(sender, **kwargs):
    machinetype = kwargs['instance']
    for interface in machinetype.all_interfaces():
        interface.update_type()

@receiver(post_save, sender=Domain)
def domain_post_save(sender, **kwargs):
    regen('dns')

@receiver(post_delete, sender=Domain)
def domain_post_delete(sender, **kwargs):
    regen('dns')

@receiver(post_save, sender=Mx)
def mx_post_save(sender, **kwargs):
    regen('dns')

@receiver(post_delete, sender=Mx)
def mx_post_delete(sender, **kwargs):
    regen('dns')

@receiver(post_save, sender=Ns)
def ns_post_save(sender, **kwargs):
    regen('dns')

@receiver(post_delete, sender=Ns)
def ns_post_delete(sender, **kwargs):
    regen('dns')

@receiver(post_save, sender=Text)
def text_post_save(sender, **kwargs):
    regen('dns')

@receiver(post_delete, sender=Text)
def text_post_delete(sender, **kwargs):
    regen('dns')