import numpy as np
from asgiref.sync import sync_to_async
from django.db.models import ManyToOneRel, Window, F, Q, Sum, Case, When, IntegerField, Prefetch
from django.db.models.functions import Rank
from django.forms import model_to_dict
from django.http import JsonResponse
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, viewsets, serializers, status
from rest_framework import viewsets, status
from rest_framework.views import APIView
from rest_framework.response import Response
import pandas as pd

from . import constants
from django.db.models import Q
from .filters import AdministreFilter, RelatedOrderingFilter, PosteFilter
from .paginations import HeavyDataPagination
from .serializers import AdministreSerializer, FileCompetenceSerializer, FmobSerializer, FormationEmploiSerializer, \
    PcpFeGroupeSerializer, \
    PosteSerializer, \
    NotationSerializer, MarqueSerializer, MarquesGroupeSerializer, FileSerializer, \
    FiliereSerializer, DomaineSerializer, FileSVSerializer, DecisionSerializer, \
    SousVivierAssociationSerializer, ScoringValidator, AlimentationReferentielSerializer
from .models import Administre, Competence, FMOB, FormationEmploi, PcpFeGroupe, Poste, Notation, \
    PreferencesListe, Marque, MarquesGroupe, \
    Filiere, Domaine, SousVivier, Grade, CustomUser, Decision, Calcul, SousVivierAssociation, \
    StatutPamChoices as StatutPam
from .utils import cleanString, impact_decisions
from .utils_extraction import to_table_administres, to_table_domaines, to_table_fe, to_table_filieres, \
    to_table_fmob, to_table_fonctions, to_table_garnisons, to_table_grades, \
    to_table_fmob, to_table_fonctions, to_table_garnisons, to_table_grades, \
    to_table_liste_preference, \
    to_table_groupesMarques, to_table_marques, to_table_postes, process_files, open_excel
from .utils_insertion import insert_Administre, insert_Domaine, insert_FMOB, insert_Filiere, insert_Fonction, \
    insert_FormationEmploi, insert_Garnison, insert_Grade, insert_MarquesGroupe, insert_Notation, \
    insert_Marque, insert_Poste, insert_liste_preference, insert_matching
from .utils_matching import matching_parfait, preprocess_matching
from .utils_calcul import lancer_calculs, notations
from .reporting import poste_vacant_vivier, reporting_taux_armement_pcp, reporting_taux_armement_gestionnaire, \
    reporting_suivi_pam_admin, \
    reporting_suivi_pam_poste
import json

from django.shortcuts import render
from django.contrib.auth.decorators import login_required


from datetime import datetime, date
from django.conf import settings
from django.db.transaction import atomic
from django.http import Http404
from rest_framework import serializers, status
from rest_framework.exceptions import APIException
from typing import Optional, Tuple
import logging
import requests
import time

logger = logging.getLogger(__name__)


def api_exception(status_code: int, message: Optional[str] = None) -> APIException:
    return type('MyException', (APIException,), {'status_code': status_code, 'default_detail': message})

# Vue de lancement pour le scoring
# Calcul les scores des administrés à muter pour les postes à pourvoir et stocke les résultats dans la table Calcul
class ScoringView(APIView):
    """
    Cette classe est dédiée au vue de lancement pour le scoring et au calcul des scores des administrés à muter avec les postes à pourvoir.
    """
    def __recuperer_chemin_requete(self, request) -> str:
        """ récupère et reconstruit le chemin de la requête (mais pas forcément tel que le client la connaît)

        :type request: rest_framework.request.Request
        :param request: Request contenant le port et l'adrresse ip du serveur

        :return:    - **return** (*string*): une chaine de caractère contenant le statut modifié du calcul.

        """

        port = request.get_port()
        return f"{request.scheme}://localhost{':' + port if port else ''}{request.path}"


    def __appeler_api_depiler_calculs(self, request):
        """ appelle l'API pour lancer le prochain calcul en attente, ce qui permet d'attribuer l'opération à un worker indéfini
        :type request: rest_framework.request.Request
        :param request: Request contenant le chemin de la requète
        """
        url = self.__recuperer_chemin_requete(request)
        headers = request.headers
        cookies = request.COOKIES
        try:
            requests.patch(url, headers=request.headers, cookies=request.COOKIES, timeout=0.001)
        except requests.exceptions.ReadTimeout:
            pass
        except Exception as e:
            logger.debug("échec de l'appel à %s: %s", url, e)

    @atomic
    def __creer_calcul(self, sv_id: str) -> Calcul:
        """ crée un calcul en base sauf s'il existe déjà un calcul en cours ou en attente pour le même sous-vivier
        Raises
        ------
        ApiException:
            un calcul est déjà en cours ou en attente pour le même sous-vivier
        IntegrityError:
            la création a échoué

        :type request: rest_framework.request.Request
        :type sv_id: str
        :param sv_id: clé primaire du sous vivier
        :return:  - **Object** (*Object*): retourne l'objet calcul crée
        """

        Statut = Calcul.Statut
        COL_ID     = 'sous_vivier_id'
        COL_STATUT = 'ca_statut'
        COL_DEBUT  = 'ca_date_debut'

        colonnes = (COL_ID, COL_DEBUT, COL_STATUT)
        df = pd.DataFrame.from_records(Calcul.objects.values_list(*colonnes), columns=colonnes)

        en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_ID].values
        if sv_id in en_cours:
            raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en cours pour ce sous-vivier')

        en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE][COL_ID].values
        if sv_id in en_attente:
            raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en attente pour ce sous-vivier')

        if en_attente.size:
            calcul_autorise = False
        else:
            nb_max = getattr(settings, 'MAX_CALCULS', None)
            calcul_autorise = not isinstance(nb_max, int) or len(en_cours) < nb_max

        date_debut = timezone.now()
        Calcul.objects.filter(Q(sous_vivier_id=sv_id) & ~Q(ca_statut__in=(Statut.EN_COURS, Statut.EN_ATTENTE))).delete()
        if calcul_autorise:
            return Calcul.objects.create(sous_vivier_id=sv_id, ca_date_debut=date_debut,ca_statut_pourcentage =0 , ca_statut=Statut.EN_COURS)
        return Calcul.objects.create(sous_vivier_id=sv_id, ca_date_debut=date_debut,ca_statut_pourcentage =0 ,ca_statut=Statut.EN_ATTENTE)


    @atomic
    def __modifier_statut_premier_calcul_en_attente(self) -> Optional[ Tuple[str, datetime] ]:
        """ lance le calcul en attente le plus ancien (si possible). Il ne s'agit que d'une modification en base.
        (ID de sous-vivier, date de début) du calcul lancé ou None si aucun calcul ne peut être lancé
        :type request: rest_framework.request.Request
        :type sv_id: str
        :param sv_id: clé primaire du sous vivier
        :type ca_date_debut : date
        :param ca_date_debut: date de début calcul en attente
        :return:  - **Object** (*Object*): retourne l'objet calcul crée
        Raises
        ------
        ApiException:
            un calcul est déjà en cours ou en attente pour le même sous-vivier
        IntegrityError:
            la création a échoué
        """

        Statut = Calcul.Statut
        COL_ID     = 'sous_vivier_id'
        COL_STATUT = 'ca_statut'
        COL_DEBUT  = 'ca_date_debut'

        colonnes = (COL_ID, COL_DEBUT, COL_STATUT)
        df = pd.DataFrame.from_records(Calcul.objects.order_by(COL_DEBUT).values_list(*colonnes), columns=colonnes)

        en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE]
        if en_attente.empty:
            return None

        en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_ID].values
        nb_max = getattr(settings, 'MAX_CALCULS', None)
        calcul_autorise = not isinstance(nb_max, int) or en_cours.size < nb_max
        if not calcul_autorise:
            return None

        a_lancer = en_attente.iloc[0]
        sv_id = a_lancer[COL_ID]
        date_attente = a_lancer[COL_DEBUT]
        date_debut = timezone.now()

        # la date de début est mise à jour aussi pour éviter de compter le temps d'attente
        mis_a_jour = (
            Calcul.objects
                  .filter(sous_vivier_id=sv_id, ca_date_debut=date_attente, ca_statut=Statut.EN_ATTENTE)
                  .update(ca_date_debut=date_debut, ca_statut=Statut.EN_COURS)
        )
        return (sv_id, date_debut) if mis_a_jour else None


    def __executer_calcul(self, sv_id: str, date_debut: datetime) -> Calcul:
        """ exécute un calcul (ID de sous-vivier, date de début) du calcul lancé ou None si aucun calcul ne peut être lancé
        :type request: rest_framework.request.Request
        :type sv_id: str
        :param sv_id: clé primaire du sous vivier
        :type ca_date_debut : date
        :param ca_date_debut: date de début calcul en attente
        :return:  - **Object** (*Object*): retourne l'objet calcul ainsi que son statut
        """

        Statut = Calcul.Statut
        COL_ID     = 'sous_vivier_id'
        COL_STATUT = 'ca_statut'
        COL_DEBUT  = 'ca_date_debut'

        qs_calcul = Calcul.objects.filter(sous_vivier_id=sv_id, ca_date_debut=date_debut)
        qs_en_cours = qs_calcul.filter(ca_statut=Statut.EN_COURS)
        try:
            lancer_calculs(sv_id)
        except SystemExit:
            logger.info("OGURE M'A TUER")
            qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE)
            raise
        except Exception as e:
            qs_calcul = Calcul.objects.filter(sous_vivier_id=sv_id, ca_date_debut=date_debut)
            if str(e) == "Arret du calcul":
                qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.ARRETER)
                raise
            else :
                qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.ERREUR)
                raise
        qs_en_cours.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE)
        return qs_calcul.first()


    def get(self, request):
        """La fonction get vérifie s'il y a un calcul en cours et renvoie des informations sur le calcul.
        :type request: rest_framework.request.Request
        :param request: Request contenant l'identifiant du sous vivier
        :return:    - **return** (*JsonResponse*): json contenant les informations sur le calcul comme statut, date_debut, date_fin, administres et postes.
        """
        debut = None
        fin = None
        administres = 0
        postes = 0
        try:
            sv_id = request.query_params['sous_vivier_id']
            calcul = Calcul.objects.get(sous_vivier_id=sv_id)
            debut = calcul.ca_date_debut
            fin = calcul.ca_date_fin
            administres_statuts = ['A_MUTER']
            administres = Administre.objects.filter(sous_vivier_id=sv_id, a_statut_pam__in=administres_statuts).count()
            postes = Poste.objects.filter(Q(sous_vivier_id=sv_id) & (
                    Q(p_nb_p1__gt=0) | Q(p_nb_p2__gt=0) | Q(p_nb_p3__gt=0) | Q(p_nb_p4__gt=0) | Q(
                p_nb_vacant__gt=0))).count()
            statut = calcul.ca_statut
            statut_pourcentage = calcul.ca_statut_pourcentage
        except:
            statut = "AUCUN"
            statut_pourcentage = 0

        return JsonResponse(
            {"statut": statut, "statut_pourcentage": statut_pourcentage, "date_debut": debut, "date_fin": fin, "administres": administres, "postes": postes})

    def patch(self, request, id=None):
        """La fonction patch est une méthode privée qui execute des méthodes privées qui exécute le calcul et le dépile.
        :type request: rest_framework.request.Request
        :param request: Request contenant l'identifiant du sous vivier et la date de dbut du calcul
        """
        Statut = Calcul.Statut
        try:
            id_et_debut = self.__modifier_statut_premier_calcul_en_attente()
            if id_et_debut is None:
                return Response({'message': "aucun calcul n'a été lancé"})

            try:
                calcul = self.__executer_calcul(id_et_debut[0], id_et_debut[1])
                return Response({'statut': calcul.ca_statut if calcul else Statut.AUCUN})
            finally:
                self.__appeler_api_depiler_calculs(request)
        except (Http404, APIException):
            raise
        except SystemExit:
            raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, 'lancement du prochain calcul arrêté')
        except:
            message = 'impossible de lancer le prochain calcul en attente'
            logger.exception(message)
            raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, message)


    def post(self, request):
        """ lance le calcul sur un sous vivier
        :type request: rest_framework.request.Request
        :param request: Request contenant l'identifiant du sous vivier
        :type sv_id: str
        :param sv_id: clé primaire du sous vivier
        :return:    - **return** (*Response*): retourne le statut des postes
        """
        Statut = Calcul.Statut
        try:
            # validation
            validator = ScoringValidator(data=request.data)
            validator.is_valid(raise_exception=True)
            sv_id = validator.validated_data.get('sous_vivier_id')
            generics.get_object_or_404(SousVivier.objects, sv_id=sv_id)

            # calcul
            calcul = self.__creer_calcul(sv_id)
            if calcul.ca_statut == Statut.EN_COURS:
                try:
                    calcul = self.__executer_calcul(sv_id, calcul.ca_date_debut)
                finally:
                    self.__appeler_api_depiler_calculs(request)

            return Response({'statut': calcul.ca_statut if calcul else Statut.AUCUN})
        except (Http404, APIException):
            raise
        except SystemExit:
            raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, 'calcul arrêté')
        except:
            message = 'impossible de lancer le calcul'
            logger.exception(message)
            raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, message)

