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.DemandeCalculImpactOperationNonIT;
import org.mte.numecoeval.calculs.domain.data.demande.MetadataCalcul;
import org.mte.numecoeval.calculs.domain.data.erreur.TypeErreurCalcul;
import org.mte.numecoeval.calculs.domain.data.indicateurs.ImpactOperationNonIT;
import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielFacteurCaracterisation;
import org.mte.numecoeval.calculs.domain.data.referentiel.ReferentielHypothese;
import org.mte.numecoeval.calculs.domain.data.trace.*;
import org.mte.numecoeval.calculs.domain.exception.CalculImpactException;
import org.mte.numecoeval.calculs.domain.port.input.service.CalculImpactOperationNonITService;
import org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactOperationNonITUtils;
import org.mte.numecoeval.calculs.domain.traceur.TraceUtils;

import java.util.Optional;

import static org.mte.numecoeval.calculs.domain.traceur.TraceCalculImpactOperationNonITUtils.*;
import static org.mte.numecoeval.calculs.domain.utils.Constants.*;

@Slf4j
@AllArgsConstructor
public class CalculImpactOperationNonITServiceImpl implements CalculImpactOperationNonITService {
    private static final String VERSION_CALCUL = "1.0";

    public ImpactOperationNonIT calculerImpactOperationNonIT(DemandeCalculImpactOperationNonIT demandeCalcul) {
        log.debug("Début de calcul d'impact d'un item non it : {}, {}, {} ",
                demandeCalcul.getEtape().getCode(),
                demandeCalcul.getCritere().getNomCritere(),
                demandeCalcul.getOperationNonIT().getNomItemNonIT());
        ImpactOperationNonIT impactErreur = null;

        try {
            return getCalculImpactOperationNonIT(demandeCalcul);
        } catch (CalculImpactException e) {
            log.debug("Erreur de calcul impact item: Type: {}, Cause: {}, Etape: {}, Critere: {}, Item Physique: {}, RefItemRetenu: {}, RefItemParDefaut: {}",
                    e.getErrorType(),
                    e.getMessage(),
                    demandeCalcul.getEtape().getCode(),
                    demandeCalcul.getCritere().getNomCritere(),
                    demandeCalcul.getOperationNonIT().getNomItemNonIT(),
                    demandeCalcul.getTypeItem().getType(),
                    demandeCalcul.getRefItemParDefaut());
            impactErreur = buildCalculImpactForError(demandeCalcul, e);
        } catch (Exception e) {
            log.debug("{} : Erreur de calcul impact item 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 ImpactOperationNonIT getCalculImpactOperationNonIT(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        MetadataCalcul metadataCalcul = switch (demandeCalcul.getTypeItem().getCategorie()) {
            case CATEGORIE_RESEAU_MOBILE, CATEGORIE_MAINTENANCE -> getFormule(demandeCalcul);
            case CATEGORIE_RESEAU_FIXE -> getFormuleReseauFixe(demandeCalcul);
            case CATEGORIE_BATIMENT -> getFormuleBatiment(demandeCalcul);
            case CATEGORIE_DEPLACEMENT_ELECTRIQUE -> getFormuleUtilisationDeplacementVehiculeElectrique(demandeCalcul);
            case CATEGORIE_DEPLACEMENT_ESSENCE -> getFormuleUtilisationDeplacementVehiculeEssence(demandeCalcul);
            case CATEGORIE_DEPLACEMENT_HYBRIDE -> getFormuleUtilisationDeplacementVehiculeHybride(demandeCalcul);
            default ->
                    throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "La catégorie renseignée est invalide");
        };
        if (metadataCalcul == null) {
            return null;
        }
        var calcul = buildCalculImpactOperationNonIT(demandeCalcul, metadataCalcul.valeurImpactUnitaire(), metadataCalcul.traceCalculImpactOperationNonIT());
        calcul.setTrace(TraceUtils.getTraceFromTraceur(metadataCalcul.traceCalculImpactOperationNonIT()));
        return calcul;
    }

    private MetadataCalcul getFormule(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        if ("UTILISATION".equals(demandeCalcul.getEtape().getCode())) {
            return getFormuleUtilisationDefaut(demandeCalcul);
        } else {
            return getFormuleDefaut(demandeCalcul);
        }
    }

