package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.solidarite.response.DemandeAideResponse;
import dev.lions.unionflow.server.api.dto.solidarite.response.PropositionAideResponse;
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

/**
 * Service intelligent de matching entre demandes et propositions d'aide
 *
 * <p>
 * Ce service utilise des algorithmes avancés pour faire correspondre les
 * demandes d'aide avec
 * les propositions les plus appropriées.
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2025-01-16
 */
@ApplicationScoped
public class MatchingService {

  private static final Logger LOG = Logger.getLogger(MatchingService.class);

  @Inject
  PropositionAideService propositionAideService;

  @Inject
  DemandeAideService demandeAideService;

  @ConfigProperty(name = "unionflow.matching.score-minimum", defaultValue = "30.0")
  double scoreMinimumMatching;

  @ConfigProperty(name = "unionflow.matching.max-resultats", defaultValue = "10")
  int maxResultatsMatching;

  @ConfigProperty(name = "unionflow.matching.boost-geographique", defaultValue = "10.0")
  double boostGeographique;

  @ConfigProperty(name = "unionflow.matching.boost-experience", defaultValue = "5.0")
  double boostExperience;

  // === MATCHING DEMANDES -> PROPOSITIONS ===

  /**
   * Trouve les propositions compatibles avec une demande d'aide
   *
   * @param demande La demande d'aide
   * @return Liste des propositions compatibles triées par score
   */
  public List<PropositionAideResponse> trouverPropositionsCompatibles(DemandeAideResponse demande) {
    LOG.infof("Recherche de propositions compatibles pour la demande: %s", demande.getId());

    long startTime = System.currentTimeMillis();

    try {
      // 1. Recherche de base par type d'aide
      List<PropositionAideResponse> candidatsOriginal = propositionAideService
          .obtenirPropositionsActives(demande.getTypeAide());
      List<PropositionAideResponse> candidats = new ArrayList<>(candidatsOriginal);

      // 2. Si pas assez de candidats, élargir à la catégorie
      if (candidats.size() < 3) {
        candidats.addAll(rechercherParCategorie(demande.getTypeAide().getCategorie()));
      }

      // 3. Filtrage et scoring
      List<ResultatMatching> resultats = candidats.stream()
          .filter(PropositionAideResponse::isActiveEtDisponible)
          .filter(p -> p.peutAccepterBeneficiaires())
          .map(
              proposition -> {
                double score = calculerScoreCompatibilite(demande, proposition);
                return new ResultatMatching(proposition, score);
              })
          .filter(resultat -> resultat.score >= scoreMinimumMatching)
          .sorted((r1, r2) -> Double.compare(r2.score, r1.score))
          .limit(maxResultatsMatching)
          .collect(Collectors.toList());

      // 4. Extraction des propositions
      List<PropositionAideResponse> propositionsCompatibles = resultats.stream()
          .map(
              resultat -> {
                // Stocker le score dans les données personnalisées
                if (resultat.proposition.getDonneesPersonnalisees() == null) {
                  resultat.proposition.setDonneesPersonnalisees(new HashMap<>());
                }
                resultat.proposition
                    .getDonneesPersonnalisees()
                    .put("scoreMatching", resultat.score);
                return resultat.proposition;
              })
          .collect(Collectors.toList());

      long duration = System.currentTimeMillis() - startTime;
      LOG.infof(
          "Matching terminé en %d ms. Trouvé %d propositions compatibles",
          duration, propositionsCompatibles.size());

      return propositionsCompatibles;

    } catch (Exception e) {
      LOG.errorf(e, "Erreur lors du matching pour la demande: %s", demande.getId());
      return new ArrayList<>();
    }
  }

