package org.mte.numecoeval.calculs.domain.port.input.service.impl;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.mte.numecoeval.calculs.domain.data.demande.DemandeCalculImpactEquipementPhysique;
import org.mte.numecoeval.calculs.domain.data.erreur.TypeErreurCalcul;
import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactEquipementPhysique;
import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielImpactEquipement;
import org.mte.numecoeval.calculs.domain.data.trace.ConsoElecAnMoyenne;
import org.mte.numecoeval.calculs.domain.data.trace.DureeDeVie;
import org.mte.numecoeval.calculs.domain.data.trace.MixElectrique;
import org.mte.numecoeval.calculs.domain.data.trace.TraceCalculImpactEquipementPhysique;
import org.mte.numecoeval.calculs.domain.exception.CalculImpactException;
import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactEquipementPhysiqueService;
import org.mte.numecoeval.calculs.domain.port.input.service.DureeDeVieEquipementPhysiqueService;
import org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactEquipementPhysiqueUtils;
import org.mte.numecoeval.calculs.domain.traceur.TraceUtils;

import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@AllArgsConstructor
public class CalculImpactEquipementPhysiqueServiceImpl implements CalculImpactEquipementPhysiqueService {
    private static final String VERSION_CALCUL = "1.0";
    public static final String NB_JOUR_UTILISE_PAR_DEFAUT = "NbJourUtiliseParDefaut";
    public static final String ERREUR_DE_CALCUL_IMPACT_EQUIPEMENT_MESSAGE = "Erreur de calcul impact équipement: Type: {}, Cause: {}, Etape: {}, Critere: {}, Equipement Physique: {}, RefEquipementRetenu: {}, RefEquipementParDefaut: {}";
    private final DureeDeVieEquipementPhysiqueService dureeDeVieEquipementPhysiqueService;


    public ImpactEquipementPhysique calculerImpactEquipementPhysique(DemandeCalculImpactEquipementPhysique demandeCalcul) {
        log.debug("Début de calcul d'impact d'équipement physique : {}, {}, {} ",
                demandeCalcul.getEtape().getCode(),
                demandeCalcul.getCritere().getNomCritere(),
                demandeCalcul.getEquipementPhysique().getNomEquipementPhysique());
        ImpactEquipementPhysique impactErreur = null;

        try {
            return getCalculImpactEquipementPhysique(demandeCalcul);
        } catch (CalculImpactException e) {
            log.debug(ERREUR_DE_CALCUL_IMPACT_EQUIPEMENT_MESSAGE, e.getErrorType(), e.getMessage(),
                    demandeCalcul.getEtape().getCode(),
                    demandeCalcul.getCritere().getNomCritere(),
                    demandeCalcul.getEquipementPhysique().getNomEquipementPhysique(),
                    demandeCalcul.getRefEquipementCible(),
                    demandeCalcul.getRefEquipementParDefaut());
            impactErreur = buildCalculImpactForError(demandeCalcul, e);
        } catch (Exception e) {
            log.debug("{} : Erreur de calcul impact équipement physique: Erreur technique de l'indicateur : {}", TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), e.getMessage());
            impactErreur = buildCalculImpactForError(demandeCalcul, new CalculImpactException(TypeErreurCalcul.ERREUR_TECHNIQUE.getCode(), "Erreur publication de l'indicateur : " + e.getMessage()));
        }

