# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import ldapdb.models
import unicodedata
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 django.core.exceptions import ValidationError
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='member', verbose_name='statut')
    type = models.CharField(max_length=20, choices=MEMBER_TYPE_CHOICES,
                            default='natural_person', verbose_name='type')

    nickname = models.CharField(max_length=64, blank=True,
                                verbose_name="nom d'usage",
                                help_text='Pseudonyme, …')
    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='téléphone fixe')
    mobile_phone_number = models.CharField(max_length=25, blank=True,
                                           verbose_name='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='adresse postale', blank=True, null=True)
    postal_code = models.CharField(max_length=5, blank=True, null=True,
                                   validators=[RegexValidator(regex=r'^\d{5}$',
                                                              message='Code postal non valide.')],
                                   verbose_name='code postal')
    city = models.CharField(max_length=200, blank=True, null=True,
                            verbose_name='commune')
    country = models.CharField(max_length=200, blank=True, null=True,
                               default='France',
                               verbose_name='pays')
    resign_date = models.DateField(null=True, blank=True,
                                   verbose_name="date de départ de "
                                   "l'association",
                                   help_text="En cas de départ prématuré")
    comments = models.TextField(blank=True, verbose_name='commentaires',
                                help_text="Commentaires libres (informations"
                                " spécifiques concernant l'adhésion,"
                                " raison du départ, etc)")

    # 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 clean(self):
        if self.type == 'legal_entity':
            if not self.organization_name:
                raise ValidationError("Le nom de l'organisme est obligatoire "
                                      "pour une personne morale")
        elif self.type == 'natural_person':
            if not (self.first_name and self.last_name):
                raise ValidationError("Le nom et prénom sont obligatoires "
                                      "pour une personne physique")

    def __unicode__(self):
        if self.type == 'legal_entity':
            return self.organization_name
        elif self.nickname:
            return self.nickname
        else:
            return self.first_name + ' ' + self.last_name

    def get_full_name(self):
        return str(self)

    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
    end_date_of_membership.short_description = "Date de fin d'adhésion"

    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_inactive_subscriptions(self, date=datetime.date.today()):
        """
        Return list of OfferSubscription which are not active today
        """
        return OfferSubscription.objects.filter(
            Q(member__exact=self.pk),
            Q(subscription_date__gt=date) |
            Q(resign_date__lt=date))

    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:
            users = LdapUser.objects
            if users.exists():
                uid_number = users.order_by('-uidNumber')[0].uidNumber + 1
            else:
                uid_number = settings.LDAP_USER_FIRST_UID
            ldap_user = LdapUser()
            ldap_user.pk = self.username
            ldap_user.uid = self.username
            ldap_user.nick_name = self.username
            ldap_user.uidNumber = uid_number

        if self.type == 'natural_person':
            ldap_user.last_name = self.last_name
            ldap_user.first_name = self.first_name
        elif self.type == 'legal_entity':
            ldap_user.last_name = self.organization_name
            ldap_user.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()

    def send_welcome_email(self):
        """ Envoie le courriel de bienvenue à ce membre """
        utils.send_templated_email(to=self.email,
                                   subject_template='members/emails/welcome_email_subject.txt',
                                   body_template='members/emails/welcome_email.html',
                                   context={'member': self})

    class Meta:
        verbose_name = 'membre'

# Hack to force email 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


def count_active_members():
    return Member.objects.filter(status='member').count()


def get_automatic_username(member):
    """
    Calcul le username automatiquement en fonction
    du nom et du prénom
    """

    # S'il s'agit d'une entreprise, utilise son nom:
    if member.type == 'legal_entity' and member.organization_name:
        username = member.organization_name
    # Sinon, si un pseudo est définit, l'utilise
    elif member.nickname:
        username = member.nickname
    # Sinon, utilise nom et prenom
    elif member.first_name and member.last_name:
        # Première lettre de chaque partie du prénom
        first_name_letters = ''.join(
            [c[0] for c in member.first_name.split('-')]
        )
        # Concaténer avec nom de famille
        username = ('%s%s' % (first_name_letters, member.last_name))
    else:
        raise Exception('Il n\'y a pas sufissement d\'informations pour déterminer un login automatiquement')

    # Remplacer ou enlever les caractères non ascii
    username = unicodedata.normalize('NFD', username)\
        .encode('ascii', 'ignore')
    # Enlever ponctuation (sauf _-.) et espace
    punctuation = ('!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~ ').encode('ascii')
    username = username.translate(None, punctuation)
    # En minuscule
    username = username.lower()
    # Maximum de 30 char
    username = username[:30]

    # Recherche dans les membres existants un username identique
    member = Member.objects.filter(username=username)
    base_username = username
    incr = 2
    # Tant qu'un membre est trouvé, incrémente un entier à la fin
    while member:
        if len(base_username) >= 30:
            username = base_username[30 - len(str(incr)):]
        else:
            username = base_username
        username = username + str(incr)
        member = Member.objects.filter(username=username)
        incr += 1

    return username


class CryptoKey(models.Model):

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

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

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

    class Meta:
        verbose_name = 'clé'


class MembershipFee(models.Model):
    PAYMENT_METHOD_CHOICES = (
        ('cash', 'Espèces'),
        ('check', 'Chèque'),
        ('transfer', 'Virement'),
        ('other', 'Autre')
    )

    member = models.ForeignKey('Member', related_name='membership_fees',
                               verbose_name='membre')
    amount = models.DecimalField(null=False, max_digits=5, decimal_places=2,
                                 default=settings.MEMBER_DEFAULT_COTISATION,
                                 verbose_name='montant', help_text='en €')
    start_date = models.DateField(
        null=False,
        blank=False,
        verbose_name='date de début de cotisation')
    end_date = models.DateField(
        null=False,
        blank=True,
        verbose_name='date de fin de cotisation',
        help_text='par défaut, la cotisation dure un an')

    payment_method = models.CharField(max_length=100, null=True, blank=True,
                                      choices=PAYMENT_METHOD_CHOICES,
                                      verbose_name='moyen de paiement')
    reference = models.CharField(max_length=125, null=True, blank=True,
                                 verbose_name='référence du paiement',
                                 help_text='numéro de chèque, '
                                 'référence de virement, commentaire...')
    payment_date = models.DateField(null=True, blank=True,
                                    verbose_name='date du paiement')

    def clean(self):
        if self.end_date is None:
            self.end_date = self.start_date + datetime.timedelta(364)

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

    class Meta:
        verbose_name = 'cotisation'


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

    uid = CharField(db_column=b'uid', unique=True, max_length=255)
    nick_name = CharField(db_column=b'cn', unique=True, primary_key=True,
                          max_length=255)
    first_name = CharField(db_column=b'givenName', max_length=255)
    last_name = CharField(db_column=b'sn', max_length=255)
    display_name = CharField(db_column=b'displayName', max_length=255,
                             blank=True)
    password = CharField(db_column=b'userPassword', max_length=255)
    uidNumber = IntegerField(db_column=b'uidNumber', unique=True)
    gidNumber = IntegerField(db_column=b'gidNumber', default=2000)
    homeDirectory = CharField(db_column=b'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):
# "ou=groups,ou=unix,o=ILLYSE,l=Villeurbanne,st=RHA,c=FR"
#     base_dn = settings.LDAP_GROUP_BASE_DN
#     object_classes = [b'posixGroup']

#     gid = IntegerField(db_column=b'gidNumber', unique=True)
#     name = CharField(db_column=b'cn', max_length=200, primary_key=True)
#     members = ListField(db_column=b'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 = get_automatic_username(instance)


@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)
