import io
import calendar
import logging
from collections import OrderedDict
from datetime import date, timedelta
from operator import attrgetter

from dal import autocomplete
from two_factor.views import SetupView as TwoFactSetupView

from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import Group, Permission
from django.contrib.auth.views import PasswordChangeView as AuthPasswordChangeView
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.postgres.aggregates import ArrayAgg
from django.core.exceptions import PermissionDenied
from django.db.models import (
    Case, Count, DurationField, F, OuterRef, Prefetch, Q, Subquery, Sum, Value, When
)
from django.db.models.deletion import ProtectedError
from django.db.models.functions import Coalesce
from django.forms import HiddenInput, modelform_factory
from django.http import FileResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse, reverse_lazy
from django.utils.crypto import get_random_string
from django.views.defaults import page_not_found as django_page_not_found
from django.views.generic import (
    CreateView, DeleteView as DjangoDeleteView, DetailView, FormView, ListView, TemplateView,
    UpdateView, View
)

from .export import ExportReporting
from . import forms
from .models import (
    Bilan, CercleScolaire, Contact, Document, Famille, Formation, Intervenant, JournalAcces,
    LibellePrestation, Niveau, Personne, Prestation, Rapport, Role, Service, Suivi,
    Utilisateur
)
from .pdf import BilanPdf, CoordonneesFamillePdf, EvaluationPdf, JournalPdf, RapportPdf
from .utils import format_d_m_Y, is_ajax

logger = logging.getLogger('django')

MSG_READ_ONLY = "Vous n'avez pas les droits nécessaires pour modifier cette page"
MSG_ACCESS_DENIED = "Vous n’avez pas la permission d’accéder à cette page."


class CreateUpdateView(UpdateView):
    """Mix generic Create and Update views."""
    is_create = False

    def get_object(self):
        return None if self.is_create else super().get_object()


class DeleteView(DjangoDeleteView):
    """
    Nous ne suivons pas la méthode Django d'afficher une page de confirmation
    avant de supprimer un objet, mais nous avertissons avec un message JS avant
    de POSTer directement la suppression. Pour cela, nous autorisons uniquement
    la méthode POST.
    """
    http_method_names = ['post']


class JournalAccesMixin:
    """
    Classe Mixin pour journaliser les accès aux familles.
    """
    def get(self, *args, **kwargs):
        acces_ordinaire = self.famille.access_ok(self.request.user)
        if not acces_ordinaire and not self.request.GET.get('confirm') == '1':
            return render(
                self.request, 'aemo/acces_famille.html',
                {'url': self.request.path, 'famille': self.famille}
            )
        JournalAcces.objects.create(
            famille=self.famille,
            utilisateur=self.request.user,
            ordinaire=acces_ordinaire
        )
        return super().get(*args, **kwargs)


class BasePDFView(View):
    obj_class = None
    pdf_class = None
    produce_kwargs = {}

    def get_object(self):
        return get_object_or_404(self.obj_class, pk=self.kwargs[getattr(self, 'pk_url_kwarg', 'pk')])

    def get(self, request, *args, **kwargs):
        instance = self.get_object()
        temp = io.BytesIO()
        pdf = self.pdf_class(temp, instance, **self.produce_kwargs)
        pdf.produce()
        filename = pdf.get_filename()
        temp.seek(0)
        return FileResponse(temp, as_attachment=True, filename=filename)


class EquipeRequiredMixin:
    def dispatch(self, request, *args, **kwargs):
        self.famille = get_object_or_404(Famille, pk=kwargs['pk'])
        self.check_access(request)
        return super().dispatch(request, *args, **kwargs)

    def check_access(self, request):
        if not self.famille.can_view(request.user):
            raise PermissionDenied(MSG_ACCESS_DENIED)
        self.readonly = not self.famille.can_edit(request.user)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        if self.readonly:
            messages.info(self.request, MSG_READ_ONLY)
        context['can_edit'] = not self.readonly
        context['famille'] = self.famille
        return context


class CheckCanEditMixin:
    def dispatch(self, request, *args, **kwargs):
        if not self.get_object().can_edit(request.user):
            raise PermissionDenied(MSG_ACCESS_DENIED)
        return super().dispatch(request, *args, **kwargs)


class HomeView(TemplateView):
    template_name = 'index.html'


class SetupView(TwoFactSetupView):
    def get(self, request, *args, **kwargs):
        # The original view is short-circuiting to complete is a device exists.
        # We want to allow adding a second device if needed.
        return super(TwoFactSetupView, self).get(request, *args, **kwargs)


class PasswordChangeView(AuthPasswordChangeView):
    success_url = reverse_lazy('home')

    def form_valid(self, form):
        response = super().form_valid(form)
        messages.success(self.request, "Votre mot de passe a bien été modifié.")
        return response


class ContactCreateView(CreateView):
    template_name = 'aemo/contact_edit.html'
    model = Contact
    form_class = forms.ContactForm
    success_url = reverse_lazy('contact-list')
    action = 'Création'

    def form_valid(self, form):
        contact = form.save()
        for_pers = self.request.GET.get('forpers')
        if for_pers:
            pers = get_object_or_404(Personne, pk=for_pers)
            pers.reseaux.add(contact)
            return HttpResponseRedirect(reverse('personne-reseau-list', args=[pers.pk]))
        return HttpResponseRedirect(self.success_url)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({
            'role_form': forms.RoleForm(),
            'service_form': forms.ServiceForm(),
        })
        return context


class ContactListView(ListView):
    template_name = 'aemo/contact_list.html'
    model = Contact
    paginate_by = 20

    def get(self, request, *args, **kwargs):
        self.filter_form = forms.ContactFilterForm(data=self.request.GET or None)
        return super().get(request, *args, **kwargs)

    def get_queryset(self):
        contacts = Contact.objects.filter(est_actif=True).exclude(utilisateur__isnull=False).prefetch_related('roles')

        if self.filter_form.is_bound and self.filter_form.is_valid():
            contacts = self.filter_form.filter(contacts)
        return contacts

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({
            'form': self.filter_form
        })
        return context


class ContactUpdateView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView):
    permission_required = 'aemo.change_contact'
    template_name = 'aemo/contact_edit.html'
    model = Contact
    form_class = forms.ContactForm
    success_url = reverse_lazy('contact-list')
    success_message = 'Le contact %(nom)s %(prenom)s a bien été modifié'
    action = 'Modification'

    def delete_url(self):
        return reverse('contact-delete', args=[self.object.pk])


class ContactDeleteView(PermissionRequiredMixin, DeleteView):
    permission_required = 'aemo.delete_contact'
    model = Contact
    success_url = reverse_lazy('contact-list')

    def form_valid(self, form):
        self.object.est_actif = False
        self.object.save()
        messages.success(self.request, "Le contact %s a bien été archivé." % self.object)
        return HttpResponseRedirect(self.success_url)


class ContactAutocompleteView(autocomplete.Select2QuerySetView):
    ope = False

    def get_queryset(self):
        qs = Contact.membres_ope() if self.ope else Contact.objects.filter(est_actif=True)
        if self.q:
            qs = qs.filter(nom__istartswith=self.q)
        return qs