    private MetadataCalcul getFormuleDefaut(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        // si l'étape n'est pas UTILISATION, base de la formule commune à toutes les catégories
        Double quantite = demandeCalcul.getOperationNonIT().getQuantite();
        ReferentielFacteurCaracterisation referentielFacteurCaracterisation = getImpactItem(demandeCalcul);
        Double valeurReferentiel = referentielFacteurCaracterisation.getValeur();
        Double valeurImpactUnitaire = quantite * valeurReferentiel;
        TraceCalculImpactOperationNonIT traceCalculImpactOperationNonIT = TraceCalculImpactOperationNonITUtils.buildTrace(null, null, referentielFacteurCaracterisation.getValeur(), referentielFacteurCaracterisation.getSource(), null, null, null);
        traceCalculImpactOperationNonIT.setFormule(getFormuleBase(quantite, valeurReferentiel));
        return new MetadataCalcul(valeurImpactUnitaire, traceCalculImpactOperationNonIT);
    }

    private MetadataCalcul getFormuleUtilisationDefaut(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        // base de la formule commune à toutes les catégories de formule
        Double quantite = demandeCalcul.getOperationNonIT().getQuantite();
        ConsoElecAnMoyenne consoElecMoyenne = getConsoElecAnMoyenne(demandeCalcul);
        MixElectrique mixelec = getMixElectrique(demandeCalcul);
        Double valeurImpactUnitaire = quantite * consoElecMoyenne.getValeur() * mixelec.getValeur();
        TraceCalculImpactOperationNonIT traceCalculImpactOperationNonIT = TraceCalculImpactOperationNonITUtils.buildTrace(consoElecMoyenne, mixelec, null, null, null, null, null);
        traceCalculImpactOperationNonIT.setFormule(getFormuleUtilisation(quantite, consoElecMoyenne.getValeur(), mixelec.getValeur()));
        traceCalculImpactOperationNonIT.setConsoElecAnMoyenne(consoElecMoyenne);
        return new MetadataCalcul(valeurImpactUnitaire, traceCalculImpactOperationNonIT);
    }

    private MetadataCalcul getFormuleReseauFixe(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        MetadataCalcul metadataCalcul = getFormule(demandeCalcul);
        Hypothese hypothese = getHypothese(demandeCalcul);
        Double fixedLineCapacity = hypothese.getValeur();
        Double valeurImpactUnitaire = metadataCalcul.valeurImpactUnitaire() / fixedLineCapacity;
        TraceCalculImpactOperationNonIT traceCalculImpactOperationNonIT = metadataCalcul.traceCalculImpactOperationNonIT();
        String traceFormuleBase = traceCalculImpactOperationNonIT.getFormule();
        traceCalculImpactOperationNonIT.setFormule(traceFormuleBase + " / CapaciteLigneFixe(%s)".formatted(fixedLineCapacity));
        traceCalculImpactOperationNonIT.setHypothese(hypothese);
        return new MetadataCalcul(valeurImpactUnitaire, traceCalculImpactOperationNonIT);
    }

    private MetadataCalcul getFormuleBatiment(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        if ("UTILISATION".equals(demandeCalcul.getEtape().getCode())) {
            return null;
        }
        MetadataCalcul metadataCalcul = getFormule(demandeCalcul);
        DureeDeVie dureeDeVie = getDureeVie(demandeCalcul);
        Double dureeDeVieValeur = dureeDeVie.getValeurRetenue();
        Double valeurImpactUnitaire = metadataCalcul.valeurImpactUnitaire() / dureeDeVieValeur;
        TraceCalculImpactOperationNonIT traceCalculImpactOperationNonIT = metadataCalcul.traceCalculImpactOperationNonIT();
        String traceFormuleBase = traceCalculImpactOperationNonIT.getFormule();
        traceCalculImpactOperationNonIT.setFormule(traceFormuleBase + " / DureeDeVie(%s)".formatted(dureeDeVieValeur));
        traceCalculImpactOperationNonIT.setDureeDeVie(dureeDeVie);
        return new MetadataCalcul(valeurImpactUnitaire, traceCalculImpactOperationNonIT);
    }

