from collections import OrderedDict, namedtuple
from datetime import date, timedelta
from operator import attrgetter
from pathlib import Path

from django.contrib.auth.models import AbstractUser, Group, UserManager
from django.contrib.postgres.aggregates import StringAgg
from django.core.validators import MaxValueValidator
from django.db import models
from django.db.models import Count, F, OuterRef, Q, Subquery, Sum
from django.db.models.functions import Coalesce, ExtractMonth, ExtractYear
from django.db.utils import IntegrityError
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property

from django_countries.fields import CountryField

from common import choices
from common.fields import ChoiceArrayField

from .utils import format_adresse, format_contact, format_d_m_Y, random_string_generator


class CercleScolaire(models.Model):
    """ Les 7 cercles scolaires du canton"""

    nom = models.CharField(max_length=50, unique=True)
    telephone = models.CharField('tél.', max_length=35, blank=True)

    class Meta:
        verbose_name = 'Cercle scolaire'
        verbose_name_plural = 'Cercles scolaires'
        ordering = ('nom',)

    def __str__(self):
        return self.nom


class Service(models.Model):
    """ Services sociaux en lien avec la Fondation Transit """

    sigle = models.CharField('Sigle', max_length=20, unique=True)
    nom_complet = models.CharField('Nom complet', max_length=80, blank=True)

    class Meta:
        ordering = ('sigle',)

    def __str__(self):
        return self.sigle

    def save(self, *args, **kwargs):
        self.sigle = self.sigle.upper()
        super().save(*args, **kwargs)


class RoleManager(models.Manager):
    def get_by_natural_key(self, nom):
        return self.get(nom=nom)


class Role(models.Model):
    NON_EDITABLE = ['Père', 'Mère', 'Enfant suivi', 'Enfant non-suivi']
    ROLES_PARENTS = ['Père', 'Mère']

    nom = models.CharField("Nom", max_length=50, unique=True)
    est_famille = models.BooleanField("Famille", default=False)
    est_intervenant = models.BooleanField("Intervenant", default=False)
    est_editeur = models.BooleanField("Éditeur", default=False, help_text=(
        "Un rôle éditeur donne le droit de modification des dossiers familles si "
        "la personne est intervenante."
    ))

    objects = RoleManager()

    class Meta:
        ordering = ('nom',)

    def __str__(self):
        return self.nom

    @property
    def editable(self):
        return self.nom not in self.NON_EDITABLE

    def natural_key(self):
        return (self.nom,)


class Contact(models.Model):
    prenom = models.CharField('Prénom', max_length=30)
    nom = models.CharField('Nom', max_length=30)
    rue = models.CharField('Rue', max_length=30, blank=True)
    npa = models.CharField('NPA', max_length=4, blank=True)
    localite = models.CharField('Localité', max_length=30, blank=True)
    tel_prive = models.CharField('Tél. privé', max_length=30, blank=True)
    tel_prof = models.CharField('Tél. prof.', max_length=30, blank=True)
    email = models.EmailField(max_length=100, blank=True)
    service = models.ForeignKey(Service, null=True, blank=True, on_delete=models.PROTECT)
    roles = models.ManyToManyField(Role, related_name='contacts', blank=True, verbose_name='Rôles')
    profession = models.CharField('Activité/prof.', max_length=100, blank=True)
    remarque = models.TextField("Remarque", blank=True)
    est_actif = models.BooleanField('actif', default=True)

    class Meta:
        verbose_name = 'Contact'
        ordering = ('nom', 'prenom')
        unique_together = ('nom', 'prenom', 'service')

    def __str__(self):
        sigle = self.service.sigle if self.service else ''
        return '{} ({})'.format(self.nom_prenom, sigle) if self.service else self.nom_prenom

    @property
    def nom_prenom(self):
        return '{} {}'.format(self.nom, self.prenom)

    @property
    def adresse(self):
        return format_adresse(self.rue, self.npa, self.localite)

    @cached_property
    def all_roles(self):
        return self.roles.all()

    def has_role(self, roles=()):
        """Return True if user has at least one of the `roles` name."""
        return bool(set(r.nom for r in self.all_roles).intersection(set(roles)))

    def roles_str(self, sep=', '):
        return sep.join([role.nom for role in self.all_roles])

    @property
    def contact(self):
        return format_contact(self.tel_prof, self.email)

    @classmethod
    def membres_ope(cls):
        return cls.objects.filter(service__sigle__startswith='OPE', est_actif=True).select_related('service')


class EquipeChoices(models.TextChoices):
    MONTAGNES = 'montagnes', 'Montagnes et V-d-T'
    LITTORAL = 'littoral', 'Littoral et V-d-R'


class Utilisateur(Contact, AbstractUser):
    sigle = models.CharField(max_length=5, blank=True)
    equipe = models.CharField("Équipe", max_length=10, choices=EquipeChoices.choices, blank=True)
    date_desactivation = models.DateField('Date désactivation', null=True, blank=True)
    taux_activite = models.PositiveSmallIntegerField(
        "Taux d’activité (en %)", blank=True, default=0, validators=[MaxValueValidator(100)]
    )
    decharge = models.PositiveSmallIntegerField("Heures de décharge", blank=True, null=True)

    objects = UserManager()

    def __str__(self):
        return self.nom_prenom

    @property
    def groupes(self):
        return [item.name for item in self.groups.all()]

    @property
    def is_psy_or_educ(self):
        return self.has_role({'Psy', 'Educ'})

    @property
    def is_responsable(self):
        return self.has_role({'Responsable/coordinateur'})

    @property
    def is_admin_ipe(self):
        return 'admin ipe' in self.groupes

    @property
    def initiales(self):
        return self.sigle if self.sigle else f'{self.prenom[0]}{self.nom[0]}'

    @property
    def charge_max(self):
        """
        Renvoie la charge maximale d'heures hebodmadaires de charge dossier en
        fonction du taux d'activité.
        """
        charge_map = {
            100: 32, 90: 28, 80: 24, 75: 22, 70: 20, 60: 16, 0: 0,
        }
        charge = charge_map.get(self.taux_activite, int(32 * self.taux_activite / 100))
        if self.decharge:
            charge -= self.decharge
        return charge

    def prestations_gen(self):
        """Renvoie les prestations générales de l’utilisateur."""
        return self.prestations.filter(famille__isnull=True)

    def temps_total_prestations(self, unite):
        return self.prestations.model.temps_total(self.prestations, tous_utilisateurs=False)

    def total_mensuel(self, unite, month, year):
        prestations = self.prestations.filter(
            date_prestation__year=year, date_prestation__month=month
        )
        return self.prestations.model.temps_total(prestations, tous_utilisateurs=False)

    def totaux_mensuels(self, unite, year):
        return [self.total_mensuel(unite, m, year) for m in range(1, 13)]

    def total_annuel(self, unite, year):
        prestations = self.prestations.filter(date_prestation__year=year)
        return self.prestations.model.temps_total(prestations, tous_utilisateurs=False)

    @classmethod
    def _intervenants(cls, groupes=(), annee=None):
        if annee is None:
            # Utilisateurs actuellement actifs
            utils = Utilisateur.objects.filter(date_desactivation__isnull=True)
        else:
            utils = Utilisateur.objects.filter(
                Q(date_desactivation__isnull=True) | Q(date_desactivation__year__gte=annee)
            )
        return utils.filter(
            groups__name__in=groupes,
            is_superuser=False
        ).exclude(
            groups__name__in=['direction']
        ).distinct()

    @classmethod
    def intervenants(cls, annee=None):
        return cls._intervenants(
            groupes=['aemo'],
            annee=annee
        )