class ContactExterneAutocompleteView(autocomplete.Select2QuerySetView):

    def get_queryset(self):
        qs = Contact.objects.\
            filter(est_actif=True).\
            exclude(service__sigle__startswith='OPE').\
            exclude(service__sigle='CRNE')
        if self.q:
            qs = qs.filter(nom__istartswith=self.q)
        return qs


class ContactTestDoublon(View):
    def post(self, request, *args, **kwargs):
        nom = request.POST.get('nom')
        prenom = request.POST.get('prenom')
        contacts = Contact.objects.filter(nom=nom, prenom=prenom)
        data = ''
        if contacts.exists():
            data = [{'nom': c.nom, 'prenom': c.prenom} for c in contacts]
        return JsonResponse(data, safe=False)


class ServiceCreateView(PermissionRequiredMixin, CreateView):
    permission_required = 'aemo.add_service'
    template_name = 'aemo/service_edit.html'
    model = Service
    form_class = forms.ServiceForm
    success_url = reverse_lazy('service-list')
    action = 'Création'

    def form_valid(self, form):
        if is_ajax(self.request):
            service = form.save()
            return JsonResponse({'pk': service.pk, 'sigle': service.sigle})
        return super().form_valid(form)

    def form_invalid(self, form):
        if is_ajax(self.request):
            return JsonResponse({'error': form.errors.as_text()})
        return super().form_invalid(form)


class ServiceListView(ListView):
    template_name = 'aemo/service_list.html'
    model = Service

    def get_queryset(self):
        return Service.objects.exclude(sigle='FAMILLE')


class ServiceUpdateView(PermissionRequiredMixin, UpdateView):
    permission_required = 'aemo.change_service'
    template_name = 'aemo/service_edit.html'
    model = Service
    form_class = forms.ServiceForm
    success_url = reverse_lazy('service-list')
    action = 'Modification'

    def delete_url(self):
        return reverse('service-delete', args=[self.object.pk])


class ServiceDeleteView(PermissionRequiredMixin, DeleteView):
    permission_required = 'aemo.delete_service'
    model = Service
    success_url = reverse_lazy('service-list')


class FormationView(SuccessMessageMixin, UpdateView):
    template_name = 'aemo/formation_edit.html'
    model = Formation
    form_class = forms.FormationForm
    success_message = 'Les modifications ont été enregistrées avec succès'

    def get_object(self, queryset=None):
        personne = get_object_or_404(Personne, pk=self.kwargs['pk'])
        self.famille = get_object_or_404(Famille, pk=personne.famille_id)
        return personne.formation

    def get_form_kwargs(self):
        return {
            **super().get_form_kwargs(),
            'readonly': not self.famille.can_edit(self.request.user)
        }

    def get_success_url(self):
        return self.famille.edit_url

    def get_context_data(self, **kwargs):
        return {**super().get_context_data(**kwargs), 'famille': self.famille}


class PersonneBaseMixin:
    template_name = 'aemo/personne_edit.html'
    model = Personne
    form_class = forms.PersonneForm
    require_edit = False

    def dispatch(self, request, *args, **kwargs):
        self.famille = get_object_or_404(Famille, pk=kwargs['pk'])
        if not self.famille.can_view(request.user):
            raise PermissionDenied("Vous n’avez pas la permission d’accéder à cette page.")
        elif self.require_edit and not self.famille.can_edit(request.user):
            raise PermissionDenied("Vous n’avez pas la permission d’accéder à cette page.")
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = {**super().get_context_data(**kwargs), 'famille': self.famille}
        if self.object and self.object.role.nom == 'Enfant suivi':
            context['enfant'] = self.object
        return context

    def get_success_url(self):
        return self.famille.edit_url


class PersonneCreateView(PersonneBaseMixin, CreateView):
    action = 'Membre de la famille '
    require_edit = True

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['famille'] = self.famille
        kwargs['role'] = self.request.GET.get('role', None)
        return kwargs

    def form_valid(self, form):
        response = super().form_valid(form)
        if self.object.famille.archived_at:
            self.object.famille.archived_at = None
            self.object.famille.save()
            messages.info(self.request, "La famille a été sortie des archives")
        return response

    def get_success_url(self):
        return self.famille.redirect_after_personne_creation(self.object)


class PersonneUpdateView(SuccessMessageMixin, PersonneBaseMixin, UpdateView):
    pk_url_kwarg = 'obj_pk'
    action = 'Modification'
    success_message = 'Les modifications ont été enregistrées avec succès'

    def get_form_kwargs(self):
        return {**super().get_form_kwargs(), 'readonly': not self.famille.can_edit(self.request.user)}

    def delete_url(self):
        return reverse('personne-delete', args=[self.famille.pk, self.object.pk])


class PersonneDeleteView(PersonneBaseMixin, DeleteView):
    require_edit = True
    form_class = DeleteView.form_class

    def get_object(self, *args, **kwargs):
        pers = get_object_or_404(Personne, pk=self.kwargs['obj_pk'])
        if pers.role.nom == "Enfant suivi":
            raise PermissionDenied(
                "Un enfant suivi ne peut pas être directement supprimé. Si c’est "
                "vraiment ce que vous voulez, mettez-le d’abord comme Enfant non suivi."
            )
        return pers


class PersonneReseauView(DetailView):
    template_name = 'aemo/personne_reseau_list.html'
    model = Personne

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        famille = get_object_or_404(Famille, pk=self.object.famille_id)
        context.update({
            'famille': famille,
            'form': forms.ContactExterneAutocompleteForm(),
            'reseau': list(self.object.reseaux.all().order_by('nom')),
        })
        try:
            pediatre = self.object.suivienfant.pediatre
        except AttributeError:
            pass
        else:
            if pediatre:
                context['reseau'].append(pediatre)
        return context


class PersonneReseauAdd(View):
    def post(self, request, *args, **kwargs):
        pers = get_object_or_404(Personne, pk=kwargs['pk'])
        obj_pk = request.POST.getlist('contacts[]')
        for obj in obj_pk:
            contact = get_object_or_404(Contact, pk=obj)
            pers.reseaux.add(contact)
        return JsonResponse({'is_valid': True})


class PersonneReseauRemove(View):
    def post(self, request, *args, **kwargs):
        pers = get_object_or_404(Personne, pk=kwargs['pk'])
        contact = get_object_or_404(Contact, pk=kwargs['obj_pk'])
        pers.reseaux.remove(contact)
        return HttpResponseRedirect(reverse('personne-reseau-list', args=[pers.pk]))


class UtilisateurListView(ListView):
    template_name = 'aemo/utilisateur_list.html'
    model = Utilisateur
    is_active = True
    paginate_by = 50

    def get_queryset(self):
        return Utilisateur.objects.filter(date_desactivation__isnull=self.is_active)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({
            'active_users': self.is_active,
        })
        return context


