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

from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

from django.shortcuts import render, get_object_or_404, redirect
from django.core import mail
from django.core.mail import EmailMessage
from django.forms import ModelForm
from django import forms
from django.forms.models import inlineformset_factory, BaseInlineFormSet
from django.contrib.auth.models import User
from django.views.generic import ListView
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.template import loader, Context
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.db.models import Min

from gestioncof.models import CofProfile
from gestioncof.petits_cours_models import *
from gestioncof.decorators import buro_required
from gestioncof.shared import lock_table, unlock_tables

from captcha.fields import ReCaptchaField

from datetime import datetime
import base64
import simplejson


def render_template(template_path, data):
    tmpl = loader.get_template(template_path)
    context = Context(data)
    return tmpl.render(context)


class DemandeListView(ListView):
    model = PetitCoursDemande
    template_name = "petits_cours_demandes_list.html"
    paginate_by = 20

    def get_queryset(self):
        return PetitCoursDemande.objects.order_by('traitee', '-id').all()

    @method_decorator(buro_required)
    def dispatch(self, *args, **kwargs):
        return super(DemandeListView, self).dispatch(*args, **kwargs)


@buro_required
def details(request, demande_id):
    demande = get_object_or_404(PetitCoursDemande, id=demande_id)
    attributions = PetitCoursAttribution.objects.filter(demande=demande).all()
    return render(request, "details_demande_petit_cours.html",
                  {"demande": demande,
                   "attributions": attributions})


def _get_attrib_counter(user, matiere):
    counter, created = PetitCoursAttributionCounter \
        .objects.get_or_create(user=user, matiere=matiere)
    if created:
        mincount = PetitCoursAttributionCounter.objects \
            .filter(matiere=matiere).exclude(user=user).all() \
            .aggregate(Min('count'))
        counter.count = mincount['count__min']
        counter.save()
    return counter


def _get_demande_candidates(demande, redo=False):
    for matiere in demande.matieres.all():
        candidates = PetitCoursAbility.objects.filter(matiere=matiere,
                                                      niveau=demande.niveau)
        candidates = candidates.filter(user__profile__is_cof=True,
                                       user__profile__petits_cours_accept=True)
        if demande.agrege_requis:
            candidates = candidates.filter(agrege=True)
        if redo:
            attributions = PetitCoursAttribution.objects \
                .filter(demande=demande, matiere=matiere).all()
            for attrib in attributions:
                candidates = candidates.exclude(user=attrib.user)
        candidates = candidates.order_by('?').select_related().all()
        yield (matiere, candidates)


@buro_required
def traitement(request, demande_id, redo=False):
    demande = get_object_or_404(PetitCoursDemande, id=demande_id)
    if demande.niveau == "other":
        return _traitement_other(request, demande, redo)
    if request.method == "POST":
        return _traitement_post(request, demande)
    proposals = {}
    proposed_for = {}
    unsatisfied = []
    attribdata = {}
    for matiere, candidates in _get_demande_candidates(demande, redo):
        if candidates:
            tuples = []
            for candidate in candidates:
                user = candidate.user
                tuples.append((candidate, _get_attrib_counter(user, matiere)))
            tuples = sorted(tuples, key=lambda c: c[1].count)
            candidates, _ = zip(*tuples)
            candidates = candidates[0:min(3, len(candidates))]
            attribdata[matiere.id] = []
            proposals[matiere] = []
            for candidate in candidates:
                user = candidate.user
                proposals[matiere].append(user)
                attribdata[matiere.id].append(user.id)
                if user not in proposed_for:
                    proposed_for[user] = [matiere]
                else:
                    proposed_for[user].append(matiere)
        else:
            unsatisfied.append(matiere)
    return _finalize_traitement(request, demande, proposals,
                                proposed_for, unsatisfied, attribdata, redo)


@buro_required
def retraitement(request, demande_id):
    return traitement(request, demande_id, redo=True)


def _finalize_traitement(request, demande, proposals, proposed_for,
                         unsatisfied, attribdata, redo=False, errors=None):
    proposals = proposals.items()
    proposed_for = proposed_for.items()
    attribdata = attribdata.items()
    proposed_mails = _generate_eleve_email(demande, proposed_for)
    mainmail = render_template("petits-cours-mail-demandeur.txt",
                               {"proposals": proposals,
                                "unsatisfied": unsatisfied,
                                "extra":
                                    '<textarea name="extra" '
                                    'style="width:99%; height: 90px;">'
                                    '</textarea>'
                                })
    return render(request, "traitement_demande_petit_cours.html",
                  {"demande": demande,
                   "unsatisfied": unsatisfied,
                   "proposals": proposals,
                   "proposed_for": proposed_for,
                   "proposed_mails": proposed_mails,
                   "mainmail": mainmail,
                   "attribdata":
                   base64.b64encode(simplejson.dumps(attribdata)),
                   "redo": redo,
                   "errors": errors,
                   })


