from datetime import datetime
from typing import Optional, Tuple

import pandas as pd
import requests
from django.conf import settings
from django.db.models import Q
from django.db.transaction import atomic
from django.http import Http404, JsonResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from ..models import Administre, StatutPamChoices
from ..models import AvisPosteChoices as AvisPoste
from ..models import Poste, SousVivier
from ..models.calcul import Calcul
from ..models.calcul import StatutCalculChoices as StatutCalcul
from ..serializers import ScoringSelectifValidator, ScoringValidator
from ..utils.decorators import class_logger
from ..utils_calcul import lancer_calculs, lancer_calculSelectif
from .commun import (GestionnairePermission, api_exception,
                     execution_time_viewset, query_count_viewset)
import _json


# @execution_time_viewset
# @query_count_viewset
@class_logger
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.
    """
    permission_classes = [IsAuthenticated, GestionnairePermission]

    def __recuperer_chemin_requete(self, request: 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: Request) -> None:
        """ 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)
        try:
            requests.patch(url, headers=request.headers, cookies=request.COOKIES, timeout=0.001)
        except requests.exceptions.ReadTimeout:
            pass
        except Exception as e:
            self.logger.debug("échec de l'appel à %s: %s", url, e)

    @atomic
    def __creer_calcul(self, pam_id: str, 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 = StatutCalcul
        COL_ID     = 'id'
        COL_SV     = 'sous_vivier_id'
        COL_PAM    = 'pam_id'
        COL_STATUT = 'ca_statut'
        COL_DEBUT  = 'ca_date_debut'

        colonnes = (COL_ID, COL_SV, COL_PAM, 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_SV].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_SV].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(id=str(sv_id)+str(pam_id)) & Q(pam_id=pam_id) & Q(sous_vivier_id=sv_id) & ~Q(ca_statut__in=(Statut.EN_COURS, Statut.EN_ATTENTE))).delete()
        if calcul_autorise:
            return Calcul.objects.create(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_COURS)
        return Calcul.objects.create(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_ATTENTE)

    @atomic
    def __creer_calculSelectif(self, sv_id: str, pam_id: str, l_a_id: list, l_p_id: list) -> 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 = StatutCalcul
        COL_ID     = 'id'
        COL_SV     = 'sous_vivier_id'
        COL_STATUT = 'ca_statut'
        COL_DEBUT  = 'ca_date_debut'
        COL_PAM    = 'pam_id'

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


        en_cours = df[df[COL_STATUT] == Statut.EN_COURS][COL_SV].values

        if l_a_id and l_p_id:
            if sv_id in en_cours:
                raise api_exception(status.HTTP_400_BAD_REQUEST, 'un calcul est déjà en cours pour ce sous-ensemble')

            en_attente = df[df[COL_STATUT] == Statut.EN_ATTENTE][COL_SV].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-ensemble')

            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(id=str(sv_id)+str(pam_id)+'selectif') & Q(pam_id=pam_id) & Q(sous_vivier_id=sv_id) & ~Q(ca_statut__in=(Statut.EN_COURS, Statut.EN_ATTENTE))).delete()
            if calcul_autorise:
                return Calcul.objects.create(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut, ca_statut_pourcentage=0, ca_statut=Statut.EN_COURS)
            return Calcul.objects.create(id=str(sv_id)+str(pam_id)+'selectif',pam_id=pam_id,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 = StatutCalcul
        COL_ID     = 'id'
        COL_SV     = 'sous_vivier_id'
        COL_STATUT = 'ca_statut'
        COL_DEBUT  = 'ca_date_debut'
        COL_PAM    = 'pam_id'


        colonnes = (COL_ID, COL_SV, COL_PAM, 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_SV].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, pam_id: str,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 = StatutCalcul
        qs_calcul = Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id, ca_date_debut=date_debut)
        qs_en_cours = qs_calcul.filter(ca_statut=Statut.EN_COURS)
        try:
            lancer_calculs(pam_id,sv_id)
        except SystemExit:
            self.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(id=str(sv_id)+str(pam_id),pam_id=pam_id,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.TERMINE_DE_FORCE)
                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 __executer_calculSelectif(self, sv_id: str, pam_id: str, l_a_id: list, l_p_id: list, 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 = StatutCalcul
        qs_calcul = Calcul.objects.filter(id=str(sv_id)+str(pam_id)+'selectif',sous_vivier_id=sv_id, pam_id=pam_id, ca_date_debut=date_debut)
        qs_en_cours = qs_calcul.filter(ca_statut=Statut.EN_COURS)
        try:
            lancer_calculSelectif(sv_id, pam_id, l_a_id, l_p_id)
            
        except SystemExit:
            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(id=str(sv_id)+str(pam_id)+'selectif',sous_vivier_id=sv_id, pam_id=pam_id,ca_date_debut=date_debut)
            if str(e) == "Arret du calcul":
                qs_calcul.update(ca_date_fin=timezone.now(), ca_statut=Statut.TERMINE_DE_FORCE)
                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: Request) -> Response:
        """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:    - réponse 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']
            pam_id =  request.query_params['pam_id']
            calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id)
            debut = calcul.ca_date_debut
            fin = calcul.ca_date_fin
            administres_statuts = [StatutPamChoices.A_MUTER,StatutPamChoices.A_ETUDIER]
            administres = Administre.objects.filter(pam__pam_id=pam_id,sous_vivier_id=sv_id,a_statut_pam__in=administres_statuts).count()
            postes = Poste.objects.values_list('p_avis').filter(Q(p_pam__pam_id=pam_id) & Q(sous_viviers=sv_id) & Q(p_avis__in=[AvisPoste.P1,AvisPoste.P2,AvisPoste.P3,AvisPoste.P4])).count()
            statut = calcul.ca_statut
            statut_pourcentage = calcul.ca_statut_pourcentage

        except:
            statut = StatutCalcul.AUCUN
            statut_pourcentage = 0

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

    def patch(self, request: Request, id=None) -> Response:
        """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 = StatutCalcul
        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 APIException('lancement du prochain calcul arrêté')
        except:
            message = 'impossible de lancer le prochain calcul en attente'
            self.logger.exception(message)
            raise APIException(message)


    def post(self, request: Request) -> Response:
        """ 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
        """
        try:
            # Gestion des 2 cas, calculs sélectifs ou calculs sur l'ensemble du sous-vivier
            if 'administre_id' in request.data.keys() and 'poste_id' in request.data.keys():
                Statut = StatutCalcul
                validator = ScoringSelectifValidator(data=request.data)
                validator.is_valid(raise_exception=True)
                sv_id = validator.validated_data.get('sous_vivier_id')
                pam_id = validator.validated_data.get('pam_id')
                get_object_or_404(SousVivier.objects, sv_id=sv_id)
                l_a_id = validator.validated_data.get('administre_id')
                l_p_id = validator.validated_data.get('poste_id')
                # calcul
                calcul = self.__creer_calculSelectif(sv_id, pam_id, l_a_id, l_p_id)
                if calcul.ca_statut == Statut.EN_COURS:
                    try:
                        calcul = self.__executer_calculSelectif(sv_id, pam_id, l_a_id, l_p_id, calcul.ca_date_debut)
                    except Exception as e:
                        Statut= Statut.ERREUR
                        calcul.ca_statut = Statut
                        calcul.save()
                        error = str(e).split(',')
                        return JsonResponse({'info': error[0]})
                    finally:
                        self.__appeler_api_depiler_calculs(request)

            else:
                Statut = StatutCalcul
                validator = ScoringValidator(data=request.data)
                # validation
                validator.is_valid(raise_exception=True)
                sv_id = validator.validated_data.get('sous_vivier_id')
                pam_id = validator.validated_data.get('pam_id')
                get_object_or_404(SousVivier.objects, sv_id=sv_id)
                # calcul
                calcul = self.__creer_calcul(pam_id,sv_id)
                if calcul.ca_statut == Statut.EN_COURS:
                    try:
                        calcul = self.__executer_calcul(pam_id,sv_id, calcul.ca_date_debut)
                    except Exception as e:

                        Statut= Statut.ERREUR
                        calcul.ca_statut = Statut
                        calcul.save()
                        error = str(e).split(',')
                        return JsonResponse({'info': error[0]})

                    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 APIException('calcul arrêté')
        except:
            message = 'impossible de lancer le calcul'
            self.logger.exception(message)
            raise APIException(message)


@class_logger
@execution_time_viewset
@query_count_viewset
class ArretCalcul(APIView):
    """
    Cette classe est dédiée a l'arrêt du calcul.
    """
    permission_classes = [IsAuthenticated, GestionnairePermission]

    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.
        """
        Statut = StatutCalcul
        sv_id = request.data['sous_vivier_id']
        pam_id = request.data['pam_id']
        calcul = Calcul.objects.get(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id)
        if Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id).exists() and calcul.ca_statut == Statut.EN_COURS:
            calcul.ca_statut = Statut.EN_ATTENTE_ARRET
            calcul.save()
        elif Calcul.objects.filter(id=str(sv_id)+str(pam_id),pam_id=pam_id,sous_vivier_id=sv_id).exists() and calcul.ca_statut == Statut.EN_ATTENTE:
            calcul.ca_statut = Statut.TERMINE
            calcul.save()
        return Response({"statut": calcul.ca_statut})