class UtilisateurReactivateView(PermissionRequiredMixin, View):
    permission_required = 'aemo.delete_utilisateur'

    def post(self, request, *args, **kwargs):
        utilisateur = get_object_or_404(Utilisateur, pk=self.kwargs['pk'])
        utilisateur.is_active = True
        utilisateur.est_actif = True
        utilisateur.date_desactivation = None
        utilisateur.save()
        return HttpResponseRedirect(reverse('utilisateur-list'))


class UtilisateurCreateView(SuccessMessageMixin, PermissionRequiredMixin, CreateView):
    permission_required = 'aemo.add_utilisateur'

    template_name = 'aemo/utilisateur_edit.html'
    model = Utilisateur
    form_class = forms.UtilisateurForm
    success_url = reverse_lazy('utilisateur-list')
    success_message = "L’utilisateur «%(username)s» a été créé avec le mot de passe «%(password)s»."

    def form_valid(self, form):
        form.instance.first_name = form.cleaned_data['prenom']
        form.instance.last_name = form.cleaned_data['nom']
        form.instance.service, _ = Service.objects.get_or_create(sigle='CRNE', defaults={'sigle': 'CRNE'})
        pwd = get_random_string(length=10)
        form.instance.set_password(pwd)
        form.cleaned_data['password'] = pwd  # for success message
        return super().form_valid(form)


class UtilisateurUpdateView(PermissionRequiredMixin, UpdateView):
    permission_required = 'aemo.change_utilisateur'

    template_name = 'aemo/utilisateur_edit.html'
    model = Utilisateur
    form_class = forms.UtilisateurForm
    success_url = reverse_lazy('utilisateur-list')

    def delete_url(self):
        return reverse('utilisateur-delete', args=[self.object.pk])


class UtilisateurPasswordReinitView(PermissionRequiredMixin, View):
    permission_required = 'aemo.change_utilisateur'

    def post(self, request, *args, **kwargs):
        utilisateur = get_object_or_404(Utilisateur, pk=self.kwargs['pk'])
        pwd = get_random_string(length=10)
        utilisateur.set_password(pwd)
        utilisateur.save()
        messages.success(request, 'Le nouveau mot de passe de «%s» est «%s».' % (utilisateur, pwd))
        return HttpResponseRedirect(reverse('utilisateur-edit', kwargs=self.kwargs))


class UtilisateurOtpDeviceReinitView(PermissionRequiredMixin, View):
    permission_required = 'aemo.change_utilisateur'

    def post(self, request, *args, **kwargs):
        utilisateur = get_object_or_404(Utilisateur, pk=self.kwargs['pk'])
        if utilisateur.totpdevice_set.exists():
            utilisateur.totpdevice_set.all().delete()
            messages.success(request, 'Le mobile de «%s» a été réinitialisé.' % utilisateur)
        else:
            messages.error(request, 'Aucune configuration mobile trouvée pour «%s»' % utilisateur)
        return HttpResponseRedirect(reverse('utilisateur-edit', kwargs=self.kwargs))


class UtilisateurJournalAccesView(PermissionRequiredMixin, ListView):
    permission_required = 'aemo.change_utilisateur'
    template_name = 'aemo/utilisateur_journal.html'
    paginate_by = 50
    model = JournalAcces

    def get_queryset(self):
        return self.model.objects.filter(utilisateur_id=self.kwargs['pk']).order_by('-quand')

    def get_context_data(self, **kwargs):
        return {
            **super().get_context_data(**kwargs),
            'utilisateur': get_object_or_404(Utilisateur, pk=self.kwargs['pk']),
        }


class UtilisateurChargeDossierView(PermissionRequiredMixin, TemplateView):
    template_name = 'aemo/charge_utilisateurs.html'
    permission_required = 'aemo.change_utilisateur'

    def get_queryset(self):
        self.filter_form = forms.EquipeFilterForm(data=self.request.GET or None)

        qs_utilisateurs = Utilisateur.objects.filter(
            roles__nom__in=['Educ', 'Psy'], date_desactivation__isnull=True
        ).exclude(
            roles__nom__in=['Responsable/coordinateur']
        ).order_by('nom', 'prenom').distinct()
        if self.filter_form.is_bound and self.filter_form.is_valid():
            qs_utilisateurs = qs_utilisateurs.filter(equipe=self.filter_form.cleaned_data['equipe'])

        # cf requete similaire dans FamilleListView
        intervs = Intervenant.objects.filter(
            Q(suivi__date_fin_suivi__isnull=True) & (
                Q(date_fin__isnull=True) | Q(date_fin__gt=date.today())
            )
        ).values('intervenant').annotate(
            familles=ArrayAgg(
                "suivi__famille_id",
                distinct=True
            ),
            nbre_coord=Count(
                'id', filter=Q(suivi__heure_coord=True),
                distinct=True
            ),
            nbre_eval=Count('id', filter=(
                Q(suivi__date_debut_suivi__isnull=True)
            ), distinct=True),
            nbre_suivi=Count('id', filter=(
                Q(suivi__date_debut_suivi__isnull=False)
            ), distinct=True),
        ).order_by('intervenant__id').values(
            'intervenant__id', 'familles', 'nbre_coord', 'nbre_eval', 'nbre_suivi'
        )
        intervs_dict = {line['intervenant__id']: line for line in intervs}

        familles = dict(Famille.objects.filter(
            suivi__date_fin_suivi__isnull=True
        ).with_niveau_interv().values_list('pk', 'niveau_interv'))

        utilisateurs = []
        charge_map = {None: 0, 0: 0, 1: 1, 2: 2, 3: 5}
        for util in qs_utilisateurs:
            for key in 'familles', 'nbre_coord', 'nbre_eval', 'nbre_suivi':
                setattr(util, key, intervs_dict.get(util.pk, {}).get(key, [] if key == 'familles' else 0))
            util.charge = sum([charge_map[familles[fam_pk]] for fam_pk in util.familles])
            if util.taux_activite == 0 and util.charge is None:
                continue
            util.heures = util.charge + util.nbre_coord if util.charge is not None else 0
            util.charge_diff = util.charge_max - util.heures
            utilisateurs.append(util)
        return utilisateurs

    def get_context_data(self, **kwargs):
        return {
            **super().get_context_data(**kwargs),
            'utilisateurs': self.get_queryset(),
            'filter_form': self.filter_form,
        }


class UtilisateurDeleteView(PermissionRequiredMixin, DeleteView):
    """Archive, ne supprime pas réellement."""
    permission_required = 'aemo.change_utilisateur'

    model = Utilisateur
    success_url = reverse_lazy('utilisateur-list')

    def form_valid(self, form):
        self.object.is_active = False  # C'est ce flag qui empêche la connexion au système
        self.object.est_actif = False
        self.object.date_desactivation = date.today()
        self.object.save()
        return HttpResponseRedirect(self.success_url)


class UtilisateurAutocompleteView(autocomplete.Select2QuerySetView):
    def get_queryset(self):
        qs = Utilisateur.objects.filter(service__sigle='CRNE', date_desactivation__isnull=True)

        if self.q:
            qs = qs.filter(nom__istartswith=self.q)
        return qs


class CercleScolaireListView(ListView):
    template_name = 'aemo/cercle_scolaire_list.html'
    model = CercleScolaire


