import calendar
from dataclasses import dataclass
from datetime import date, timedelta
from operator import attrgetter

from django import forms
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import (
    Case, Count, DurationField, ExpressionWrapper, F, IntegerField, OuterRef, Q,
    Subquery, Sum, When
)
from django.db.models.functions import Coalesce, TruncMonth
from django.utils.dates import MONTHS
from django.views.generic import TemplateView

from .forms import DateYearForm
from .models import Intervenant, LibellePrestation, Niveau, Personne, Prestation, Suivi, Utilisateur
from .utils import format_d_m_Y, format_duree
from common.choices import (
    MOTIF_DEMANDE_CHOICES, MOTIFS_FIN_SUIVI_CHOICES, PROVENANCE_DESTINATION_CHOICES,
    SERVICE_ORIENTEUR_CHOICES,
)
from .export import ExportStatistique


class DateLimitForm(forms.Form):
    YEAR_CHOICES = tuple(
        (str(y), str(y))
        for y in range(2020, date.today().year + (1 if date.today().month < 12 else 2))
    )
    start_month = forms.ChoiceField(choices=[(str(m), MONTHS[m]) for m in range(1, 13)])
    start_year = forms.ChoiceField(choices=YEAR_CHOICES)
    end_month = forms.ChoiceField(choices=[(str(m), MONTHS[m]) for m in range(1, 13)])
    end_year = forms.ChoiceField(choices=YEAR_CHOICES)

    def __init__(self, data, **kwargs):
        if not data:
            today = date.today()
            data = {
                'start_year': today.year, 'start_month': today.month,
                'end_year': today.year if today.month < 12 else today.year + 1,
                'end_month': (today.month + 1) if today.month < 12 else 1,
            }
        super().__init__(data, **kwargs)

    def clean(self):
        cleaned_data = super().clean()
        if not self.errors and self.start > self.end:
            raise forms.ValidationError("Les dates ne sont pas dans l’ordre.")
        return cleaned_data

    @property
    def start(self):
        return date(int(self.cleaned_data['start_year']), int(self.cleaned_data['start_month']), 1)

    @property
    def end(self):
        return date(
            int(self.cleaned_data['end_year']),
            int(self.cleaned_data['end_month']),
            calendar.monthrange(int(self.cleaned_data['end_year']), int(self.cleaned_data['end_month']))[1]
        )


@dataclass
class Month:
    year: int
    month: int

    def __str__(self):
        return f'{self.month:0>2}.{self.year}'

    def __lt__(self, other):
        return (self.year, self.month) < (other.year, other.month)

    def __hash__(self):
        return hash((self.year, self.month))

    def next(self):
        if self.month == 12:
            return Month(self.year + 1, 1)
        else:
            return Month(self.year, self.month + 1)

    @classmethod
    def from_date(cls, dt):
        return Month(dt.year, dt.month)

    def is_future(self):
        return date(self.year, self.month, 1) > date.today()


class StatsMixin:
    permission_required = 'aemo.export_stats'

    def get_months(self):
        """Return a list of tuples [(year, month), ...] from date_start to date_end."""
        months = [Month(self.date_start.year, self.date_start.month)]
        while True:
            next_m = months[-1].next()
            if next_m > Month(self.date_end.year, self.date_end.month):
                break
            months.append(next_m)
        return months

    def month_limits(self, month):
        """From (2020, 4), return (date(2020, 4, 1), date(2020, 5, 1))."""
        next_m = month.next()
        return (
            date(month.year, month.month, 1),
            date(next_m.year, next_m.month, 1)
        )

    @staticmethod
    def init_counters(names, months, default=0, total=0):
        """
        Create stat counters:
            {<counter_name>: {(2020, 3): 0, (2020, 4): 0, …, 'total': 0}, …}
        """
        counters = {}
        for count_name in names:
            counters[count_name] = {}
            for month in months:
                counters[count_name][month] = '-' if month.is_future() else default
            counters[count_name]['total'] = total
        return counters

    def get_stats(self, months):
        # Here subclasses produce stats to be merged in view context
        return {}

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        get_params = self.request.GET.copy()
        self.export_flag = get_params.pop('export', None)
        date_form = DateLimitForm(get_params)
        context['date_form'] = date_form
        if not date_form.is_valid():
            return context
        self.date_start = date_form.start
        self.date_end = date_form.end
        months = self.get_months()
        context.update({
            'date_form': date_form,
            'months': months,
        })
        context.update(self.get_stats(months))
        return context

    def export_as_openxml(self, context):
        export = ExportStatistique(col_widths=[50])
        export.fill_data(self.export_lines(context))
        return export.get_http_response(self.__class__.__name__.replace('View', '') + '.xlsx')

    def render_to_response(self, context, **response_kwargs):
        if self.export_flag:
            return self.export_as_openxml(context)
        else:
            return super().render_to_response(context, **response_kwargs)