class GroupInfo(models.Model):
    group = models.OneToOneField(Group, on_delete=models.CASCADE)
    description = models.TextField(blank=True)

    def __str__(self):
        return f"Description of group {self.group.name}"


class Region(models.Model):
    nom = models.CharField(max_length=30, unique=True)
    rue = models.CharField("Rue", max_length=100, blank=True)

    def __str__(self):
        return self.nom

    def __lt__(self, other):
        return self.nom < (other.nom if other else '')


class FamilleQuerySet(models.QuerySet):
    def with_duos(self):
        """
        Renvoie une liste des groupes Psy/Educ différents, sous forme de chaînes
        de sigles séparées par "/".
        Peut aussi renvoyer 1 ou 3 intervenants, le cas échéant.
        """
        return self.annotate(
            duo=StringAgg(
                F('suivi__intervenant__intervenant__sigle'),
                delimiter='/',
                filter=(
                    Q(suivi__intervenant__date_fin__isnull=True) &
                    Q(suivi__intervenant__role__nom__in=['Educ', 'Psy'])
                ),
                ordering='suivi__intervenant__intervenant__sigle',
            )
        )

    def with_niveau_interv(self):
        """
        Annote les familles avec le niveau d’intervention le plus récent.
        """
        return self.annotate(
            niveau_interv=Subquery(
                Niveau.objects.filter(
                    famille=OuterRef('pk')
                ).order_by('-date_debut').values('niveau_interv')[:1]
            )
        )


class FamilleManager(models.Manager):
    def get_queryset(self):
        return FamilleQuerySet(self.model, using=self._db).filter(
            suivi__isnull=False
        ).prefetch_related(
            models.Prefetch('membres', queryset=Personne.objects.select_related('role'))
        )

    def create_famille(self, equipe='', **kwargs):
        kwargs.pop('_state', None)
        famille = self.create(**kwargs)
        Suivi.objects.create(
            famille=famille,
            equipe=equipe
        )
        famille.suivi.date_demande = date.today()
        famille.suivi.save()
        return famille


