# -*- coding: utf-8 -*-
import ldapdb.models
import unicodedata
import string
import datetime
from django.db import models
from django.db.models import Q
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.contrib.auth.models import AbstractUser
from django.conf import settings
from django.core.validators import RegexValidator
from ldapdb.models.fields import CharField, IntegerField, ListField

from coin.offers.models import OfferSubscription
from coin.mixins import CoinLdapSyncMixin
from coin import utils


class Member(CoinLdapSyncMixin, AbstractUser):

    # USERNAME_FIELD = 'login'
    REQUIRED_FIELDS = ['first_name', 'last_name', 'email', ]

    MEMBER_TYPE_CHOICES = (
        ('natural_person', 'Personne physique'),
        ('legal_entity', 'Personne morale'),
    )
    MEMBER_STATUS_CHOICES = (
        ('member', 'Adhérent'),
        ('not_member', 'Non adhérent'),
        ('pending', "Demande d'adhésion"),
    )

    status = models.CharField(max_length=50, choices=MEMBER_STATUS_CHOICES,
                              default='pending')
    type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
                            default='natural_person')

    organization_name = models.CharField(max_length=200, blank=True,
                                         verbose_name='Nom de l\'organisme',
                                         help_text='Pour une personne morale')
    home_phone_number = models.CharField(max_length=25, blank=True,
                                         verbose_name=u'Téléphone fixe')
    mobile_phone_number = models.CharField(max_length=25, blank=True,
                                           verbose_name=u'Téléphone mobile')
    # TODO: use a django module that provides an address model? (would
    # support more countries and address types)
    address = models.TextField(verbose_name=u'adresse', blank=True, null=True)
    postal_code = models.CharField(max_length=5, blank=True, null=True,
                                   validators=[RegexValidator(regex=r'^\d{5}$',
                                               message=u'Code postal non valide.')],
                                   verbose_name=u'code postal')
    city = models.CharField(max_length=200, blank=True, null=True,
                            verbose_name=u'Commune')
    country = models.CharField(max_length=200, blank=True, null=True,
                               default='France',
                               verbose_name=u'Pays')
    entry_date = models.DateField(null=False,
                                  blank=False,
                                  default=datetime.date.today,
                                  verbose_name='Date de première adhésion')
    # TODO: for data retention, prevent deletion of a user 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 départ de '
                                   'l\'association')

    # Following fields are managed by the parent class AbstractUser :
    # username, first_name, last_name, email
    # However we hack the model to force theses fields to be required. (see below)
    
    # This property is used to change password in LDAP. Used in sync_to_ldap.
    # Should not be defined manually. Prefer use set_password method that hash
    # passwords for both ldap and local db
    _password_ldap = None


    def __unicode__(self):
        name = self.first_name + ' ' + self.last_name
        if self.organization_name:
            name += ' (%s)' % self.organization_name
        return name

    def get_full_name(self):
        return '%s %s' % (self.first_name, self.last_name)

    def get_short_name(self):
        return self.username

    # Renvoie la date de fin de la dernière cotisation du membre
    def end_date_of_membership(self):
        try:
            return self.membership_fees.order_by('-end_date')[0].end_date
        #TODO: bad practice de tout matcher comme ca
        except:
            return None

    def is_paid_up(self):
        """
        True si le membre est à jour de cotisation. False sinon
        """
        if self.end_date_of_membership() \
                and self.end_date_of_membership() >= datetime.date.today():
            return True
        else:
            return False

    def set_password(self, new_password, *args, **kwargs):
        """
        Définit le mot de passe a sauvegarder en base et dans le LDAP
        """
        super(Member, self).set_password(new_password, *args, **kwargs)
        self._password_ldap = utils.ldap_hash(new_password)
        
    def get_active_subscriptions(self, date=datetime.date.today()):
        """
        Return list of OfferSubscription which are active today
        """
        return OfferSubscription.objects.filter(
            Q(member__exact=self.pk),
            Q(subscription_date__lte=date),
            Q(resign_date__isnull=True) | Q(resign_date__gte=date))

    def get_automatic_username(self):
        """
        Calcul le username / ldap cn automatiquement en fonction
        du nom et du prénom
        """

        # Première lettre de chaque partie du prénom
        first_name_letters = ''.join(
            [c[0] for c in self.first_name.split('-')]
        )
        # Concaténer avec nom de famille
        username = ('%s%s' % (first_name_letters, self.last_name))
        # Remplacer ou enlever les caractères non ascii
        username = unicodedata.normalize('NFD', username)\
            .encode('ascii', 'ignore')
        # Enlever ponctuation et espace
        username = username.translate(None, string.punctuation + ' ')
        # En minuscule
        username = username.lower()
        # Maximum de 30 char
        username = username[:30]

        return username

    def sync_to_ldap(self, creation, update_fields, *args, **kwargs):
        """
        Update LDAP data when a member is saved
        """

        # Do not perform LDAP query if no usefull fields to update are specified
        # in update_fields
        # Ex : at login, last_login field is updated by django auth module.
        if update_fields and set(['username', 'last_name', 'first_name']).isdisjoint(set(update_fields)):
            return

        # Fail if no username specified
        assert self.username, ('Can\'t sync with LDAP because missing username '
                              'value for the Member : %s' % self)

        # If try to sync a superuser in creation mode
        # Try to retrieve the user in ldap. If exists, switch to update mode
        # This allow to create a superuser without having to delete corresponding
        # username in LDAP
        if self.is_superuser and creation:
            try:
                ldap_user = LdapUser.objects.get(pk=self.username)
                creation = False
            except LdapUser.DoesNotExist:
                pass

        if not creation:
            ldap_user = LdapUser.objects.get(pk=self.username)

        if creation:
            max_uid_number = LdapUser.objects.order_by('-uidNumber')[0].uidNumber
            ldap_user = LdapUser()
            ldap_user.pk = self.username
            ldap_user.uid = self.username
            ldap_user.nick_name = self.username
            ldap_user.uidNumber = max_uid_number + 1

        ldap_user.last_name = self.last_name
        ldap_user.first_name = self.first_name
        
        # If a password is definied in _password_ldap, change it in LDAP
        if self._password_ldap:
            # Make sure password is hashed
            ldap_user.password = utils.ldap_hash(self._password_ldap)

        ldap_user.save()

        # if creation:
        #     ldap_group = LdapGroup.objects.get(pk='coin')
        #     ldap_group.members.append(ldap_user.pk)
        #     ldap_group.save()

    def delete_from_ldap(self):
        """
        Delete member from the LDAP
        """
        assert self.username, ('Can\'t delete from LDAP because missing '
                              'username value for the Member : %s' % self)

        #Delete user from LDAP
        ldap_user = LdapUser.objects.get(pk=self.username)
        ldap_user.delete()

        # Lorsqu'un membre est supprimé du SI, son utilisateur LDAP
        # correspondant est sorti du groupe "coin" afin qu'il n'ait plus
        # accès au SI
        # ldap_group = LdapGroup.objects.get(pk='coin')
        # if self.username in ldap_group.members:
        #     ldap_group.members.remove(self.username)
        #     ldap_group.save()

    class Meta:
        verbose_name = 'membre'