class StatistiquesView(StatsMixin, PermissionRequiredMixin, TemplateView):
    template_name = 'statistiques/statistiques.html'
    labels = {
        'familles_total': 'Total familles (évaluation et/ou accomp.)',
        'enfants_total': 'Total enfants suivis (évaluation et/ou accomp.)',
        'familles_evaluees': 'Familles évaluées',
        'enfants_evalues': 'Enfants évalués',
        'enfants_evalues_non_suivis': 'Enfants non suivis de familles évaluées',
        'familles_eval_sans_suivi': 'Familles évaluées sans aboutir à un suivi',
        'familles_suivies': 'Familles suivies',
        'enfants_suivis': 'Enfants suivis',
        'enfants_suivis_non_suivis': 'Enfants non suivis de familles suivies',
        'familles_accueil': 'dont Familles d’accueil',
        'familles_connues': 'dont Familles déjà suivies',
        'prioritaires': 'Demandes prioritaires',
        'rdv_manques': 'Rendez-vous manqués',
        'duree_attente': 'Durée moyenne entre demande et début de suivi',
    }

    def suivi_stats(self, model, months):
        suivis_base = model.objects.annotate(
            date_fin=Coalesce('date_fin_suivi', date.today()),
            date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
        ).filter(
            date_debut__lte=self.date_end,
            date_fin__gte=self.date_start
        ).exclude(motif_fin_suivi='erreur')

        # Annotations pour Count("Enfant suivi"), Count("Enfant non-suivi"), duree_attente
        suivis = suivis_base.annotate(enf_suivis=Count(Case(
            When(famille__membres__role__nom="Enfant suivi", then=1),
            output_field=IntegerField(),
        )), enf_nonsuivis=Count(Case(
            When(famille__membres__role__nom="Enfant non-suivi", then=1),
            output_field=IntegerField(),
        )), duree_attente=F('date_debut_suivi') - F('date_demande'),
        ).select_related('famille')

        count_keys = [
            'familles_total', 'enfants_total',
            'familles_evaluees', 'enfants_evalues', 'enfants_evalues_non_suivis',
            'familles_eval_sans_suivi', 'familles_suivies', 'enfants_suivis',
            'enfants_suivis_non_suivis', 'familles_accueil', 'familles_connues',
            'prioritaires', 'rdv_manques',
        ]
        if not getattr(model, 'demande_prioritaire', False):
            count_keys.remove('prioritaires')
        self.counters = self.init_counters(count_keys, months)
        self.counters['duree_attente'] = {'familles': 0, 'total': timedelta(), 'moyenne': timedelta()}
        count_keys.append('duree_attente')
        for suivi in suivis:
            self.update_counters(suivi, months)
        if self.counters['duree_attente']['familles'] > 0:
            self.counters['duree_attente']['moyenne'] = (
                self.counters['duree_attente']['total'] / self.counters['duree_attente']['familles']
            )

        # Rendez-vous manqués
        rdv_manques = suivis_base.annotate(
            month=TruncMonth('famille__prestations__date_prestation'),
        ).values('month').annotate(
            rdv_manques=Count(
                'famille__prestations',
                filter=Q(**{'famille__prestations__manque': True})
            )
        )
        for line in rdv_manques:
            if not line['month']:
                continue
            month = Month.from_date(line['month'])
            if month not in self.counters['rdv_manques']:
                continue
            self.counters['rdv_manques'][month] = line['rdv_manques']
            self.counters['rdv_manques']['total'] += line['rdv_manques']
        data = {key: self.counters[key] for key in count_keys}
        data['total_familles'] = len(suivis)
        return data

    def update_counters(self, suivi, months):
        def suivi_entre(date_deb, date_fin):
            return (
                suivi.date_debut_suivi and suivi.date_debut_suivi < date_fin and
                (not suivi.date_fin_suivi or suivi.date_fin_suivi > date_deb)
            )

        def evaluation_entre(date_deb, date_fin):
            # Dès la demande éventuelle, on considère la famille en cours d'évaluation
            # pour les stats, même sans date de debut d'évaluation
            debut_eval = suivi.date_demande or suivi.date_debut_evaluation
            fin_eval = suivi.date_fin_evaluation or suivi.date_fin_suivi
            return (
                debut_eval and debut_eval < date_fin
                and (not fin_eval or fin_eval > date_deb)
            )

        # Pour chaque mois:
        for month in months:
            if month.is_future():
                continue
            month_start, month_end = self.month_limits(month)
            month_evalue = evaluation_entre(month_start, month_end)
            month_suivi = suivi_entre(month_start, month_end)
            if month_evalue:
                self.counters['familles_evaluees'][month] += 1
                self.counters['enfants_evalues'][month] += suivi.enf_suivis
                self.counters['enfants_evalues_non_suivis'][month] += suivi.enf_nonsuivis
                if suivi.famille.accueil:
                    self.counters['familles_accueil'][month] += 1
                if suivi.famille.connue:
                    self.counters['familles_connues'][month] += 1
            if month_suivi:
                self.counters['familles_suivies'][month] += 1
                self.counters['enfants_suivis'][month] += suivi.enf_suivis
                self.counters['enfants_suivis_non_suivis'][month] += suivi.enf_nonsuivis
                if suivi.famille.accueil:
                    self.counters['familles_accueil'][month] += 1
                if suivi.famille.connue:
                    self.counters['familles_connues'][month] += 1
            if month_evalue or month_suivi:
                if getattr(suivi, 'demande_prioritaire', False):
                    self.counters['prioritaires'][month] += 1
                    self.counters['prioritaires']['total'] += 1
                self.counters['familles_total'][month] += 1
                self.counters['enfants_total'][month] += suivi.enf_suivis

            if not suivi.date_debut_suivi and suivi.motif_fin_suivi and (
                suivi.date_fin_suivi >= month_start and suivi.date_fin_suivi < month_end
            ):
                self.counters['familles_eval_sans_suivi'][month] += 1
                self.counters['familles_eval_sans_suivi']['total'] += 1

        # Au total:
        suivi_evalue = evaluation_entre(self.date_start, self.date_end)
        suivi_acc = suivi_entre(self.date_start, self.date_end)
        if suivi_evalue:
            self.counters['familles_evaluees']['total'] += 1
            self.counters['enfants_evalues']['total'] += suivi.enf_suivis
            self.counters['enfants_evalues_non_suivis']['total'] += suivi.enf_nonsuivis
            if suivi.famille.accueil:
                self.counters['familles_accueil']['total'] += 1
            if suivi.famille.connue:
                self.counters['familles_connues']['total'] += 1
        if suivi_acc:
            self.counters['familles_suivies']['total'] += 1
            self.counters['enfants_suivis']['total'] += suivi.enf_suivis
            self.counters['enfants_suivis_non_suivis']['total'] += suivi.enf_nonsuivis
            if suivi.famille.accueil:
                self.counters['familles_accueil']['total'] += 1
            if suivi.famille.connue:
                self.counters['familles_connues']['total'] += 1
        if suivi_evalue or suivi_acc:
            self.counters['familles_total']['total'] += 1
            self.counters['enfants_total']['total'] += suivi.enf_suivis
        if suivi.duree_attente is not None and (
            suivi.date_debut_suivi >= self.date_start and suivi.date_debut_suivi <= self.date_end
        ):
            self.counters['duree_attente']['familles'] += 1
            self.counters['duree_attente']['total'] += suivi.duree_attente

    def get_stats(self, months):
        return {
            'familles': self.suivi_stats(Suivi, months),
        }

    def export_lines(self, context):
        months = context['months']

        def stats_for(values):
            for key, vals in values.items():
                if key in ['duree_attente', 'total_familles']:
                    continue
                yield [self.labels[key]] + [vals[month] for month in months] + [vals['total']]
            yield (
                [self.labels['duree_attente'] + ' (jours)'] +
                (len(months)) * [''] +
                [values['duree_attente']['moyenne'].days]
            )

        yield ['BOLD', 'Familles AEMO'] + [str(month) for month in months] + ['Total']
        yield from stats_for(context['familles'])