class Famille(models.Model):
    created_at = models.DateTimeField(auto_now_add=True, editable=False)
    archived_at = models.DateTimeField('Archivée le', blank=True, null=True)
    nom = models.CharField('Nom de famille', max_length=40)
    rue = models.CharField('Rue', max_length=60, blank=True)
    npa = models.CharField('NPA', max_length=4, blank=True)
    localite = models.CharField('Localité', max_length=30, blank=True)
    region = models.ForeignKey(to=Region, null=True, blank=True, default=None, on_delete=models.SET_NULL)
    telephone = models.CharField('Tél.', max_length=60, blank=True)
    autorite_parentale = models.CharField("Autorité parentale", max_length=20,
                                          choices=choices.AUTORITE_PARENTALE_CHOICES, blank=True)
    monoparentale = models.BooleanField('Famille monoparent.', default=None, blank=True, null=True)
    statut_marital = models.CharField("Statut marital", max_length=20, choices=choices.STATUT_MARITAL_CHOICES,
                                      blank=True)
    connue = models.BooleanField("famille déjà suivie", default=False)
    accueil = models.BooleanField("famille d'accueil", default=False)
    # Pour CIPE
    besoins_part = models.BooleanField("famille à besoins particuliers", default=False)
    sap = models.BooleanField(default=False, verbose_name="famille s@p")
    garde = models.CharField(
        'Type de garde', max_length=20, choices=choices.TYPE_GARDE_CHOICES, blank=True
    )
    provenance = models.CharField(
        'Provenance', max_length=30, choices=choices.PROVENANCE_DESTINATION_CHOICES, blank=True)
    destination = models.CharField(
        'Destination', max_length=30, choices=choices.PROVENANCE_DESTINATION_CHOICES, blank=True)
    statut_financier = models.CharField(
        'Statut financier', max_length=30, choices=choices.STATUT_FINANCIER_CHOICES, blank=True)
    remarques = models.TextField(blank=True)

    objects = FamilleManager()

    class Meta:
        ordering = ('nom', 'npa')
        permissions = (
            ("can_manage_waiting_list", "Gérer la liste d'attente"),
            ("can_archive", "Archiver les dossiers AEMO"),
            ('export_stats', 'Exporter les statistiques'),
        )

    def __str__(self):
        return '{} - {}'.format(self.nom, self.adresse)

    @property
    def adresse(self):
        return format_adresse(self.rue, self.npa, self.localite)

    @property
    def suivi(self):
        return getattr(self, self.suivi_name) if self.suivi_name else None

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

    @property
    def edit_url(self):
        return reverse('famille-edit', args=[self.pk])

    @property
    def add_person_url(self):
        return reverse('personne-add', args=[self.pk])

    def redirect_after_personne_creation(self, personne):
        return self.edit_url

    @property
    def print_coords_url(self):
        return reverse('print-coord-famille', args=[self.pk])

    @classmethod
    def actives(cls, date=None):
        qs = Famille.objects.filter(suivi__isnull=False).order_by('nom', 'npa')
        if date is not None:
            return qs.filter(suivi__date_demande__lte=date).filter(
                models.Q(suivi__date_fin_suivi__isnull=True) |
                models.Q(suivi__date_fin_suivi__gte=date)
            )
        else:
            return qs.filter(suivi__date_fin_suivi__isnull=True)

    def interventions_actives(self, dt=None):
        dt = dt or date.today()
        return self.suivi.intervenant_set.filter(
            Q(date_debut__lte=dt) & (
                Q(date_fin__isnull=True) | Q(date_fin__gte=dt)
            )
        ).select_related('intervenant', 'role')

    def niveau_actuel(self):
        try:
            return [
                niv.niveau_interv for niv in sorted(
                    self.niveaux.all(), key=attrgetter('date_debut')
                )
            ][-1]
        except IndexError:
            return None

    def membres_suivis(self):
        if 'membres' in getattr(self, '_prefetched_objects_cache', {}):
            return sorted([
                pers for pers in self._prefetched_objects_cache['membres'] if pers.role.nom == "Enfant suivi"
            ], key=lambda p: p.date_naissance or date(1950, 1, 1))
        else:
            return self.membres.filter(role__nom="Enfant suivi").order_by('date_naissance')

    def enfants_non_suivis(self):
        if 'membres' in getattr(self, '_prefetched_objects_cache', {}):
            return sorted([
                pers for pers in self._prefetched_objects_cache['membres'] if pers.role.nom == "Enfant non-suivi"
            ], key=lambda p: p.date_naissance or date(1950, 1, 1))
        else:
            return self.membres.filter(role__nom='Enfant non-suivi').order_by('date_naissance')

    def parents(self):
        if 'membres' in getattr(self, '_prefetched_objects_cache', {}):
            parents = [
                pers for pers in self._prefetched_objects_cache['membres']
                if pers.role.nom in Role.ROLES_PARENTS
            ]
        else:
            parents = [
                pers for pers in self.membres.filter(role__nom__in=Role.ROLES_PARENTS)
            ]
        return sorted(parents, key=lambda p: p.role.nom)  # Mère avant Père

    def autres_parents(self):
        excluded_roles = Role.ROLES_PARENTS + ['Enfant suivi', 'Enfant non-suivi']
        if 'membres' in getattr(self, '_prefetched_objects_cache', {}):
            return [
                pers for pers in self._prefetched_objects_cache['membres']
                if pers.role.nom not in excluded_roles
            ]
        else:
            return self.membres.exclude(role__nom__in=excluded_roles)

    def access_ok(self, user):
        if user.has_perm('aemo.change_famille') or user.is_responsable:
            return True
        intervs = self.interventions_actives()
        return not intervs or user in [interv.intervenant for interv in intervs]

    def can_view(self, user):
        return user.has_perm('aemo.view_famille')

    def can_edit(self, user):
        if user.has_perm('aemo.change_famille') or user.is_responsable:
            return True
        if not self.suivi.date_fin_suivi or self.suivi.date_fin_suivi >= date.today():
            intervs = self.interventions_actives()
            if (
                self.can_view(user) and
                (not intervs or user in [
                    interv.intervenant for interv in intervs if interv.role.est_editeur
                ])
            ):
                return True
        return False

    def can_be_deleted(self, user):
        if not user.has_perm('aemo.change_famille'):
            return False
        if self.suivi.motif_fin_suivi == 'erreur':
            return True
        return False

    def can_be_archived(self, user):
        if self.archived_at:
            return False
        closed_since = date.today() - self.suivi.date_fin_suivi
        return (
            user.has_perm('aemo.can_archive') and
            self.suivi.date_fin_suivi is not None and
            closed_since.days > 180 and
            self.suivi.date_fin_suivi.year < date.today().year and
            (date.today().month > 1 or closed_since.days > 400)
        )

    def temps_total_prestations(self, interv=None):
        """
        Temps total des prestations liées à la famille, quel que soit le nombre
        de membres suivis.
        Filtré facultativement par intervenant.
        """
        prest = self.prestations if interv is None else self.prestations.filter(intervenant=interv)
        return prest.annotate(num_util=Count('intervenants')).aggregate(
            total=Sum(F('duree') * F('num_util'), output_field=models.DurationField())
        )['total'] or timedelta()

    def temps_total_prestations_reparti(self):
        """
        Temps total des prestations liées à la famille, divisé par le nombre d'enfants suivis.
        """
        duree = self.temps_total_prestations()
        duree //= (len(self.membres_suivis()) or 1)
        return duree

    def total_mensuel(self, date_debut_mois):
        """
        Temps total de prestations sur un mois donné
        """
        return self.prestations.filter(
            date_prestation__month=date_debut_mois.month,
            date_prestation__year=date_debut_mois.year
        ).aggregate(total=Sum('duree'))['total'] or timedelta()

    def total_mensuel_par_prestation(self, prest_codes, date_debut_mois):
        """
        Temps total d'évaluation sur un mois donné par prestation.
        """
        return self.prestations.annotate(
            num_util=Count('intervenants')
        ).filter(
            lib_prestation__code__in=prest_codes,
            date_prestation__month=date_debut_mois.month,
            date_prestation__year=date_debut_mois.year
        ).aggregate(
            total=Sum(F('duree') * F('num_util'), output_field=models.DurationField())
        )['total'] or timedelta()

    def total_mensuel_evaluation(self, date_debut_mois):
        return self.total_mensuel_par_prestation(['aemo01'], date_debut_mois)

    def total_mensuel_suivi(self, date_debut_mois):
        return self.total_mensuel_par_prestation(['aemo02', 'aemo04', 'aemo05', 'aemo06', 'aemo07'], date_debut_mois)

    def prestations_historiques(self):
        return Prestation.prestations_historiques(self.prestations.all())

    def prestations_du_mois(self):
        """
        Retourne le détail des prestations pour cette famille
        à partir du mois courant
        """
        date_ref = date(date.today().year, date.today().month, 1)
        return self.prestations.filter(date_prestation__gte=date_ref)

    @classmethod
    def suivis_en_cours(cls, debut, fin):
        base = cls.objects.annotate(
            date_deb=Coalesce(
                'suivi__date_demande',
                'suivi__date_debut_evaluation', 'suivi__date_debut_suivi'
            ),
        ).prefetch_related('membres')
        return base.filter(
            date_deb__lte=fin
        ).filter(
            Q(**{'suivi__date_fin_suivi__isnull': True}) |
            Q(**{'suivi__date_fin_suivi__gte': debut})
        ).exclude(**{'suivi__motif_fin_suivi': 'erreur'})

    @classmethod
    def suivis_nouveaux(cls, annee, mois):
        base = cls.objects.annotate(
            date_dem=F('suivi__date_demande')
        ).prefetch_related('membres')
        return base.filter(**{
            'date_dem__year': annee, 'date_dem__month': mois,
        }).exclude(**{'suivi__motif_fin_suivi': 'erreur'})

    @classmethod
    def suivis_termines(cls, annee, mois):
        base = cls.objects.all().prefetch_related('membres')
        return base.filter(**{
            'suivi__date_fin_suivi__year': annee,
            'suivi__date_fin_suivi__month': mois,
        }).exclude(**{'suivi__motif_fin_suivi': 'erreur'})

    def can_be_reactivated(self, user):
        return user.has_perm("aemo.change_famille") and self.suivi.date_fin_suivi is not None

    def anonymiser(self):
        # Famille
        self.nom = random_string_generator()
        self.rue = ''
        self.telephone = ''
        self.remarques = ''
        self.archived_at = timezone.now()
        self.save()

        # Personne
        for personne in self.membres.all().select_related('role'):
            if personne.role.nom == 'Enfant suivi':
                personne.nom = random_string_generator()
                personne.prenom = random_string_generator()
                personne.validite = None
                personne.contractant = False
                fields = ['rue', 'npa', 'localite', 'telephone', 'email', 'remarque', 'remarque_privee',
                          'profession', 'filiation', 'allergies', 'employeur', 'permis', 'animaux']
                [setattr(personne, field, '') for field in fields]
                personne.save()
                personne.reseaux.clear()
                if hasattr(personne, 'formation'):
                    personne.formation.cercle_scolaire = None
                    fields = ['college', 'classe', 'enseignant', 'creche', 'creche_resp', 'entreprise',
                              'maitre_apprentissage', 'remarque']
                    [setattr(personne.formation, field, '') for field in fields]
                    personne.formation.save()
            else:
                personne.delete()
        # Suivi
        fields = ['difficultes', 'aides', 'competences', 'autres_contacts', 'disponibilites', 'remarque',
                  'remarque_privee', 'motif_detail', 'referent_note', 'collaboration', 'ressources', 'crise',
                  'pers_famille_presentes', 'ref_presents', 'autres_pers_presentes']
        [setattr(self.suivi, field, '') for field in fields]
        self.suivi.save()

        # Related
        for doc in self.documents.all():
            doc.delete()
        for bilan in self.bilans.all():
            bilan.delete()
        for rapport in self.rapports.all():
            rapport.delete()
        self.prestations.all().update(texte='')
        for prestation in self.prestations.exclude(fichier=''):
            prestation.fichier.delete()
            prestation.save()