    private MetadataCalcul getFormuleUtilisationDeplacementVehiculeEssence(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        if (!"UTILISATION".equals(demandeCalcul.getEtape().getCode())) {
            return null;
        }
        Double quantite = demandeCalcul.getOperationNonIT().getQuantite();
        Hypothese hypothese = getHypothese(demandeCalcul);
        Double consoMoyenne = hypothese.getValeur();
        ReferentielFacteurCaracterisation referentielFacteurCaracterisation = getImpactItem(demandeCalcul);
        Double valeurReferentiel = referentielFacteurCaracterisation.getValeur();
        Double valeurImpactUnitaire = consoMoyenne * quantite * valeurReferentiel;
        TraceCalculImpactOperationNonIT traceCalculImpactOperationNonIT = TraceCalculImpactOperationNonITUtils.buildTrace(null, null, referentielFacteurCaracterisation.getValeur(), referentielFacteurCaracterisation.getSource(), hypothese, null, null);
        traceCalculImpactOperationNonIT.setFormule(getFormuleDeplacement(consoMoyenne, quantite) + " * ReferentielFacteurCaracterisation(%s))".formatted(valeurReferentiel));
        return new MetadataCalcul(valeurImpactUnitaire, traceCalculImpactOperationNonIT);

    }

    private MetadataCalcul getFormuleUtilisationDeplacementVehiculeHybride(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        if (!"UTILISATION".equals(demandeCalcul.getEtape().getCode())) {
            return null;
        }
        Double quantite = demandeCalcul.getOperationNonIT().getQuantite();
        Hypothese hypothese = getHypothese(demandeCalcul);
        Double consoMoyenne = hypothese.getValeur();
        ReferentielFacteurCaracterisation referentielFacteurCaracterisation = getImpactItem(demandeCalcul);
        Double valeurReferentiel = referentielFacteurCaracterisation.getValeur();
        Double valeurImpactUnitaire = consoMoyenne * quantite * valeurReferentiel;
        TraceCalculImpactOperationNonIT traceCalculImpactOperationNonIT = TraceCalculImpactOperationNonITUtils.buildTrace(null, null, referentielFacteurCaracterisation.getValeur(), referentielFacteurCaracterisation.getSource(), hypothese, null, null);
        Hypothese hypotheseTauxHybride = getTauxHybride(demandeCalcul);
        valeurImpactUnitaire = valeurImpactUnitaire * hypotheseTauxHybride.getValeur();
        traceCalculImpactOperationNonIT.setFormule(getFormuleDeplacement(consoMoyenne, quantite) + " * ReferentielFacteurCaracterisation(%s) * TauxHybride(%s))".formatted(valeurReferentiel, hypotheseTauxHybride.getValeur()));
        return new MetadataCalcul(valeurImpactUnitaire, traceCalculImpactOperationNonIT);
    }

    private MetadataCalcul getFormuleUtilisationDeplacementVehiculeElectrique(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        if (!"UTILISATION".equals(demandeCalcul.getEtape().getCode())) {
            return null;
        }
        Double quantite = demandeCalcul.getOperationNonIT().getQuantite();
        Hypothese hypothese = getHypothese(demandeCalcul);
        Double consoMoyenne = hypothese.getValeur();
        MixElectrique mixelec = getMixElectrique(demandeCalcul);
        Double valeurImpactUnitaire = consoMoyenne * quantite * mixelec.getValeur();
        TraceCalculImpactOperationNonIT traceCalculImpactOperationNonIT = TraceCalculImpactOperationNonITUtils.buildTrace(null, mixelec, null, null, null, null, null);
        traceCalculImpactOperationNonIT.setFormule(getFormuleDeplacement(consoMoyenne, quantite) + " * MixElectrique(%s))".formatted(mixelec.getValeur()));
        return new MetadataCalcul(valeurImpactUnitaire, traceCalculImpactOperationNonIT);
    }

    private ImpactOperationNonIT buildCalculImpactOperationNonIT(DemandeCalculImpactOperationNonIT demandeCalcul, Double valeurImpactUnitaire, TraceCalculImpactOperationNonIT traceCalculImpactOperationNonIT) {
        ImpactOperationNonIT result = ImpactOperationNonIT
                .builder()
                .nomItemNonIT(demandeCalcul.getOperationNonIT().getNomItemNonIT())
                .etapeACV(demandeCalcul.getEtape().getCode())
                .critere(demandeCalcul.getCritere().getNomCritere())
                .dateCalcul(demandeCalcul.getDateCalcul())
                .versionCalcul(VERSION_CALCUL)
                .typeItem(demandeCalcul.getOperationNonIT().getType())
                .quantite(demandeCalcul.getOperationNonIT().getQuantite())
                .statutIndicateur("OK")
                .impactUnitaire(valeurImpactUnitaire)
                .unite(demandeCalcul.getCritere().getUnite())
                .nomLot(demandeCalcul.getOperationNonIT().getNomLot())
                .nomSourceDonnee(demandeCalcul.getOperationNonIT().getNomSourceDonnee())
                .dateLot(demandeCalcul.getOperationNonIT().getDateLot())
                .nomEntite(demandeCalcul.getOperationNonIT().getNomEntite())
                .nomOrganisation(demandeCalcul.getOperationNonIT().getNomOrganisation())
                .qualite(demandeCalcul.getOperationNonIT().getQualite())
                .build();
        if (traceCalculImpactOperationNonIT.getConsoElecAnMoyenne() != null) {
            result.setConsoElecMoyenne(traceCalculImpactOperationNonIT.getConsoElecAnMoyenne().getValeur());
        }
        return result;
    }