class ArretCalul(APIView):
    """
    Cette classe est dédiée a l'arret du calcul.
    """
    def post(self, request):
        """La fonction post change le statut d'un calcul à EN_ATTENTE_ARRET

        :type request: rest_framework.request.Request
        :param request: Request contenant l'identifiant du sous vivier

        :return:    - **return** (*json*): json contenant le statut modifié du calcul.
        """
        sv_id = request.data['sous_vivier_id']
        calcul =  Calcul.objects.get(sous_vivier_id=sv_id)
        if Calcul.objects.filter(sous_vivier_id=sv_id).exists() and calcul.ca_statut == 'EN_COURS':
            calcul.ca_statut = 'EN_ATTENTE_ARRET'
            calcul.save()
        elif Calcul.objects.filter(sous_vivier_id=sv_id).exists() and calcul.ca_statut == 'EN_ATTENTE':
            calcul.ca_statut = 'ARRETER'
            calcul.save()
        return JsonResponse(
            {"statut": calcul.ca_statut})

# Vue pour la fiche détaillée des administrés
class FicheDetailleeView(APIView):

    """
    Cette classe est dédiée au vue de la fiche détaillée des administrés
    """
    def get(self, request):
        """La fonction get recupére les infos administrés

        :type request: rest_framework.request.Request
        :param request: Request contenant l'identifiant de l'administré

        :return:    - **Response** (*objet*): informations concernant l'administré, informations sur le formulaire de mobilité.
        """
        res = {"result": "error"}
        if 'administre_id' in request.query_params:
            administre_id = request.query_params['administre_id']
            try:
                administre = Administre.objects.get(a_id_sap=administre_id)
                res = model_to_dict(administre)

                fmobs = FMOB.objects.filter(administre_id=administre_id).order_by('-fmob_millesime')

                res["fmob"] = None

                if fmobs.exists():
                    fmob_dict = model_to_dict(fmobs[0])
                    res["fmob"] = fmob_dict

                res["fe"] = None
                fe = list(FormationEmploi.objects.filter(fe_code=administre.formation_emploi_id).values(
                    *('fe_code', 'garnison__gar_code_postal', 'garnison__gar_lieu', 'fe_libelle')))
                # if fe.exists():
                #     fe = fe[0]
                #     garnison_dict = model_to_dict()
                #     res["garnison"] = garnison_dict
                if len(fe):
                    res["fe"] = fe[0]
                res["conjoint"] = None
                conjoint = Administre.objects.filter(a_id_sap=administre.a_sap_conjoint)
                if conjoint.exists():
                    print('has conjoint')
                    conjoint_dict = model_to_dict(conjoint[0])
                    conjoint_dict["fe"] = None
                    fe_conjoint = list(FormationEmploi.objects.filter(fe_code=conjoint[0].formation_emploi_id).values(
                        *('fe_code', 'garnison__gar_code_postal', 'garnison__gar_lieu', 'fe_libelle')))
                    if len(fe):
                        conjoint_dict["fe"] = fe_conjoint[0]
                    res["conjoint"] = conjoint_dict

            except Exception as e:
                print(e)
                print(e.args)
                print(res)
                # TODO: return error

        return Response(res)

