# -*- coding: utf-8 -*-

from __future__ import (absolute_import, division,
                        print_function, unicode_literals)
from builtins import *

from django.db import models
from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.validators import RegexValidator
from django.contrib.auth.models import User
from gestioncof.models import CofProfile
from django.utils.six.moves import reduce
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.db import transaction
from django.db.models import F
from django.core.cache import cache
from datetime import date, timedelta
import re
import hashlib

def choices_length(choices):
    return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)

def default_promo():
    now = date.today()
    return now.month <= 8 and now.year-1 or now.year

@python_2_unicode_compatible
class Account(models.Model):
    cofprofile = models.OneToOneField(
        CofProfile, on_delete = models.PROTECT,
        related_name = "account_kfet")
    trigramme = models.CharField(
        unique     = True,
        max_length = 3,
        validators = [RegexValidator(regex='^[^a-z]{3}$')],
        db_index   = True)
    balance = models.DecimalField(
        max_digits = 6, decimal_places = 2,
        default = 0)
    is_frozen = models.BooleanField(default = False)
    created_at = models.DateTimeField(auto_now_add = True, null = True)
    # Optional
    PROMO_CHOICES = [(r,r) for r in range(1980, date.today().year+1)]
    promo = models.IntegerField(
        choices = PROMO_CHOICES,
        blank = True, null = True, default = default_promo())
    nickname = models.CharField(
        max_length = 255,
        blank = True, default = "")
    password = models.CharField(
        max_length = 255,
        unique = True,
        blank = True, null = True, default = None)

    def __str__(self):
        return '%s (%s)' % (self.trigramme, self.name)

    # Propriétés pour accéder aux attributs de user et cofprofile et user
    @property
    def user(self):
        return self.cofprofile.user
    @property
    def username(self):
        return self.cofprofile.user.username
    @property
    def first_name(self):
        return self.cofprofile.user.first_name
    @property
    def last_name(self):
        return self.cofprofile.user.last_name
    @property
    def email(self):
        return self.cofprofile.user.email
    @property
    def departement(self):
        return self.cofprofile.departement
    @property
    def is_cof(self):
        return self.cofprofile.is_cof

    # Propriétés supplémentaires
    @property
    def real_balance(self):
        if (hasattr(self, 'negative')):
            return self.balance - self.negative.balance_offset
        return self.balance

    @property
    def name(self):
        return self.user.get_full_name()

    @property
    def is_cash(self):
        return self.trigramme == 'LIQ'

    @property
    def need_comment(self):
        return self.trigramme == '#13'

    @staticmethod
    def is_validandfree(trigramme):
        data = { 'is_valid' : False, 'is_free' : False }
        pattern = re.compile("^[^a-z]{3}$")
        data['is_valid'] = pattern.match(trigramme) and True or False
        try:
            account = Account.objects.get(trigramme=trigramme)
        except Account.DoesNotExist:
            data['is_free'] = True
        return data

    def perms_to_perform_operation(self, amount):
        overdraft_duration_max = Settings.OVERDRAFT_DURATION()
        overdraft_amount_max = Settings.OVERDRAFT_AMOUNT()
        perms = set()
        stop_ope = False
        # Checking is cash account
        if self.is_cash:
            # Yes, so no perms and no stop
            return set(), False
        if self.need_comment:
            perms.add('kfet.perform_commented_operations')
        # Checking is frozen account
        if self.is_frozen:
            perms.add('kfet.override_frozen_protection')
        new_balance = self.balance + amount
        if new_balance < 0 and amount < 0:
            # Retrieving overdraft amount limit
            if (hasattr(self, 'negative')
                and self.negative.authz_overdraft_amount is not None):
                overdraft_amount = - self.negative.authz_overdraft_amount
            else:
                overdraft_amount = - overdraft_amount_max
            # Retrieving overdraft datetime limit
            if (hasattr(self, 'negative')
                    and self.negative.authz_overdraft_until is not None):
                overdraft_until = self.negative.authz_overdraft_until
            elif hasattr(self, 'negative'):
                overdraft_until = \
                    self.negative.start + overdraft_duration_max
            else:
                overdraft_until = timezone.now() + overdraft_duration_max
            # Checking it doesn't break 1 rule
            if new_balance < overdraft_amount or timezone.now() > overdraft_until:
                stop_ope = True
            perms.add('kfet.perform_negative_operations')
        return perms, stop_ope

    # Surcharge Méthode save() avec gestions de User et CofProfile
    # Args:
    # - data : datas pour User et CofProfile
    # Action:
    # - Enregistre User, CofProfile à partir de "data"
    # - Enregistre Account
    def save(self, data = {}, *args, **kwargs):

        if self.pk and data:
            # Account update

            # Updating User with data
            user = self.user
            user.first_name = data.get("first_name", user.first_name)
            user.last_name  = data.get("last_name", user.last_name)
            user.email      = data.get("email", user.email)
            user.save()
            # Updating CofProfile with data
            cof = self.cofprofile
            cof.departement = data.get("departement", cof.departement)
            cof.save()
        elif data:
            # New account

            # Checking if user has already an account
            username = data.get("username")
            try:
                user = User.objects.get(username=username)
                if hasattr(user.profile, "account_kfet"):
                    trigramme = user.profile.account_kfet.trigramme
                    raise Account.UserHasAccount(trigramme)
            except User.DoesNotExist:
                pass

            # Creating or updating User instance
            (user, _) = User.objects.get_or_create(username=username)
            if "first_name" in data:
                user.first_name = data['first_name']
            if "last_name" in data:
                user.last_name = data['last_name']
            if "email" in data:
                user.email = data['email']
            user.save()
            # Creating or updating CofProfile instance
            (cof, _) = CofProfile.objects.get_or_create(user=user)
            if "login_clipper" in data:
                cof.login_clipper = data['login_clipper']
            if "departement" in data:
                cof.departement = data['departement']
            cof.save()
        if data:
            self.cofprofile = cof
        super(Account, self).save(*args, **kwargs)

    def change_pwd(self, pwd):
        pwd_sha256 = hashlib.sha256(pwd.encode('utf-8'))\
                            .hexdigest()
        self.password = pwd_sha256

    # Surcharge de delete
    # Pas de suppression possible
    # Cas à régler plus tard
    def delete(self, *args, **kwargs):
        pass

    class UserHasAccount(Exception):
        def __init__(self, trigramme):
            self.trigramme = trigramme

