from django.db import models
from django.db.models import JSONField, Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import FieldError

from uncloud import COUNTRIES
from .selectors import filter_for_when

class UncloudModel(models.Model):
    """
    This class extends the standard model with an
    extra_data field that can be used to include public,
    but internal information.

    For instance if you migrate from an existing virtualisation
    framework to uncloud.

    The extra_data attribute should be considered a hack and whenever
    data is necessary for running uncloud, it should **not** be stored
    in there.

    """

    extra_data = JSONField(editable=False, blank=True, null=True)

    class Meta:
        abstract = True

# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
class UncloudStatus(models.TextChoices):
    PENDING          = 'PENDING', _('Pending')
    AWAITING_PAYMENT = 'AWAITING_PAYMENT', _('Awaiting payment')
    BEING_CREATED    = 'BEING_CREATED', _('Being created')
    SCHEDULED        = 'SCHEDULED', _('Scheduled') # resource selected, waiting for dispatching
    ACTIVE           = 'ACTIVE', _('Active')
    MODIFYING        = 'MODIFYING', _('Modifying') # Resource is being changed
    DELETED          = 'DELETED', _('Deleted')     # Resource has been deleted
    DISABLED         = 'DISABLED', _('Disabled')   # Is usable, but cannot be used for new things
    UNUSABLE         = 'UNUSABLE', _('Unusable'),  # Has some kind of error



###
# General address handling
class CountryField(models.CharField):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('choices', COUNTRIES)
        kwargs.setdefault('default', 'CH')
        kwargs.setdefault('max_length', 2)

        super().__init__(*args, **kwargs)

    def get_internal_type(self):
        return "CharField"


class UncloudAddress(models.Model):
    full_name = models.CharField(max_length=256, null=False)
    organization = models.CharField(max_length=256, blank=True, null=True)
    street = models.CharField(max_length=256, null=False)
    city = models.CharField(max_length=256, null=False)
    postal_code = models.CharField(max_length=64)
    country = CountryField(blank=False, null=False)

    class Meta:
        abstract = True


class UncloudValidTimeFrame(models.Model):
    """
    A model that allows to limit validity of something to a certain
    time frame. Used for versioning basically.

    Logic:

    """

    class Meta:
        abstract = True

        constraints = [
            models.UniqueConstraint(fields=['owner'],
                                    condition=models.Q(active=True),
                                    name='one_active_card_per_user')
        ]


    valid_from = models.DateTimeField(default=timezone.now, null=True, blank=True)
    valid_to = models.DateTimeField(null=True, blank=True)

    @classmethod
    def get_current(cls, *args, **kwargs):
        now = timezone.now()

        # With both given
        cls.objects.filter(valid_from__lte=now,
                           valid_to__gte=now)

        # With to missing
        cls.objects.filter(valid_from__lte=now,
                           valid_to__isnull=true)

        # With from missing
        cls.objects.filter(valid_from__isnull=true,
                           valid_to__gte=now)

        # Both missing
        cls.objects.filter(valid_from__isnull=true,
                           valid_to__gte=now)





###
# UncloudNetworks are used as identifiers - such they are a base of uncloud

class UncloudNetwork(models.Model):
    """
    Storing IP networks
    """

    network_address = models.GenericIPAddressField(null=False, unique=True)
    network_mask = models.IntegerField(null=False,
                                       validators=[MinValueValidator(0),
                                                   MaxValueValidator(128)]
                                       )

    description = models.CharField(max_length=256)

    @classmethod
    def populate_db_defaults(cls):
        for net, desc in [
                ( "2a0a:e5c0:11::", "uncloud Billing" ),
                ( "2a0a:e5c0:11:1::", "uncloud Referral" ),
                ( "2a0a:e5c0:11:2::", "uncloud Coupon" )
                ]:
            obj, created = cls.objects.get_or_create(network_address=net,
                                                     defaults= {
                                                         'network_mask': 64,
                                                         'description': desc
                                                         }
                                                     )


    def save(self, *args, **kwargs):
        if not ':' in self.network_address and self.network_mask > 32:
            raise FieldError("Mask cannot exceed 32 for IPv4")

        super().save(*args, **kwargs)


    def __str__(self):
        return f"{self.network_address}/{self.network_mask} {self.description}"

###
# Who is running / providing this instance of uncloud?

class UncloudProvider(UncloudAddress):
    """
    A class resembling who is running this uncloud instance.
    This might change over time so we allow starting/ending dates

    This also defines the taxation rules.

    starting/ending date define from when to when this is valid. This way
    we can model address changes and have it correct in the bills.
    """

    # Meta:
    # FIXMe: only allow non overlapping time frames -- how to define this as a constraint?
    starting_date = models.DateField()
    ending_date = models.DateField(blank=True, null=True)

    billing_network = models.ForeignKey(UncloudNetwork, related_name="uncloudproviderbill", on_delete=models.CASCADE)
    referral_network = models.ForeignKey(UncloudNetwork, related_name="uncloudproviderreferral", on_delete=models.CASCADE)
    coupon_network = models.ForeignKey(UncloudNetwork, related_name="uncloudprovidercoupon", on_delete=models.CASCADE)


    @classmethod
    def get_provider(cls, when=None):
        """
        Find active provide at a certain time - if there was any
        """


        return cls.objects.get(Q(starting_date__gte=when, ending_date__lte=when) |
                               Q(starting_date__gte=when, ending_date__isnull=True))


    @classmethod
    def populate_db_defaults(cls):
        obj, created = cls.objects.get_or_create(full_name="ungleich glarus ag",
                                                 street="Bahnhofstrasse 1",
                                                 postal_code="8783",
                                                 city="Linthal",
                                                 country="CH",
                                                 starting_date=timezone.now(),
                                                 billing_network=UncloudNetwork.objects.get(description="uncloud Billing"),
                                                 referral_network=UncloudNetwork.objects.get(description="uncloud Referral"),
                                                 coupon_network=UncloudNetwork.objects.get(description="uncloud Coupon")
                                                 )


    def __str__(self):
        return f"{self.full_name} {self.country}"