class StatistiquesParIntervView(StatsMixin, PermissionRequiredMixin, TemplateView):
    """This view is currently unused (#344). It might be deleted in the future."""
    template_name = 'statistiques/stats-interv.html'

    def interv_stats(self, model, months):
        intervs = model.objects.annotate(
            date_fin=Coalesce('suivi__date_fin_suivi', date.today()),
            date_debut=Coalesce('suivi__date_debut_evaluation', 'suivi__date_debut_suivi')
        ).filter(
            date_debut__lte=self.date_end,
            date_fin__gte=self.date_start
        ).exclude(
            suivi__motif_fin_suivi='erreur'
        ).annotate(
            enf_suivis=Count(Case(
                When(suivi__famille__membres__role__nom="Enfant suivi", then=1),
                output_field=IntegerField(),
            ))
        ).select_related('suivi', 'intervenant')

        counters = {}
        for interv in intervs:
            if interv.intervenant not in counters:
                counters[interv.intervenant] = self.init_counters(['num_familles', 'num_enfants'], months)
            counters[interv.intervenant]['num_familles']['total'] += 1
            counters[interv.intervenant]['num_enfants']['total'] += interv.enf_suivis
            for month in months:
                month_start, month_end = self.month_limits(month)
                if interv.date_debut <= month_end and interv.date_fin >= month_start:
                    counters[interv.intervenant]['num_familles'][month] += 1
                    counters[interv.intervenant]['num_enfants'][month] += interv.enf_suivis
        return {key: counters[key] for key in sorted(counters.keys(), key=attrgetter('nom'))}

    def get_stats(self, months):
        return {
            'intervs': self.interv_stats(Intervenant, months),
        }

    def export_lines(self, context):
        months = context['months']

        def stats_for(values):
            for key, vals in values.items():
                yield [key.nom_prenom, 'Familles'] + [vals['num_familles'][month] for month in months]
                yield [key.nom_prenom, 'Enfants'] + [vals['num_enfants'][month] for month in months]

        yield ['BOLD', 'AEMO', ''] + [str(month) for month in months]
        yield from stats_for(context['intervs'])