class AccountNegative(models.Model):
    account = models.OneToOneField(
        Account, on_delete = models.PROTECT,
        related_name = "negative")
    start = models.DateTimeField(
        blank = True, null = True, default = None)
    balance_offset = models.DecimalField(
        max_digits = 6, decimal_places = 2,
        blank = True, null = True, default = None)
    authz_overdraft_amount = models.DecimalField(
        max_digits = 6, decimal_places = 2,
        blank = True, null = True, default = None)
    authz_overdraft_until = models.DateTimeField(
        blank = True, null = True, default = None)
    comment = models.CharField(max_length = 255, blank = True)

@python_2_unicode_compatible
class Checkout(models.Model):
    created_by = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        related_name = "+")
    name = models.CharField(max_length = 45)
    valid_from = models.DateTimeField()
    valid_to   = models.DateTimeField()
    balance = models.DecimalField(
        max_digits = 6, decimal_places = 2,
        default = 0)
    is_protected = models.BooleanField(default = False)

    def get_absolute_url(self):
        return reverse('kfet.checkout.read', kwargs={'pk': self.pk})

    class Meta:
        ordering = ['-valid_to']

    def __str__(self):
        return self.name

class CheckoutTransfer(models.Model):
    from_checkout = models.ForeignKey(
        Checkout, on_delete = models.PROTECT,
        related_name = "transfers_from")
    to_checkout = models.ForeignKey(
        Checkout, on_delete = models.PROTECT,
        related_name = "transfers_to")
    amount = models.DecimalField(
        max_digits = 6, decimal_places = 2)