  /**
   * Trouve les demandes compatibles avec une proposition d'aide
   *
   * @param proposition La proposition d'aide
   * @return Liste des demandes compatibles triées par score
   */
  public List<DemandeAideResponse> trouverDemandesCompatibles(PropositionAideResponse proposition) {
    LOG.infof("Recherche de demandes compatibles pour la proposition: %s", proposition.getId());

    try {
      // Recherche des demandes actives du même type
      Map<String, Object> filtres = Map.of(
          "typeAide", proposition.getTypeAide(),
          "statut",
          List.of(
              dev.lions.unionflow.server.api.enums.solidarite.StatutAide.SOUMISE,
              dev.lions.unionflow.server.api.enums.solidarite.StatutAide.EN_ATTENTE,
              dev.lions.unionflow.server.api.enums.solidarite.StatutAide.EN_COURS_EVALUATION,
              dev.lions.unionflow.server.api.enums.solidarite.StatutAide.APPROUVEE));

      List<DemandeAideResponse> candidats = demandeAideService.rechercherAvecFiltres(filtres);

      // Scoring et tri
      return candidats.stream()
          .map(
              demande -> {
                double score = calculerScoreCompatibilite(demande, proposition);
                // Stocker le score temporairement
                if (demande.getDonneesPersonnalisees() == null) {
                  demande.setDonneesPersonnalisees(new HashMap<>());
                }
                demande.getDonneesPersonnalisees().put("scoreMatching", score);
                return demande;
              })
          .filter(
              demande -> (Double) demande.getDonneesPersonnalisees().get("scoreMatching") >= scoreMinimumMatching)
          .sorted(
              (d1, d2) -> {
                Double score1 = (Double) d1.getDonneesPersonnalisees().get("scoreMatching");
                Double score2 = (Double) d2.getDonneesPersonnalisees().get("scoreMatching");
                return Double.compare(score2, score1);
              })
          .limit(maxResultatsMatching)
          .collect(Collectors.toList());

    } catch (Exception e) {
      LOG.errorf(e, "Erreur lors du matching pour la proposition: %s", proposition.getId());
      return new ArrayList<>();
    }
  }

  // === MATCHING SPÉCIALISÉ ===

  /**
   * Recherche spécialisée de proposants financiers pour une demande approuvée
   *
   * @param demande La demande d'aide financière approuvée
   * @return Liste des proposants financiers compatibles
   */
  public List<PropositionAideResponse> rechercherProposantsFinanciers(DemandeAideResponse demande) {
    LOG.infof("Recherche de proposants financiers pour la demande: %s", demande.getId());

    if (!demande.getTypeAide().isFinancier()) {
      LOG.warnf("La demande %s n'est pas de type financier", demande.getId());
      return new ArrayList<>();
    }

    // Filtres spécifiques pour les aides financières
    Map<String, Object> filtres = Map.of(
        "typeAide",
        demande.getTypeAide(),
        "estDisponible",
        true,
        "montantMaximum",
        demande.getMontantApprouve() != null
            ? demande.getMontantApprouve()
            : (demande.getMontantDemande() != null ? demande.getMontantDemande() : BigDecimal.ZERO));

    List<PropositionAideResponse> propositions = propositionAideService.rechercherAvecFiltres(filtres);

    // Scoring spécialisé pour les aides financières
    return propositions.stream()
        .map(
            proposition -> {
              double score = calculerScoreFinancier(demande, proposition);
              if (proposition.getDonneesPersonnalisees() == null) {
                proposition.setDonneesPersonnalisees(new HashMap<>());
              }
              proposition.getDonneesPersonnalisees().put("scoreFinancier", score);
              return proposition;
            })
        .filter(p -> (Double) p.getDonneesPersonnalisees().get("scoreFinancier") >= 40.0)
        .sorted(
            (p1, p2) -> {
              Double score1 = (Double) p1.getDonneesPersonnalisees().get("scoreFinancier");
              Double score2 = (Double) p2.getDonneesPersonnalisees().get("scoreFinancier");
              return Double.compare(score2, score1);
            })
        .limit(5) // Limiter à 5 pour les aides financières
        .collect(Collectors.toList());
  }