# Vue de calcul des indicateurs
# Renvoie les indicateurs pour les vues :
#          - taux armement FE pour les PCP
#          - taux armement FE pour les gestionnaires
#          - suivi pam des gestionnaires pour les administrés
#          - suivi pam des gestionnaires pour les postes
class ReportingView(APIView):
    """
    Cette classe est dédiée au vue du calcul des indicateurs
    - taux armement FE pour les PCP
    - taux armement FE pour les gestionnaires
    - suivi pam des gestionnaires pour les administrés
    - suivi pam des gestionnaires pour les postes

    """
    def get(self, request):
        """La fonction get renvoie les indicateurs

        :type request: rest_framework.request.Request
        :param request: Request contenant le type, sous_vivier id, niveau fonctinonel et l'id de la formation emploi.

        :return:        - **return** (*json*): Si le type est SUIVI_PAM_GESTIONNAIRE le json contiendra l'information sur le suivi pam des
        gestionnaires pour les administrés et les postes. Si le type est TAUX_ARMEMENT_FE le json contiendra les indicateurs sur les taux d'armement.


        """
        type = request.query_params["type"]

        sv_id = []
        if 'sv_id' in request.query_params:
            sv_id = request.query_params["sv_id"]

        if 'vue' in request.query_params:
            vue = request.query_params["vue"]

        if "nf" in request.query_params:
            nf = request.query_params.getlist("nf")
        else:
            nf = ['1A', '1B', '1C', '2.', '3A', '3B', '3B NFS', '4.', '5A', '5B', '5C', '6A', '6B']

        if 'fe_id' in request.query_params:  # a voir
            fe_id = request.query_params.getlist('fe_id')
        else:
            fe_id = list(FormationEmploi.objects.values_list('fe_code', flat=True))

        # Vues pour les indicateurs sur le suivi du pam
        if (type == "SUIVI_PAM_GESTIONNAIRE"):
            if "d_code" in request.query_params:
                d_id = request.query_params.getlist("d_code")
            else:
                filieres = list(SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('filiere_id',
                                                                                                           flat=True).distinct())
                d_id = list(Filiere.objects.filter(f_code__in=filieres).values_list('domaine_id', flat=True).distinct())
            if "f_code" in request.query_params:
                f_id = request.query_params.getlist("f_code")
            else:
                f_id = list(SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('filiere_id',
                                                                                                       flat=True).distinct())
            if "categorie" in request.query_params:
                categorie = request.query_params.getlist("categorie")
            else:
                categorie = list(
                    SousVivierAssociation.objects.filter(sous_vivier__sv_id=sv_id).values_list('sva_categorie',
                                                                                               flat=True).distinct())
            # résultats pour la vue suivi pam des gestionnaires pour les administrés
            nb_a_etudier, nb_a_muter, nb_a_maintenir, nb_non_etudie_administres, nb_a_partant, nb_a_non_dispo, nb_prepos_administres, nb_pos_administres, nb_omi_active_administres, nb_omi_en_cours_administres, reste_a_realiser_administres, reste_a_realiser_a_etudier, reste_a_realiser_a_muter = reporting_suivi_pam_admin(
                sv_id, f_id, d_id, nf, categorie)
            # résultats pour la vue suivi pam des gestionnaires pour les postes
            nb_p1, nb_p2, nb_p3, nb_p4, nb_gele, nb_non_etudie_postes, reste_a_realiser_postes, reste_a_realiser_p1, reste_a_realiser_p2, reste_a_realiser_p3, reste_a_realiser_p4, nb_prepos_postes, nb_pos_postes, nb_omi_active_postes, nb_omi_en_cours_postes = reporting_suivi_pam_poste(
                sv_id, f_id, d_id, nf, categorie)
            return JsonResponse(
                {"nb_a_etudier": nb_a_etudier, "nb_a_muter": nb_a_muter, "nb_a_maintenir": nb_a_maintenir,
                 "nb_a_partant": nb_a_partant, "nb_a_non_dispo": nb_a_non_dispo,
                 "nb_non_etudie_administres": nb_non_etudie_administres,
                 "nb_prepos_administres": nb_prepos_administres, "nb_pos_administres": nb_pos_administres,
                 "nb_omi_en_cours_administres": nb_omi_en_cours_administres,
                 "nb_omi_active_administres": nb_omi_active_administres,
                 "reste_a_realiser_administres": reste_a_realiser_administres,
                 "reste_a_realiser_a_etudier": reste_a_realiser_a_etudier,
                 "reste_a_realiser_a_muter": reste_a_realiser_a_muter,
                 "nb_p1": nb_p1, "nb_p2": nb_p2, "nb_p3": nb_p3, "nb_p4": nb_p4, "nb_gele": nb_gele,
                 "nb_non_etudie_postes": nb_non_etudie_postes, "reste_a_realiser_postes": reste_a_realiser_postes,
                 "reste_a_realiser_p1": reste_a_realiser_p1, "reste_a_realiser_p2": reste_a_realiser_p2,
                 "reste_a_realiser_p3": reste_a_realiser_p3, "reste_a_realiser_p4": reste_a_realiser_p4,
                 "nb_prepos_postes": nb_prepos_postes, "nb_pos_postes": nb_pos_postes,
                 "nb_omi_en_cours_postes": nb_omi_en_cours_postes, "nb_omi_active_postes": nb_omi_active_postes},
                safe=False)
        # Vues pour les indicateurs sur les taux d'armement
        elif (type == 'TAUX_ARMEMENT_FE'):
            if "d_code" in request.query_params:
                d_id = request.query_params.getlist("d_code")
            else:
                d_id = list(Domaine.objects.all().values_list('d_code', flat=True))
            if "f_code" in request.query_params:
                f_id = request.query_params.getlist("f_code")
            else:
                f_id = list(Filiere.objects.all().values_list('f_code', flat=True))
            if "categorie" in request.query_params:
                categorie = request.query_params.getlist("categorie")
            else:
                categorie = ['MDR', 'SOFF', 'OFF', 'OGX']

            if len(sv_id) == 0:
                # résultats pour la vue taux armement FE pour les PCP
                nb_militaires_actuel, nb_postes_actuel, ecart_actuel, taux_armement_actuel, nb_militaires_entrants, nb_militaires_sortants, nb_militaires_projete, taux_armement_projete, taux_armement_cible = reporting_taux_armement_pcp(
                    fe_id, f_id, d_id, nf, categorie)
            else:
                # résultats pour la vue taux armement FE pour les gestionnaires
                nb_militaires_actuel, nb_postes_actuel, ecart_actuel, taux_armement_actuel, nb_militaires_entrants, nb_militaires_sortants, nb_militaires_projete, taux_armement_projete, taux_armement_cible = reporting_taux_armement_gestionnaire(
                    fe_id, f_id, d_id, nf, categorie, sv_id)
            return JsonResponse({'nb_militaires_actuel': nb_militaires_actuel, "nb_postes_actuel": nb_postes_actuel,
                                 'ecart_actuel': ecart_actuel, 'taux_armement_actuel': taux_armement_actuel,
                                 'nb_militaires_entrants': nb_militaires_entrants,
                                 'nb_militaires_sortants': nb_militaires_sortants,
                                 'nb_militaires_projete': nb_militaires_projete,
                                 'taux_armement_projete': taux_armement_projete,
                                 'taux_armement_cible': taux_armement_cible}, safe=False)
        else:
            return JsonResponse({"autre vue": "autres indicateurs demandes"})


# Vue pour afficher le nombre de postes vacants par fe et sous-vivier
class PosteVacantView(APIView):
    """
     Cette classe est dédiée au vue pour afficher le nombre de postes vacants par fe et sous-vivier
    """

    def get(self, request):
        """La fonction get renvoie les postes vaccants

        :type request: rest_framework.request.Request
        :param request: Requset contenant l'identifiant de la formation emploi et l'identifiant du sous-vivier.


        :return:    - **return** (*json*): json contenant le nombre de poste vaccant du sous vivier.
        """

        fe_id = request.query_params["fe_id"]
        sv_id = request.query_params["sv_id"]
        nb_poste_vacant_vivier = poste_vacant_vivier(sv_id, fe_id)
        return JsonResponse({'nb_poste_vacant_vivier': nb_poste_vacant_vivier}, safe=False)