class StatistiquesParLocaliteView(StatsMixin, PermissionRequiredMixin, TemplateView):
    template_name = 'statistiques/stats-localite.html'

    def localite_query(self, model):
        return model.objects.annotate(
            date_fin=Coalesce('date_fin_suivi', date.today()),
            date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
        ).filter(
            date_debut__lte=self.date_end,
            date_fin__gte=self.date_start
        ).exclude(
            motif_fin_suivi='erreur'
        ).annotate(
            enf_suivis=Count(Case(
                When(famille__membres__role__nom="Enfant suivi", then=1),
                output_field=IntegerField(),
            ))
        ).values('date_debut', 'date_fin', 'famille__npa', 'famille__localite', 'enf_suivis')

    def localite_stats(self, model, months):
        suivis = self.localite_query(model)
        counters = self.init_counters(['totals'], months)
        for suivi in suivis:
            loc_key = f"{suivi['famille__npa']} {suivi['famille__localite']}"
            if loc_key not in counters:
                counters.update(self.init_counters([loc_key], months))
            counters[loc_key]['total'] += suivi['enf_suivis']
            counters['totals']['total'] += suivi['enf_suivis']
            for month in months:
                month_start, month_end = self.month_limits(month)
                if suivi['date_debut'] <= month_end and suivi['date_fin'] >= month_start:
                    counters[loc_key][month] += suivi['enf_suivis']
                    counters['totals'][month] += suivi['enf_suivis']
        return {localite: counters[localite] for localite in sorted(counters.keys())}

    def get_stats(self, months):
        return {
            'localites': self.localite_stats(Suivi, months),
        }

    def export_lines(self, context):
        months = context['months']

        def stats_for(values):
            for key, vals in values.items():
                yield ['Totaux' if key == 'totals' else key] + [vals[month] for month in months] + [vals['total']]

        yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
        yield from stats_for(context['localites'])


class StatistiquesParRegionView(StatsMixin, PermissionRequiredMixin, TemplateView):
    template_name = 'statistiques/stats-region.html'

    def region_query(self, model, region_key):
        return model.objects.annotate(
            date_fin=Coalesce('date_fin_suivi', date.today()),
            date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
        ).filter(
            date_debut__lte=self.date_end,
            date_fin__gte=self.date_start
        ).exclude(
            motif_fin_suivi='erreur'
        ).annotate(
            enf_suivis=Count(Case(
                When(famille__membres__role__nom="Enfant suivi", then=1),
                output_field=IntegerField(),
            ))
        ).values('date_debut', 'date_fin', region_key, 'enf_suivis')

    def region_stats(self, model, months, region_key):
        suivis = self.region_query(model, region_key)
        counters = self.init_counters(['totals'], months)
        for suivi in suivis:
            loc_key = suivi[region_key] or '?'
            if loc_key not in counters:
                counters.update(self.init_counters([loc_key], months))
            counters[loc_key]['total'] += suivi['enf_suivis']
            counters['totals']['total'] += suivi['enf_suivis']
            for month in months:
                month_start, month_end = self.month_limits(month)
                if suivi['date_debut'] <= month_end and suivi['date_fin'] >= month_start:
                    counters[loc_key][month] += suivi['enf_suivis']
                    counters['totals'][month] += suivi['enf_suivis']
        return {region: counters[region] for region in sorted(counters.keys())}

    def get_stats(self, months):
        return {
            'regions': self.region_stats(Suivi, months, 'famille__region__nom'),
        }

    def export_lines(self, context):
        months = context['months']

        def stats_for(values):
            for key, vals in values.items():
                yield (
                    ['Totaux' if key == 'totals' else key] +
                    [vals[month] for month in months] +
                    [vals['total']]
                )

        yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
        yield from stats_for(context['regions'])