class CercleScolaireUpdateView(PermissionRequiredMixin, UpdateView):
    permission_required = 'aemo.change_cerclescolaire'
    template_name = 'aemo/cercle_scolaire_edit.html'
    model = CercleScolaire
    fields = '__all__'
    success_url = reverse_lazy('cercle-list')

    def delete_url(self):
        return reverse('cercle-delete', args=[self.object.pk])


class CercleScolaireCreateView(PermissionRequiredMixin, CreateView):
    permission_required = 'aemo.add_cerclescolaire'
    template_name = 'aemo/cercle_scolaire_edit.html'
    model = CercleScolaire
    fields = '__all__'
    success_url = reverse_lazy('cercle-list')


class CercleScolaireDeleteView(PermissionRequiredMixin, DeleteView):
    permission_required = 'aemo.delete_cerclescolaire'
    model = CercleScolaire
    success_url = reverse_lazy('cercle-list')


class RoleListView(ListView):
    template_name = 'aemo/role_list.html'
    model = Role

    def get_context_data(self, **kwargs):
        return {
            **super().get_context_data(**kwargs),
            'editeur_help': self.model._meta.get_field('est_editeur').help_text,
        }


class RoleUpdateView(PermissionRequiredMixin, UpdateView):
    permission_required = 'aemo.change_role'
    template_name = 'aemo/role_edit.html'
    model = Role
    fields = '__all__'
    success_url = reverse_lazy('role-list')

    def delete_url(self):
        if self.object.personne_set.count() == 0:
            return reverse('role-delete', args=[self.object.pk])


class RoleCreateView(PermissionRequiredMixin, CreateView):
    permission_required = 'aemo.add_role'
    template_name = 'aemo/role_edit.html'
    model = Role
    fields = '__all__'
    success_url = reverse_lazy('role-list')

    def form_valid(self, form):
        if is_ajax(self.request):
            role = form.save()
            return JsonResponse({'pk': role.pk, 'nom': role.nom})
        return super().form_valid(form)

    def form_invalid(self, form):
        if is_ajax(self.request):
            return JsonResponse({'error': form.errors.as_text()})
        return super().form_invalid(form)


class RoleDeleteView(PermissionRequiredMixin, DeleteView):
    permission_required = 'aemo.delete_role'
    model = Role
    success_url = reverse_lazy('role-list')

    def form_valid(self, form):
        try:
            return super().form_valid(form)
        except ProtectedError as e:
            # TODO: Il y a certainement mieux...
            messages.add_message(self.request, messages.ERROR, str(e), "Suppression impossible")
            return HttpResponseRedirect(reverse('role-list'))


class FamilleCreateView(CreateView):
    template_name = 'aemo/famille_edit.html'
    model = Famille
    form_class = forms.FamilleCreateForm
    action = 'Nouvelle famille'

    def dispatch(self, request, *args, **kwargs):
        if not request.user.has_perm('aemo.add_famille'):
            raise PermissionDenied(MSG_ACCESS_DENIED)
        return super().dispatch(request, *args, **kwargs)

    def form_valid(self, form):
        famille = form.save()
        return HttpResponseRedirect(reverse('famille-edit', args=[famille.pk]))


class FamilleUpdateView(EquipeRequiredMixin, SuccessMessageMixin, UpdateView):
    template_name = 'aemo/famille_edit.html'
    model = Famille
    famille_model = Famille
    form_class = forms.FamilleForm
    context_object_name = 'famille'
    title = 'Modification'
    archive_url = None
    success_message = 'La famille %(nom)s a bien été modifiée.'

    def get_success_url(self):
        return self.success_url or reverse('famille-edit', args=[self.object.pk])

    def get_form_kwargs(self):
        return {**super().get_form_kwargs(), 'readonly': self.readonly}

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({
            'enfant_suivi': Role.objects.get(nom='Enfant suivi'),
            'enfant_non_suivi': Role.objects.get(nom='Enfant non-suivi'),
        })
        return context


class FamilleListView(ListView):
    template_name = 'aemo/famille_list.html'
    model = Famille
    paginate_by = 20
    mode = 'normal'
    normal_cols = [
        ('Nom', 'nom'), ('Adresse', 'localite'), ('Réf. AEMO', 'referents'),
        ('Réf. OPE', 'referents_ope'), ('Statut', 'suivi'), ('Prior.', 'prioritaire'),
        ('Niveau', 'niveau_interv'),
    ]
    attente_cols = [
        ('Nom', 'nom'), ('Adresse', 'localite'), ('Région', 'region'),
        ('Réf. AEMO', 'referents'), ('Réf. OPE', 'referents_ope'), ('Prior.', 'prioritaire'),
        ('Demande', 'date_demande'), ('Évaluation', 'evaluation'),
    ]

    def get(self, request, *args, **kwargs):
        if self.mode == 'attente':
            self.paginate_by = None
            form_class = forms.EquipeFilterForm
        else:
            form_class = forms.FamilleFilterForm
        self.filter_form = form_class(data=self.request.GET or None)
        return super().get(request, *args, **kwargs)

    def get_queryset(self):
        familles = super().get_queryset().with_niveau_interv().filter(
            suivi__isnull=False, suivi__date_fin_suivi__isnull=True
        ).select_related(
            'suivi__ope_referent', 'suivi__ope_referent_2'
        ).prefetch_related(
            Prefetch(
                'suivi__intervenant_set',
                queryset=Intervenant.objects.actifs().select_related('intervenant', 'role')
            ),
            Prefetch(
                'niveaux', queryset=Niveau.objects.order_by('-date_debut')
            ),
            'suivi__intervenants', 'bilans', 'rapports'
        ).order_by('nom', 'npa')

        if self.mode == 'attente':
            familles = familles.filter(
                suivi__date_debut_suivi__isnull=True
            ).order_by(
                '-suivi__demande_prioritaire', 'suivi__date_demande',
            )
        if self.filter_form.is_bound and self.filter_form.is_valid():
            familles = self.filter_form.filter(familles)
        return familles

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        if self.mode == 'attente':
            cols = self.attente_cols
        else:
            cols = self.normal_cols
        # cf requete similaire dans UtilisateurChargeDossierView
        current_user = self.request.user
        ma_charge = current_user.interventions.filter(
            Q(intervenant__date_fin__isnull=True) | Q(intervenant__date_fin__gt=date.today())
        ).annotate(
            niveau_interv=Subquery(
                Niveau.objects.filter(
                    famille=OuterRef('famille_id')
                ).order_by('-date_debut').values('niveau_interv')[:1]
            ),
        ).aggregate(
            charge=Sum(Case(
                When(niveau_interv__lt=3, then='niveau_interv'),
                When(niveau_interv=3, then=5),
            ), filter=Q(date_fin_suivi__isnull=True)),
            nbre_coord=Count('heure_coord', filter=Q(heure_coord=True, date_fin_suivi__isnull=True)),
            nbre_eval=Count('id', filter=(
                Q(date_fin_suivi__isnull=True) & Q(date_debut_suivi__isnull=True)
            )),
            nbre_suivi=Count('id', filter=(
                Q(date_fin_suivi__isnull=True) & Q(date_debut_suivi__isnull=False)
            )),
        )

        context.update({
            'labels': [c[0] for c in cols],
            'col_keys': [c[1] for c in cols],
            'form': self.filter_form,
            'ma_charge': {
                'heures': ma_charge['charge'] + ma_charge['nbre_coord'],
                'nbre_eval': ma_charge['nbre_eval'],
                'nbre_suivi': ma_charge['nbre_suivi'],
                'charge_diff': current_user.charge_max - (ma_charge['charge'] + ma_charge['nbre_coord']),
            } if ma_charge['charge'] is not None else '',
        })
        return context


