package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.analytics.KPITrendResponse;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;

/**
 * Service d'analyse des tendances et prédictions pour les KPI
 *
 * <p>Ce service calcule les tendances, effectue des analyses statistiques et génère des prédictions
 * basées sur l'historique des données.
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2025-01-16
 */
@ApplicationScoped
@Slf4j
public class TrendAnalysisService {

  @Inject AnalyticsService analyticsService;

  @Inject KPICalculatorService kpiCalculatorService;

  @Inject dev.lions.unionflow.server.repository.OrganisationRepository organisationRepository;

  /**
   * Calcule la tendance d'un KPI sur une période donnée
   *
   * @param typeMetrique Le type de métrique à analyser
   * @param periodeAnalyse La période d'analyse
   * @param organisationId L'ID de l'organisation (optionnel)
   * @return Les données de tendance du KPI
   */
  public KPITrendResponse calculerTendance(
      TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) {
    log.info(
        "Calcul de la tendance pour {} sur la période {} et l'organisation {}",
        typeMetrique,
        periodeAnalyse,
        organisationId);

    LocalDateTime dateDebut = periodeAnalyse.getDateDebut();
    LocalDateTime dateFin = periodeAnalyse.getDateFin();

    // Génération des points de données historiques
    List<KPITrendResponse.PointDonneeDTO> pointsDonnees =
        genererPointsDonnees(typeMetrique, dateDebut, dateFin, organisationId);

    // Calculs statistiques
    StatistiquesDTO stats = calculerStatistiques(pointsDonnees);

    // Analyse de tendance (régression linéaire simple)
    TendanceDTO tendance = calculerTendanceLineaire(pointsDonnees);

    // Prédiction pour la prochaine période
    BigDecimal prediction = calculerPrediction(pointsDonnees, tendance);

    // Détection d'anomalies
    detecterAnomalies(pointsDonnees, stats);

    return KPITrendResponse.builder()
        .typeMetrique(typeMetrique)
        .periodeAnalyse(periodeAnalyse)
        .organisationId(organisationId)
        .nomOrganisation(obtenirNomOrganisation(organisationId))
        .dateDebut(dateDebut)
        .dateFin(dateFin)
        .pointsDonnees(pointsDonnees)
        .valeurActuelle(stats.valeurActuelle)
        .valeurMinimale(stats.valeurMinimale)
        .valeurMaximale(stats.valeurMaximale)
        .valeurMoyenne(stats.valeurMoyenne)
        .ecartType(stats.ecartType)
        .coefficientVariation(stats.coefficientVariation)
        .tendanceGenerale(tendance.pente)
        .coefficientCorrelation(tendance.coefficientCorrelation)
        .pourcentageEvolutionGlobale(calculerEvolutionGlobale(pointsDonnees))
        .predictionProchainePeriode(prediction)
        .margeErreurPrediction(calculerMargeErreur(tendance))
        .seuilAlerteBas(calculerSeuilAlerteBas(stats))
        .seuilAlerteHaut(calculerSeuilAlerteHaut(stats))
        .alerteActive(verifierAlertes(stats.valeurActuelle, stats))
        .intervalleRegroupement(periodeAnalyse.getIntervalleRegroupement())
        .formatDate(periodeAnalyse.getFormatDate())
        .dateDerniereMiseAJour(LocalDateTime.now())
        .frequenceMiseAJourMinutes(determinerFrequenceMiseAJour(periodeAnalyse))
        .build();
  }