class StatistiquesParAgeView(StatsMixin, PermissionRequiredMixin, TemplateView):
    template_name = 'statistiques/stats-age.html'

    def age_stats(self, model, months):
        enfants_suivis = Personne.objects.filter(role__nom='Enfant suivi')
        enfants_suivis = enfants_suivis.exclude(
            famille__suivi__motif_fin_suivi='erreur'
        ).annotate(
            date_fin=Coalesce(
                'famille__suivi__date_fin_suivi', date.today()
            ),
            date_debut=Coalesce(
                'famille__suivi__date_demande',
                'famille__suivi__date_debut_evaluation', 'famille__suivi__date_debut_suivi'
            )
        )
        enfants_suivis = enfants_suivis.filter(
            date_debut__lte=self.date_end,
            date_fin__gte=self.date_start
        )

        counters = {}
        means = {'total': [], **{m: [] for m in months}}
        for enfant in enfants_suivis:
            age = enfant.age_a(enfant.date_debut + ((enfant.date_fin - enfant.date_debut) / 2))
            if age is None:
                continue
            age = age_real = int(age)
            if age > 18:
                age = 18
            if age not in counters:
                counters.update(self.init_counters([age], months))
            counters[age]['total'] += 1
            means['total'].append(age_real)
            for month in months:
                month_start, month_end = self.month_limits(month)
                if enfant.date_debut <= month_end and enfant.date_fin >= month_start:
                    counters[age][month] += 1
                    means[month].append(age_real)
        stats = {str(age): counters[age] for age in sorted(counters.keys())}
        if '18' in stats:
            stats['18 et plus'] = stats['18']
            del stats['18']
        # Calcul des moyennes à 1 décimale
        means['total'] = int(sum(means['total']) / max(len(means['total']), 1) * 10) / 10
        for month in months:
            if month.is_future():
                means[month] = '-'
            else:
                means[month] = int(sum(means[month]) / max(len(means[month]), 1) * 10) / 10
        return stats, means

    def get_stats(self, months):
        stats, means = self.age_stats(Suivi, months)
        return {
            'ages': stats,
            'means': means,
        }

    def export_lines(self, context):
        months = context['months']

        def stats_for(values):
            for key, vals in values.items():
                yield ['Totaux' if key == 'totals' else key] + [vals[month] for month in months] + [vals['total']]

        yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
        yield from stats_for(context['ages'])
        yield ['Âge moyen'] + [context['means'][month] for month in months] + [context['means']['total']]


class StatistiquesParDureeView(StatsMixin, PermissionRequiredMixin, TemplateView):
    template_name = 'statistiques/stats-duree.html'

    slices = {
        Suivi: [
            {'label': '0 à 3 mois', 'start': 0, 'stop': 120},
            {'label': '4 à 6 mois', 'start': 121, 'stop': 210},
            {'label': '7 à 9 mois', 'start': 211, 'stop': 300},
            {'label': '10 à 12 mois', 'start': 301, 'stop': 394},
            {'label': '13 à 15 mois', 'start': 395, 'stop': 484},
            {'label': '16 à 18 mois', 'start': 485, 'stop': 574},
            {'label': '19 à 24 mois', 'start': 575, 'stop': 760},
            {'label': '25 à 36 mois', 'start': 761, 'stop': 1125},
            {'label': '37 à 48 mois', 'start': 1126, 'stop': 1490},
            {'label': '49 mois et plus', 'start': 1491, 'stop': 100000},
        ],
    }

    def duree_stats(self, model, months):
        suivis = model.objects.filter(
            date_debut_suivi__isnull=False,
            date_fin_suivi__lte=self.date_end,
            date_fin_suivi__gte=self.date_start
        ).exclude(
            motif_fin_suivi='erreur'
        ).annotate(
            duree_suivi=F('date_fin_suivi') - F('date_debut_suivi')
        )
        counters = {sl['label']: 0 for sl in self.slices[model]}
        for suivi in suivis:
            duree_days = suivi.duree_suivi.days
            for sl in self.slices[model]:
                if sl['stop'] > duree_days > sl['start']:
                    counters[sl['label']] += 1
                    break
        return counters

    def get_stats(self, months):
        return {
            'slices': self.slices[Suivi],
            'durees': self.duree_stats(Suivi, months),
        }

    def export_lines(self, context):
        def stats_for(values, slices):
            for _slice in slices:
                yield [_slice['label'], values[_slice['label']]]

        yield ['BOLD', f'AEMO, du {format_d_m_Y(self.date_start)} au {format_d_m_Y(self.date_end)}']
        yield ['BOLD', 'Durée', 'Nombre de suivis']
        yield from stats_for(context['durees'], self.slices[Suivi])