class FamilleArchivableListe(View):
    """Return all family ids which are archivable by the current user."""

    def get(self, request, *args, **kwargs):
        data = [
            famille.pk for famille in Famille.objects.filter(
                archived_at__isnull=True, suivi__date_fin_suivi__isnull=False
            )
            if famille.can_be_archived(request.user)
        ]
        return JsonResponse(data, safe=False)


class SuiviView(EquipeRequiredMixin, SuccessMessageMixin, JournalAccesMixin, UpdateView):
    template_name = 'aemo/suivi_edit.html'
    model = Suivi
    famille_model = Famille
    form_class = forms.SuiviForm
    success_message = 'Les modifications ont été enregistrées avec succès'

    def get_object(self, queryset=None):
        return get_object_or_404(self.model, famille__pk=self.kwargs['pk'])

    def get_form_kwargs(self):
        return {**super().get_form_kwargs(), 'readonly': self.readonly}

    def get_success_url(self):
        return self.object.famille.suivi_url

    def get_context_data(self, **kwargs):
        return {
            **super().get_context_data(**kwargs),
            'famille': self.object.famille,
            'intervenants': self.object.famille.interventions_actives().order_by('role'),
            'niveaux': self.object.famille.niveaux.all().annotate(
                date_fin_calc=Coalesce('date_fin', self.object.date_fin_suivi)
            ).order_by('pk'),
        }


class SuiviIntervenantCreate(EquipeRequiredMixin, CreateView):
    model = Intervenant
    form_class = forms.IntervenantForm
    template_name = 'aemo/form_in_popup.html'
    titre_page = "Ajout d’un intervenant"
    titre_formulaire = "Intervenant"

    def form_valid(self, form):
        form.instance.suivi = Suivi.objects.get(famille__pk=self.kwargs['pk'])
        return super().form_valid(form)

    def get_success_url(self):
        return reverse('famille-suivi', args=[self.kwargs['pk']])


class SuiviIntervenantUpdateView(EquipeRequiredMixin, UpdateView):
    model = Intervenant
    form_class = forms.IntervenantEditForm
    template_name = 'aemo/form_in_popup.html'
    titre_page = "Modification d’une intervention"
    titre_formulaire = "Intervenant"

    pk_url_kwarg = 'obj_pk'

    def get_success_url(self):
        return reverse('famille-suivi', args=[self.kwargs['pk']])


class SuivisTerminesListView(FamilleListView):
    template_name = 'aemo/suivis_termines_list.html'

    def get_queryset(self):
        familles = self.model.objects.filter(**{
            'suivi__date_fin_suivi__isnull': False,
            'archived_at__isnull': True
        }).select_related('suivi')

        if self.filter_form.is_bound and self.filter_form.is_valid():
            familles = self.filter_form.filter(familles)
        return familles


class AgendaSuiviView(EquipeRequiredMixin, SuccessMessageMixin, UpdateView):
    template_name = 'aemo/agenda_suivi.html'
    model = Suivi
    famille_model = Famille
    form_class = forms.AgendaForm
    success_message = 'Les modifications ont été enregistrées avec succès'

    def get_object(self, queryset=None):
        return get_object_or_404(self.model, famille__pk=self.kwargs['pk'])

    def get_form_kwargs(self):
        return {**super().get_form_kwargs(),
                'readonly': self.readonly,
                'destination': self.get_object().famille.destination,
                'request': self.request}

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        dfin = self.object.date_fin_suivi
        ddebut = self.object.date_debut_suivi
        context.update({
            'bilans_et_rapports': sorted(
                list(self.famille.bilans.all()) + list(self.famille.rapports.all()),
                key=attrgetter('date'),
            ),
            'mode': 'reactivation' if dfin else ('evaluation' if ddebut is None else 'suivi'),
            'interv_temporaires': self.object.intervenant_set.filter(date_fin__isnull=False),
            'niveaux': self.object.famille.niveaux.all().annotate(
                date_fin_calc=Coalesce('date_fin', self.object.date_fin_suivi)
            ).order_by('pk'),
        })
        return context

    def form_valid(self, form):
        response = super().form_valid(form)
        if (
            'date_debut_suivi' in form.changed_data and form.cleaned_data['date_debut_suivi'] and
            form.cleaned_data['date_debut_suivi'] < date.today()
        ):
            # Contrôle attribution des prestations (accompagnement) depuis début suivi.
            self.famille.prestations.filter(
                date_prestation__gte=form.cleaned_data['date_debut_suivi'], lib_prestation__code='aemo01'
            ).update(lib_prestation=LibellePrestation.objects.get(code='aemo02'))
        return response

    def get_success_url(self):
        if self.object.date_fin_suivi:
            return reverse('famille-list')
        return reverse('famille-agenda', args=[self.object.famille.pk])


class DemandeView(EquipeRequiredMixin, SuccessMessageMixin, UpdateView):
    template_name = 'aemo/demande_edit.html'
    model = Suivi
    famille_model = Famille
    form_class = forms.DemandeForm
    success_message = 'Les modifications ont été enregistrées avec succès'

    def get_form_kwargs(self):
        return {**super().get_form_kwargs(), 'readonly': self.readonly}

    def get_object(self, queryset=None):
        suivi = self.famille.suivi
        items = [
            "Selon le professionnel",
            "Selon les parents",
            "Selon l'enfant",
        ]
        if suivi.difficultes == '':
            suivi.difficultes = ''.join(
                ['<p><strong>{}</strong></p><p></p>'.format(item) for item in items if suivi.difficultes == '']
            )
        if suivi.aides == '':
            suivi.aides = ''.join(
                ['<strong>{}</strong></p><p></p>'.format(item) for item in items if suivi.aides == '']
            )
        if suivi.disponibilites == '':
            suivi.disponibilites = ''.join(
                ['<strong>{}</strong></p><p></p>'.format(item) for item in ['Parents', 'Enfants']
                 if suivi.disponibilites == '']
            )
        return suivi

    def get_success_url(self):
        return reverse('demande', args=[self.object.famille.pk])


class EvaluationPDFView(BasePDFView):
    obj_class = Famille
    pdf_class = EvaluationPdf


class CoordonneesPDFView(BasePDFView):
    obj_class = Famille
    pdf_class = CoordonneesFamillePdf


class JournalPDFView(BasePDFView):
    obj_class = Famille
    pdf_class = JournalPdf


class BilanPDFView(BasePDFView):
    obj_class = Bilan
    pdf_class = BilanPdf