        return impactErreur;
    }

    private ImpactEquipementPhysique getCalculImpactEquipementPhysique(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException {
        double valeurImpactUnitaire;
        Double consoElecMoyenne = null;
        TraceCalculImpactEquipementPhysique traceCalculImpactEquipementPhysique;
        var quantite = demandeCalcul.getEquipementPhysique().getQuantite();
        double tauxUtilisationEqPhysique = getTauxUtilisationEqPhysique(demandeCalcul);
        if ("UTILISATION".equals(demandeCalcul.getEtape().getCode())) {
            var consoElecAnMoyenne = getConsoElecAnMoyenne(demandeCalcul);
            var mixElectrique = getMixElectrique(demandeCalcul);
            valeurImpactUnitaire = quantite
                    * consoElecAnMoyenne.getValeur()
                    * mixElectrique.getValeur()
                    * tauxUtilisationEqPhysique
            ;
            consoElecMoyenne = consoElecAnMoyenne.getValeur();
            traceCalculImpactEquipementPhysique = TraceCalculImpactEquipementPhysiqueUtils.buildTracePremierScenario(quantite, consoElecAnMoyenne, mixElectrique, tauxUtilisationEqPhysique);
        } else {
            var refImpactEquipementOpt = getImpactEquipement(demandeCalcul);
            if (refImpactEquipementOpt.isPresent()) {
                ReferentielImpactEquipement referentielImpactEquipement = refImpactEquipementOpt.get();
                var valeurReferentiel = referentielImpactEquipement.getValeur();
                valeurImpactUnitaire = quantite * valeurReferentiel * tauxUtilisationEqPhysique;
                DureeDeVie dureeVie = dureeDeVieEquipementPhysiqueService.calculerDureeVie(demandeCalcul);
                if (dureeVie != null && dureeVie.getValeurRetenue() != null && dureeVie.getValeurRetenue() > 0) {
                    var valeurDureeVie = dureeVie.getValeurRetenue();
                    valeurImpactUnitaire = valeurImpactUnitaire / valeurDureeVie;
                    traceCalculImpactEquipementPhysique = TraceCalculImpactEquipementPhysiqueUtils.buildTraceSecondScenario(quantite, valeurReferentiel, referentielImpactEquipement.getSource(), dureeVie, tauxUtilisationEqPhysique);
                } else {
                    throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Durée de vie de l'équipement inconnue");
                }
            } else {
                throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Référentiel Impact Equipement inconnu");
            }
        }

        var calcul = buildCalculImpactEquipementPhysique(demandeCalcul, valeurImpactUnitaire, consoElecMoyenne);
        calcul.setTrace(TraceUtils.getTraceFromTraceur(traceCalculImpactEquipementPhysique));
        return calcul;
    }

    private Optional<ReferentielImpactEquipement> getImpactEquipement(DemandeCalculImpactEquipementPhysique demandeCalcul) {

        Optional<ReferentielImpactEquipement> resultFromRefEquipementCible = Optional.empty();
        if (StringUtils.isNotBlank(demandeCalcul.getRefEquipementCible())) {
            resultFromRefEquipementCible = demandeCalcul.getImpactEquipement(
                    demandeCalcul.getCorrespondanceRefEquipement().getRefEquipementCible()
            );
        }
        if (resultFromRefEquipementCible.isEmpty()) {
            return demandeCalcul.getImpactEquipement(
                    demandeCalcul.getRefEquipementParDefaut()
            );
        }
        return resultFromRefEquipementCible;
    }

    private ImpactEquipementPhysique buildCalculImpactEquipementPhysique(DemandeCalculImpactEquipementPhysique demandeCalcul, Double valeurImpactUnitaire, Double consoElecMoyenne) {
        return ImpactEquipementPhysique
                .builder()
                .nomEquipement(demandeCalcul.getEquipementPhysique().getNomEquipementPhysique())
                .etapeACV(demandeCalcul.getEtape().getCode())
                .critere(demandeCalcul.getCritere().getNomCritere())
                .dateCalcul(demandeCalcul.getDateCalcul())
                .versionCalcul(VERSION_CALCUL)
                .reference(StringUtils.defaultIfEmpty(demandeCalcul.getRefEquipementCible(), demandeCalcul.getRefEquipementParDefaut()))
                .typeEquipement(demandeCalcul.getEquipementPhysique().getType())
                .quantite(demandeCalcul.getEquipementPhysique().getQuantite())
                .statutEquipementPhysique(demandeCalcul.getEquipementPhysique().getStatut())
                .statutIndicateur("OK")
                .impactUnitaire(valeurImpactUnitaire)
                .unite(demandeCalcul.getCritere().getUnite())
                .consoElecMoyenne(consoElecMoyenne)
                .nomLot(demandeCalcul.getEquipementPhysique().getNomLot())
                .nomSourceDonnee(demandeCalcul.getEquipementPhysique().getNomSourceDonnee())
                .dateLot(demandeCalcul.getEquipementPhysique().getDateLot())
                .nomEntite(demandeCalcul.getEquipementPhysique().getNomEntite())
                .nomOrganisation(demandeCalcul.getEquipementPhysique().getNomOrganisation())
                .qualite(demandeCalcul.getEquipementPhysique().getQualite())
                .build();
    }

    private ImpactEquipementPhysique buildCalculImpactForError(DemandeCalculImpactEquipementPhysique demandeCalcul, CalculImpactException exception) {
        return ImpactEquipementPhysique
                .builder()
                .nomEquipement(demandeCalcul.getEquipementPhysique().getNomEquipementPhysique())
                .etapeACV(demandeCalcul.getEtape().getCode())
                .critere(demandeCalcul.getCritere().getNomCritere())
                .dateCalcul(demandeCalcul.getDateCalcul())
                .versionCalcul(VERSION_CALCUL)
                .reference(StringUtils.defaultIfEmpty(demandeCalcul.getRefEquipementCible(), demandeCalcul.getRefEquipementParDefaut()))
                .typeEquipement(demandeCalcul.getEquipementPhysique().getType())
                .quantite(demandeCalcul.getEquipementPhysique().getQuantite())
                .statutEquipementPhysique(demandeCalcul.getEquipementPhysique().getStatut())
                .statutIndicateur("ERREUR")
                .impactUnitaire(null)
                .unite(demandeCalcul.getCritere().getUnite())
                .consoElecMoyenne(null)
                .nomLot(demandeCalcul.getEquipementPhysique().getNomLot())
                .nomSourceDonnee(demandeCalcul.getEquipementPhysique().getNomSourceDonnee())
                .dateLot(demandeCalcul.getEquipementPhysique().getDateLot())
                .nomEntite(demandeCalcul.getEquipementPhysique().getNomEntite())
                .nomOrganisation(demandeCalcul.getEquipementPhysique().getNomOrganisation())
                .qualite(demandeCalcul.getEquipementPhysique().getQualite())
                .trace(TraceUtils.getTraceFromTraceur(TraceCalculImpactEquipementPhysiqueUtils.buildTraceErreur(exception)))
                .build();
    }

    private ConsoElecAnMoyenne getConsoElecAnMoyenne(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException {
        if (demandeCalcul.getEquipementPhysique().getConsoElecAnnuelle() != null) {
            var valeur = demandeCalcul.getEquipementPhysique().getConsoElecAnnuelle();
            return ConsoElecAnMoyenne.builder()
                    .valeurItemConsoElecAnnuelle(valeur)
                    .valeur(valeur)
                    .build();
        }


        if (StringUtils.isAllBlank(demandeCalcul.getRefEquipementCible(), demandeCalcul.getRefEquipementParDefaut())) {
            throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Référentiel Impact Equipement inconnu");
        }

        var referentielImpactEquipementOpt = getImpactEquipement(demandeCalcul);
        if (referentielImpactEquipementOpt.isEmpty()) {
            throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Référentiel Impact Equipement inconnu");
        }

        if (referentielImpactEquipementOpt.get().getConsoElecMoyenne() == null) {
            throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(),
                    "Donnée de consommation electrique manquante : equipementPhysique : " + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique() +
                            ", RefEquipementCible : " + demandeCalcul.getRefEquipementCible() +
                            ", RefEquipementParDefaut : " + demandeCalcul.getRefEquipementParDefaut()
            );
        }

        var valeur = referentielImpactEquipementOpt.get().getConsoElecMoyenne();
        return ConsoElecAnMoyenne.builder()
                .sourceReferentielFacteurCaracterisation(referentielImpactEquipementOpt.get().getSource())
                .valeurReferentielConsoElecMoyenne(valeur)
                .valeur(valeur)
                .build();

    }

    private MixElectrique getMixElectrique(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException {
        // Taiga #918 - EquipementPhysique.dataCenter != null est équivalent à EquipementPhysique.nomCourtDatacenter est renseigné + le DataCenter existe dans les données
        if (demandeCalcul.getEquipementPhysique().getDataCenter() != null
                && StringUtils.isNotBlank(demandeCalcul.getEquipementPhysique().getDataCenter().getLocalisation())) {
            var pays = demandeCalcul.getEquipementPhysique().getDataCenter().getLocalisation();
            var refMixElecOpt = demandeCalcul.getMixElectrique(pays);
            if (refMixElecOpt.isPresent()) {
                var refMixElec = refMixElecOpt.get().getValeur();
                var pue = getDataCenterPUE(demandeCalcul);
                return MixElectrique.builder()
                        .isServeur(true)
                        .dataCenterPue(pue)
                        .valeurReferentielMixElectrique(refMixElec)
                        .sourceReferentielMixElectrique(refMixElecOpt.get().getSource())
                        .valeur(refMixElec * pue)
                        .build();
            }
        }
        if (StringUtils.isNotBlank(demandeCalcul.getEquipementPhysique().getPaysDUtilisation())) {
            var refMixElecOpt = demandeCalcul.getMixElectrique(demandeCalcul.getEquipementPhysique().getPaysDUtilisation());
            if (refMixElecOpt.isPresent()) {
                var refMixElec = refMixElecOpt.get();
                return MixElectrique.builder()
                        .valeurReferentielMixElectrique(refMixElec.getValeur())
                        .sourceReferentielMixElectrique(refMixElec.getSource())
                        .valeur(refMixElec.getValeur())
                        .build();
            }
        }
        throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(),
                "Il n'existe pas de Mix électrique pour cet équipement : critere: " + demandeCalcul.getCritere().getNomCritere() +
                        " - equipement: " + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique() +
                        " - Pays d'utilisation: " + demandeCalcul.getEquipementPhysique().getPaysDUtilisation() +
                        " - NomCourtDatacenter: " + demandeCalcul.getEquipementPhysique().getNomCourtDatacenter()

        );

    }

    private Double getDataCenterPUE(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException {
        if (demandeCalcul.getEquipementPhysique().getDataCenter().getPue() != null) {
            return demandeCalcul.getEquipementPhysique().getDataCenter().getPue();
        }
        var optionalPUEParDefaut = demandeCalcul.getHypotheseFromCode("PUEParDefaut");
        if (optionalPUEParDefaut.isPresent()) {
            return optionalPUEParDefaut.get().getValeur();
        }
        throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(),
                "Le PUE est manquant et ne permet le calcul de l'impact à l'usage de l'équipement :" +
                        " equipement: " + demandeCalcul.getEquipementPhysique().getNomEquipementPhysique() +
                        " - RefEquipementCible: " + demandeCalcul.getRefEquipementCible() +
                        " - refEquipementParDefaut: " + demandeCalcul.getRefEquipementParDefaut() +
                        " - NomCourtDatacenter: " + demandeCalcul.getEquipementPhysique().getNomCourtDatacenter()

        );
    }


    /**
     * Récupère le taux d'utilisation d'un équipement physique
     * si le tauxUtilisation existe, on utilise la valeur, entre 0 et 1
     * si le modeUtilisation existe, on récupère la valeur dans les hypotheses
     * sinon, on retourne 1 (=100% d'utilisation)
     *
     * @param demandeCalcul la demande de calcul
     * @return le taux d'utilsiation pour l'equipement physique
     * @throws CalculImpactException si taux utilisation existe mais n'est pas entre 0 et 1 ou si le mode d'utilisation n'est pas connu
     */
    private Double getTauxUtilisationEqPhysique(DemandeCalculImpactEquipementPhysique demandeCalcul) throws CalculImpactException {

        var tauxUtilisation = demandeCalcul.getEquipementPhysique().getTauxUtilisation();

        if (tauxUtilisation != null && (tauxUtilisation < 0 || tauxUtilisation > 1)) {
            throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Le taux d'utilisation doit être entre 0 et 1");
        }

        if (tauxUtilisation != null) {
            return tauxUtilisation;
        }

        if (demandeCalcul.getEquipementPhysique().getModeUtilisation() == null) {
            return 1.0;
        }

        final var tauxUtilisationStr = "taux_utilisation_";
        var tauxUtilisationEqPhysique = demandeCalcul.getHypotheseFromCode(tauxUtilisationStr + demandeCalcul.getEquipementPhysique().getModeUtilisation());
        if (tauxUtilisationEqPhysique.isPresent()) {
            return tauxUtilisationEqPhysique.get().getValeur();
        } else {
            var valeursTauxUtilisation = demandeCalcul.getHypothesesStartingWith(tauxUtilisationStr).stream()
                    .map(hypothese -> hypothese.getCode().replace(tauxUtilisationStr, ""))
                    .sorted()
                    .collect(Collectors.joining(", "));

            throw new CalculImpactException(
                    TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(),
                    valeursTauxUtilisation.isEmpty() ?
                            "Il n'y a pas d'hypotheses pour le taux d'utilisation" :
                            "Le mode d'utilisation doit avoir les valeurs " + valeursTauxUtilisation);
        }
    }
}