class PersonneManager(models.Manager):

    def create_personne(self, **kwargs):
        kwargs.pop('_state', None)
        pers = self.create(**kwargs)
        return self.add_formation(pers)

    def add_formation(self, pers):
        if pers.role.nom == 'Enfant suivi' and not hasattr(pers, 'formation'):
            moins_de_4ans = bool(pers.age and pers.age < 4)
            Formation.objects.create(
                personne=pers, statut='pre_scol' if moins_de_4ans else ''
            )
            if moins_de_4ans:
                try:
                    pers.famille.suivi.demande_prioritaire = True
                    pers.famille.suivi.save()
                except AttributeError:
                    pass
        return pers


class Personne(models.Model):
    """ Classe de base des personnes """
    created_at = models.DateTimeField(auto_now_add=True, editable=False, null=True)

    nom = models.CharField('Nom', max_length=30)
    prenom = models.CharField('Prénom', max_length=30, blank=True)
    date_naissance = models.DateField('Date de naissance', null=True, blank=True)
    genre = models.CharField('Genre', max_length=1, choices=(('M', 'M'), ('F', 'F')), default='M')
    rue = models.CharField('Rue', max_length=60, blank=True)
    npa = models.CharField('NPA', max_length=4, blank=True)
    localite = models.CharField('Localité', max_length=30, blank=True)
    telephone = models.CharField('Tél.', max_length=60, blank=True)
    email = models.EmailField('Courriel', blank=True)
    pays_origine = CountryField('Nationalité', blank=True)
    remarque = models.TextField(blank=True)
    remarque_privee = models.TextField('Remarque privée', blank=True)
    famille = models.ForeignKey(Famille, on_delete=models.CASCADE, related_name='membres')
    role = models.ForeignKey('Role', on_delete=models.PROTECT)
    profession = models.CharField('Profession', max_length=50, blank=True)
    filiation = models.CharField('Filiation', max_length=80, blank=True)
    decedee = models.BooleanField('Cette personne est décédée', default=False)
    reseaux = models.ManyToManyField(Contact, blank=True)
    allergies = models.TextField('Allergies', blank=True)
    # Champs spécifiques SIFP
    employeur = models.CharField('Adresse empl.', max_length=50, blank=True)
    permis = models.CharField('Permis/séjour', max_length=30, blank=True)
    validite = models.DateField('Date validité', blank=True, null=True)
    animaux = models.BooleanField('Animaux', default=None, null=True)

    objects = PersonneManager()

    class Meta:
        verbose_name = 'Personne'
        ordering = ('nom', 'prenom')

    def __str__(self):
        return '{} {}'.format(self.nom, self.prenom)

    @property
    def nom_prenom(self):
        return str(self)

    @property
    def adresse(self):
        return format_adresse(self.rue, self.npa, self.localite)

    @property
    def age(self):
        return self.age_a(date.today())

    def age_str(self, date_=None, format_='auto'):
        """Renvoie '2 ans, 4 mois', '5 ans', '8 mois'.
        Param:
            'auto': application du format
                - 'jour' si l'âge < 30 jours
                - 'sem_jour' si l'âge < 77 jours (11 semaines)
                - 'mois_sem' si l'âge < 690 jours (23 mois)
                - 'an_mois': autres cas
            'jour': âge en jours
            'sem_jour': âge en semaines et jours
            'mois_sem': âge en mois et semaines
        """
        if not self.date_naissance:
            # Pourrait disparaître si date_naissance devient non null
            return ''
        if format_ not in ['auto', 'jour', 'sem_jour', 'mois_sem', 'an_mois']:
            raise ValueError('Paramètre erroné')
        age_jours = self.age_jours(date_)

        if (age_jours < 30 and format_ == 'auto') or format_ == 'jour':
            age_str = f"{age_jours} jour{'s' if age_jours > 1 else ''}"
        elif (age_jours < 77 and format_ == 'auto') or format_ == 'sem_jour':
            sem, jours = age_jours // 7, age_jours % 7
            age_str = f"{sem} sem."
            age_str += f" {jours} jour{'s' if jours > 1 else ''}" if jours > 0 else ''
        elif (age_jours < 690 and format_ == 'auto') or format_ == 'mois_sem':
            mois, sem = age_jours // 30, int((age_jours % 30)/7)
            age_str = f"{mois} mois"
            age_str += f" {sem} sem." if sem > 0 else ''
        else:
            ans, mois = int(age_jours / 365.25), int((age_jours % 365.25) / 30)
            age_str = f"{ans} an{'s' if ans > 1 else ''}"
            age_str += " %d mois" % mois if mois > 0 else ''
        return age_str

    def age_a(self, date_):
        if not self.date_naissance:
            # Pourrait disparaître si date_naissance devient non null
            return None
        age = (date_ - self.date_naissance).days / 365.25
        return int(age * 10) / 10  # 1 décimale arrondi vers le bas

    def age_jours(self, date_=None):
        if date_ is None:
            date_ = date.today()
        return (date_ - self.date_naissance).days

    def age_mois(self, date_=None):
        """Âge en mois à la date_"""
        if date_ is None:
            date_ = date.today()
        if not self.date_naissance or date_ < self.date_naissance:
            return None
        return self.age_jours(date_) / (365 / 12)

    @property
    def localite_display(self):
        return '{} {}'.format(self.npa, self.localite)

    @property
    def edit_url(self):
        return reverse('personne-edit', args=[self.famille_id, self.pk])

    def can_edit(self, user):
        return self.famille.can_edit(user)

    def can_be_deleted(self, user):
        return self.famille.can_be_deleted(user)