# Vue de chargement des sous-viviers :
#       - Charge et traite le fichier de définition des sous-viviers
#       - Attribue les sous-viviers présents aux administrés et postes correspondants
class ChargementSVView(APIView):
    """
    Cette classe est dédiée au vue de chargement des sous-viviers. Charge et traite le fichier de définition des sous-viviers et attribue les sous-viviers présents aux administrés et postes correspondants
    """

    serializer_class = FileSVSerializer

    def get(self, request):
        """La fonction get renvoie une reponse contenant ok

        :type request: rest_framework.request.Request
        :param request: Requset


        :return:    - **return** (*json*): json contenant "ok".
        """
        return Response({"get": "ok"})

    def post(self, request):
        """La fonction post charge les sous-viviers

        :type request: rest_framework.request.Request
        :param request: Request contenant le fichier SV

        :return:    - **Response** (*Response*): Reponse contient la liste des sous-viviers créés, ignorés et où il y a eu une erreur.
        """
        serializer = FileSVSerializer(data=request.data)

        if not serializer.is_valid():
            return Response(
                data=serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )

        sv_file = request.data['SV']

        sv_df = open_excel(sv_file, sheetname="GESTIONNAIRES", engine='openpyxl')

        sv = pd.DataFrame(columns=['gestionnaire_id_sap', 'f_code', 'asso_sv_categorie', 'arme'])

        for i in range(len(sv_df)):
            sv.loc[i] = [sv_df.iloc[i, 0], sv_df.iloc[i, 3], sv_df.iloc[i, 4], sv_df.iloc[i, 5]]

        sv.dropna(subset=['f_code'], inplace=True)
        sv['arme'] = sv['arme'].replace({np.nan: None})
        sv.reset_index(drop=True, inplace=True)
        sv['asso_sv_categorie'].replace({"SOUS-OFFICIER": "SOFF", "OFFICIER": "OFF", "MILITAIRE DU RANG": "MDR"},
                                        inplace=True)
        sv['sv_id'] = sv.f_code + '_' + sv.asso_sv_categorie
        sv.f_code = sv.f_code.apply(lambda x: x.split(','))
        sv = sv.explode(column='f_code')
        sv.reset_index(drop=True, inplace=True)
        errors = []
        created = []
        ignored = []
        sv.fillna(np.nan, inplace=True)
        sv.replace([np.nan], [None], inplace=True)
        print(sv.head())

        for i in range(len(sv)):
            try:
                filiere = Filiere.objects.get(f_code=sv.at[i, "f_code"])
                categorie = sv.at[i, "asso_sv_categorie"]
                asso_sv = SousVivierAssociation.objects.filter(sva_categorie=categorie,
                                                               filiere__f_code=sv.at[i, "f_code"])

                # Vérifier que filiere et catégorie n'ont pas déjà de sous vivier
                if asso_sv.count() == 0 and categorie is not None:
                    # Créer le sous-vivier correspondant dans la table SousVivier
                    sous_vivier = SousVivier(sv_id=sv.at[i, "sv_id"],
                                             sv_libelle=str(sv.at[i, "sv_id"]))
                    print(model_to_dict(sous_vivier))
                    sous_vivier.save()

                    print(sv.at[i, "sv_id"])

                    # Insertion du nouveau sous-vivier dans la table d'association (sans le droit arme dans un premier temps)
                    sva = SousVivierAssociation(sva_id=i, sous_vivier_id=sous_vivier.sv_id, filiere_id=filiere.f_code,
                                                sva_categorie=categorie, sva_arme=sv.at[i, "arme"])
                    sva.save()

                    # Mise à jour du sous_vivier_id pour les postes et administres ayant la filiere et la categorie en cours
                    Administre.objects.filter(a_categorie=categorie, a_filiere_id=filiere.f_code).update(
                        sous_vivier_id=sous_vivier.sv_id)
                    Poste.objects.filter(p_categorie=categorie, p_filiere_id=filiere.f_code).update(
                        sous_vivier_id=sous_vivier.sv_id)
                    created.append(str(sv.at[i, "asso_sv_categorie"]) + " " + str(sv.at[i, "f_code"]))

                else:
                    ignored.append(str(sv.at[i, "asso_sv_categorie"]) + " " + str(sv.at[i, "f_code"]))
                    # Si l'association appartient déjà à un sous_vivier, on ne fait rien
            except Filiere.DoesNotExist:
                errors.append("Filiere " + sv.at[i, 'f_code'] + "non retrouvée")

        return Response({"created": created, "ignored": ignored, "errors": errors})


# Vue de chargement des competences :
#       - Charge et traite les fichiers de compétences
#       - Attribue les compétences présentes aux administrés et postes correspondants
class ChargementCompetenceView(APIView):
    """
    Cette classe est dédiée au vue de chargement des competences. Charge et traite les fichiers de compétences.
    Attribue les compétences présentes aux administrés et postes correspondants

    """
    serializer_class = FileCompetenceSerializer

    def get(self, request):
        """La fonction get renvoie une reponse contenant ok

        :type request: rest_framework.request.Request
        :param request: Requset


        :return:    - **return** (*json*): json contenant "ok".
        """
        return Response({"get": "ok"})

    def post(self, request):
        """ La fonction post charge les competences.

        :type request: rest_framework.request.Request
        :param request: Request contenant le fichier de competence

        :return:    - **Response** (*Response*): Reponse contient un message de la réussite pour charger la compétence .
        """
        serializer = FileCompetenceSerializer(data=request.data)

        if not serializer.is_valid():
            return Response(
                data=serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )

        competence_file_v1 = request.data['competence_v1']
        competence_file_v2 = request.data['competence_v2']
        competence_poste_v1 = open_excel(competence_file_v1, sheetname='Feuil1', engine='openpyxl')
        competence_poste_v1.drop_duplicates(subset=['Macro compétence (0 à n par officier)'], inplace=True)
        competence_mili = open_excel(competence_file_v2, sheetname="MILITAIRE", engine='openpyxl')
        competence_mili.reset_index(drop=True, inplace=True)
        competence_poste = open_excel(competence_file_v2, sheetname='REO', engine='openpyxl')
        competence_poste.dropna(subset=['COMPETENCE 1'], inplace=True)
        competence_poste.reset_index(drop=True, inplace=True)

        print("Preprocessing OK")

        # Ajout des compétences du fichier version 1, dans la table Compétence
        for i in range(len(competence_poste_v1)):
            comp = Competence(comp_id=cleanString(competence_poste_v1.at[i, 'Macro compétence (0 à n par officier)']),
                              comp_libelle=competence_poste_v1.at[i, 'Macro compétence (0 à n par officier)'])
            comp.save()

        print("Ajout des compétences du fichier 1 dans la table Competence --> OK")

        # Ajout des compétences du fichier version 2, dans la table Compétence
        competence_mili_1 = competence_mili[['COMPETENCE 1']].drop_duplicates().reset_index(drop=True)
        competence_mili_2 = competence_mili[['COMPETENCE 2']].drop_duplicates().reset_index(drop=True).rename(
            columns={'COMPETENCE 2': "COMPETENCE 1"})
        competence_mili_3 = competence_mili[['COMPETENCE 2.1']].drop_duplicates().reset_index(drop=True).rename(
            columns={'COMPETENCE 2.1': "COMPETENCE 1"})
        competence_poste_1 = competence_poste[['COMPETENCE 1']].drop_duplicates(subset=["COMPETENCE 1"]).reset_index(
            drop=True)
        total_competences = pd.concat([competence_mili_1, competence_mili_2, competence_mili_3,
                                       competence_poste_1]).drop_duplicates().reset_index(drop=True)
        total_competences.dropna(inplace=True)
        total_competences.reset_index(drop=True, inplace=True)

        for i in range(len(total_competences)):
            competence = Competence(comp_id=cleanString(total_competences.at[i, "COMPETENCE 1"]),
                                    comp_libelle=total_competences.at[i, 'COMPETENCE 1'])
            competence.save()

        print('Ajout des compétences du fichier 2 dans la table Competence --> OK')

        # Ajout des compétences du fichier 2 dans la table Poste
        print(competence_poste)
        for i in range(len(competence_poste)):
            postes = Poste.objects.filter(p_code_fonction=competence_poste.at[i, 'CODE FONCTION'],
                                          formation_emploi_id=competence_poste.at[i, 'CODE FE'],
                                          p_domaine=competence_poste.at[i, 'DOM EIP'],
                                          p_filiere=competence_poste.at[i, "FIL EIP"],
                                          p_nf=competence_poste.at[i, 'NR EIP'])

            postes.update(competence_id=cleanString(competence_poste.at[i, "COMPETENCE 1"]))
        print('Ajout des compétences du fichier 2 dans la table Poste --> OK')
        # Ajout des compétences du fichier 2 dans la table d'association Administrés et Compétences
        liste_admin = []
        for i in range(len(competence_mili)):
            sap = competence_mili.at[i, 'N SAP']
            admin_competences = ""

            if not pd.isna(competence_mili.at[i, 'COMPETENCE 1']):
                ca1_id = cleanString(competence_mili.at[i, 'COMPETENCE 1'])
                admin_competences = admin_competences + ca1_id + ","

            if not pd.isna(competence_mili.at[i, 'COMPETENCE 2']):
                ca2_id = cleanString(competence_mili.at[i, 'COMPETENCE 2'])
                admin_competences = admin_competences + ca2_id + ","

            if not pd.isna(competence_mili.at[i, 'COMPETENCE 2.1']):
                ca3_id = cleanString(competence_mili.at[i, 'COMPETENCE 2.1'])
                admin_competences = admin_competences + ca3_id + ","

            liste_admin.append(Administre(a_id_sap=sap, a_liste_id_competences=admin_competences))

        Administre.objects.bulk_update(liste_admin, fields=['a_liste_id_competences'])
        print('Ajout des compétences du fichier 2 dans la table CompetenceAdministre --> OK')

        return JsonResponse({"msg": "Chargement competence ok"})