  /** Génère les points de données historiques pour la période */
  private List<KPITrendResponse.PointDonneeDTO> genererPointsDonnees(
      TypeMetrique typeMetrique,
      LocalDateTime dateDebut,
      LocalDateTime dateFin,
      UUID organisationId) {
    List<KPITrendResponse.PointDonneeDTO> points = new ArrayList<>();

    // Déterminer l'intervalle entre les points
    ChronoUnit unite = determinerUniteIntervalle(dateDebut, dateFin);
    long intervalleValeur = determinerValeurIntervalle(dateDebut, dateFin, unite);

    LocalDateTime dateCourante = dateDebut;
    int index = 0;

    while (!dateCourante.isAfter(dateFin)) {
      LocalDateTime dateFinIntervalle = dateCourante.plus(intervalleValeur, unite);
      if (dateFinIntervalle.isAfter(dateFin)) {
        dateFinIntervalle = dateFin;
      }

      // Calcul de la valeur pour cet intervalle
      BigDecimal valeur =
          calculerValeurPourIntervalle(
              typeMetrique, dateCourante, dateFinIntervalle, organisationId);

      KPITrendResponse.PointDonneeDTO point =
          KPITrendResponse.PointDonneeDTO.builder()
              .date(dateCourante)
              .valeur(valeur)
              .libelle(formaterLibellePoint(dateCourante, unite))
              .anomalie(false) // Sera déterminé plus tard
              .prediction(false)
              .build();

      points.add(point);
      dateCourante = dateCourante.plus(intervalleValeur, unite);
      index++;
    }

    log.info("Généré {} points de données pour la tendance", points.size());
    return points;
  }

  /** Calcule les statistiques descriptives des points de données */
  private StatistiquesDTO calculerStatistiques(List<KPITrendResponse.PointDonneeDTO> points) {
    if (points.isEmpty()) {
      return new StatistiquesDTO();
    }

    List<BigDecimal> valeurs = points.stream().map(KPITrendResponse.PointDonneeDTO::getValeur).toList();

    BigDecimal valeurActuelle = points.get(points.size() - 1).getValeur();
    BigDecimal valeurMinimale = valeurs.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
    BigDecimal valeurMaximale = valeurs.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);

    // Calcul de la moyenne
    BigDecimal somme = valeurs.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
    BigDecimal moyenne = somme.divide(new BigDecimal(valeurs.size()), 4, RoundingMode.HALF_UP);

    // Calcul de l'écart-type
    BigDecimal sommeDifferencesCarrees =
        valeurs.stream()
            .map(v -> v.subtract(moyenne).pow(2))
            .reduce(BigDecimal.ZERO, BigDecimal::add);

    BigDecimal variance =
        sommeDifferencesCarrees.divide(new BigDecimal(valeurs.size()), 4, RoundingMode.HALF_UP);
    BigDecimal ecartType =
        new BigDecimal(Math.sqrt(variance.doubleValue())).setScale(4, RoundingMode.HALF_UP);

    // Coefficient de variation
    BigDecimal coefficientVariation =
        moyenne.compareTo(BigDecimal.ZERO) != 0
            ? ecartType.divide(moyenne, 4, RoundingMode.HALF_UP)
            : BigDecimal.ZERO;