class Formation(models.Model):

    FORMATION_CHOICES = (
        ('pre_scol', 'Pré-scolaire'),
        ('cycle1', 'Cycle 1'),
        ('cycle2', 'Cycle 2'),
        ('cycle3', 'Cycle 3'),
        ('apprenti', 'Apprentissage'),
        ('etudiant', 'Etudiant'),
        ('en_emploi', 'En emploi'),
        ('sans_emploi', 'Sans emploi'),
        ('sans_occupation', 'Sans occupation'),
    )

    personne = models.OneToOneField(Personne, on_delete=models.CASCADE)

    statut = models.CharField('Scolarité', max_length=20, choices=FORMATION_CHOICES, blank=True)
    cercle_scolaire = models.ForeignKey(
        CercleScolaire, blank=True, null=True, on_delete=models.SET_NULL,
        related_name='+', verbose_name='Cercle scolaire'
    )
    college = models.CharField('Collège', max_length=50, blank=True)
    classe = models.CharField('Classe', max_length=50, blank=True)
    enseignant = models.CharField('Enseignant', max_length=50, blank=True)

    creche = models.CharField('Crèche', max_length=50, blank=True)
    creche_resp = models.CharField('Resp.crèche', max_length=50, blank=True)

    entreprise = models.CharField('Entreprise', max_length=50, blank=True)
    maitre_apprentissage = models.CharField("Maître d'appr.", max_length=50, blank=True)

    remarque = models.TextField(blank=True)

    class Meta:
        verbose_name = 'Scolarité'

    def __str__(self):
        return 'Scolarité de {}'.format(self.personne)

    def can_edit(self, user):
        return self.personne.famille.can_edit(user)

    def delete(self, **kwargs):
        if self.personne:
            raise IntegrityError
        else:
            super().delete(**kwargs)

    @property
    def sse(self):
        return "Champ à définir"

    def pdf_data(self):
        if self.personne.formation.statut == 'pre_scol':
            data = [
                ['Crèche: {}'.format(self.creche), 'responsable: {}'.format(self.creche_resp)],
            ]
        elif self.personne.formation.statut in ['cycle1', 'cycle2', 'cycle3', 'etudiant']:
            data = [
                ['Cercle: {}'.format(self.cercle_scolaire), 'collège: {}'.format(self.college)],
                ['Classe: {}'.format(self.classe), 'Enseignant-e: {}'.format(self.enseignant)]
            ]

        elif self.personne.formation.statut == 'apprenti':
            data = [
                ['Employeur: {}'.format(self.entreprise), 'resp. apprenti-e: {}'.format(self.maitre_apprentissage)],
            ]
        else:
            data = [self.personne.formation.statut]
        return data

    def info_scol(self):
        if self.statut == 'pre_scol':
            creche = f"Crèche: {self.creche}" if self.creche else ''
            resp = f" - resp.: {self.creche_resp}" if self.creche_resp else ''
            data = ''.join([creche, resp])
        elif self.statut in ['cycle1', 'cycle2', 'cycle3', 'etudiant']:
            college = f"Collège: {self.college}" if self.college else ''
            classe = f" - classe: {self.classe}" if self.classe else ''
            ens = f" - ens.: {self.enseignant}" if self.enseignant else ''
            data = ''.join([college, classe, ens])
        elif self.statut == 'apprenti':
            emp = f"Employeur: {self.entreprise}" if self.entreprise else ''
            resp = f" - resp.: {self.maitre_apprentissage}" if self.maitre_apprentissage else ''
            data = ''.join([emp, resp])
        elif self.statut:
            data = self.get_statut_display() if self.statut else ''
        else:
            data = "Formation: aucune info. saisie"
        return data


class Document(models.Model):
    famille = models.ForeignKey(Famille, related_name='documents', on_delete=models.CASCADE)
    fichier = models.FileField("Nouveau fichier", upload_to='doc')
    titre = models.CharField(max_length=100)

    class Meta:
        unique_together = ('famille', 'titre')

    def __str__(self):
        return self.titre

    def delete(self):
        Path(self.fichier.path).unlink(missing_ok=True)
        return super().delete()

    def can_edit(self, user):
        return self.famille.can_edit(user)

    def can_be_deleted(self, user):
        return self.famille.can_be_deleted(user)


class Bilan(models.Model):
    famille = models.ForeignKey(Famille, related_name='bilans', on_delete=models.CASCADE)
    date = models.DateField("Date du bilan")
    auteur = models.ForeignKey(
        Utilisateur, related_name='bilans', on_delete=models.PROTECT, null=True,
    )
    objectifs = models.TextField("Objectifs")
    rythme = models.TextField("Rythme et fréquence")
    sig_famille = models.BooleanField("Apposer signature de la famille", default=True)
    sig_interv = models.ManyToManyField(Utilisateur, blank=True, verbose_name="Signature des intervenants")
    fichier = models.FileField("Fichier/image", blank=True, upload_to='bilans')

    delete_urlname = 'famille-bilan-delete'

    def __str__(self):
        return "Bilan du {} pour la famille {}".format(self.date, self.famille)

    def get_absolute_url(self):
        return reverse('famille-bilan-view', args=[self.famille_id, self.pk])

    def get_print_url(self):
        return reverse('print-bilan', args=[self.pk])

    def edit_url(self):
        return reverse('famille-bilan-edit', args=[self.famille_id, self.pk])

    def can_edit(self, user):
        return self.famille.can_edit(user) or user in self.famille.suivi.intervenants.all()

    def title(self):
        return f"Bilan du {format_d_m_Y(self.date)}"


class Rapport(models.Model):
    """
    Rapport est l'appellation historique, Résumé étant l'appellation moderne (dès février 2023).
    Les champs «techniques» n'ont pas été renommés.
    """
    famille = models.ForeignKey(Famille, related_name='rapports', on_delete=models.CASCADE)
    date = models.DateField("Date du résumé")
    auteur = models.ForeignKey(Utilisateur, on_delete=models.PROTECT)
    pres_interv = models.ManyToManyField(
        Utilisateur, blank=True, verbose_name="Intervenants cités dans le résumé", related_name='+'
    )
    sig_interv = models.ManyToManyField(
        Utilisateur, blank=True, verbose_name="Signature des intervenants", related_name='+'
    )
    situation = models.TextField("Situation / contexte familial", blank=True)
    observations = models.TextField("Observations, évolution et hypothèses", blank=True)
    projet = models.TextField("Perspectives d’avenir", blank=True)

    def __str__(self):
        return "Résumé du {} pour la famille {}".format(format_d_m_Y(self.date), self.famille)

    def get_absolute_url(self):
        return reverse('famille-rapport-view', args=[self.famille.pk, self.pk])

    def edit_url(self):
        return reverse('famille-rapport-edit', args=[self.famille.pk, self.pk])

    def get_print_url(self):
        return reverse('famille-rapport-print', args=[self.famille.pk, self.pk])

    def can_edit(self, user):
        return self.famille.can_edit(user) or user in self.famille.suivi.intervenants.all()

    def can_delete(self, user):
        return self.famille.can_edit(user)

    def title(self):
        return f"Résumé du {format_d_m_Y(self.date)}"

    def intervenants(self):
        return [i.intervenant for i in self.famille.interventions_actives(self.date)]