def _generate_eleve_email(demande, proposed_for):
    proposed_mails = []
    for user, matieres in proposed_for:
        msg = render_template("petits-cours-mail-eleve.txt",
                              {"demande": demande, "matieres": matieres})
        proposed_mails.append((user, msg))
    return proposed_mails


def _traitement_other_preparing(request, demande):
    redo = "redo" in request.POST
    unsatisfied = []
    proposals = {}
    proposed_for = {}
    attribdata = {}
    errors = []
    for matiere, candidates in _get_demande_candidates(demande, redo):
        if candidates:
            candidates = dict([(candidate.user.id, candidate.user)
                               for candidate in candidates])
            attribdata[matiere.id] = []
            proposals[matiere] = []
            for choice_id in range(min(3, len(candidates))):
                choice = int(
                    request.POST["proposal-%d-%d" % (matiere.id, choice_id)])
                if choice == -1:
                    continue
                if choice not in candidates:
                    errors.append("Choix invalide pour la proposition %d"
                                  "en %s" % (choice_id + 1, matiere))
                    continue
                user = candidates[choice]
                if user in proposals[matiere]:
                    errors.append("La proposition %d en %s est un doublon"
                                  % (choice_id + 1, matiere))
                    continue
                proposals[matiere].append(user)
                attribdata[matiere.id].append(user.id)
                if user not in proposed_for:
                    proposed_for[user] = [matiere]
                else:
                    proposed_for[user].append(matiere)
            if not proposals[matiere]:
                errors.append("Aucune proposition pour %s" % (matiere,))
            elif len(proposals[matiere]) < 3:
                errors.append("Seulement %d proposition%s pour %s"
                              % (len(proposals[matiere]),
                                 "s" if len(proposals[matiere]) > 1 else "",
                                 matiere))
        else:
            unsatisfied.append(matiere)
    return _finalize_traitement(request, demande, proposals, proposed_for,
                                unsatisfied, attribdata, errors=errors)


def _traitement_other(request, demande, redo):
    if request.method == "POST":
        if "preparing" in request.POST:
            return _traitement_other_preparing(request, demande)
        else:
            return _traitement_post(request, demande)
    proposals = {}
    proposed_for = {}
    unsatisfied = []
    attribdata = {}
    for matiere, candidates in _get_demande_candidates(demande, redo):
        if candidates:
            tuples = []
            for candidate in candidates:
                user = candidate.user
                tuples.append((candidate, _get_attrib_counter(user, matiere)))
            tuples = sorted(tuples, key=lambda c: c[1].count)
            candidates, _ = zip(*tuples)
            attribdata[matiere.id] = []
            proposals[matiere] = []
            for candidate in candidates:
                user = candidate.user
                proposals[matiere].append(user)
                attribdata[matiere.id].append(user.id)
                if user not in proposed_for:
                    proposed_for[user] = [matiere]
                else:
                    proposed_for[user].append(matiere)
        else:
            unsatisfied.append(matiere)
    proposals = proposals.items()
    proposed_for = proposed_for.items()
    return render(request, "traitement_demande_petit_cours_autre_niveau.html",
                  {"demande": demande,
                   "unsatisfied": unsatisfied,
                   "proposals": proposals,
                   "proposed_for": proposed_for,
                   })


