import json

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core import mail
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect, render
from django.template import loader
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import DetailView, ListView

from gestioncof.decorators import buro_required
from gestioncof.models import CofProfile
from petitscours.forms import DemandeForm, MatieresFormSet
from petitscours.models import (
    PetitCoursAbility,
    PetitCoursAttribution,
    PetitCoursAttributionCounter,
    PetitCoursDemande,
)


class DemandeListView(ListView):
    queryset = PetitCoursDemande.objects.prefetch_related("matieres").order_by(
        "traitee", "-id"
    )
    template_name = "petitscours/demande_list.html"
    paginate_by = 20


class DemandeDetailView(DetailView):
    model = PetitCoursDemande
    queryset = PetitCoursDemande.objects.prefetch_related(
        "petitcoursattribution_set", "matieres"
    )
    template_name = "petitscours/demande_detail.html"
    context_object_name = "demande"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        obj = self.object
        context["attributions"] = obj.petitcoursattribution_set.all()
        return context


@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, unsatisfied = demande.get_proposals(redo=redo, max_candidates=3)
    return _finalize_traitement(request, demande, proposals, unsatisfied, redo)


def _finalize_traitement(
    request, demande, proposals, unsatisfied, redo=False, errors=None
):
    attribdata = [
        (matiere.id, [user.id for user in users])
        for matiere, users in proposals.items()
    ]
    proposed_for = {}
    for matiere, users in proposals.items():
        for user in users:
            proposed_for.setdefault(user, []).append(matiere)

    proposed_mails = _generate_eleve_email(demande, proposed_for)
    mainmail = (
        "Cours particuliers ENS",
        loader.render_to_string(
            "petitscours/mails/demandeur.txt",
            context={
                "proposals": proposals.items(),
                "unsatisfied": unsatisfied,
                "extra": '<textarea name="extra" '
                'style="width:99%; height: 90px;">'
                "</textarea>",
            },
        ),
    )
    if errors is not None:
        for error in errors:
            messages.error(request, error)
    return render(
        request,
        "petitscours/traitement_demande.html",
        {
            "demande": demande,
            "unsatisfied": unsatisfied,
            "proposals": proposals.items(),
            "proposed_for": proposed_for.items(),
            "proposed_mails": proposed_mails,
            "mainmail": mainmail,
            "attribdata": json.dumps(attribdata),
            "redo": redo,
        },
    )


def _generate_eleve_email(demande, proposed_for):
    subject = "Petits cours ENS par le COF"
    return [
        (
            user,
            (
                subject,
                loader.render_to_string(
                    "petitscours/mails/eleve.txt",
                    context={"demande": demande, "matieres": matieres},
                ),
            ),
        )
        for user, matieres in proposed_for.items()
    ]


def _traitement_other_preparing(request, demande):
    redo = "redo" in request.POST
    unsatisfied = []
    proposals = {}
    errors = []
    for matiere, candidates in demande.get_candidates(redo):
        if candidates:
            candidates = dict(
                [(candidate.user.id, candidate.user) for candidate in candidates]
            )
            proposals[matiere] = []
            for choice_id in range(min(3, len(candidates))):
                choice = int(
                    request.POST["proposal-{:d}-{:d}".format(matiere.id, choice_id)]
                )
                if choice == -1:
                    continue
                if choice not in candidates:
                    errors.append(
                        "Choix invalide pour la proposition {:d}"
                        "en {!s}".format(choice_id + 1, matiere)
                    )
                    continue
                user = candidates[choice]
                if user in proposals[matiere]:
                    errors.append(
                        "La proposition {:d} en {!s} est un doublon".format(
                            choice_id + 1, matiere
                        )
                    )
                    continue
                proposals[matiere].append(user)
            if not proposals[matiere]:
                errors.append("Aucune proposition pour {!s}".format(matiere))
            elif len(proposals[matiere]) < 3:
                errors.append(
                    "Seulement {:d} proposition{:s} pour {!s}".format(
                        len(proposals[matiere]),
                        "s" if len(proposals[matiere]) > 1 else "",
                        matiere,
                    )
                )
        else:
            unsatisfied.append(matiere)
    return _finalize_traitement(request, demande, proposals, unsatisfied, 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, unsatisfied = demande.get_proposals(redo=redo)
    return render(
        request,
        "petitscours/traitement_demande_autre_niveau.html",
        {
            "demande": demande,
            "unsatisfied": unsatisfied,
            "proposals": proposals.items(),
        },
    )


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(json.loads(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)
    proposed_mails = _generate_eleve_email(demande, proposed_for)
    mainmail_object = "Cours particuliers ENS"
    mainmail_body = loader.render_to_string(
        "petitscours/mails/demandeur.txt",
        context={
            "proposals": proposals.items(),
            "unsatisfied": unsatisfied,
            "extra": extra,
        },
    )
    frommail = settings.MAIL_DATA["petits_cours"]["FROM"]
    bccaddress = settings.MAIL_DATA["petits_cours"]["BCC"]
    replyto = settings.MAIL_DATA["petits_cours"]["REPLYTO"]
    mails_to_send = []
    for (user, (mail_object, body)) in proposed_mails:
        msg = mail.EmailMessage(
            mail_object,
            body,
            frommail,
            [user.email],
            [bccaddress],
            headers={"Reply-To": replyto},
        )
        mails_to_send.append(msg)
    mails_to_send.append(
        mail.EmailMessage(
            mainmail_object,
            mainmail_body,
            frommail,
            [demande.email],
            [bccaddress],
            headers={"Reply-To": replyto},
        )
    )
    connection = mail.get_connection(fail_silently=False)
    connection.send_messages(mails_to_send)
    with transaction.atomic():
        for matiere, users in proposals.items():
            for rank, user in enumerate(users):
                # TODO(AD): Prefer PetitCoursAttributionCounter.get_uptodate()
                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()
    demande.traitee = True
    demande.traitee_par = request.user
    demande.processed = timezone.now()
    demande.save()
    return render(
        request,
        "petitscours/traitement_demande_success.html",
        {"demande": demande, "redo": redo},
    )


@login_required
def inscription(request):
    profile, created = CofProfile.objects.get_or_create(user=request.user)
    if not profile.is_cof:
        return redirect("cof-denied")
    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()
            with transaction.atomic():
                abilities = PetitCoursAbility.objects.filter(user=request.user).all()
                for ability in abilities:
                    PetitCoursAttributionCounter.get_uptodate(
                        ability.user, ability.matiere
                    )
            success = True
            formset = MatieresFormSet(instance=request.user)
    else:
        formset = MatieresFormSet(instance=request.user)
    return render(
        request,
        "petitscours/inscription.html",
        {
            "formset": formset,
            "success": success,
            "receive_proposals": profile.petits_cours_accept,
            "remarques": profile.petits_cours_remarques,
        },
    )


@csrf_exempt
def demande(request, *, raw: bool = False):
    success = False
    if request.method == "POST":
        form = DemandeForm(request.POST)
        if form.is_valid():
            form.save()
            success = True
    else:
        form = DemandeForm()
    template_name = "petitscours/demande.html"
    if raw:
        template_name = "petitscours/demande_raw.html"
    return render(request, template_name, {"form": form, "success": success})