class Niveau(models.Model):
    INTERV_CHOICES = [(0, '0'), (1, '1'), (2, '2'), (3, '3')]
    famille = models.ForeignKey(
        Famille, related_name='niveaux', on_delete=models.CASCADE, verbose_name="Famille"
    )
    niveau_interv = models.PositiveSmallIntegerField("Niveau d’intervention", choices=INTERV_CHOICES)
    date_debut = models.DateField('Date début', blank=True, null=True)
    date_fin = models.DateField('Date fin', blank=True, null=True)

    def __str__(self):
        return (
            f"{self.famille.nom} - {self.niveau_interv} - du {format_d_m_Y(self.date_debut)} au "
            f"{format_d_m_Y(self.date_fin)}"
        )


class LibellePrestation(models.Model):
    code = models.CharField('Code', max_length=6, unique=True)
    nom = models.CharField('Nom', max_length=30)
    actes = models.TextField('Actes à prester', blank=True)

    class Meta:
        ordering = ('code',)

    def __str__(self):
        return self.nom


class Prestation(models.Model):
    famille = models.ForeignKey(Famille, related_name='prestations', null=True, blank=True,
                                on_delete=models.SET_NULL, verbose_name="Famille")
    auteur = models.ForeignKey(Utilisateur, on_delete=models.PROTECT, verbose_name='auteur')
    intervenants = models.ManyToManyField(Utilisateur, related_name='prestations')
    date_prestation = models.DateField("date de l’intervention")
    duree = models.DurationField("durée")
    lib_prestation = models.ForeignKey(
        LibellePrestation, on_delete=models.SET_NULL, null=True, default=None,
        related_name='prestations_%(app_label)s'
    )
    # Nombre de familles actives au moment de la prestation, utilisé pour ventiler les heures
    # dans les statistiques.
    familles_actives = models.PositiveSmallIntegerField(blank=True, default=0)
    texte = models.TextField('Contenu', blank=True)
    manque = models.BooleanField('Rendez-vous manqué', default=False)
    fichier = models.FileField('Fichier/image', upload_to='prestations', blank=True)

    # Nbre de jours maximum après fin d'un mois où il est encore possible de saisir
    # des données du mois précédent.
    DELAI_SAISIE_SUPPL = 14

    class Meta:
        ordering = ('-date_prestation',)
        permissions = (
            ('edit_prest_prev_month', 'Modifier prestations du mois précédent'),
        )

    def __str__(self):
        if self.famille:
            return f'Prestation pour la famille {self.famille} le {self.date_prestation} : {self.duree}'
        return f'Prestation générale le {self.date_prestation} : {self.duree}'

    def can_edit(self, user):
        return (
            (user == self.auteur or user in self.intervenants.all() or
             user.has_perm('aemo.edit_prest_prev_month')
            ) and self.check_date_allowed(user, self.date_prestation)
        )

    def edit_url(self):
        return reverse('prestation-edit', args=[self.famille.pk if self.famille else 0, self.pk])

    def save(self, *args, **kwargs):
        if self.famille is None:
            self.familles_actives = Famille.actives(self.date_prestation).count()
        super().save(*args, **kwargs)

    @classmethod
    def check_date_allowed(cls, user, dt):
        """Contrôle si la date `dt` est située dans le mois courant + DELAI_SAISIE_SUPPL."""
        today = date.today()
        delai = cls.DELAI_SAISIE_SUPPL
        if user.has_perm('aemo.edit_prest_prev_month'):
            delai += 31
        if today.day <= delai:
            return dt >= (today - timedelta(days=delai + 1)).replace(day=1)
        else:
            return dt.year == today.year and dt.month == today.month

    @classmethod
    def prestations_historiques(cls, prestations):
        """
        Renvoie un queryset avec toutes les prestations regroupées par annee/mois
        et le total correspondant.
        """
        return prestations.annotate(
            annee=ExtractYear('date_prestation'),
            mois=ExtractMonth('date_prestation')
        ).values('annee', 'mois').order_by('-annee', '-mois').annotate(total=Sum('duree'))

    @classmethod
    def temps_total(cls, prestations, tous_utilisateurs=False):
        """
        Renvoie le temps total des `prestations` (QuerySet) sous forme de chaîne '12:30'.
        Si tous_utilisateurs = True, multiplie le temps de chaque prestation par son
        nombre d'intervenants.
        """
        if prestations.count() > 0:
            if tous_utilisateurs:
                duree = prestations.annotate(num_util=Count('intervenants')).aggregate(
                    total=Sum(F('duree') * F('num_util'), output_field=models.DurationField())
                )['total'] or timedelta()
            else:
                duree = prestations.aggregate(total=Sum('duree'))['total'] or timedelta()
        else:
            duree = timedelta()
        return duree

    @classmethod
    def temps_total_general(cls, annee, familles=True):
        """
        Renvoie le temps total des prestations (familiales ou générales selon
        familles) pour l'année civile `annee`.
        """
        prest_tot = cls.objects.filter(
            famille__isnull=not familles,
            date_prestation__year=annee
        ).aggregate(total=Sum('duree'))['total'] or timedelta()
        return prest_tot

    @classmethod
    def temps_total_general_fam_gen(cls, annee):
        """
        Renvoie le temps total des prestations familiales ET générales pour l'année civile `annee`.
        """
        prest_tot = cls.objects.filter(
            date_prestation__year=annee
        ).aggregate(total=Sum('duree'))['total'] or timedelta()
        return prest_tot

    @classmethod
    def temps_totaux_mensuels(cls, annee):
        """
        Renvoie une liste du total mensuel de toutes les prestations familiales
        (sans prestations générales) pour l'année en cours (de janv. à déc.).
        """
        data = []
        for month in range(1, 13):
            tot = cls.objects.filter(
                famille__isnull=False,
                date_prestation__year=annee, date_prestation__month=month,
            ).aggregate(total=Sum('duree'))['total'] or timedelta()
            data.append(tot)
        return data


class JournalAcces(models.Model):
    """Journalisation des accès aux familles."""
    famille = models.ForeignKey(Famille, on_delete=models.CASCADE, related_name='+')
    utilisateur = models.ForeignKey(Utilisateur, on_delete=models.CASCADE, related_name='+')
    ordinaire = models.BooleanField(default=True)
    quand = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"Accès de «{self.utilisateur}» à la famille «{self.famille}», le {self.quand}"