def _traitement_post(request, demande):
    proposals = {}
    proposed_for = {}
    unsatisfied = []
    extra = request.POST["extra"].strip()
    redo = "redo" in request.POST
    attribdata = request.POST["attribdata"]
    attribdata = dict(simplejson.loads(base64.b64decode(attribdata)))
    for matiere in demande.matieres.all():
        if matiere.id not in attribdata:
            unsatisfied.append(matiere)
        else:
            proposals[matiere] = []
            for user_id in attribdata[matiere.id]:
                user = User.objects.get(pk=user_id)
                proposals[matiere].append(user)
                if user not in proposed_for:
                    proposed_for[user] = [matiere]
                else:
                    proposed_for[user].append(matiere)
    proposals_list = proposals.items()
    proposed_for = proposed_for.items()
    proposed_mails = _generate_eleve_email(demande, proposed_for)
    mainmail = render_template("petits-cours-mail-demandeur.txt",
                               {"proposals": proposals_list,
                                "unsatisfied": unsatisfied,
                                "extra": extra})
    frommail = settings.PETITS_COURS_FROM
    bccaddress = settings.PETITS_COURS_BCC
    replyto = settings.PETITS_COURS_REPLYTO
    mails_to_send = []
    for (user, msg) in proposed_mails:
        msg = EmailMessage("Petits cours ENS par le COF", msg,
                           frommail, [user.email],
                           [bccaddress], headers={'Reply-To': replyto})
        mails_to_send.append(msg)
    mails_to_send.append(EmailMessage("Cours particuliers ENS", mainmail,
                                      frommail, [demande.email],
                                      [bccaddress],
                                      headers={'Reply-To': replyto}))
    connection = mail.get_connection(fail_silently=True)
    connection.send_messages(mails_to_send)
    lock_table(PetitCoursAttributionCounter, PetitCoursAttribution, User)
    for matiere in proposals:
        for rank, user in enumerate(proposals[matiere]):
            counter = PetitCoursAttributionCounter.objects.get(user=user,
                                                               matiere=matiere)
            counter.count += 1
            counter.save()
            attrib = PetitCoursAttribution(user=user, matiere=matiere,
                                           demande=demande, rank=rank + 1)
            attrib.save()
    unlock_tables()
    demande.traitee = True
    demande.traitee_par = request.user
    demande.processed = datetime.now()
    demande.save()
    return render(request, "traitement_demande_petit_cours_success.html",
                  {"demande": demande,
                   "redo": redo,
                   })


class BaseMatieresFormSet(BaseInlineFormSet):
    def clean(self):
        super(BaseMatieresFormSet, self).clean()
        if any(self.errors):
            # Don't bother validating the formset unless each form is
            # valid on its own
            return
        matieres = []
        for i in range(0, self.total_form_count()):
            form = self.forms[i]
            if not form.cleaned_data:
                continue
            matiere = form.cleaned_data['matiere']
            niveau = form.cleaned_data['niveau']
            delete = form.cleaned_data['DELETE']
            if not delete and (matiere, niveau) in matieres:
                raise forms.ValidationError(
                    "Vous ne pouvez pas vous inscrire deux fois pour la "
                    "même matiere avec le même niveau.")
            matieres.append((matiere, niveau))


@login_required
def inscription(request):
    profile, created = CofProfile.objects.get_or_create(user=request.user)
    if not profile.is_cof:
        return redirect("cof-denied")
    MatieresFormSet = inlineformset_factory(User, PetitCoursAbility,
                                            fields=("matiere", "niveau",
                                                    "agrege",),
                                            formset=BaseMatieresFormSet)
    success = False
    if request.method == "POST":
        formset = MatieresFormSet(request.POST, instance=request.user)
        if formset.is_valid():
            formset.save()
            profile.petits_cours_accept = "receive_proposals" in request.POST
            profile.petits_cours_remarques = request.POST["remarques"]
            profile.save()
            lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User,
                       PetitCoursSubject)
            abilities = PetitCoursAbility.objects \
                .filter(user=request.user).all()
            for ability in abilities:
                counter = _get_attrib_counter(ability.user, ability.matiere)
            unlock_tables()
            success = True
            formset = MatieresFormSet(instance=request.user)
    else:
        formset = MatieresFormSet(instance=request.user)
    return render(request, "inscription-petit-cours.html",
                  {"formset": formset, "success": success,
                   "receive_proposals": profile.petits_cours_accept,
                   "remarques": profile.petits_cours_remarques})


class DemandeForm(ModelForm):
    captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'})

    def __init__(self, *args, **kwargs):
        super(DemandeForm, self).__init__(*args, **kwargs)
        self.fields['matieres'].help_text = ''

    class Meta:
        model = PetitCoursDemande
        fields = ('name', 'email', 'phone', 'quand', 'freq', 'lieu',
                  'matieres', 'agrege_requis', 'niveau', 'remarques')
        widgets = {'matieres': forms.CheckboxSelectMultiple}


@csrf_exempt
def demande(request):
    success = False
    if request.method == "POST":
        form = DemandeForm(request.POST)
        if form.is_valid():
            form.save()
            success = True
    else:
        form = DemandeForm()
    return render(request, "demande-petit-cours.html", {"form": form,
                                                        "success": success})


@csrf_exempt
def demande_raw(request):
    success = False
    if request.method == "POST":
        form = DemandeForm(request.POST)
        if form.is_valid():
            form.save()
            success = True
    else:
        form = DemandeForm()
    return render(request, "demande-petit-cours-raw.html",
                  {"form": form, "success": success})