class CurrentUserView(APIView):
    """
    Cette classe est dédiée au vue de l'utilisateur courant:
    """

    def get(self, request):
        """La fonction get verifie si l'utilisateur est authentifié

        :type request: rest_framework.request.Request
        :param request: Request contenant l'identifiant de l'utilisateur


        :return:    - **Response** (*json*): json contenant l'identité et le role de l'utilisateur. En plus de la formation emploi group et le sous-vivier specifique à cet utilisateur.
        """
        if request.user.is_authenticated:
            current_user = CustomUser.objects.get(id=request.user.id)
            try:
                administre = model_to_dict(current_user.administre)
            except:
                administre = None
            current_user = model_to_dict(current_user, fields=[field.name for field in CustomUser._meta.get_fields() if
                                                               field.name not in ["password", "logentry", "groups",
                                                                                  "user_permissions"]])
            current_user['sous_viviers'] = list(SousVivier.objects.filter(gestionnaire_id=request.user.id).values())
            current_user['identite'] = administre
            try:
                pcpFeGroupe = model_to_dict(PcpFeGroupe.objects.get(gestionnaire_id=request.user.id))
            except:
                pcpFeGroupe = None

            current_user['fe_groupe'] = pcpFeGroupe
            current_user['role'] = "ADMINISTRATEUR" if request.user.is_superuser else "GESTIONNAIRE"
            return Response(current_user)
        return Response({"msg": "Authentification requise"})


class ReferencesView(APIView):

    """
    Cette classe est dédiée au vue de la reference.
    """
    def get(self, request):
        """
        La fonction get envoie les references

        :return:    - **JsonReponse** (*json*):  json contenant les groupes marques, les marques, les competences, les domaines et les filieres.
        """
        groupesMarques = [obj.as_dict() for obj in MarquesGroupe.objects.all().order_by('gm_code')]
        marques = [obj.as_dict() for obj in Marque.objects.all().order_by('mar_code')]
        competences = [obj.as_dict() for obj in Competence.objects.all().order_by('comp_id')]
        domaines = [obj.as_dict() for obj in Domaine.objects.all().order_by('d_code')]
        filieres = [obj.as_dict() for obj in Filiere.objects.all().order_by('domaine__d_code', 'f_code')]
        # TODO: Vérifier la présence de l'ordre sur les postes'
        grades = [obj.as_dict() for obj in Grade.objects.all()]
        FEs = [obj.as_dict() for obj in FormationEmploi.objects.all().order_by('fe_code')]

        return JsonResponse(
            {"groupesMarques": groupesMarques, "marques": marques,
             "domaines": domaines, "filieres": filieres,
             "grades": grades, "competences": competences, "FEs": FEs}, safe=False)


#  Vue pour l'alimentation et l'update en BDD :
#       - Charge et traite les fichiers de données
#       - Remplit la base de données pour les modèles Domaines, Filieres, Grade, Fonction, Garnison, FormationEmploi, Marque, MarquesGroupe, Administre, Poste, FMOB,
class AlimentationView(APIView):
    """
    Cette classe est dédiée au vue de l'alimentation et l'update en BDD. Charge et traite les fichiers de données et remplit la base de données pour les modèles Domaines, Filieres, Grade, Fonction, Garnison, FormationEmploi, Marque, MarquesGroupe, Administre, Poste, FMOB.
    """
    serializer_class = FileSerializer

    def get(self, request):
        """La fonction get renvoie une reponse contenant Formulaire d'alimentation d'OGURE NG

        :type request: rest_framework.request.Request
        :param request: Request


        :return:    - **return** (*json*): json contenant "Formulaire d'alimentation d'OGURE NG".
        """
        return Response("Formulaire d'alimentation d'OGURE NG")

    def post(self, request):
        """La fonction post charge les fichiers.

        :type request: rest_framework.request.Request
        :param request: Request contenant les fichiers

        :return:    - **Response** (*Response*): Reponse contient les erreurs de chargement des données .
        """
        # TODO: préparer une variable qui renvoie les erreurs sous 3 axes : données référentielles, administrés, postes
        serializer = FileSerializer(data=request.data)
        if not serializer.is_valid():
            return Response(
                data=serializer.errors,
                status=status.HTTP_400_BAD_REQUEST
            )

        logger.info('---------------------Upload begining---------------------------')
        start_time = time.time()

        file_donnees_bo = request.data['Donnees_BO_ADT']
        file_zpropaf = request.data['ZPROPAF']
        file_reo = request.data['REO']
        file_fmob = request.data['FMOB']
        file_filiere_domaine = request.data['domaine_filiere']
        file_insee = request.data['insee_maping']
        file_diplomes = request.data['diplomes']
        # file_suivi_pam = request.data['suivi_pam']

        logger.info('---------------------Upload ending-----------------------------')
        logger.debug("--------------upload time -- %d seconds ------------------------", time.time() - start_time)

        # ETL from xlsx source : Ordre : domaines, filieres, garnisons, fonctions, grades, fe, groupesMarques, marques, gestionnaires, administres, flag_pam_admnistres, postes, fmob, listePreferences
        filiere_domaine_df, donnees_bo_df, zpropaf_df, reo_df, fmob_df, femp_df, insee_df, diplomes_df = process_files(
            file_filiere_domaine,
            file_donnees_bo,
            file_zpropaf,
            file_reo,
            file_fmob,
            file_insee,
            file_diplomes)

        logger.info('---------------------Insert beginning---------------------------')
        start_time_insert = time.time()

        insert_Domaine(to_table_domaines(filiere_domaine_df))
        logger.debug('1. Domaine -----------> Success')
        insert_Garnison(to_table_garnisons(insee_df, donnees_bo_df))
        logger.debug('2. Garnison ----------> Success')
        insert_Fonction(to_table_fonctions(reo_df))
        logger.debug('3. Fonction ----------> Success')
        insert_Grade()
        logger.debug('4. Grade -------------> Success')
        insert_Filiere(to_table_filieres(filiere_domaine_df))
        logger.debug('5. Filières -----------> Success')
        insert_MarquesGroupe(to_table_groupesMarques())
        logger.debug('6. MarquesGroupe -----> Success')
        insert_Marque(to_table_marques())
        logger.debug('7. Marque ------------> Success')
        insert_FormationEmploi(to_table_fe(reo_df, donnees_bo_df))
        logger.debug('8. FormationEmplois ---> Success')
        administres_list, administres_error = insert_Administre(
            to_table_administres(donnees_bo_df, zpropaf_df, insee_df, diplomes_df))
        logger.debug('9. Administre --------> Success')

        postes_list = insert_Poste(to_table_postes(reo_df))
        logger.debug('10. Poste --------------> Success')

        logger.debug('time: %d s', time.time() - start_time_insert)
        fmob_error = insert_FMOB(to_table_fmob(fmob_df, femp_df, zpropaf_df))
        logger.debug('11. Fmob ----------------> Success')
        logger.info('---------------------Insert ending -----------------------------')

        logger.info('-------------------- Début des mises à jour --------------------')
        # administre : le statut PAM par défaut est A_TRAITER s'il y a un FMOB
        (Administre.objects
            .filter(a_statut_pam=StatutPam.A_ETUDIER, fmobs__isnull=False)
            .update(a_statut_pam=StatutPam.A_TRAITER))
        logger.info('--------------------- Fin des mises à jour ---------------------')

        logger.debug("------------------insert time -- %d seconds --------------------", time.time() - start_time_insert)
        # # result = {"administres_error": administres_error, "fmob_error": fmob_error}
        # result = {"administres_error": "", "fmob_error": fmob_error}
        return Response({"administres_error": ""})