class Etape:
    """
        1. Numéro d'ordre pour tri dans la vue
        2. Code
        3. Affichage dans tableau
        4. Nom visible de l'étape
        5. Code de l'étape suivante
        6. Délai jusqu'à l'échéance (en jours)
        7. Code de l'étape précédente
        8. Date obligatoire précédente
        9. Saisie obligatoire ou non (True / False)
    """

    def __init__(self, num, code, abrev, nom, suivante, delai, precedente, preced_oblig, oblig):
        self.num = num
        self.code = code
        self.abrev = abrev
        self.nom = nom
        self.suivante = suivante
        self.delai = delai
        self.precedente = precedente
        self.preced_oblig = preced_oblig
        self.oblig = oblig

    def __str__(self):
        return self.nom

    def __repr__(self):
        return '<Etape - {}/{}>'.format(self.num, self.code)

    def date(self, suivi):
        return getattr(suivi, 'date_{}'.format(self.code))

    def delai_depuis(self, suivi, date_base):
        """Délai de cette étape à partir de la date date_base (en général date précédente étape)."""
        if not date_base:
            return None
        return date_base + timedelta(days=self.delai)

    def date_nom(self):
        return 'date_{}'.format(self.code)

    def etape_suivante(self, suivi):
        return Suivi.WORKFLOW[self.suivante] if self.suivante else None

    def etape_precedente(self):
        return Suivi.WORKFLOW[self.precedente] if self.precedente else None


class EtapeDebutSuivi(Etape):
    def date(self, suivi):
        return suivi.debut_suivi_selon_niveau


class EtapeBilan(Etape):
    delai_standard = 3 * 30  # 3 mois
    delai_niveau3 = 30  # 1 mois

    def date(self, suivi):
        """Date du dernier bilan"""
        if self.etape_suivante(suivi).code == 'bilan_suivant':
            return None
        return suivi.date_dernier_bilan()

    def etape_suivante(self, suivi):
        # Soit bilan suivant, soit rapport
        if suivi.date_dernier_rapport():
            return Suivi.WORKFLOW['resume']
        date_bilan = suivi.date_dernier_bilan()
        prochain_rapport = suivi.date_prochain_rapport()
        if date_bilan and prochain_rapport and (prochain_rapport - date_bilan) < timedelta(days=4 * 30):
            return Suivi.WORKFLOW['resume']
        else:
            return Suivi.WORKFLOW['bilan_suivant']

    def delai_depuis(self, suivi, date_base):
        """Délai de cette étape à partir de la date date_base (en général date précédente étape)."""
        if not date_base:
            return None
        base = suivi.date_dernier_bilan() or date_base
        delai = self.delai_niveau3 if suivi.famille.niveau_actuel() == 3 else self.delai_standard
        return base + timedelta(days=delai)


class EtapeResume(Etape):
    def date(self, suivi):
        """Date du dernier rapport"""
        return suivi.date_dernier_rapport()


class EtapeFin(Etape):
    delai_standard = 365 + 180  # 18 mois
    delai_niveau3 = 270  # 9 mois

    def delai_depuis(self, suivi, *args):
        if not suivi.date_debut_suivi:
            return None
        debut_suivi = suivi.debut_suivi_selon_niveau
        delai = self.delai_niveau3 if suivi.famille.niveau_actuel() == 3 else self.delai_standard
        return debut_suivi + timedelta(days=delai)


Equipe = namedtuple("Equipe", ['code', 'nom', 'perm'])

EQUIPES = [
    Equipe(code='montagnes', nom='Montagnes et V-d-T', perm=None),
    Equipe(code='littoral', nom='Littoral et V-d-R', perm=None),
    # Anciennes équipes, conservées pour anciens dossiers
    Equipe(code='neuch_ville', nom='Neuchâtel-ville (archives)', perm=None),
    Equipe(code='litt_est', nom='Littoral Est (archives)', perm=None),
    Equipe(code='litt_ouest', nom='Littoral Ouest (archives)', perm=None),
]