@python_2_unicode_compatible
class CheckoutStatement(models.Model):
    by = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        related_name = "+")
    checkout = models.ForeignKey(
        Checkout, on_delete = models.PROTECT,
        related_name = "statements")
    balance_old  = models.DecimalField(max_digits = 6, decimal_places = 2)
    balance_new  = models.DecimalField(max_digits = 6, decimal_places = 2)
    amount_taken = models.DecimalField(max_digits = 6, decimal_places = 2)
    amount_error = models.DecimalField(max_digits = 6, decimal_places = 2)
    at = models.DateTimeField(auto_now_add = True)
    not_count = models.BooleanField(default=False)

    taken_001 = models.PositiveSmallIntegerField(default=0)
    taken_002 = models.PositiveSmallIntegerField(default=0)
    taken_005 = models.PositiveSmallIntegerField(default=0)
    taken_01 = models.PositiveSmallIntegerField(default=0)
    taken_02 = models.PositiveSmallIntegerField(default=0)
    taken_05 = models.PositiveSmallIntegerField(default=0)
    taken_1 = models.PositiveSmallIntegerField(default=0)
    taken_2 = models.PositiveSmallIntegerField(default=0)
    taken_5 = models.PositiveSmallIntegerField(default=0)
    taken_10 = models.PositiveSmallIntegerField(default=0)
    taken_20 = models.PositiveSmallIntegerField(default=0)
    taken_50 = models.PositiveSmallIntegerField(default=0)
    taken_100 = models.PositiveSmallIntegerField(default=0)
    taken_200 = models.PositiveSmallIntegerField(default=0)
    taken_500 = models.PositiveSmallIntegerField(default=0)
    taken_cheque = models.DecimalField(default=0, max_digits=6, decimal_places=2)

    def __str__(self):
        return '%s %s' % (self.checkout, self.at)

    def save(self, *args, **kwargs):
        if not self.pk:
            checkout_id = self.checkout_id
            self.balance_old = (Checkout.objects
                .values_list('balance', flat=True).get(pk=checkout_id))
            if self.not_count:
                self.balance_new = self.balance_old - self.amount_taken
            self.amount_error = (
                    self.balance_new + self.amount_taken - self.balance_old)
            with transaction.atomic():
                Checkout.objects.filter(pk=checkout_id).update(balance=self.balance_new)
                super(CheckoutStatement, self).save(*args, **kwargs)
        else:
            self.amount_error = (
                self.balance_new + self.amount_taken - self.balance_old)
            # Si on modifie le dernier relevé d'une caisse et que la nouvelle
            # balance est modifiée alors on modifie la balance actuelle de la caisse
            last_statement = (CheckoutStatement.objects
                    .filter(checkout=self.checkout)
                    .order_by('at')
                    .last())
            if (last_statement.pk == self.pk
                    and last_statement.balance_new != self.balance_new):
                Checkout.objects.filter(pk=self.checkout_id).update(
                    balance=F('balance') - last_statement.balance_new + self.balance_new)
            super(CheckoutStatement, self).save(*args, **kwargs)

@python_2_unicode_compatible
class ArticleCategory(models.Model):
    name = models.CharField(max_length = 45)

    def __str__(self):
        return self.name