class StatistiquesMotifsView(StatsMixin, PermissionRequiredMixin, TemplateView):
    template_name = 'statistiques/stats-motifs.html'
    labels = {
        'ann': 'Motif d’annonce',
        'orient': 'Service annonceur',
        'fin_preeval': 'Abandon avant évaluation',
        'fin_posteval': 'Abandon après évaluation',
        'fin_postacc': 'Fin de l’accompagnement',
        'fin_total': 'Total',
        'prov': 'Provenance',
        'dest': 'Destination',
    }

    def motifs_stats(self, model, months):
        suivis = model.objects.annotate(
            date_fin=Coalesce('date_fin_suivi', date.today()),
            date_debut=Coalesce('date_demande', 'date_debut_evaluation', 'date_debut_suivi')
        ).filter(
            date_debut__lte=self.date_end,
            date_fin__gte=self.date_start
        ).exclude(
            motif_fin_suivi='erreur'
        ).values(
            'famille_id', 'date_debut', 'date_fin', 'date_debut_evaluation', 'date_debut_suivi',
            'motif_demande', 'motif_fin_suivi',
            'service_orienteur', 'famille__provenance', 'famille__destination',
        )

        # Initiate all counters to 0 for each month.
        counters = {
            'dem': {}, 'orient': {}, 'prov': {}, 'dest': {},
            'fin': {'preeval': {}, 'posteval': {}, 'postacc': {}, 'total': {}}
        }
        for key, choices in (
                ('dem', MOTIF_DEMANDE_CHOICES), ('fin', MOTIFS_FIN_SUIVI_CHOICES),
                ('orient', SERVICE_ORIENTEUR_CHOICES), ('prov', PROVENANCE_DESTINATION_CHOICES),
                ('dest', PROVENANCE_DESTINATION_CHOICES)):
            for ch in choices:
                if key == 'fin':
                    if ch[0] == 'erreur':
                        continue
                    for subkey in counters['fin'].keys():
                        counters[key][subkey].update(self.init_counters([ch[0]], months))
                else:
                    counters[key].update(self.init_counters([ch[0]], months))
        counters['orient'].update(self.init_counters(['undefined'], months))

        def suivi_in_month(suivi, month):
            month_start, month_end = self.month_limits(month)
            return suivi['date_debut'] <= month_end and suivi['date_fin'] >= month_start

        for suivi in suivis:
            # Stats motif demande
            for motif in (suivi['motif_demande'] or []):
                counters['dem'][motif]['total'] += 1
            for month in months:
                if suivi_in_month(suivi, month):
                    for motif in (suivi['motif_demande'] or []):
                        counters['dem'][motif][month] += 1
            # Stats service annonceur
            counters['orient'][suivi['service_orienteur'] or 'undefined']['total'] += 1
            for month in months:
                if suivi_in_month(suivi, month):
                    counters['orient'][suivi['service_orienteur'] or 'undefined'][month] += 1
            # Stats motif fin de suivi
            if suivi['motif_fin_suivi'] and suivi['motif_fin_suivi'] != 'erreur':
                counters['fin']['total'][suivi['motif_fin_suivi']]['total'] += 1
                if not suivi['date_debut_evaluation'] and not suivi['date_debut_suivi']:
                    counters['fin']['preeval'][suivi['motif_fin_suivi']]['total'] += 1
                elif not suivi['date_debut_suivi']:
                    counters['fin']['posteval'][suivi['motif_fin_suivi']]['total'] += 1
                else:
                    counters['fin']['postacc'][suivi['motif_fin_suivi']]['total'] += 1
                for month in months:
                    if suivi_in_month(suivi, month):
                        counters['fin']['total'][suivi['motif_fin_suivi']][month] += 1
                        if not suivi['date_debut_evaluation'] and not suivi['date_debut_suivi']:
                            counters['fin']['preeval'][suivi['motif_fin_suivi']][month] += 1
                        elif not suivi['date_debut_suivi']:
                            counters['fin']['posteval'][suivi['motif_fin_suivi']][month] += 1
                        else:
                            counters['fin']['postacc'][suivi['motif_fin_suivi']][month] += 1
            # Stats provenance
            if suivi['famille__provenance']:
                counters['prov'][suivi['famille__provenance']]['total'] += 1
                for month in months:
                    if suivi_in_month(suivi, month):
                        counters['prov'][suivi['famille__provenance']][month] += 1
            # Stats destination
            if suivi['famille__destination']:
                counters['dest'][suivi['famille__destination']]['total'] += 1
                for month in months:
                    if suivi_in_month(suivi, month):
                        counters['dest'][suivi['famille__destination']][month] += 1
        return counters

    def get_stats(self, months):
        motif_ann_dict = dict(MOTIF_DEMANDE_CHOICES)
        annonceur_dict = dict(SERVICE_ORIENTEUR_CHOICES)
        motif_fin_dict = dict(MOTIFS_FIN_SUIVI_CHOICES)
        provdest_dict = dict(PROVENANCE_DESTINATION_CHOICES)
        stats = self.motifs_stats(Suivi, months)
        return {
            'data': {
                'ann': {motif_ann_dict[key]: data for key, data in stats['dem'].items()},
                'orient': {annonceur_dict.get(key, 'Non défini'): data for key, data in stats['orient'].items()},
                'fin_preeval': {motif_fin_dict[key]: data for key, data in stats['fin']['preeval'].items()},
                'fin_posteval': {motif_fin_dict[key]: data for key, data in stats['fin']['posteval'].items()},
                'fin_postacc': {motif_fin_dict[key]: data for key, data in stats['fin']['postacc'].items()},
                'fin_total': {motif_fin_dict[key]: data for key, data in stats['fin']['total'].items()},
                'prov': {provdest_dict[key]: data for key, data in stats['prov'].items()},
                'dest': {provdest_dict[key]: data for key, data in stats['dest'].items()},
            }
        }

    def export_lines(self, context):
        months = context['months']

        def stats_for(values):
            for key1, vals in values.items():
                yield ['BOLD', self.labels[key1]]
                for key2, subvals in vals.items():
                    yield [key2] + [subvals[month] for month in months] + [subvals['total']]

        yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
        yield from stats_for(context['data'])


