# -*- coding: utf-8 -*-
import datetime

from django.db import models
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver

from coin.resources.models import IPSubnet


class Offer(models.Model):
    """Description of an offer available to subscribers.

    Implementation notes: achieving genericity is difficult, especially
    because different technologies may have very different configuration
    parameters.

    Technology-specific configuration (e.g. for VPN) is implemented as a
    model having a OneToOne relation to OfferSubscription.  In order to
    reach the technology-specific configuration model from an
    OfferSubscription, the OneToOne relation MUST have a related_name
    equal to one of the backends in OFFER_BACKEND_CHOICES (for instance
    "openvpn_ldap").

    """
    OFFER_BACKEND_CHOICES = (
        ('openvpn_ldap', 'OpenVPN (LDAP)'),
        # Use this if you don't actually want to implement a backend, for
        # instance if you resell somebody else's offers and don't manage
        # technical information yourself.
        ('none', 'None'),
    )
    name = models.CharField(max_length=255, blank=False, null=False,
                            verbose_name='Nom de l\'offre')
    type = models.CharField(max_length=50,
                            help_text="Type of offer, for instance technology used (informative only)")
    billing_period = models.IntegerField(blank=False, null=False, default=1,
                                         verbose_name='Période de facturation',
                                         help_text='en mois')
    period_fees = models.DecimalField(max_digits=5, decimal_places=2,
                                      blank=False, null=False,
                                      verbose_name='Montant par période de '
                                                   'facturation',
                                      help_text='en €')
    initial_fees = models.DecimalField(max_digits=5, decimal_places=2,
                                      blank=False, null=False,
                                      verbose_name='Frais de mise en service',
                                      help_text='en €')
    # TODO: really ensure that this field does not change (as it would
    # seriously break subscriptions)
    backend = models.CharField(max_length=50, choices=OFFER_BACKEND_CHOICES)

    def __unicode__(self):
        return u'%s : %s - %d€ / %im [%s]' % (self.type, self.name,
                                              self.period_fees,
                                              self.billing_period, self.backend)

    class Meta:
        verbose_name = 'offre'


class OfferSubscription(models.Model):
    """Only contains administrative details about a subscription, not
    technical.  Nothing here should end up into the LDAP backend.

    Implementation notes: the model actually implementing the backend
    (technical configuration for the technology) MUST relate to this class
    with a OneToOneField whose related name is a member of
    OFFER_BACKEND_CHOICES, for instance:

      models.OneToOneField('offers.OfferSubscription', related_name="openvpn_ldap")

    """
    subscription_date = models.DateField(
        null=False,
        blank=False,
        default=datetime.date.today,
        verbose_name='Date de souscription à l\'offre')
    # TODO: for data retention, prevent deletion of a subscription object
    # while the resign date is recent enough (e.g. one year in France).
    resign_date = models.DateField(
        null=True,
        blank=True,
        verbose_name='Date de résiliation')
    # TODO: move this to offers?
    commitment = models.IntegerField(blank=False, null=False,
                                     verbose_name='Période d\'engagement',
                                     help_text = 'en mois',
                                     default=0)
    member = models.ForeignKey('members.Member', verbose_name='Membre')
    offer = models.ForeignKey('Offer', verbose_name='Offre')

    @property
    def configuration(self):
        """Returns the configuration object associated to this subscription,
        according to the backend type specified in the offer.  Yes, this
        is hand-made genericity.  If you can think of a better way, feel
        free to propose something.
        """
        if self.offer.backend == 'none' or not hasattr(self, self.offer.backend):
            return
        return getattr(self, self.offer.backend)

    def __unicode__(self):
        return u'%s - %s - %s - %s' % (self.member, self.offer.name,
                                       self.subscription_date,
                                       self.configuration)

    class Meta:
        verbose_name = 'abonnement'


@receiver(post_save, sender=IPSubnet)
def subnet_save_event(sender, **kwargs):
    """Fires when a subnet is saved (created/modified).  We tell the
    configuration backend to do whatever it needs to do with it.

    We should use a pre_save signal, so that if anything goes wrong in the
    backend (exception raised), nothing is actually saved in the database.
    But it has a big problem: the configuration backend will not see the
    change, since it has not been saved into the database yet.

    That's why we use a post_save signal instead.  But surprisingly, all
    is well: if we raise an exception here, the IPSubnet object will not
    be saved in the database.  But the backend *does* see the new state of
    the database.  It looks like the database rollbacks if an exception is
    raised.  Whatever the reason, this is not a documented feature of
    Django signals.
    """
    subnet = kwargs['instance']
    config = subnet.offer_subscription.configuration
    if config:
        config.save_subnet(subnet, kwargs['created'])


@receiver(post_delete, sender=IPSubnet)
def subnet_delete_event(sender, **kwargs):
    """Fires when a subnet is deleted.  We tell the configuration backend to
    do whatever it needs to do with it.
    """
    subnet = kwargs['instance']
    config = subnet.offer_subscription.configuration
    if config:
        config.delete_subnet(subnet)