    private ImpactOperationNonIT buildCalculImpactForError(DemandeCalculImpactOperationNonIT demandeCalcul, CalculImpactException exception) {

        return ImpactOperationNonIT
                .builder()
                .nomItemNonIT(demandeCalcul.getOperationNonIT().getNomItemNonIT())
                .etapeACV(demandeCalcul.getEtape().getCode())
                .critere(demandeCalcul.getCritere().getNomCritere())
                .dateCalcul(demandeCalcul.getDateCalcul())
                .versionCalcul(VERSION_CALCUL)
                .typeItem(demandeCalcul.getOperationNonIT().getType())
                .quantite(demandeCalcul.getOperationNonIT().getQuantite())
                .statutIndicateur("ERREUR")
                .impactUnitaire(null)
                .unite(demandeCalcul.getCritere().getUnite())
                .consoElecMoyenne(null)
                .nomLot(demandeCalcul.getOperationNonIT().getNomLot())
                .nomSourceDonnee(demandeCalcul.getOperationNonIT().getNomSourceDonnee())
                .dateLot(demandeCalcul.getOperationNonIT().getDateLot())
                .nomEntite(demandeCalcul.getOperationNonIT().getNomEntite())
                .nomOrganisation(demandeCalcul.getOperationNonIT().getNomOrganisation())
                .trace(TraceUtils.getTraceFromTraceur(TraceCalculImpactOperationNonITUtils.buildTraceErreur(exception)))
                .qualite(demandeCalcul.getOperationNonIT().getQualite())
                .build();
    }

    private Hypothese getHypothese(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        Optional<ReferentielHypothese> hypothese = demandeCalcul.getHypotheseFromCode(demandeCalcul.getTypeItem().getRefHypothese());
        if (hypothese.isPresent() && hypothese.get().getValeur() != null) {
            return Hypothese.builder()
                    .valeur(hypothese.get().getValeur())
                    .nom_hypothese(hypothese.get().getCode())
                    .source(hypothese.get().getSource())
                    .build();
        }
        throw new CalculImpactException(
                TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(),
                "Il n'y a pas d'hypothese pour l'item pour la refHypothese '" + demandeCalcul.getTypeItem().getRefHypothese() + "' de l'item " + demandeCalcul.getOperationNonIT().getNomItemNonIT());

    }

    private Hypothese getTauxHybride(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        var hypothese = demandeCalcul.getHypotheseFromCode("TAUX_VEHICULE_HYBRIDE");
        if (hypothese.isPresent() && hypothese.get().getValeur() != null) {
            return Hypothese.builder()
                    .valeur(hypothese.get().getValeur())
                    .nom_hypothese(hypothese.get().getCode())
                    .source(hypothese.get().getSource())
                    .build();
        }
        throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Il n'y a pas de taux renseigné pour les véhicules hybrides dans la table des références");
    }

    private ReferentielFacteurCaracterisation getImpactItem(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        Optional<ReferentielFacteurCaracterisation> facteurCaracterisation = demandeCalcul.getFacteurCaracterisation(demandeCalcul.getTypeItem().getRefItemParDefaut());
        if (facteurCaracterisation.isPresent() && facteurCaracterisation.get().getValeur() != null) {
            return facteurCaracterisation.get();
        } else {
            throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Référentiel Facteur Caractérisation inconnu");
        }
    }

    private ConsoElecAnMoyenne getConsoElecAnMoyenne(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        if (demandeCalcul.getOperationNonIT().getConsoElecAnnuelle() != null) {
            var valeur = demandeCalcul.getOperationNonIT().getConsoElecAnnuelle();
            return ConsoElecAnMoyenne.builder()
                    .valeurItemConsoElecAnnuelle(valeur)
                    .valeur(valeur)
                    .build();
        }
        if (StringUtils.isBlank(demandeCalcul.getRefItemParDefaut())) {
            throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "Référentiel Facteur Caractérisation inconnu");
        }