class PrestationMenu(ListView):
    template_name = 'aemo/prestation_menu.html'
    model = Prestation
    paginate_by = 15
    context_object_name = 'familles'

    def get_queryset(self):
        user = self.request.user
        rdv_manques = dict(
            Famille.actives().annotate(
                rdv_manques=ArrayAgg(
                    'prestations__date_prestation',
                    filter=Q(prestations__manque=True),
                    ordering='prestations__date_prestation',
                    default=Value([])
                )
            ).values_list('pk', 'rdv_manques')
        )
        familles = Famille.actives(
        ).annotate(
            user_prest=Sum('prestations__duree',
                           filter=Q(**{'prestations__intervenants': user}))
        ).annotate(
            aemo1=Sum('prestations__duree',
                     filter=(Q(**{'prestations__lib_prestation__code': 'aemo01'}))
                     ) or None
        ).annotate(
            aemo2=Sum('prestations__duree',
                     filter=(Q(**{'prestations__lib_prestation__code': 'aemo02'}))
                     ) or None
        )
        for famille in familles:
            famille.rdv_manques = [format_d_m_Y(rdv) for rdv in rdv_manques[famille.pk]]
        return familles

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({
            'annee': date.today().year
        })
        return context


class PrestationBaseView:
    model = Prestation
    famille_model = Famille
    form_class = forms.PrestationForm
    template_name = 'aemo/prestation_edit.html'

    def dispatch(self, *args, **kwargs):
        self.famille = get_object_or_404(self.famille_model, pk=self.kwargs['pk']) if self.kwargs.get('pk') else None
        return super().dispatch(*args, **kwargs)

    def get_context_data(self, **kwargs):
        return {**super().get_context_data(**kwargs), 'famille': self.famille}

    def get_success_url(self):
        if self.famille:
            return reverse('journal-list', args=[self.famille.pk])
        return reverse('prestation-gen-list')


class PrestationListView(PrestationBaseView, ListView):
    template_name = 'aemo/prestation_list.html'
    model = Prestation
    paginate_by = 15
    context_object_name = 'prestations'

    def get(self, request, **kwargs):
        self.filter_form = forms.JournalAuteurFilterForm(famille=self.famille, data=request.GET or None)
        return super().get(request, **kwargs)

    def get_queryset(self):
        if self.famille:
            prestations = self.famille.prestations.all().order_by('-date_prestation')
            if self.filter_form.is_bound and self.filter_form.is_valid():
                prestations = self.filter_form.filter(prestations)
            return prestations
        return self.request.user.prestations.filter(famille=None)

    def get_context_data(self, **kwargs):
        return super().get_context_data(**kwargs, filter_form=self.filter_form)


class PrestationCreateView(PrestationBaseView, CreateView):
    def dispatch(self, request, *args, **kwargs):
        if not request.user.has_perm('aemo.add_prestation'):
            raise PermissionDenied("Vous n'avez pas les droits pour ajouter une prestation.")
        return super().dispatch(request, *args, **kwargs)

    def get_initial(self):
        initial_text = (
            "<p><b>Observations:\n"
            "(relations et interactions dans la famille / disponibilité des parents / "
            "réponses données aux besoins des enfants / …)</b></p><p></p>"
            "<p><b>Discussion (thème-s abordé-s) / activités:</b></p><p></p>"
            "<p><b>Particularités en termes de ressources et/ou de limites:</b></p><p></p>"
            "<p><b>Ressentis de l’intervenant-e:</b></p><p></p>"
            "<p><b>Objectif-s traité-s:</b></p><p></p>"
            "<p><b>Objectif-s à suivre:</b></p><p></p>"
        )
        initial = {
            **super().get_initial(),
            'intervenants': [self.request.user.pk],
            'texte': initial_text if self.famille else '',
        }
        return initial

    def get_form_kwargs(self):
        return {**super().get_form_kwargs(), 'famille': self.famille, 'user': self.request.user}

    def get_lib_prestation(self, date_prest):
        """
        Renvoie la prestation en fonction de la famille et du rôle de l'utilisateur connecté.
        """
        if self.famille is None:
            code = 'aemo03'  # Prestation générale
        elif self.famille.suivi.date_debut_suivi and date_prest >= self.famille.suivi.date_debut_suivi:
            code = 'aemo02'  # Accompagnement
        else:
            code = 'aemo01'  # Évaluation
        return LibellePrestation.objects.get(code=code)

    def form_valid(self, form):
        if self.famille:
            form.instance.famille = self.famille
        form.instance.auteur = self.request.user
        form.instance.lib_prestation = self.get_lib_prestation(form.cleaned_data['date_prestation'])
        if 'duree' not in form.cleaned_data:
            form.instance.duree = timedelta()
        return super().form_valid(form)


class PrestationUpdateView(CheckCanEditMixin, PrestationBaseView, UpdateView):
    pk_url_kwarg = 'obj_pk'

    def get_form_kwargs(self):
        return {**super().get_form_kwargs(), 'famille': self.famille, 'user': self.request.user}

    def delete_url(self):
        fam_id = self.famille.pk if self.famille else 0
        return reverse('prestation-delete', args=[fam_id, self.object.pk])


class PrestationDeleteView(PrestationBaseView, DeleteView):
    template_name = 'aemo/object_confirm_delete.html'
    pk_url_kwarg = 'obj_pk'
    form_class = DeleteView.form_class