class AlimentationReferentielView(APIView):
    """ Vue pour alimenter la base à partir de référentiels """

    serializer_class = AlimentationReferentielSerializer

    def get(self, request):
        return Response("Formulaire d'alimentation d'OGURE NG (référentiel")

    def post(self, request):
        """
        Charge le(s) fichier(s) et met à jour la base.

        :param request: requête, contient les fichiers
        :type  request: class:`rest_framework.request.Request`

        :raises: class:`rest_framework.exceptions.APIException`
        :return: réponse
        :rtype:  class:`rest_framework.response.Response`
        """

        try:
            validator = AlimentationReferentielSerializer(data=request.data)
            validator.is_valid(raise_exception=True)

            # récupération des fichiers
            referentiel_fe = request.data.get('referentiel_fe', None)

            if referentiel_fe:
                def process_referentiel_fe(referentiel) -> pd.DataFrame:
                    """
                    Fonction de lecture du référentiel de FE.

                    :param referentiel: Fichier du référentiel FE
                    :type  referentiel: XLSX

                    :return: DataFrame
                    :rtype:  class:`pandas.DataFrame`
                    """
                    return open_excel(referentiel, sheetname=0, engine='openpyxl')

                def convertir_fe(df: pd.DataFrame) -> pd.DataFrame:
                    """
                    Fonction de conversion du DataFrame de FE.

                    :param df: DataFrame du référentiel FE
                    :type  df: class:`pandas.DataFrame`

                    :return: DataFrame
                    :rtype:  class:`pandas.DataFrame`
                    """

                    col_pk_avant       = 'FE CREDO'
                    col_mere_avant     = 'FE mère CREDO'
                    col_zone_def_avant = 'Zone de Défense'

                    Cols = FormationEmploi.Cols
                    col_pk_apres       = Cols.PK
                    col_mere_apres     = Cols.REL_MERE
                    col_zone_def_apres = Cols.ZONE_DEFENSE

                    return (df[[col_pk_avant, col_mere_avant, col_zone_def_avant]]
                        .drop_duplicates(subset=col_pk_avant, keep='first')
                        .rename(columns={
                            col_pk_avant:       col_pk_apres,
                            col_mere_avant:     col_mere_apres,
                            col_zone_def_avant: col_zone_def_apres
                        }))

                def mettre_a_jour_fe(df: pd.DataFrame) -> None:
                    """
                    Met à jour les FE base à partir du DataFrame de FE.

                    :param df: DataFrame du référentiel FE
                    :type  df: class:`pandas.DataFrame`

                    :return: DataFrame
                    :rtype:  class:`pandas.DataFrame`
                    """

                    TypeModele = FormationEmploi
                    Cols = TypeModele.Cols
                    champs_maj = (Cols.REL_MERE, Cols.ZONE_DEFENSE)
                    modeles_en_base = { m.pk: m for m in TypeModele.objects.select_related(Cols.REL_MERE).only('pk', *champs_maj) }

                    taille_batch = 100
                    dict_create = {}
                    dict_update = {}
                    for rec in df.to_dict('records'):
                        pk = str(rec.get(Cols.PK, None))
                        id_mere = rec.get(Cols.REL_MERE, None)
                        mere = modeles_en_base.get(str(id_mere), None) if id_mere else None
                        zone_defense = rec.get(Cols.ZONE_DEFENSE, None)

                        en_base = modeles_en_base.get(pk, None)
                        if not en_base or (mere != getattr(en_base, Cols.REL_MERE, None) or zone_defense != getattr(en_base, Cols.ZONE_DEFENSE, None)):
                            try:
                                modele = TypeModele(pk=pk, mere=mere, zone_defense=zone_defense)
                            except Exception as e:
                                raise RuntimeError(f'la création d\'un modèle de type "{TypeModele.__name__}" a échoué') from e

                            (dict_create if not en_base else dict_update).setdefault(pk, modele)

                    # TODO pas de création pour l'instant (ni de suppression)
                    # if dict_create:
                    #     FormationEmploi.objects.bulk_create(dict_create.values(), batch_size=taille_batch)
                    if dict_update and champs_maj:
                        FormationEmploi.objects.bulk_update(dict_update.values(), batch_size=taille_batch, fields=champs_maj)
                        logger.debug(f"MAJ: %s", len(dict_update))

                df_referentiel_fe = process_referentiel_fe(referentiel_fe)
                df_referentiel_fe = convertir_fe(df_referentiel_fe)
                mettre_a_jour_fe(df_referentiel_fe)

            return Response({'message':'OK'})
        except (Http404, APIException):
            raise
        except:
            message = "impossible d'alimenter le référentiel"
            logger.exception(message)
            raise api_exception(status.HTTP_500_INTERNAL_SERVER_ERROR, message)

# Vue API des administrés
# Pagination OK
# Ordering OK
# Search NOK
# Filtering NOK