        var referentielFacteurCaracterisation = getImpactItem(demandeCalcul);
        if (referentielFacteurCaracterisation.getConsoElecMoyenne() == null) {
            throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(),
                    "Donnée de consommation electrique manquante : operationNonIT : " + demandeCalcul.getOperationNonIT().getNomItemNonIT() +
                            ", RefItemParDefaut : " + demandeCalcul.getTypeItem().getRefItemParDefaut()

            );
        }

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

    }

    private MixElectrique getMixElectrique(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {
        if (StringUtils.isNotBlank(demandeCalcul.getOperationNonIT().getLocalisation())) {
            Optional<ReferentielFacteurCaracterisation> refMixElecOpt = demandeCalcul.getMixElectriqueFromFacteurCaracterisation(demandeCalcul.getOperationNonIT().getLocalisation());
            if (refMixElecOpt.isPresent()) {
                var refMixElec = refMixElecOpt.get();
                return MixElectrique.builder()
                        .valeur(refMixElec.getValeur())
                        .valeurReferentielMixElectrique(refMixElec.getValeur())
                        .sourceReferentielMixElectrique(refMixElec.getSource())
                        .build();
            }
        }
        throw new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(),
                "Il n'existe pas de Mix électrique pour cet item non it : critere: " + demandeCalcul.getCritere().getNomCritere() +
                        " - operation non it: " + demandeCalcul.getOperationNonIT().getNomItemNonIT() +
                        " - Localisation: " + demandeCalcul.getOperationNonIT().getLocalisation()
        );

    }

    /**
     * Si la duree de vie est renseignée dans l'opérationNonIT, on retourne cette valeur
     * Sinon, si la valeur est renseignée dans la table ref_typeitem correspondant à cet item, on retourne cette valeur
     * Sinon, si dans la table ref_hypothèse on a une valeur pour la clé "dureeVieDatacenter", on utilise cette valeur
     * Sinon, on renvoie une erreur
     *
     * @param demandeCalcul la demande de calcul
     * @return dureeDeVie
     * @throws CalculImpactException sinon
     */
    public DureeDeVie getDureeVie(DemandeCalculImpactOperationNonIT demandeCalcul) throws CalculImpactException {

        var dureeDeVie = DureeDeVie.builder().build();

        Double dureeInventaire = demandeCalcul.getOperationNonIT().getDureeDeVie();
        if (dureeInventaire != null) {
            dureeDeVie.setValeurRetenue(dureeInventaire < 1 ? 1.0 : dureeInventaire);
            return dureeDeVie;
        }

        // dureeInventaire est null
        // recuperation duree de vie via typeItem
        if (demandeCalcul.getTypeItem() != null) {
            var dureeVieDefaut = demandeCalcul.getTypeItem().getDureeVieDefaut();

            if (dureeVieDefaut != null) {
                Double duree = dureeVieDefaut < 1 ? 1 : dureeVieDefaut;
                dureeDeVie.setDureeDeVieParDefaut(DureeDeVieParDefaut.builder()
                        .valeurTypeItemDureeVieDefaut(duree)
                        .valeur(duree)
                        .sourceTypeItemDureeVieDefaut(demandeCalcul.getTypeItem().getSource())
                        .build());

                dureeDeVie.setValeurRetenue(duree);
                return dureeDeVie;
            }
        }

        // dureeInventaire est null
        // typeItem est null
        // recuperation duree de vie via hypothese 'dureeVieBatimentParDefaut'
        var hypothese = demandeCalcul.getHypotheseFromCode("dureeVieBatimentParDefaut")
                .orElseThrow(() -> new CalculImpactException(TypeErreurCalcul.ERREUR_FONCTIONNELLE.getCode(), "La durée de vie par défaut de l'item n'a pas pu être déterminée"));

        if (hypothese.getValeur() != null) {
            Double duree = hypothese.getValeur() < 1 ? 1 : hypothese.getValeur();
            dureeDeVie.setDureeDeVieParDefaut(DureeDeVieParDefaut.builder()
                    .valeurReferentielHypothese(duree)
                    .sourceReferentielHypothese(hypothese.getSource())
                    .valeur(duree)
                    .build());

            dureeDeVie.setValeurRetenue(duree);
        }

        return dureeDeVie;
    }
}