@python_2_unicode_compatible
class Article(models.Model):
    name     = models.CharField(max_length = 45)
    is_sold  = models.BooleanField(default = True)
    hidden   = models.BooleanField(default=False,
                                   help_text="Si oui, ne sera pas affiché "
                                             "au public ; par exemple "
                                             "sur la carte.")
    price    = models.DecimalField(
        max_digits = 6, decimal_places = 2,
        default = 0)
    stock    = models.IntegerField(default = 0)
    category = models.ForeignKey(
        ArticleCategory, on_delete = models.PROTECT,
        related_name = "articles")
    BOX_TYPE_CHOICES = (
        ("caisse", "caisse"),
        ("carton", "carton"),
        ("palette", "palette"),
        ("fût", "fût"),
    )
    box_type = models.CharField(
        choices    = BOX_TYPE_CHOICES,
        max_length = choices_length(BOX_TYPE_CHOICES),
        blank = True, null = True, default = None)
    box_capacity = models.PositiveSmallIntegerField(
        blank = True, null = True, default = None)

    def __str__(self):
        return '%s - %s' % (self.category.name, self.name)

    def get_absolute_url(self):
        return reverse('kfet.article.read', kwargs={'pk': self.pk})

class ArticleRule(models.Model):
    article_on = models.OneToOneField(
        Article, on_delete = models.PROTECT,
        related_name = "rule_on")
    article_to = models.OneToOneField(
        Article, on_delete = models.PROTECT,
        related_name = "rule_to")
    ratio = models.PositiveSmallIntegerField()

class Inventory(models.Model):
    articles = models.ManyToManyField(
        Article,
        through      = 'InventoryArticle',
        related_name = "inventories")
    by = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        related_name = "+")
    at = models.DateTimeField(auto_now_add = True)
    # Optional
    order = models.OneToOneField(
        'Order', on_delete = models.PROTECT,
        related_name = "inventory",
        blank = True, null = True, default = None)

    class Meta:
        ordering = ['-at']

class InventoryArticle(models.Model):
    inventory = models.ForeignKey(
        Inventory, on_delete = models.PROTECT)
    article = models.ForeignKey(
        Article, on_delete = models.PROTECT)
    stock_old   = models.IntegerField()
    stock_new   = models.IntegerField()
    stock_error = models.IntegerField(default = 0)

    def save(self, *args, **kwargs):
        # S'il s'agit d'un inventaire provenant d'une livraison, il n'y a pas
        # d'erreur
        if not self.inventory.order:
            self.stock_error = self.stock_new - self.stock_old
        super(InventoryArticle, self).save(*args, **kwargs)

@python_2_unicode_compatible
class Supplier(models.Model):
    articles = models.ManyToManyField(
        Article,
        through      = 'SupplierArticle',
        related_name = "suppliers")
    name = models.CharField(max_length = 45)
    address = models.TextField()
    email = models.EmailField()
    phone = models.CharField(max_length = 10)
    comment = models.TextField()

    def __str__(self):
        return self.name

class SupplierArticle(models.Model):
    supplier = models.ForeignKey(
        Supplier, on_delete = models.PROTECT)
    article = models.ForeignKey(
        Article, on_delete = models.PROTECT)
    at = models.DateTimeField(auto_now_add = True)
    price_HT = models.DecimalField(
        max_digits = 7, decimal_places = 4,
        blank = True, null = True, default = None)
    TVA = models.DecimalField(
        max_digits = 4, decimal_places = 2,
        blank = True, null = True, default = None)
    rights = models.DecimalField(
        max_digits = 7, decimal_places = 4,
        blank = True, null = True, default = None)

class Order(models.Model):
    supplier = models.ForeignKey(
        Supplier, on_delete = models.PROTECT,
        related_name = "orders")
    articles = models.ManyToManyField(
        Article,
        through        = "OrderArticle",
        related_name   = "orders")
    at = models.DateTimeField(auto_now_add = True)
    amount = models.DecimalField(
        max_digits = 6, decimal_places = 2, default = 0)

    class Meta:
        ordering = ['-at']

class OrderArticle(models.Model):
    order = models.ForeignKey(
        Order, on_delete = models.PROTECT)
    article = models.ForeignKey(
        Article, on_delete = models.PROTECT)
    quantity_ordered = models.IntegerField()
    quantity_received = models.IntegerField(default = 0)

class TransferGroup(models.Model):
    at = models.DateTimeField(auto_now_add = True)
    # Optional
    comment = models.CharField(
        max_length = 255,
        blank = True, default = "")
    valid_by = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        related_name = "+",
        blank = True, null = True, default = None)