  /**
   * Matching d'urgence pour les demandes critiques
   *
   * @param demande La demande d'aide urgente
   * @return Liste des propositions d'urgence
   */
  public List<PropositionAideResponse> matchingUrgence(DemandeAideResponse demande) {
    LOG.infof("Matching d'urgence pour la demande: %s", demande.getId());

    // Recherche élargie pour les urgences
    List<PropositionAideResponse> candidats = new ArrayList<>();

    // 1. Même type d'aide
    candidats.addAll(propositionAideService.obtenirPropositionsActives(demande.getTypeAide()));

    // 2. Types d'aide de la même catégorie
    candidats.addAll(rechercherParCategorie(demande.getTypeAide().getCategorie()));

    // 3. Propositions généralistes (type AUTRE)
    candidats.addAll(propositionAideService.obtenirPropositionsActives(TypeAide.AUTRE));

    // Scoring avec bonus d'urgence
    return candidats.stream()
        .distinct()
        .filter(PropositionAideResponse::isActiveEtDisponible)
        .map(
            proposition -> {
              double score = calculerScoreCompatibilite(demande, proposition);
              // Bonus d'urgence
              score += 20.0;

              if (proposition.getDonneesPersonnalisees() == null) {
                proposition.setDonneesPersonnalisees(new HashMap<>());
              }
              proposition.getDonneesPersonnalisees().put("scoreUrgence", score);
              return proposition;
            })
        .filter(p -> (Double) p.getDonneesPersonnalisees().get("scoreUrgence") >= 25.0)
        .sorted(
            (p1, p2) -> {
              Double score1 = (Double) p1.getDonneesPersonnalisees().get("scoreUrgence");
              Double score2 = (Double) p2.getDonneesPersonnalisees().get("scoreUrgence");
              return Double.compare(score2, score1);
            })
        .limit(15) // Plus de résultats pour les urgences
        .collect(Collectors.toList());
  }

  // === ALGORITHMES DE SCORING ===

  /** Calcule le score de compatibilité entre une demande et une proposition */
  private double calculerScoreCompatibilite(
      DemandeAideResponse demande, PropositionAideResponse proposition) {
    double score = 0.0;

    // 1. Correspondance du type d'aide (40 points max)
    if (demande.getTypeAide() == proposition.getTypeAide()) {
      score += 40.0;
    } else if (demande
        .getTypeAide()
        .getCategorie()
        .equals(proposition.getTypeAide().getCategorie())) {
      score += 25.0;
    } else if (proposition.getTypeAide() == TypeAide.AUTRE) {
      score += 15.0;
    }

    // 2. Compatibilité financière (25 points max)
    if (demande.getTypeAide().isNecessiteMontant() && proposition.getMontantMaximum() != null) {
      BigDecimal montantDemande = demande.getMontantApprouve() != null
          ? demande.getMontantApprouve()
          : demande.getMontantDemande();

      if (montantDemande != null) {
        if (montantDemande.compareTo(proposition.getMontantMaximum()) <= 0) {
          score += 25.0;
        } else {
          // Pénalité proportionnelle au dépassement
          double ratio = proposition.getMontantMaximum().divide(montantDemande, 4, java.math.RoundingMode.HALF_UP)
              .doubleValue();
          score += 25.0 * ratio;
        }
      }
    } else if (!demande.getTypeAide().isNecessiteMontant()) {
      score += 25.0; // Pas de contrainte financière
    }

    // 3. Expérience du proposant (15 points max)
    if (proposition.getNombreBeneficiairesAides() != null && proposition.getNombreBeneficiairesAides() > 0) {
      score += Math.min(15.0, proposition.getNombreBeneficiairesAides() * boostExperience);
    }

    // 4. Réputation (10 points max)
    if (proposition.getNoteMoyenne() != null && proposition.getNombreEvaluations() != null
        && proposition.getNombreEvaluations() >= 3) {
      score += (proposition.getNoteMoyenne() - 3.0) * 3.33; // 0 à 10 points
    }

    // 5. Disponibilité et capacité (10 points max)
    if (proposition.peutAccepterBeneficiaires()) {
      double ratioCapacite = (double) proposition.getPlacesRestantes()
          / proposition.getNombreMaxBeneficiaires();
      score += 10.0 * ratioCapacite;
    }

    // Bonus et malus additionnels
    score += calculerBonusGeographique(demande, proposition);
    score += calculerBonusTemporel(demande, proposition);
    score -= calculerMalusDelai(demande, proposition);

    return Math.max(0.0, Math.min(100.0, score));
  }