class StatistiquesPrestationView(PermissionRequiredMixin, TemplateView):
    permission_required = 'aemo.export_stats'
    template_name = 'statistiques/stats-prestations.html'

    def _sum_list(self, liste):
        tot = timedelta()
        for data in liste:
            if data != '-':
                tot += data
        return tot

    @staticmethod
    def temps_totaux_mensuels_fam_gen(prest_model, annee):
        """
        Renvoie un dictionnaire avec les totaux mensuels de toutes les prestations familiales
        et générales pour l'année en cours (de janv. à déc.).
        """
        query = prest_model.objects.filter(
            date_prestation__year=annee
        ).annotate(
            month=TruncMonth('date_prestation'),
            duree_tot=ExpressionWrapper(Count('intervenants') * F('duree'), output_field=DurationField())
        ).values('month', 'lib_prestation__nom', 'duree_tot')
        # La somme est calculée en Python, car Django ne sait pas faire la somme de duree_tot.
        months = [Month(annee, num_month) for num_month in range(1, 13)]
        data = {month: {'total': '-' if month.is_future() else timedelta(0)} for month in months}
        for result in query:
            month = Month.from_date(result['month'])
            if result['lib_prestation__nom'] not in data[month]:
                data[month][result['lib_prestation__nom']] = '-' if month.is_future() else timedelta(0)
            data[month][result['lib_prestation__nom']] += result['duree_tot']
            data[month]['total'] += result['duree_tot']
        return data

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        get_params = self.request.GET.copy()
        self.export_flag = get_params.pop('export', None)
        date_form = DateYearForm(get_params)
        context['date_form'] = date_form
        if not date_form.is_valid():
            return context
        if date_form.is_valid():
            annee = int(date_form.cleaned_data['year'])
        counters = {}
        tot_dus_mensuels = [timedelta()] * 12
        tot_ecarts_mensuels = [timedelta()] * 12

        intervenants = Utilisateur.objects.annotate(
            num_prest=Count('prestations', filter=Q(prestations__date_prestation__year=annee))
        ).filter(num_prest__gt=0).order_by('nom', 'prenom')
        for interv in intervenants:
            h_prestees = interv.totaux_mensuels('aemo', annee)
            h_prestees = [('-' if Month(annee, idx).is_future() else h) for idx, h in enumerate(h_prestees, start=1)]

            tot_prestees = self._sum_list(h_prestees)
            counters[interv] = {
                'heures_prestees': h_prestees,
                'tot_prestees': tot_prestees,
            }

        tot_prest_mensuels = self.temps_totaux_mensuels_fam_gen(Prestation, annee)

        tot_dus_mensuels.append(self._sum_list(tot_dus_mensuels))
        tot_ecarts_mensuels.append(self._sum_list(tot_ecarts_mensuels))

        totaux_par_prest = {}
        for month, data in tot_prest_mensuels.items():
            for label, duration in data.items():
                if label not in totaux_par_prest:
                    totaux_par_prest[label] = timedelta(0)
                if duration != '-':
                    totaux_par_prest[label] += duration

        context.update({
            'annee': annee,
            'titre': 'Prestations AEMO',
            'intervenants': counters,
            'months': [date(annee, m, 1) for m in range(1, 13)],
            'libelles_prest': LibellePrestation.objects.all().order_by('code'),
            'totaux_prest_mensuels': tot_prest_mensuels,
            'totaux_par_prest': totaux_par_prest,
            'total_gen': totaux_par_prest['total'],
        })
        return context

    def render_to_response(self, context, **response_kwargs):
        if self.export_flag:
            export = ExportStatistique(col_widths=[30])
            export.fill_data(self.export_lines(context))
            return export.get_http_response(self.__class__.__name__.replace('View', '') + '.xlsx')
        else:
            return super().render_to_response(context, **response_kwargs)

    def export_lines(self, context):
        months = context['months']
        yield ['BOLD', 'AEMO'] + [str(month) for month in months] + ['Total']
        for user, vals in context['intervenants'].items():
            yield ([user.nom_prenom] + [format_duree(val) for val in vals['heures_prestees']]
                   + [format_duree(vals['tot_prestees'])])
        yield ['BOLD', 'Par type d’intervention']
        for prest in context['libelles_prest']:
            yield ([prest.nom] + [
                format_duree(context['totaux_prest_mensuels'][month].get(prest.nom,
                                                                         '-' if month.is_future() else timedelta(0)))
                for month in [Month.from_date(m) for m in months]
            ] + [format_duree(context['totaux_par_prest'].get(prest.nom, timedelta(0)))])