class Transfer(models.Model):
    group = models.ForeignKey(
        TransferGroup, on_delete = models.PROTECT,
        related_name = "transfers")
    from_acc = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        related_name = "transfers_from")
    to_acc = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        related_name = "transfers_to")
    amount = models.DecimalField(max_digits = 6, decimal_places = 2)
    # Optional
    canceled_by = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        null = True, blank = True, default = None,
        related_name = "+")
    canceled_at = models.DateTimeField(
        null = True, blank = True, default = None)

class OperationGroup(models.Model):
    on_acc = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        related_name = "opesgroup")
    checkout = models.ForeignKey(
        Checkout, on_delete = models.PROTECT,
        related_name = "opesgroup")
    at = models.DateTimeField(auto_now_add = True)
    amount = models.DecimalField(
        max_digits = 6, decimal_places = 2,
        default = 0)
    is_cof = models.BooleanField(default = False)
    # Optional
    comment = models.CharField(
        max_length = 255,
        blank = True, default = "")
    valid_by = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        related_name = "+",
        blank = True, null = True, default = None)

    def __str__(self):
        return ', '.join(map(str, self.opes.all()))


class Operation(models.Model):
    PURCHASE = 'purchase'
    DEPOSIT  = 'deposit'
    WITHDRAW = 'withdraw'
    INITIAL = 'initial'

    TYPE_ORDER_CHOICES = (
        (PURCHASE, 'Achat'),
        (DEPOSIT, 'Charge'),
        (WITHDRAW, 'Retrait'),
        (INITIAL, 'Initial'),
    )

    group = models.ForeignKey(
        OperationGroup, on_delete = models.PROTECT,
        related_name = "opes")
    type = models.CharField(
        choices = TYPE_ORDER_CHOICES,
        max_length = choices_length(TYPE_ORDER_CHOICES))
    amount = models.DecimalField(
        max_digits = 6, decimal_places = 2,
        blank = True, default = 0)
    is_checkout = models.BooleanField(default = True)
    # Optional
    article = models.ForeignKey(
        Article, on_delete = models.PROTECT,
        related_name = "operations",
        blank = True, null = True, default = None)
    article_nb = models.PositiveSmallIntegerField(
        blank = True, null = True, default = None)
    canceled_by = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        related_name = "+",
        blank = True, null = True, default = None)
    canceled_at = models.DateTimeField(
        blank = True, null = True, default = None)
    addcost_for = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        related_name = "addcosts",
        blank = True, null = True, default = None)
    addcost_amount = models.DecimalField(
        max_digits = 6, decimal_places = 2,
        blank = True, null = True, default = None)

    def __str__(self):
        templates = {
                self.PURCHASE: "{nb} {article.name} ({amount}€)",
                self.DEPOSIT: "charge ({amount})",
                self.WITHDRAW: "retrait ({amount})",
                self.INITIAL: "initial ({amount})",
            }
        return templates[self.type].format(nb=self.article_nb,
                                           article=self.article,
                                           amount=self.amount)


class GlobalPermissions(models.Model):
    class Meta:
        managed = False
        permissions = (
            ('is_team', 'Is part of the team'),
            ('perform_deposit', 'Effectuer une charge'),
            ('perform_negative_operations',
                'Enregistrer des commandes en négatif'),
            ('override_frozen_protection', "Forcer le gel d'un compte"),
            ('cancel_old_operations', 'Annuler des commandes non récentes'),
            ('manage_perms', 'Gérer les permissions K-Fêt'),
            ('manage_addcosts', 'Gérer les majorations'),
            ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'),
            ('view_negs', 'Voir la liste des négatifs'),
            ('order_to_inventory', "Générer un inventaire à partir d'une commande"),
            ('edit_balance_account', "Modifier la balance d'un compte"),
            ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"),
            ('special_add_account', "Créer un compte avec une balance initiale")
        )