    return new StatistiquesDTO(
        valeurActuelle, valeurMinimale, valeurMaximale, moyenne, ecartType, coefficientVariation);
  }

  /** Calcule la tendance linéaire (régression linéaire simple) */
  private TendanceDTO calculerTendanceLineaire(List<KPITrendResponse.PointDonneeDTO> points) {
    if (points.size() < 2) {
      return new TendanceDTO(BigDecimal.ZERO, BigDecimal.ZERO);
    }

    int n = points.size();
    BigDecimal sommeX = BigDecimal.ZERO;
    BigDecimal sommeY = BigDecimal.ZERO;
    BigDecimal sommeXY = BigDecimal.ZERO;
    BigDecimal sommeX2 = BigDecimal.ZERO;
    BigDecimal sommeY2 = BigDecimal.ZERO;

    for (int i = 0; i < n; i++) {
      BigDecimal x = new BigDecimal(i); // Index comme variable X
      BigDecimal y = points.get(i).getValeur(); // Valeur comme variable Y

      sommeX = sommeX.add(x);
      sommeY = sommeY.add(y);
      sommeXY = sommeXY.add(x.multiply(y));
      sommeX2 = sommeX2.add(x.multiply(x));
      sommeY2 = sommeY2.add(y.multiply(y));
    }

    // Calcul de la pente (coefficient directeur)
    BigDecimal nBD = new BigDecimal(n);
    BigDecimal numerateur = nBD.multiply(sommeXY).subtract(sommeX.multiply(sommeY));
    BigDecimal denominateur = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX));

    BigDecimal pente = numerateur.divide(denominateur, 6, RoundingMode.HALF_UP);

    // Calcul du coefficient de corrélation R²
    BigDecimal numerateurR = numerateur;
    BigDecimal denominateurR1 = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX));
    BigDecimal denominateurR2 = nBD.multiply(sommeY2).subtract(sommeY.multiply(sommeY));

    BigDecimal coefficientCorrelation = BigDecimal.ZERO;
    BigDecimal produit = denominateurR1.multiply(denominateurR2);
    if (produit.compareTo(BigDecimal.ZERO) > 0) {
      BigDecimal denominateurR = new BigDecimal(Math.sqrt(produit.doubleValue()));
      BigDecimal r = numerateurR.divide(denominateurR, 6, RoundingMode.HALF_UP);
      coefficientCorrelation = r.multiply(r); // R²
    }

    return new TendanceDTO(pente, coefficientCorrelation);
  }

  /** Calcule une prédiction pour la prochaine période */
  private BigDecimal calculerPrediction(
      List<KPITrendResponse.PointDonneeDTO> points, TendanceDTO tendance) {
    if (points.isEmpty()) return BigDecimal.ZERO;

    BigDecimal derniereValeur = points.get(points.size() - 1).getValeur();
    BigDecimal prediction = derniereValeur.add(tendance.pente);

    // S'assurer que la prédiction est positive
    return prediction.max(BigDecimal.ZERO);
  }

  /** Détecte les anomalies dans les points de données */
  private void detecterAnomalies(List<KPITrendResponse.PointDonneeDTO> points, StatistiquesDTO stats) {
    BigDecimal seuilAnomalie = stats.ecartType.multiply(new BigDecimal("2")); // 2 écarts-types

    for (KPITrendResponse.PointDonneeDTO point : points) {
      BigDecimal ecartMoyenne = point.getValeur().subtract(stats.valeurMoyenne).abs();
      if (ecartMoyenne.compareTo(seuilAnomalie) > 0) {
        point.setAnomalie(true);
      }
    }
  }

  // === MÉTHODES UTILITAIRES ===

  private ChronoUnit determinerUniteIntervalle(LocalDateTime dateDebut, LocalDateTime dateFin) {
    long joursTotal = ChronoUnit.DAYS.between(dateDebut, dateFin);

    if (joursTotal <= 7) return ChronoUnit.DAYS;
    if (joursTotal <= 90) return ChronoUnit.DAYS;
    if (joursTotal <= 365) return ChronoUnit.WEEKS;
    return ChronoUnit.MONTHS;
  }

  private long determinerValeurIntervalle(
      LocalDateTime dateDebut, LocalDateTime dateFin, ChronoUnit unite) {
    long dureeTotal = unite.between(dateDebut, dateFin);

    // Viser environ 10-20 points de données
    if (dureeTotal <= 20) return 1;
    if (dureeTotal <= 40) return 2;
    if (dureeTotal <= 100) return 5;
    return dureeTotal / 15; // Environ 15 points
  }

  private BigDecimal calculerValeurPourIntervalle(
      TypeMetrique typeMetrique,
      LocalDateTime dateDebut,
      LocalDateTime dateFin,
      UUID organisationId) {
    // Utiliser le service KPI pour calculer la valeur
    return switch (typeMetrique) {
      case NOMBRE_MEMBRES_ACTIFS -> {
        // Calcul direct via le service KPI
        var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
        yield kpis.getOrDefault(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, BigDecimal.ZERO);
      }
      case TOTAL_COTISATIONS_COLLECTEES -> {
        var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
        yield kpis.getOrDefault(TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, BigDecimal.ZERO);
      }
      case NOMBRE_EVENEMENTS_ORGANISES -> {
        var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
        yield kpis.getOrDefault(TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, BigDecimal.ZERO);
      }
      case NOMBRE_DEMANDES_AIDE -> {
        var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin);
        yield kpis.getOrDefault(TypeMetrique.NOMBRE_DEMANDES_AIDE, BigDecimal.ZERO);
      }
      default -> BigDecimal.ZERO;
    };
  }

  private String formaterLibellePoint(LocalDateTime date, ChronoUnit unite) {
    return switch (unite) {
      case DAYS -> date.toLocalDate().toString();
      case WEEKS -> "S" + date.get(java.time.temporal.WeekFields.ISO.weekOfYear());
      case MONTHS -> date.getMonth().toString() + " " + date.getYear();
      default -> date.toString();
    };
  }

  private BigDecimal calculerEvolutionGlobale(List<KPITrendResponse.PointDonneeDTO> points) {
    if (points.size() < 2) return BigDecimal.ZERO;

    BigDecimal premiereValeur = points.get(0).getValeur();
    BigDecimal derniereValeur = points.get(points.size() - 1).getValeur();

    if (premiereValeur.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO;

    return derniereValeur
        .subtract(premiereValeur)
        .divide(premiereValeur, 4, RoundingMode.HALF_UP)
        .multiply(new BigDecimal("100"));
  }

  private BigDecimal calculerMargeErreur(TendanceDTO tendance) {
    // Marge d'erreur basée sur le coefficient de corrélation
    BigDecimal precision = tendance.coefficientCorrelation;
    BigDecimal margeErreur = BigDecimal.ONE.subtract(precision).multiply(new BigDecimal("100"));
    return margeErreur.min(new BigDecimal("50")); // Plafonnée à 50%
  }

  private BigDecimal calculerSeuilAlerteBas(StatistiquesDTO stats) {
    return stats.valeurMoyenne.subtract(stats.ecartType.multiply(new BigDecimal("1.5")));
  }

  private BigDecimal calculerSeuilAlerteHaut(StatistiquesDTO stats) {
    return stats.valeurMoyenne.add(stats.ecartType.multiply(new BigDecimal("1.5")));
  }

  private Boolean verifierAlertes(BigDecimal valeurActuelle, StatistiquesDTO stats) {
    BigDecimal seuilBas = calculerSeuilAlerteBas(stats);
    BigDecimal seuilHaut = calculerSeuilAlerteHaut(stats);

    return valeurActuelle.compareTo(seuilBas) < 0 || valeurActuelle.compareTo(seuilHaut) > 0;
  }

  private Integer determinerFrequenceMiseAJour(PeriodeAnalyse periode) {
    return switch (periode) {
      case AUJOURD_HUI, HIER -> 15; // 15 minutes
      case CETTE_SEMAINE, SEMAINE_DERNIERE -> 60; // 1 heure
      case CE_MOIS, MOIS_DERNIER -> 240; // 4 heures
      default -> 1440; // 24 heures
    };
  }

  private String obtenirNomOrganisation(UUID organisationId) {
    if (organisationId == null) return null;
    return organisationRepository.findByIdOptional(organisationId)
        .map(org -> org.getNom())
        .orElse(null);
  }

  // === CLASSES INTERNES ===

  private static class StatistiquesDTO {
    final BigDecimal valeurActuelle;
    final BigDecimal valeurMinimale;
    final BigDecimal valeurMaximale;
    final BigDecimal valeurMoyenne;
    final BigDecimal ecartType;
    final BigDecimal coefficientVariation;

    StatistiquesDTO() {
      this(
          BigDecimal.ZERO,
          BigDecimal.ZERO,
          BigDecimal.ZERO,
          BigDecimal.ZERO,
          BigDecimal.ZERO,
          BigDecimal.ZERO);
    }

    StatistiquesDTO(
        BigDecimal valeurActuelle,
        BigDecimal valeurMinimale,
        BigDecimal valeurMaximale,
        BigDecimal valeurMoyenne,
        BigDecimal ecartType,
        BigDecimal coefficientVariation) {
      this.valeurActuelle = valeurActuelle;
      this.valeurMinimale = valeurMinimale;
      this.valeurMaximale = valeurMaximale;
      this.valeurMoyenne = valeurMoyenne;
      this.ecartType = ecartType;
      this.coefficientVariation = coefficientVariation;
    }
  }

  private static class TendanceDTO {
    final BigDecimal pente;
    final BigDecimal coefficientCorrelation;

    TendanceDTO(BigDecimal pente, BigDecimal coefficientCorrelation) {
      this.pente = pente;
      this.coefficientCorrelation = coefficientCorrelation;
    }
  }
}