class StatistiquesNiveauxView(StatsMixin, PermissionRequiredMixin, TemplateView):
    permission_required = 'aemo.export_stats'
    template_name = 'statistiques/stats-niveaux.html'

    def get_stats(self, months):
        prest_list = ['aemo04', 'aemo05', 'aemo06', 'aemo07']
        prest_query = Prestation.objects.filter(
            famille__isnull=False,
            date_prestation__range=(self.date_start, self.date_end),
            lib_prestation__code__in=prest_list,
        ).annotate(
            month=TruncMonth('date_prestation'),
            niveau_interv=Subquery(
                Niveau.objects.filter(
                    Q(famille=OuterRef('famille_id')) &
                    Q(date_debut__lt=OuterRef('date_prestation')) & (
                        Q(date_fin__isnull=True) | Q(date_fin__gt=OuterRef('date_prestation'))
                    )
                ).order_by('-date_debut').values('niveau_interv')[:1]
            ),
        ).values(
            'month', 'niveau_interv', 'lib_prestation__code',
        ).annotate(
            sum_hours=Sum('duree'),
        )
        niveaux = {
            niv: {
                p: {
                    **{m: timedelta(0) for m in months}, 'total': timedelta(0)
                } for p in prest_list
            } for niv in [0, 1, 2, 3]
        }
        for line in prest_query:
            if line['niveau_interv'] is None:
                continue
            niveaux[line['niveau_interv']][line['lib_prestation__code']][Month.from_date(line['month'])] = line['sum_hours']
            niveaux[line['niveau_interv']][line['lib_prestation__code']]['total'] += line['sum_hours']
        return {
            'stats': niveaux,
            'prest_map': LibellePrestation.objects.in_bulk(prest_list, field_name='code'),
        }

    def export_lines(self, context):
        months = context['months']
        yield ['BOLD', 'Ressources par niveau'] + [str(month) for month in months] + ['Total']
        for niv, prests in context['stats'].items():
            yield ['BOLD', niv]
            for prest, numbers in prests.items():
                yield [context['prest_map'][prest].nom] + [numbers[m] for m in months] + [numbers['total']]