class Suivi(models.Model):
    WORKFLOW = OrderedDict([
        ('demande', Etape(
            1, 'demande', 'dem', 'Demande déposée', 'debut_evaluation', 0, 'demande', 'demande', True
        )),
        ('debut_evaluation', Etape(
            2, 'debut_evaluation', 'deb_eva', "Début de l’évaluation", 'fin_evaluation', 40, 'demande', 'demande', True
        )),
        ('fin_evaluation', Etape(
            3, 'fin_evaluation', "fin_eva", "Fin de l’évaluation", 'debut_suivi', 40,
            'debut_evaluation', 'debut_evaluation', True
        )),
        ('debut_suivi', EtapeDebutSuivi(
            4, 'debut_suivi', "dsuiv", 'Début du suivi', 'bilan_suivant', 40, 'fin_evaluation', 'fin_evaluation', False
        )),
        ('bilan_suivant',  EtapeBilan(
            5, 'bilan_suivant', "bil", 'Bilan suivant', 'resume', 90, 'debut_suivi', 'fin_evaluation', False
        )),
        ('resume', EtapeResume(
            6, 'resume', 'rés', 'Résumé', 'fin_suivi', 90, 'bilan_suivant', 'fin_evaluation', False
        )),
        ('fin_suivi', EtapeFin(
            7, 'fin_suivi', "fsuiv", 'Fin du suivi', 'archivage', 365 + 180, 'resume', 'fin_evaluation', True
        )),
        ('archivage', Etape(
            8, 'arch_dossier', 'arch', 'Archivage du dossier', None, 0, 'fin_suivi', 'fin_suivi', False
        )),
    ])

    EQUIPES_CHOICES = [(equ.code, equ.nom) for equ in EQUIPES]

    famille = models.OneToOneField(Famille, on_delete=models.CASCADE)
    equipe = models.CharField('Équipe', max_length=15, choices=EQUIPES_CHOICES)
    heure_coord = models.BooleanField("Heure de coordination", default=False)
    difficultes = models.TextField("Difficultés", blank=True)
    aides = models.TextField("Aides souhaitées", blank=True)
    competences = models.TextField('Ressources/Compétences', blank=True)
    dates_demande = models.CharField('Dates', max_length=128, blank=True)
    autres_contacts = models.TextField("Autres services contactés", blank=True)
    disponibilites = models.TextField('Disponibilités', blank=True)
    remarque = models.TextField(blank=True)
    remarque_privee = models.TextField('Remarque privée', blank=True)
    service_orienteur = models.CharField(
        "Orienté vers l’AEMO par", max_length=15, choices=choices.SERVICE_ORIENTEUR_CHOICES, blank=True
    )
    service_annonceur = models.CharField('Service annonceur', max_length=60, blank=True)
    motif_demande = ChoiceArrayField(
        models.CharField(max_length=60, choices=choices.MOTIF_DEMANDE_CHOICES),
        verbose_name="Motif de la demande", blank=True, null=True)
    motif_detail = models.TextField('Motif', blank=True)

    # Référents
    intervenants = models.ManyToManyField(
        Utilisateur, through='Intervenant', related_name='interventions', blank=True
    )
    ope_referent = models.ForeignKey(Contact, blank=True, null=True, related_name='+',
                                     on_delete=models.SET_NULL, verbose_name='as. OPE')
    ope_referent_2 = models.ForeignKey(
        Contact, blank=True, null=True, related_name='+', on_delete=models.SET_NULL,
        verbose_name='as. OPE 2'
    )
    mandat_ope = ChoiceArrayField(
        models.CharField(max_length=65, choices=choices.MANDATS_OPE_CHOICES, blank=True),
        verbose_name="Mandat OPE", blank=True, null=True,
    )
    sse_referent = models.ForeignKey(Contact, blank=True, null=True, related_name='+',
                                     on_delete=models.SET_NULL, verbose_name='SSE')
    referent_note = models.TextField('Autres contacts', blank=True)
    collaboration = models.TextField('Collaboration', blank=True)
    ressource = models.TextField('Ressource', blank=True)
    crise = models.TextField('Gestion de crise', blank=True)

    date_demande = models.DateField("Demande déposée le", blank=True, null=True, default=None)
    date_debut_evaluation = models.DateField("Début de l’évaluation le", blank=True, null=True, default=None)
    date_fin_evaluation = models.DateField("Fin de l’évaluation le", blank=True, null=True, default=None)
    date_debut_suivi = models.DateField("Début du suivi le", blank=True, null=True, default=None)
    date_fin_suivi = models.DateField("Fin du suivi le", blank=True, null=True, default=None)

    demande_prioritaire = models.BooleanField("Demande prioritaire", default=False)
    demarche = ChoiceArrayField(models.CharField(max_length=60, choices=choices.DEMARCHE_CHOICES, blank=True),
                                verbose_name="Démarche", blank=True, null=True)

    pers_famille_presentes = models.CharField('Membres famille présents', max_length=200, blank=True)
    ref_presents = models.CharField('Intervenants présents', max_length=250, blank=True)
    autres_pers_presentes = models.CharField('Autres pers. présentes', max_length=100, blank=True)
    motif_fin_suivi = models.CharField('Motif de fin de suivi', max_length=20,
                                       choices=choices.MOTIFS_FIN_SUIVI_CHOICES, blank=True)

    def __str__(self):
        return 'Suivi pour la famille {} '.format(self.famille)

    @property
    def date_fin_theorique(self):
        if self.date_fin_suivi:
            return self.date_fin_suivi
        if self.date_debut_suivi is None:
            return None
        return self.debut_suivi_selon_niveau + timedelta(days=548)  # env. 18 mois

    @cached_property
    def debut_suivi_selon_niveau(self):
        """
        Date de début de suivi tenant compte d'éventuel changement de niveau entre 2 et 3.
        """
        debut = self.date_debut_suivi
        if debut is None:
            return debut
        niv_prec = None
        for niv in sorted(self.famille.niveaux.all(), key=lambda niv: niv.date_debut):
            if (
                (niv_prec in [1, 2] and niv.niveau_interv == 3) or
                (niv_prec == 3 and niv.niveau_interv in [1, 2])
            ):
                debut = niv.date_debut
            niv_prec = niv.niveau_interv
        return debut

    def date_prochain_rapport(self):
        if self.date_debut_suivi is None or self.date_fin_suivi is not None:
            return None
        date_dernier_rapport = self.date_dernier_rapport()
        niveau = self.famille.niveau_actuel()
        if date_dernier_rapport is None:
            # Premier à 9 ou 18 mois, selon niveau
            delai = 30 * 9 if niveau == 3 else 365 + 182
            return self.date_debut_suivi + timedelta(days=delai)
        else:
            # Suivants tous les 9 ou 12 mois, selon niveau
            delai = 30 * 9 if niveau == 3 else 365
            return date_dernier_rapport + timedelta(days=delai)

    def date_dernier_rapport(self):
        # Using rapports.all() to leverage prefetchs
        debut_suivi = self.debut_suivi_selon_niveau
        return max([
            rapp.date for rapp in self.famille.rapports.all() if rapp.date > debut_suivi
        ], default=None)

    def date_dernier_bilan(self):
        debut_suivi = self.debut_suivi_selon_niveau
        # Using bilans.all() to leverage prefetchs
        return max([
            bilan.date for bilan in self.famille.bilans.all() if bilan.date > debut_suivi
        ], default=None)

    @property
    def ope_referent_display(self):
        return self.ope_referent.nom_prenom if self.ope_referent else '-'

    @cached_property
    def etape(self):
        """L’étape *terminée* du suivi."""
        for key, etape in reversed(self.WORKFLOW.items()):
            if key != 'archivage':
                if etape.date(self):
                    return etape

    @cached_property
    def etape_suivante(self):
        return self.etape.etape_suivante(self) if self.etape else None

    def date_suivante(self):
        etape_date = self.etape.date(self)
        if not self.etape_suivante or not etape_date:
            return None
        return self.etape_suivante.delai_depuis(self, etape_date)

    def get_mandat_ope_display(self):
        dic = dict(choices.MANDATS_OPE_CHOICES)
        return '; '.join([dic[value] for value in self.mandat_ope]) if self.mandat_ope else '-'

    def get_motif_demande_display(self):
        dic = dict(choices.MOTIF_DEMANDE_CHOICES)
        return '; '.join([dic[value] for value in self.motif_demande]) if self.motif_demande else ''

    @property
    def ope_referents(self):
        return [ope for ope in [self.ope_referent, self.ope_referent_2] if ope]


class IntervenantManager(models.Manager):
    def actifs(self, _date=None):
        return self.filter(Q(date_fin__isnull=True) | Q(date_fin__gt=_date or date.today()))


class Intervenant(models.Model):
    """
    Modèle M2M entre Suivi et Utilisateur (utilisé par through de Suivi.intervenants).
    """
    suivi = models.ForeignKey(Suivi, on_delete=models.CASCADE)
    intervenant = models.ForeignKey(Utilisateur, on_delete=models.CASCADE)
    role = models.ForeignKey(Role, on_delete=models.CASCADE)
    date_debut = models.DateField('Date début', default=timezone.now)
    # date_fin est utilisé pour les interventions s'arrêtant avant la fin du suivi.
    date_fin = models.DateField('Date fin', null=True, blank=True)

    objects = IntervenantManager()

    def __str__(self):
        return '{}, {} pour {}'.format(self.intervenant, self.role, self.suivi)

    def can_edit(self, user):
        return self.suivi.famille.can_edit(user)