class Settings(models.Model):
    name = models.CharField(
        max_length = 45,
        unique = True,
        db_index = True)
    value_decimal = models.DecimalField(
        max_digits = 6, decimal_places = 2,
        blank = True, null = True, default = None)
    value_account = models.ForeignKey(
        Account, on_delete = models.PROTECT,
        blank = True, null = True, default = None)
    value_duration = models.DurationField(
        blank = True, null = True, default = None)

    @staticmethod
    def setting_inst(name):
        return Settings.objects.get(name=name)

    @staticmethod
    def SUBVENTION_COF():
        subvention_cof = cache.get('SUBVENTION_COF')
        if subvention_cof:
            return subvention_cof
        try:
            subvention_cof = Settings.setting_inst("SUBVENTION_COF").value_decimal
        except Settings.DoesNotExist:
            subvention_cof = 0
        cache.set('SUBVENTION_COF', subvention_cof)
        return subvention_cof

    @staticmethod
    def ADDCOST_AMOUNT():
        try:
            return Settings.setting_inst("ADDCOST_AMOUNT").value_decimal
        except Settings.DoesNotExist:
            return 0

    @staticmethod
    def ADDCOST_FOR():
        try:
            return Settings.setting_inst("ADDCOST_FOR").value_account
        except Settings.DoesNotExist:
            return None;

    @staticmethod
    def OVERDRAFT_DURATION():
        overdraft_duration = cache.get('OVERDRAFT_DURATION')
        if overdraft_duration:
            return overdraft_duration
        try:
            overdraft_duration = Settings.setting_inst("OVERDRAFT_DURATION").value_duration
        except Settings.DoesNotExist:
            overdraft_duration = timedelta()
        cache.set('OVERDRAFT_DURATION', overdraft_duration)
        return overdraft_duration

    @staticmethod
    def OVERDRAFT_AMOUNT():
        overdraft_amount = cache.get('OVERDRAFT_AMOUNT')
        if overdraft_amount:
            return overdraft_amount
        try:
            overdraft_amount = Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal
        except Settings.DoesNotExist:
            overdraft_amount = 0
        cache.set('OVERDRAFT_AMOUNT', overdraft_amount)
        return  overdraft_amount

    @staticmethod
    def CANCEL_DURATION():
        cancel_duration = cache.get('CANCEL_DURATION')
        if cancel_duration:
            return cancel_duration
        try:
            cancel_duration = Settings.setting_inst("CANCEL_DURATION").value_duration
        except Settings.DoesNotExist:
            cancel_duration = timedelta()
        cache.set('CANCEL_DURATION', cancel_duration)
        return cancel_duration

    @staticmethod
    def create_missing():
        s, created = Settings.objects.get_or_create(name='SUBVENTION_COF')
        if created:
            s.value_decimal = 25
            s.save()
        s, created = Settings.objects.get_or_create(name='ADDCOST_AMOUNT')
        if created:
            s.value_decimal = 0.5
            s.save()
        s, created = Settings.objects.get_or_create(name='ADDCOST_FOR')
        s, created = Settings.objects.get_or_create(name='OVERDRAFT_DURATION')
        if created:
            s.value_duration = timedelta(days=1) # 24h
            s.save()
        s, created = Settings.objects.get_or_create(name='OVERDRAFT_AMOUNT')
        if created:
            s.value_decimal = 20
            s.save()
        s, created = Settings.objects.get_or_create(name='CANCEL_DURATION')
        if created:
            s.value_duration = timedelta(minutes=5) # 5min
            s.save()

    @staticmethod
    def empty_cache():
        cache.delete_many([
            'SUBVENTION_COF', 'OVERDRAFT_DURATION', 'OVERDRAFT_AMOUNT',
            'CANCEL_DURATION', 'ADDCOST_AMOUNT', 'ADDCOST_FOR',
        ])

class GenericTeamToken(models.Model):
    token = models.CharField(max_length = 50, unique = True)