class NiveauCreateUpdateView(CreateUpdateView):
    model = Niveau
    form_class = forms.NiveauForm
    template_name = 'aemo/form_in_popup.html'

    def dispatch(self, request, *args, **kwargs):
        self.is_create = 'pk' not in kwargs
        self.famille = get_object_or_404(Famille, pk=kwargs['obj_pk'])
        self.titre_page = f"Famille: {self.famille.nom}"
        self.titre_formulaire = "Nouveau niveau d'intervention" if self.is_create \
            else "Modification de l'enregistrement"
        return super().dispatch(request, *args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['famille'] = self.famille
        return kwargs

    def form_valid(self, form):
        if self.is_create:
            der_niv = self.famille.niveaux.last() if self.famille.niveaux.count() > 0 else None
            if der_niv:
                der_niv.date_fin = form.cleaned_data['date_debut'] - timedelta(days=1)
                der_niv.save()

            form.instance.famille = self.famille
            form.instance.date_fin = None
        return super().form_valid(form)

    def get_success_url(self):
        return reverse('famille-suivi', args=[self.famille.pk])

    def delete_url(self):
        return reverse('niveau-delete', args=[self.famille.pk, self.object.pk])


class NiveauDeleteView(DeleteView):
    template_name = 'aemo/object_confirm_delete.html'
    form_class = DeleteView.form_class
    model = Niveau

    def get_success_url(self):
        return reverse('famille-suivi', args=[self.kwargs['obj_pk']])


class DocumentBaseView:
    template_name = 'aemo/document_upload.html'
    titre_page = ''
    titre_formulaire = ''

    def dispatch(self, request, *args, **kwargs):
        self.famille = get_object_or_404(Famille, pk=kwargs['pk'])
        self.titre_page = self.titre_page.format(famille=self.famille.nom)
        return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        return self.famille.suivi_url


class DocumentUploadView(DocumentBaseView, CreateView):
    form_class = forms.DocumentUploadForm
    titre_page = 'Documents externes de la famille {famille}'
    titre_formulaire = 'Nouveau document'

    def get_initial(self):
        initial = super().get_initial()
        initial['famille'] = self.famille
        return initial

    def form_valid(self, form):
        form.instance.famille = self.famille
        form.instance.auteur = self.request.user
        return super().form_valid(form)


class DocumentDeleteView(DeleteView):
    model = Document
    pk_url_kwarg = 'doc_pk'

    def form_valid(self, form):
        response = super().form_valid(form)
        messages.success(self.request, "Le document a été supprimé avec succès")
        return response

    def get_success_url(self):
        return self.request.META.get('HTTP_REFERER') or self.object.famille.suivi_url



class BilanEditView(DocumentUploadView, CreateUpdateView):
    pk_url_kwarg = 'obj_pk'
    titre_page = 'Bilan pour la famille {famille}'
    titre_formulaire = 'Nouveau bilan'
    model = Bilan
    form_class = forms.BilanForm

    def get_form_class(self):
        if self.form_class:
            return self.form_class
        return modelform_factory(
            model=self.model,
            exclude=['famille', 'auteur'],
            field_classes={
                'objectifs': forms.RichTextField,
                'rythme': forms.RichTextField,
            },
            widgets={
                'famille': HiddenInput,
                'date': forms.PickDateWidget,
            },
            labels={'fichier': ''},
        )

    def get_initial(self):
        initial = super().get_initial()
        if self.is_create:
            initial['objectifs'] = (
                "<p><b>Besoin de l’enfant</b></p><p></p>\n"
                "<p><b>Objectifs de la famille</b></p><p></p>\n"
                "<p><b>Moyens</b></p><p></p>\n"
                "<p><b>Critères</b></p><p></p>"
            )
        return initial

    def get_success_url(self):
        return reverse('famille-agenda', args=[self.famille.pk])


class BilanDetailView(DetailView):
    model = Bilan
    pk_url_kwarg = 'obj_pk'
    template_name = 'aemo/bilan.html'

    def get_context_data(self, **kwargs):
        meta = self.model._meta
        data_fields = [
            f.name for f in meta.get_fields()
            if f.name not in ['id', 'date', 'famille', 'auteur', 'fichier', 'phase', 'sig_famille', 'sig_interv']
        ]
        return {
            **super().get_context_data(**kwargs),
            'famille': self.object.famille,
            'data': [(meta.get_field(f).verbose_name, getattr(self.object, f)) for f in data_fields]
        }


class BilanDeleteView(DeleteView):
    model = Bilan
    pk_url_kwarg = 'obj_pk'

    def dispatch(self, request, *args, **kwargs):
        self.famille = get_object_or_404(Famille, pk=kwargs['pk'])
        if not self.get_object().can_edit(request.user):
            raise PermissionDenied("Vous n'avez pas les droits de supprimer ce bilan.")
        return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        return reverse('famille-agenda', args=[self.famille.pk])


class BaseRapportView:
    model = Rapport

    def dispatch(self, request, *args, **kwargs):
        self.famille = get_object_or_404(Famille, pk=kwargs['pk'])
        return super().dispatch(request, *args, **kwargs)


class RapportCreateView(DocumentUploadView, BaseRapportView, CreateView):
    form_class = forms.RapportEditForm
    template_name = 'aemo/rapport_edit.html'

    def get_form_class(self):
        return modelform_factory(self.model, form=self.form_class)

    def get_success_url(self):
        return reverse('famille-agenda', args=[self.famille.pk])


class RapportDisplayView(BaseRapportView, DetailView):
    pk_url_kwarg = 'obj_pk'
    template_name = 'aemo/rapport.html'

    def get_context_data(self, **kwargs):
        meta = self.model._meta
        fields = ['situation', 'observations',  'projet']
        return {
            **super().get_context_data(**kwargs),
            'famille': self.object.famille,
            'rapport': self.object,
            'intervenants': ', '.join([i.nom_prenom for i in self.object.intervenants()]),
            'enfants': '\n'.join(
                [f"{enfant.nom_prenom} (*{format_d_m_Y(enfant.date_naissance)})"
                 for enfant in self.object.famille.membres_suivis()]
            ),
            'data': [
                (meta.get_field(f).verbose_name, getattr(self.object, f)) for f in fields
                if getattr(self.object, f) or f in ['situation', 'projet']
            ]
        }


class RapportUpdateView(DocumentUploadView, BaseRapportView, CreateUpdateView):
    pk_url_kwarg = 'obj_pk'
    form_class = forms.RapportEditForm
    template_name = 'aemo/rapport_edit.html'

    def get_form_class(self):
        return modelform_factory(self.model, form=self.form_class)

    def get_form_kwargs(self):
        return {
            **super().get_form_kwargs(),
            'user': self.request.user,
        }

    def get_success_url(self):
        return reverse('famille-agenda', args=[self.famille.pk])


class RapportDeleteView(BaseRapportView, DeleteView):
    model = Rapport
    pk_url_kwarg = 'obj_pk'

    def form_valid(self, form):
        if not self.object.can_edit(self.request.user):
            raise PermissionDenied("Vous n'avez pas le droit de supprimer ce résumé.")
        return super().form_valid(form)

    def get_success_url(self):
        return reverse('famille-agenda', args=[self.famille.pk])


class RapportPDFView(BaseRapportView, BasePDFView):
    pdf_class = RapportPdf
    pk_url_kwarg = 'obj_pk'

    @property
    def obj_class(self):
        return self.model


class FamilleAdresseChangeView(UpdateView):
    template_name = 'aemo/famille_adresse.html'
    form_class = forms.FamilleAdresseForm
    context_object_name = 'famille'

    def dispatch(self, request, *args, **kwargs):
        if not self.get_object().can_edit(request.user):
            raise PermissionDenied("Vous n’avez pas la permission d’accéder à cette page.")
        return super().dispatch(request, *args, **kwargs)

    def get_object(self):
        return get_object_or_404(Famille, pk=self.kwargs['pk'])

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['famille'] = self.get_object()
        return kwargs

    def form_valid(self, form):
        famille = form.save()
        for membre_id in form.cleaned_data['membres']:
            membre = get_object_or_404(Personne, pk=membre_id)
            membre.rue = famille.rue
            membre.npa = famille.npa
            membre.localite = famille.localite
            membre.save()
        return HttpResponseRedirect(famille.edit_url)


class FamilleAutoCompleteView(autocomplete.Select2QuerySetView):
    def get_queryset(self):
        qs = Famille.objects.filter(archived_at__isnull=True)
        if self.q:
            qs = qs.filter(nom__icontains=self.q)
        return qs.prefetch_related('membres')

    def get_result_label(self, result):
        enfants = ", ".join(enf.prenom for enf in result.membres_suivis())
        label = f'{result.nom}, {result.npa} {result.localite}'
        if enfants:
            label += f" ({enfants})"
        return label


class PermissionOverview(TemplateView):
    template_name = 'aemo/permissions.html'
    perm_map = {'add': 'création', 'change': 'modification', 'delete': 'suppression', 'view': 'affichage'}

    def get_context_data(self, **kwargs):
        def verbose(perm):
            return self.perm_map.get(perm.codename.split('_')[0], perm.codename)

        context = super().get_context_data(**kwargs)
        groups = Group.objects.all().prefetch_related('permissions')
        grp_perms = {gr.name: [perm.codename for perm in gr.permissions.all()] for gr in groups}
        objects = [CercleScolaire, Contact, Role, Service, Utilisateur]
        all_perms = Permission.objects.filter(
            content_type__app_label__in=['aemo'],
            content_type__model__in=[o.__name__.lower() for o in objects]
        )

        def perms_for_model(model):
            return [
                (perm.codename, verbose(perm)) for perm in all_perms
                if perm.content_type.model == model.__name__.lower()
            ]

        perm_groups = OrderedDict()
        # {'Contact': [('view_contact', 'affichage), ('change_contact', 'modification'), ...]}
        for obj in objects:
            perm_groups[obj._meta.verbose_name] = perms_for_model(obj)
        perm_groups['AEMO'] = list(Suivi._meta.permissions)
        perm_groups['AEMO'].extend([('delete_famille', 'Supprimer une famille')])
        context.update({
            'groups': groups,
            'grp_perms': grp_perms,
            'perms_by_categ': perm_groups,
        })
        return context


class ExportPrestationView(PermissionRequiredMixin, FormView):
    permission_required = 'aemo.export_stats'
    template_name = 'aemo/export.html'
    form_class = forms.MonthSelectionForm
    filename = 'aemo_reporting_{}'.format(date.strftime(date.today(), '%Y-%m-%d'))

    def _prepare_query(self, query, prest_gen):
        """Return a list of families, sorted by prest and name."""
        return query.annotate(
            prest_gen=Value(prest_gen, output_field=DurationField())
        ).order_by('nom')

    def get_initial(self):
        initial = super().get_initial()
        initial.update({
            'mois': date.today().month - 1 if date.today().month > 1 else 12,
            'annee': date.today().year if date.today().month > 1 else date.today().year - 1,
        })
        return initial

    def form_valid(self, form):
        mois = int(form.cleaned_data['mois'])
        annee = int(form.cleaned_data['annee'])
        debut_mois = date(annee, mois, 1)
        fin_mois = date(annee, mois, calendar.monthrange(annee, mois)[1])

        export = ExportReporting()
        familles_en_cours = Famille.suivis_en_cours(debut_mois, fin_mois)
        # Considérer au min. 1 enfant par famille, car les familles sans enfant
        # suivi occupent aussi une ligne de la stat des familles en cours.
        num_enfants = sum(
            [len(fam.membres_suivis()) or 1 for fam in familles_en_cours]
        )
        # Calcul des prestations générales réparties sur chaque enfant suivi
        # Renvoie une durée (timedelta()) par enfant suivi
        prest_gen = Prestation.objects.filter(
            date_prestation__month=mois,
            date_prestation__year=annee,
            famille=None
        ).annotate(
            num_util=Count('intervenants')
        ).aggregate(
            total=Sum(F('duree') * F('num_util'), output_field=DurationField())
        )['total'] or timedelta()
        logger.info("Total heures prest. gén. du mois: %s", prest_gen.total_seconds() / 3600)
        prest_gen_par_enfant = prest_gen / (num_enfants or 1)

        # Demandes en cours
        export.produce_suivis(
            'En_cours_{}'.format(debut_mois.strftime("%m.%Y")),
            self._prepare_query(familles_en_cours, prest_gen_par_enfant),
            debut_mois
        )

        #  Nouvelles demandes
        familles_nouvelles = Famille.suivis_nouveaux(annee, mois)
        export.produce_nouveaux(
            "Nouveaux_{}".format(debut_mois.strftime("%m.%Y")),
            self._prepare_query(familles_nouvelles, prest_gen_par_enfant),
        )

        #  Fins de suivis
        familles_terminees = Famille.suivis_termines(annee, mois)
        export.produce_termines(
            "Terminés_{}".format(debut_mois.strftime("%m.%Y")),
            self._prepare_query(familles_terminees, prest_gen_par_enfant),
        )
        export.produce_totaux("Totaux heures du mois")

        return export.get_http_response(
            'aemo_reporting_{}.xlsx'.format(date.strftime(debut_mois, '%m_%Y'))
        )


class BasePrestationGeneraleEtPersonnelle(ListView):

    def dispatch(self, request, *args, **kwargs):
        str_curdate = request.GET.get('date', None)
        if str_curdate:
            cur_year = int(str_curdate[-4:])
            cur_month = int(str_curdate[:2])
        else:
            today = date.today()
            cur_year = today.year
            cur_month = today.month
        self.dfrom = date(cur_year, cur_month, 1)
        self.dto = self.dfrom + timedelta(days=-1 + calendar.monthrange(self.dfrom.year, self.dfrom.month)[1])
        return super().dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        prev_month = self.dfrom - timedelta(days=2)
        next_month = self.dfrom + timedelta(days=33)

        context.update({
            'current_date': self.dfrom,
            'prev_month': prev_month if prev_month.year > 2019 else None,
            'next_month': next_month if next_month.month <= date.today().month else None,
        })
        return context


class PrestationPersonnelleListView(BasePrestationGeneraleEtPersonnelle):
    template_name = 'aemo/prestation_personnelle.html'

    def get_queryset(self):
        return self.request.user.prestations.filter(
            date_prestation__gte=self.dfrom, date_prestation__lte=self.dto
        )

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        context.update({
            'totaux': [(pp.code, self.get_queryset().filter(
                    lib_prestation__code=f"{pp.code}"
                ).aggregate(
                    tot=Sum('duree')
                )['tot']) for pp in LibellePrestation.objects.all()],
            'total_final': self.get_queryset().aggregate(tot=Sum('duree'))['tot'],
        })
        return context


class PrestationGeneraleListView(BasePrestationGeneraleEtPersonnelle):
    model = Prestation
    template_name = 'aemo/prestation_generale.html'

    def get_queryset(self):
        return super().get_queryset().filter(
            famille=None,
            date_prestation__gte=self.dfrom,
            date_prestation__lte=self.dto
        )

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({
            'prestations': self.get_queryset(),
        })
        return context


class FamilleReactivationView(View):

    def post(self, request, *args, **kwargs):
        famille = get_object_or_404(Famille, pk=kwargs['pk'])
        if famille.can_be_reactivated(request.user):
            famille.suivi.date_fin_suivi = None
            famille.suivi.motif_fin_suivi = ""
            famille.suivi.save()
            famille.destination = ''
            famille.save()
        return HttpResponseRedirect(reverse('famille-list'))


def page_not_found(request, *args, **kwargs):
    if not request.user.is_authenticated:
        kwargs['template_name'] = '404-public.html'
    return django_page_not_found(request, *args, **kwargs)