# Hack to force email, first_name ans last_name to be required by Member model
Member._meta.get_field('email')._unique = True
Member._meta.get_field('email').blank = False
Member._meta.get_field('email').null = False
Member._meta.get_field('first_name').blank = False
Member._meta.get_field('first_name').null = False
Member._meta.get_field('last_name').blank = False
Member._meta.get_field('last_name').null = False


class CryptoKey(models.Model):

    KEY_TYPE_CHOICES = (('RSA', 'RSA'), ('GPG', 'GPG'))

    type = models.CharField(max_length=3, choices=KEY_TYPE_CHOICES)
    key = models.TextField(verbose_name=u'Clé')
    member = models.ForeignKey('Member', verbose_name=u'Membre')

    def __unicode__(self):
        return u'Clé %s de %s' % (self.type, self.member)

    class Meta:
        verbose_name = 'clé'


def in_one_year():
    return datetime.date.today() + datetime.timedelta(365)

class MembershipFee(models.Model):
    member = models.ForeignKey('Member', related_name='membership_fees',
                               verbose_name=u'Membre')
    #TODO: config: valeur par défaut à externaliser dans la configuration
    amount = models.IntegerField(null=False, default='20', help_text='en €',
                                 verbose_name=u'Montant')
    start_date = models.DateField(
        null=False,
        blank=False,
        default=datetime.date.today,
        verbose_name='Date de début de cotisation')
    end_date = models.DateField(
        null=False,
        blank=False,
        default=in_one_year,
        verbose_name='Date de fin de cotisation')

    def __unicode__(self):
        return u'%s - %s - %i€' % (self.member, self.start_date, self.amount)

    class Meta:
        verbose_name = 'cotisation'


class LdapUser(ldapdb.models.Model):
    base_dn = settings.LDAP_USER_BASE_DN #"ou=users,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
    object_classes = ['inetOrgPerson', 'organizationalPerson', 'person',
                      'top', 'posixAccount']

    uid = CharField(db_column='uid', unique=True, max_length=255)
    nick_name = CharField(db_column='cn', unique=True, primary_key=True,
                          max_length=255)
    first_name = CharField(db_column='givenName', max_length=255)
    last_name = CharField(db_column='sn', max_length=255)
    display_name = CharField(db_column='displayName', max_length=255,
                             blank=True)
    password = CharField(db_column='userPassword', max_length=255)
    uidNumber = IntegerField(db_column='uidNumber', unique=True)
    gidNumber = IntegerField(db_column='gidNumber', default=2000)
    homeDirectory = CharField(db_column='homeDirectory', max_length=255,
                              default='/tmp')

    def __unicode__(self):
        return self.display_name

    class Meta:
        managed = False  # Indique à Django de ne pas intégrer ce model en base


class LdapGroup(ldapdb.models.Model):
    base_dn = settings.LDAP_GROUP_BASE_DN #"ou=groups,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
    object_classes = ['posixGroup']

    gid = IntegerField(db_column='gidNumber', unique=True)
    name = CharField(db_column='cn', max_length=200, primary_key=True)
    members = ListField(db_column='memberUid')

    def __unicode__(self):
        return self.name

    class Meta:
        managed = False  # Indique à Django de ne pas intégrer ce model en base


@receiver(pre_save, sender=Member)
def define_username(sender, instance, **kwargs):
    """
    Lors de la sauvegarde d'un membre. Si le champ username n'est pas définit,
    le calcul automatiquement en fonction du nom et du prénom
    """
    if not instance.username and not instance.pk:
        instance.username = instance.get_automatic_username()

@receiver(pre_save, sender=LdapUser)
def define_display_name(sender, instance, **kwargs):
    """
    Lors de la sauvegarde d'un utilisateur Ldap, le champ display_name est la
    concaténation de first_name et last_name
    """
    if not instance.display_name:
        instance.display_name = '%s %s' % (instance.first_name,
                                           instance.last_name)