  /** Calcule le score spécialisé pour les aides financières */
  private double calculerScoreFinancier(DemandeAideResponse demande, PropositionAideResponse proposition) {
    double score = calculerScoreCompatibilite(demande, proposition);

    // Bonus spécifiques aux aides financières

    // 1. Historique de versements
    if (proposition.getMontantTotalVerse() != null && proposition.getMontantTotalVerse() > 0) {
      score += Math.min(10.0, proposition.getMontantTotalVerse() / 10000.0);
    }

    // 2. Fiabilité (ratio versements/promesses)
    if (proposition.getNombreDemandesTraitees() != null && proposition.getNombreDemandesTraitees() > 0) {
      // Simulation d'un ratio de fiabilité
      double ratioFiabilite = 0.9; // À calculer réellement
      score += ratioFiabilite * 15.0;
    }

    // 3. Rapidité de réponse
    if (proposition.getDelaiReponseHeures() != null && proposition.getDelaiReponseHeures() <= 24) {
      score += 10.0;
    } else if (proposition.getDelaiReponseHeures() != null && proposition.getDelaiReponseHeures() <= 72) {
      score += 5.0;
    }

    return Math.max(0.0, Math.min(100.0, score));
  }

  /** Calcule le bonus géographique */
  private double calculerBonusGeographique(DemandeAideResponse demande, PropositionAideResponse proposition) {
    // Simulation - dans une vraie implémentation, ceci utiliserait les données de
    // localisation
    if (demande.getLocalisation() != null && proposition.getZonesGeographiques() != null) {
      // Logique de proximité géographique
      return boostGeographique;
    }
    return 0.0;
  }

  /** Calcule le bonus temporel (urgence, disponibilité) */
  private double calculerBonusTemporel(DemandeAideResponse demande, PropositionAideResponse proposition) {
    double bonus = 0.0;

    // Bonus pour demande urgente
    if (demande.estUrgente()) {
      bonus += 5.0;
    }

    // Bonus pour proposition récente
    if (proposition.getDateCreation() != null) {
      long joursDepuisCreation = java.time.Duration.between(proposition.getDateCreation(), LocalDateTime.now()).toDays();
      if (joursDepuisCreation <= 30) {
        bonus += 3.0;
      }
    }

    return bonus;
  }

  /** Calcule le malus de délai */
  private double calculerMalusDelai(DemandeAideResponse demande, PropositionAideResponse proposition) {
    double malus = 0.0;

    // Malus si la demande est en retard
    if (demande.estDelaiDepasse()) {
      malus += 5.0;
    }

    // Malus si la proposition a un délai de réponse long
    if (proposition.getDelaiReponseHeures() != null && proposition.getDelaiReponseHeures() > 168) { // Plus d'une
                                                                                                    // semaine
      malus += 3.0;
    }

    return malus;
  }

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

  /** Recherche des propositions par catégorie */
  private List<PropositionAideResponse> rechercherParCategorie(String categorie) {
    Map<String, Object> filtres = Map.of("estDisponible", true);

    return propositionAideService.rechercherAvecFiltres(filtres).stream()
        .filter(p -> p.getTypeAide().getCategorie().equals(categorie))
        .collect(Collectors.toList());
  }

  /** Classe interne pour stocker les résultats de matching */
  private static class ResultatMatching {
    final PropositionAideResponse proposition;
    final double score;

    ResultatMatching(PropositionAideResponse proposition, double score) {
      this.proposition = proposition;
      this.score = score;
    }
  }
}