class AdministreView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue de l'administre.
    """
    serializer_class = AdministreSerializer

    filter_backends = [DjangoFilterBackend, RelatedOrderingFilter]
    # filterset_fields = ['category', 'in_stock']
    filterset_class = AdministreFilter
    ordering_fields = 'a_id_sap'
    ordering = ['a_id_sap']
    pagination_class = HeavyDataPagination

    # important : mettre à jour quand le serializer change
    def get_queryset(self):
        Cols = Administre.Cols
        return Administre.objects.select_related(
            Cols.REL_FONCTION, Cols.REL_GRADE,
        ).prefetch_related(
            Cols.M2M_COMPETENCES,
            Cols.O2M_FMOB,
            Prefetch(Cols.REL_DECISION, queryset=Decision.objects.select_related('poste__formation_emploi__garnison')),
            Prefetch(Cols.REL_FORMATION_EMPLOI, queryset=FormationEmploi.objects.select_related(FormationEmploi.Cols.REL_GARNISON, FormationEmploi.Cols.REL_MERE)),
            Prefetch(Cols.REL_SOUS_VIVIER, queryset=SousVivier.objects.select_related(SousVivier.Cols.REL_GESTIONNAIRE)),
        )

    @atomic
    def put(self, request):
        """La fonction put met à jour une liste d'administres.

        :type request: rest_framework.request.Request
        :param request: Request contenant la liste d'administres.

        :return:    - **Response** (*Response*): Reponse contient un message de la réussite de met à jour des administres .
        """
        try:
            req_data = request.data
            is_list = isinstance(req_data, list)
            validator = self.serializer_class(data=req_data, many=is_list, partial=True)
            validator.is_valid(raise_exception=True)

            today = date.today()
            Cols = Administre.Cols

            def copy_data_item(input, pk):
                """Copie et complète si besoin le dictionnaire en entrée"""
                result = {**input}
                if pk and Cols.PK not in result:
                    result[Cols.PK] = pk
                return result

            if is_list:  # Update multiple elements
                data = [copy_data_item(item, req_data[idx][Cols.PK]) for idx, item in enumerate(validator.validated_data)]
                fields = [key for key in data[0].keys() if key != Cols.PK]
                objs = [Administre(**data_item) for data_item in data]

                if Cols.STATUT_PAM in fields:
                    list_error = []
                    for i in range(len(objs)):
                        old_administre = Administre.objects.get(a_id_sap=objs[i].a_id_sap)
                        administre = objs[i]
                        eip = administre.a_eip
                        fe_code = old_administre.formation_emploi_id
                        avis = administre.a_statut_pam
                        old_avis = old_administre.a_statut_pam
                        try:
                            categorie = constants.CATEGORIE_BY_NF[old_administre.a_nf.upper()]
                        except:
                            erreur = 'la catégorie de l\'administré n\'est pas reconnue dans ogure.'
                            print(erreur)
                            raise Exception(erreur)

                        liste_error = impact_decisions(old_administre, administre, old_avis, avis, eip, fe_code, categorie)

                Administre.objects.bulk_update(objs, fields=fields)

            else:  # Update one element
                data = copy_data_item(validator.validated_data, validator.validated_data[Cols.PK])
                # c'est un bug la récupération de a_id_sap en-dessous, non ?
                a_id_sap = Administre.objects.update(data)
                db_instance = Administre.objects.filter(a_id_sap=a_id_sap).first()
                db_instance.tag.clear()

            return Response({'msg': 'updated'})
        except APIException:
            raise
        except Exception:
            message = 'échec de la mise à jour de N administrés'
            logger.exception(message)
            raise APIException(message)

    def partial_update(self, request, pk=None):
        """ La fonction put met à jour un administre.

        :type request: rest_framework.request.Request
        :param request: Request contenant l'administre.

        :type pk: integer
        :param pk: Primary Key de l'administre.

        :return:    - **Response** (*Response*): Reponse contient un message de la réussite de met à jour de l'administre .
        """
        try:
            logger.debug("Mise à jour administré")
            req_data = request.data

            # TODO valider les données (il faut d'abord corriger la MAJ des compétences)
            # validator = self.serializer_class(data=req_data, partial=True)
            # validator.is_valid(raise_exception=True)

            Cols = Administre.Cols

            # Copie et complète si besoin le dictionnaire en entrée
            # administre = {**validator.validated_data}
            administre = {**req_data}
            administre[Cols.PK] = pk

            fields = [key for key in administre.keys() if key != Cols.PK] # and key != 'a_liste_id_competences']

            if 'a_domaine_futur' in fields:
                administre['a_domaine_futur_id'] = administre['a_domaine_futur']
                del administre['a_domaine_futur']

            if 'a_filiere_futur' in fields:
                administre['a_filiere_futur_id'] = administre['a_filiere_futur']
                del administre['a_filiere_futur']

            if Cols.STATUT_PAM in fields:
                old_administre = Administre.objects.get(a_id_sap=pk)
                eip = old_administre.a_eip
                fe_code = old_administre.formation_emploi_id
                avis = administre[Cols.STATUT_PAM]
                old_avis = old_administre.a_statut_pam
                try:
                    categorie = constants.CATEGORIE_BY_NF[old_administre.a_nf.upper()]
                except:
                    erreur = 'la catégorie de l\'administré n\'est pas reconnue dans ogure.'
                    print(erreur)
                    raise APIException(erreur)

                liste_error = impact_decisions(old_administre, Administre(**administre), old_avis, avis, eip, fe_code,
                                                    categorie)


            if 'a_liste_id_competences' in fields:
                adm=Administre(pk=pk)
                #adm=request.data['a_liste_id_competences']
                if administre['a_liste_id_competences']:
                    adm.a_liste_id_competences.set(administre['a_liste_id_competences'].split(','))
                else:
                    adm.a_liste_id_competences.set("")

            else:
                copy = { k: v for k, v in administre.items() if k != 'a_liste_id_competences'}
                adm = Administre(**copy)

                if fields:
                    Administre.objects.bulk_update([adm], fields=fields)

            return Response({'msg': 'updated'})

        except (Http404, APIException):
            raise
        except Exception:
            message = "échec de la mise à jour de l'administré"
            logger.exception(message)
            raise APIException(message)



# GET /api/administres => 0 administrés
# POST /api/administres => Crée 1 nouvel administres => id=1
# GET /api/administres/1 => 1 administre précédemment créé
# POST /api/administres/1 => 1 administre précédemment créé


class PosteView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue des postes.
    """
    lookup_value_regex = r"[\w.]+"
    serializer_class = PosteSerializer
    pagination_class = HeavyDataPagination
    filter_backends = [DjangoFilterBackend, RelatedOrderingFilter]
    # filterset_fields = ['category', 'in_stock']
    filterset_class = PosteFilter
    ordering_fields = '__all_related__'
    ordering = ['p_id']

    # important : mettre à jour quand le serializer change
    def get_queryset(self):
        """Cette fonction permet d'ajouter plus de logique à l'attribut queryset."""

        Cols = Poste.Cols
        return Poste.objects.select_related(
            Cols.REL_FONCTION
        ).prefetch_related(
            Cols.M2M_COMPETENCES,
            Cols.O2M_DECISION,
            Prefetch(Cols.REL_FORMATION_EMPLOI, queryset=FormationEmploi.objects.select_related(FormationEmploi.Cols.REL_GARNISON, FormationEmploi.Cols.REL_MERE)),
            Prefetch(Cols.REL_SOUS_VIVIER, queryset=SousVivier.objects.select_related(SousVivier.Cols.REL_GESTIONNAIRE)),
        ).annotate(
            p_nb_prepositionne=Sum(
                Case(When(decisions__de_decision=constants.DECISION_PREPOSITIONNE, then=1), default=0,
                     output_field=IntegerField())),
            p_nb_positionne=Sum(Case(When(decisions__de_decision=constants.DECISION_POSITIONNE, then=1), default=0,
                                     output_field=IntegerField())),
            p_nb_omi_en_cours=Sum(Case(When(decisions__de_decision=constants.DECISION_OMI_EN_COURS, then=1), default=0,
                                       output_field=IntegerField())),
            p_nb_omi_active=Sum(Case(When(decisions__de_decision=constants.DECISION_OMI_ACTIVE, then=1), default=0,
                                     output_field=IntegerField())),
        )

    def put(self, request):
        """La fonction put met à jour une liste des postes.

        :type request: rest_framework.request.Request
        :param request: Request contenant la liste des postes.

        :return:    - **Response** (*Response*): Reponse contient un message de la réussite de met à jour des postes .
        """
        data = request.data
        serialized = self.serializer_class(data=data, many=isinstance(data, list), partial=True)
        serialized.is_valid(raise_exception=True)
        if isinstance(data, list):  # Update multiple elements
            try:
                fields = [key for key in data[0].keys() if key != 'p_id' ]
                objs = [Poste(**data[i]) for i in range(len(data))]

            except:
                raise Exception("bulk update error")

            Poste.objects.bulk_update(objs, fields=fields)
        else:  # Update one element
            a_id_sap = Poste.objects.update(serialized.validated_data)
            db_instance = Poste.objects.filter(
                a_id_sap=a_id_sap).first()
            db_instance.tag.clear()

        return Response({'msg': 'updated'})

    def partial_update(self, request, pk=None):
        """ La fonction put met à jour un poste.

		:type request: rest_framework.request.Request
		:param request: Request contenant le poste.

		:type pk: integer
		:param pk: Primary Key du poste.
		:return:    - **Response** (*Response*): Reponse contient un message de la réussite de met à jour du poste .
		"""
        poste = request.data
        fields = [key for key in poste.keys() if key != 'p_id']# and key != 'competence']
        poste['p_id'] = pk

        if 'competence' in fields:
            pos=Poste(pk=pk)
            if poste['competence']:
                pos.competences.set(poste['competence'].split(','))
            else:
                pos.competences.set("")
        else:
            copy = { k: v for k, v in poste.items() if k != 'competence'}
            pos = Poste(**copy)

            if fields:
                Poste.objects.bulk_update([pos], fields=fields)


        # copy = { k: v for k, v in poste.items() if k != 'competence'}
        # pos = Poste(**copy)

        # if fields:
        #     Poste.objects.bulk_update([copy], fields=fields)

        # if 'competence' in poste.items():
        #     pos.competence.set(poste['competence'].split(','))
        #     pos.save()


        return Response({'msg': 'updated'})


class NotationView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue des notations.
    """
    serializer_class = NotationSerializer
    queryset = Notation.objects.all()
    pagination_class = HeavyDataPagination

    def list(self, request, pk=None):
        """La fonction list envoie le classement des postes pour un administré particulier et inversement.

        :type request: rest_framework.request.Request
        :param request: Request contenant l'administre ou le poste.

        :return:    - **JsonResponse** (*JsonResponse*): Json contenant le classement.
        """
        notations_list = []
        q = 'poste'
        if 'administre_id' in request.query_params:
            administre_id = request.query_params['administre_id']
            administres_keys = (
                'poste__p_id', 'poste__p_nf', 'poste__p_domaine', 'poste__p_filiere', 'poste__p_eip',
                'poste__formation_emploi__fe_code', 'poste__competences',
                'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques', 'poste__p_code_fonction',
                'poste__p_avis',
                'poste__p_nb_p1',
                'poste__p_nb_p2',
                'poste__p_nb_p3',
                'poste__p_nb_p4',
                'poste__p_dep',
                'poste__formation_emploi__fe_libelle', 'poste__competences', 'poste__p_fonction',
                'poste__formation_emploi__garnison',
                'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques',
                'poste__formation_emploi__garnison__gar_lieu', 'no_score_administre',
                'no_flag_cple_ideal')

            counter = 10

            notation_qs = Notation.objects.filter(administre_id=administre_id).order_by('-no_score_administre')[
                          :counter].select_related('poste')

            notation_qs_matching_parfait = Notation.objects.filter(administre_id=administre_id,
                                                                   no_flag_cple_ideal=True).select_related('poste')

            notation_qs.union(notation_qs_matching_parfait)
            notation_qs = notation_qs | notation_qs_matching_parfait
            notations_list = list(notation_qs.values(*administres_keys))

            for notation in notations_list:
                poste_id = notation['poste__p_id']
                topCounter = 0
                allNotationsInvolved = Notation.objects.filter(poste_id=poste_id)
                for note in allNotationsInvolved:
                    topList = list(
                        Notation.objects.filter(no_id=note.no_id).order_by('-no_score_administre')[:10].values(
                            'poste_id'))
                    topPostes = [poste['poste_id'] for poste in topList]
                    if poste_id in topPostes:
                        topCounter += 1

                notation['poste__nb_top'] = topCounter

        if 'poste_id' in request.query_params:
            q = "administre"
            poste_id = request.query_params['poste_id']
            poste_unit = Poste.objects.get(p_id=poste_id)
            try:
                counter = poste_unit.p_nb_p1 + poste_unit.p_nb_p2 + poste_unit.p_nb_p3 + poste_unit.p_nb_p4
                if counter < 10:
                    counter = 10
            except:
                counter = 10
            postes_keys = (
                'administre__a_id_sap', 'administre__a_nom', 'administre__a_prenom', 'administre__a_statut_pam',
                'administre__grade_id', 'administre__a_liste_id_marques', 'administre__decision__de_decision',
                'administre__decision__de_date_decision', 'no_score_administre', 'no_flag_cple_ideal', 'administre__a_notes_gestionnaire',
                'administre__a_fonction', 'administre__a_code_fonction',
                'administre__a_liste_id_marques',
                'administre__a_liste_id_competences',
                'administre__decision__poste_id')
            notation_qs = Notation.objects.filter(poste_id=poste_id).order_by('-no_score_administre')[
                          :counter].select_related(
                'administre')
            notation_qs_matching_parfait = Notation.objects.filter(poste_id=poste_id,
                                                                   no_flag_cple_ideal=True).select_related('administre')
            notation_qs.union(notation_qs_matching_parfait)
            notations_list = list(
                notation_qs.values(*postes_keys))

            for notation in notations_list:
                administre_id = notation['administre__a_id_sap']
                topCounter = 0
                allNotationsInvolved = Notation.objects.filter(administre_id=administre_id)
                for note in allNotationsInvolved:
                    topList = list(
                        Notation.objects.filter(no_id=note.no_id).order_by('-no_score_administre')[:10].values(
                            'administre_id'))
                    topAdministres = [administre['administre_id'] for administre in topList]
                    if administre_id in topAdministres:
                        topCounter += 1

                notation['administre__nb_top'] = topCounter

        result = []
        for notation in notations_list:
            res_notation = {q: {}}
            for key in notation:
                if (q + "__") in key:
                    res_notation[q][key.replace(q + '__', '')] = notation[key]
                else:
                    res_notation[key] = notation[key]
            if q == "poste":
                res_notation[q]['p_nb_prepositionne'] = Decision.objects.filter(
                    de_decision=constants.DECISION_PREPOSITIONNE).count()
                res_notation[q]['p_nb_positionne'] = Decision.objects.filter(
                    de_decision=constants.DECISION_POSITIONNE).count()
                res_notation[q]['p_nb_omi_active'] = Decision.objects.filter(
                    de_decision=constants.DECISION_OMI_ACTIVE).count()
                res_notation[q]['p_nb_omi_en_cours'] = Decision.objects.filter(
                    de_decision=constants.DECISION_OMI_EN_COURS).count()
            result.append(res_notation)
        return Response(result)


class DecisionView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue des decisions.
    """
    serializer_class = DecisionSerializer
    queryset = Decision.objects.all()

    def list(self, request, pk=None):
        """Cette fonction envoie les informations du poste lié à l'administre dans une décision avec le score de l'administre et inversement.

        :type request: rest_framework.request.Request
        :param request: Request contenant l'administre ou le poste.

        :return:    - **res_decision** (*JsonResponse*): Json contenant le classement.
        """
        decision = []
        q = 'poste'
        administre_id = None
        poste_id = None
        if 'administre_id' in request.query_params:
            administre_id = request.query_params['administre_id']
            administres_keys = (
                'poste__p_id', 'poste__p_nf', 'poste__p_domaine', 'poste__p_filiere',
                'poste__p_eip','poste__p_avis',
                'poste__formation_emploi__fe_code', 'poste__competences',
                'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques',
                'poste__formation_emploi__fe_libelle', 'poste__formation_emploi__garnison__gar_lieu',
                'poste__p_dep', 'poste__p_code_fonction', 'poste__competences',
                'poste__p_fonction',
                # 'poste__decision__de_decision',
                'poste__p_nb_p1',
                'poste__p_nb_p2',
                'poste__p_nb_p3',
                'poste__p_nb_p4',
                'poste__p_notes_gestionnaire', 'poste__p_liste_id_marques',
                'de_decision', 'de_date_decision', 'de_notes_gestionnaire')
            decision = Decision.objects.filter(administre_id=administre_id).select_related('poste')
            decision = list(decision.values(*administres_keys))

        if 'poste_id' in request.query_params:
            q = "administre"
            poste_id = request.query_params['poste_id']
            postes_keys = (
                'administre__a_id_sap', 'administre__a_nom', 'administre__a_prenom', 'administre__a_statut_pam',
                'administre__a_fonction', 'administre__a_code_fonction', 'administre__a_liste_id_competences',
                'administre__grade_id', 'administre__a_liste_id_marques', 'administre__decision__de_decision',
                'administre__decision__de_date_decision', 'de_decision', 'de_date_decision',
                'de_notes_gestionnaire', 'administre__a_notes_gestionnaire', 'administre__a_liste_id_competences',
                'administre__decision__poste_id')

            decision = Decision.objects.filter(poste_id=poste_id).select_related('administre')

            decision = list(decision.values(*postes_keys))
        res_decision = []
        if len(decision) > 0:
            for k in range(len(decision)):
                decision_unit = decision[k]
                try:
                    administre_id = decision_unit.administre_id
                except:
                    administre_id = None

                try:
                    poste_id = decision_unit.poste_id
                except:
                    poste_id = None

                try:
                    res_decision_unit = {q: {}, 'no_score_administre': Notation.objects.get(
                        administre_id=administre_id,
                        poste_id=poste_id).no_score_administre}
                except:
                    res_decision_unit = {q: {}, 'no_score_administre': None}
                for key in decision_unit:
                    if (q + "__") in key:
                        res_decision_unit[q][key.replace(q + '__', '')] = decision_unit[key]
                    else:
                        res_decision_unit[key] = decision_unit[key]
                # Ajout du relevé des décisions sur le poste (cas q = "poste")
                if q == "poste":
                    res_decision_unit[q]['p_nb_prepositionne'] = Decision.objects.filter(
                        de_decision=constants.DECISION_PREPOSITIONNE).count()
                    res_decision_unit[q]['p_nb_positionne'] = Decision.objects.filter(
                        de_decision=constants.DECISION_POSITIONNE).count()
                    res_decision_unit[q]['p_nb_omi_active'] = Decision.objects.filter(
                        de_decision=constants.DECISION_OMI_ACTIVE).count()
                    res_decision_unit[q]['p_nb_omi_en_cours'] = Decision.objects.filter(
                        de_decision=constants.DECISION_OMI_EN_COURS).count()
                res_decision.append(res_decision_unit)
        return Response(res_decision)

    def create(self, request):  # équivalent post
        """Cette fonction crée une decision.

        :type request: rest_framework.request.Request
        :param request: Request contenant l'administre, le poste et la decision.

        :return:    - **response** (*JsonResponse*): Json contenant le message "Décision changée".
        """
        result = {}
        # TODO : try except
        administre_id = request.data['administre_id']
        poste_id = request.data['poste_id']
        de_decision = request.data['de_decision']
        decision = Decision(administre_id=administre_id, poste_id=poste_id)
        administre_decision = Decision.objects.filter(administre_id=administre_id)
        # Lorsqu'on crée une décision on supprime toute décision précédente sur l'administré
        # poste_decision = Decision.objects.filter(poste_id=poste_id)
        if administre_decision.exists():
            administre_decision.delete()

        decision.de_decision = de_decision
        decision.de_date_decision = timezone.now()
        decision.save()

        poste = Poste.objects.get(p_id=poste_id)
        poste.p_nb_vacant = poste.p_nb_vacant - 1
        poste.save()

        response = JsonResponse({'status': 'success', 'message': 'Décision changée'})

        return response


class FormationEmploiView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue des FormationEmplois.
    """
    serializer_class = FormationEmploiSerializer

    # important : mettre à jour quand le serializer change
    def get_queryset(self):
        Cols = FormationEmploi.Cols
        return FormationEmploi.objects.select_related(Cols.REL_GARNISON, Cols.REL_MERE)


class ListesPreferencesView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue des ListesPreferences.
    """
    serializer_class = PreferencesListe
    queryset = PreferencesListe.objects.all()


class MarqueView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue des marques.
    """
    serializer_class = MarqueSerializer
    queryset = Marque.objects.all()


class MarquesGroupeView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue des marquegroupes.
    """
    serializer_class = MarquesGroupeSerializer
    queryset = MarquesGroupe.objects.all()


class FmobView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue des Fmob.
    """
    serializer_class = FmobSerializer
    queryset = FMOB.objects.all()


class FiliereView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue des filieres.
    """
    serializer_class = FiliereSerializer
    queryset = Filiere.objects.all()


class DomaineView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue des domaines.
    """
    serializer_class = DomaineSerializer
    queryset = Domaine.objects.all()


class SousVivierAssociationView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue du SousVivierAssociation.
    """
    serializer_class = SousVivierAssociationSerializer
    queryset = SousVivierAssociation.objects.all()


class PcpFeGroupeView(viewsets.ModelViewSet):
    """
    Cette classe est dédiée au vue du PcpFeGroupe.
    """
    serializer_class = PcpFeGroupeSerializer
    queryset = PcpFeGroupe.objects.all()
