package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.cotisation.request.CreateCotisationRequest;
import dev.lions.unionflow.server.api.dto.cotisation.request.UpdateCotisationRequest;
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationSummaryResponse;
import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.CotisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.service.support.SecuriteHelper;
import dev.lions.unionflow.server.service.ComptabiliteService;
import dev.lions.unionflow.server.security.RlsEnabled;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;

/**
 * Service métier pour la gestion des cotisations.
 * Contient la logique métier et les règles de validation.
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2025-01-15
 */
@ApplicationScoped
@Slf4j
@RlsEnabled
public class CotisationService {

  @Inject
  CotisationRepository cotisationRepository;

  @Inject
  MembreRepository membreRepository;

  @Inject
  OrganisationRepository organisationRepository;

  @Inject
  DefaultsService defaultsService;

  @Inject
  SecuriteHelper securiteHelper;

  @Inject
  OrganisationService organisationService;

  @Inject
  ComptabiliteService comptabiliteService;

  @Inject
  EmailTemplateService emailTemplateService;

  /**
   * Récupère toutes les cotisations avec pagination.
   *
   * @param page numéro de page (0-based)
   * @param size taille de la page
   * @return liste des cotisations converties en Summary Response
   */
  public List<CotisationResponse> getAllCotisations(int page, int size) {
    log.debug("Récupération des cotisations - page: {}, size: {}", page, size);

    jakarta.persistence.TypedQuery<Cotisation> query = cotisationRepository.getEntityManager().createQuery(
        "SELECT c FROM Cotisation c LEFT JOIN FETCH c.membre LEFT JOIN FETCH c.organisation ORDER BY c.dateEcheance DESC",
        Cotisation.class);
    query.setFirstResult(page * size);
    query.setMaxResults(size);
    List<Cotisation> cotisations = query.getResultList();

    return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
  }

  /**
   * Récupère une cotisation par son ID.
   *
   * @param id identifiant UUID de la cotisation
   * @return Response de la cotisation
   * @throws NotFoundException si la cotisation n'existe pas
   */
  public CotisationResponse getCotisationById(@NotNull UUID id) {
    log.debug("Récupération de la cotisation avec ID: {}", id);

    Cotisation cotisation = cotisationRepository
        .findByIdOptional(id)
        .orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id));

    return convertToResponse(cotisation);
  }

  /**
   * Récupère une cotisation par son numéro de référence.
   *
   * @param numeroReference numéro de référence unique
   * @return Response de la cotisation
   * @throws NotFoundException si la cotisation n'existe pas
   */
  public CotisationResponse getCotisationByReference(@NotNull String numeroReference) {
    log.debug("Récupération de la cotisation avec référence: {}", numeroReference);

    Cotisation cotisation = cotisationRepository
        .findByNumeroReference(numeroReference)
        .orElseThrow(
            () -> new NotFoundException(
                "Cotisation non trouvée avec la référence: " + numeroReference));

    return convertToResponse(cotisation);
  }

  /**
   * Crée une nouvelle cotisation.
   *
   * @param request données de la cotisation à créer
   * @return Response de la cotisation créée
   */
  @Transactional
  public CotisationResponse createCotisation(@Valid CreateCotisationRequest request) {
    log.info("Création d'une nouvelle cotisation pour le membre: {}", request.membreId());

    // Validation du membre
    Membre membre = membreRepository
        .findByIdOptional(request.membreId())
        .orElseThrow(
            () -> new NotFoundException(
                "Membre non trouvé avec l'ID: " + request.membreId()));

    // Validation de l'organisation
    Organisation organisation = organisationRepository
        .findByIdOptional(request.organisationId())
        .orElseThrow(
            () -> new NotFoundException(
                "Organisation non trouvée avec l'ID: " + request.organisationId()));

    // Conversion Request vers entité
    Cotisation cotisation = Cotisation.builder()
        .typeCotisation(request.typeCotisation())
        .libelle(request.libelle())
        .description(request.description())
        .montantDu(request.montantDu())
        .montantPaye(BigDecimal.ZERO)
        .codeDevise(request.codeDevise() != null ? request.codeDevise() : defaultsService.getDevise())
        .statut("EN_ATTENTE")
        .dateEcheance(request.dateEcheance())
        .periode(request.periode())
        .annee(request.annee() != null ? request.annee() : LocalDate.now().getYear())
        .mois(request.mois())
        .recurrente(request.recurrente() != null ? request.recurrente() : false)
        .observations(request.observations())
        .membre(membre)
        .organisation(organisation)
        .build();

    // Génération du numéro de référence (si pas encore géré par PrePersist ou
    // builder)
    cotisation.setNumeroReference(Cotisation.genererNumeroReference());

    // Validation des règles métier
    validateCotisationRules(cotisation);

    // Persistance
    cotisationRepository.persist(cotisation);

    log.info("Cotisation créée avec succès - ID: {}, Référence: {}",
        cotisation.getId(),
        cotisation.getNumeroReference());

    return convertToResponse(cotisation);
  }

  /**
   * Met à jour une cotisation existante.
   *
   * @param id      identifiant UUID de la cotisation
   * @param request nouvelles données
   * @return Response de la cotisation mise à jour
   */
  @Transactional
  public CotisationResponse updateCotisation(@NotNull UUID id, @Valid UpdateCotisationRequest request) {
    log.info("Mise à jour de la cotisation avec ID: {}", id);

    Cotisation cotisationExistante = cotisationRepository
        .findByIdOptional(id)
        .orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id));

    // Mise à jour des champs modifiables
    if (request.libelle() != null)
      cotisationExistante.setLibelle(request.libelle());
    if (request.description() != null)
      cotisationExistante.setDescription(request.description());
    if (request.montantDu() != null)
      cotisationExistante.setMontantDu(request.montantDu());
    if (request.dateEcheance() != null)
      cotisationExistante.setDateEcheance(request.dateEcheance());
    if (request.observations() != null)
      cotisationExistante.setObservations(request.observations());
    if (request.statut() != null)
      cotisationExistante.setStatut(request.statut());
    if (request.annee() != null)
      cotisationExistante.setAnnee(request.annee());
    if (request.mois() != null)
      cotisationExistante.setMois(request.mois());
    if (request.recurrente() != null)
      cotisationExistante.setRecurrente(request.recurrente());

    // Validation des règles métier
    validateCotisationRules(cotisationExistante);

    log.info("Cotisation mise à jour avec succès - ID: {}", id);

    return convertToResponse(cotisationExistante);
  }

  /**
   * Enregistre le paiement d'une cotisation.
   */
  @Transactional
  public CotisationResponse enregistrerPaiement(
      @NotNull UUID id,
      BigDecimal montantPaye,
      LocalDate datePaiement,
      String modePaiement,
      String reference) {
    log.info("Enregistrement du paiement pour la cotisation ID: {}", id);

    Cotisation cotisation = cotisationRepository
        .findByIdOptional(id)
        .orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id));

    if (montantPaye != null) {
      cotisation.setMontantPaye(montantPaye);
    }
    if (datePaiement != null) {
      cotisation.setDatePaiement(java.time.LocalDateTime.of(datePaiement, java.time.LocalTime.MIDNIGHT));
    }

    // Déterminer le statut en fonction du montant payé
    boolean etaitDejaPayee = "PAYEE".equals(cotisation.getStatut());
    if (cotisation.getMontantPaye() != null && cotisation.getMontantDu() != null
        && cotisation.getMontantPaye().compareTo(cotisation.getMontantDu()) >= 0) {
      cotisation.setStatut("PAYEE");
    } else if (cotisation.getMontantPaye() != null
        && cotisation.getMontantPaye().compareTo(BigDecimal.ZERO) > 0) {
      cotisation.setStatut("PARTIELLEMENT_PAYEE");
    }

    // Génération écriture SYSCOHADA + email si cotisation vient de passer à PAYEE
    if (!etaitDejaPayee && "PAYEE".equals(cotisation.getStatut())) {
      try {
        comptabiliteService.enregistrerCotisation(cotisation);
      } catch (Exception e) {
        log.warn("Écriture SYSCOHADA cotisation ignorée (non bloquant) : {}", e.getMessage());
      }
      // Email de confirmation asynchrone (non bloquant)
      if (cotisation.getMembre() != null && cotisation.getMembre().getEmail() != null) {
        try {
          String periode = cotisation.getPeriode() != null ? cotisation.getPeriode()
              : (cotisation.getDateEcheance() != null
                  ? cotisation.getDateEcheance().getYear() + "/" + cotisation.getDateEcheance().getMonthValue()
                  : "—");
          emailTemplateService.envoyerConfirmationCotisation(
              cotisation.getMembre().getEmail(),
              cotisation.getMembre().getPrenom() != null ? cotisation.getMembre().getPrenom() : "",
              cotisation.getMembre().getNom() != null ? cotisation.getMembre().getNom() : "",
              cotisation.getOrganisation() != null ? cotisation.getOrganisation().getNom() : "",
              periode,
              reference != null ? reference : "",
              modePaiement != null ? modePaiement : "—",
              datePaiement,
              cotisation.getMontantPaye());
        } catch (Exception e) {
          log.warn("Email confirmation cotisation ignoré (non bloquant) : {}", e.getMessage());
        }
      }
    }

    log.info("Paiement enregistré - ID: {}, Statut: {}", id, cotisation.getStatut());
    return convertToResponse(cotisation);
  }

  /**
   * Supprime (annule) une cotisation.
   *
   * @param id identifiant UUID de la cotisation
   */
  @Transactional
  public void deleteCotisation(@NotNull UUID id) {
    log.info("Suppression de la cotisation avec ID: {}", id);

    Cotisation cotisation = cotisationRepository
        .findByIdOptional(id)
        .orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id));

    if ("PAYEE".equals(cotisation.getStatut())) {
      throw new IllegalStateException("Impossible de supprimer une cotisation déjà payée");
    }

    cotisation.setStatut("ANNULEE");

    log.info("Cotisation supprimée avec succès - ID: {}", id);
  }

  /**
   * Récupère les cotisations d'un membre.
   */
  public List<CotisationResponse> getCotisationsByMembre(@NotNull UUID membreId, int page, int size) {
    log.debug("Récupération des cotisations du membre: {}", membreId);

    if (!membreRepository.findByIdOptional(membreId).isPresent()) {
      throw new NotFoundException("Membre non trouvé avec l'ID: " + membreId);
    }

    List<Cotisation> cotisations = cotisationRepository.findByMembreId(
        membreId, Page.of(page, size), Sort.by("dateEcheance").descending());

    return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
  }

  /**
   * Récupère les cotisations par statut.
   */
  public List<CotisationResponse> getCotisationsByStatut(@NotNull String statut, int page, int size) {
    log.debug("Récupération des cotisations avec statut: {}", statut);

    List<Cotisation> cotisations = cotisationRepository.findByStatut(statut, Page.of(page, size));

    return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
  }

  /**
   * Récupère les cotisations en retard.
   */
  public List<CotisationResponse> getCotisationsEnRetard(int page, int size) {
    log.debug("Récupération des cotisations en retard");

    List<Cotisation> cotisations = cotisationRepository.findCotisationsEnRetard(LocalDate.now(), Page.of(page, size));

    return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
  }

  /**
   * Recherche avancée de cotisations.
   */
  public List<CotisationResponse> rechercherCotisations(
      UUID membreId,
      String statut,
      String typeCotisation,
      Integer annee,
      Integer mois,
      int page,
      int size) {
    log.debug("Recherche avancée de cotisations avec filtres");

    List<Cotisation> cotisations = cotisationRepository.rechercheAvancee(
        membreId, statut, typeCotisation, annee, mois, Page.of(page, size));

    return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
  }

  /**
   * Statistiques par période.
   */
  public Map<String, Object> getStatistiquesPeriode(int annee, Integer mois) {
    return cotisationRepository.getStatistiquesPeriode(annee, mois);
  }

  /**
   * Statistiques globales.
   */
  public Map<String, Object> getStatistiquesCotisations() {
    log.debug("Calcul des statistiques des cotisations");

    long totalCotisations = cotisationRepository.count();
    long cotisationsPayees = cotisationRepository.compterParStatut("PAYEE");
    long cotisationsEnRetard = cotisationRepository
        .findCotisationsEnRetard(LocalDate.now(), Page.of(0, Integer.MAX_VALUE))
        .size();
    BigDecimal montantTotalPaye = cotisationRepository.sommeMontantPayeParStatut("PAYEE");

    BigDecimal totalMontant = cotisationRepository.sommeMontantDu();
    Map<String, Object> map = new java.util.HashMap<>();
    map.put("totalCotisations", totalCotisations);
    map.put("cotisationsPayees", cotisationsPayees);
    map.put("cotisationsEnRetard", cotisationsEnRetard);
    map.put("tauxPaiement", totalCotisations > 0 ? (cotisationsPayees * 100.0 / totalCotisations) : 0.0);
    map.put("montantTotalPaye", montantTotalPaye != null ? montantTotalPaye : BigDecimal.ZERO);
    map.put("totalMontant", totalMontant != null ? totalMontant : BigDecimal.ZERO);
    return map;
  }

  /**
   * Convertit une entité Cotisation en Response DTO.
   */
  private CotisationResponse convertToResponse(Cotisation cotisation) {
    if (cotisation == null)
      return null;

    CotisationResponse response = new CotisationResponse();
    response.setId(cotisation.getId());
    response.setNumeroReference(cotisation.getNumeroReference());

    if (cotisation.getMembre() != null) {
      dev.lions.unionflow.server.entity.Membre m = cotisation.getMembre();
      response.setMembreId(m.getId());
      String nomComplet = m.getNomComplet();
      response.setNomMembre(nomComplet);
      response.setNomCompletMembre(nomComplet);
      response.setNumeroMembre(m.getNumeroMembre());
      response.setInitialesMembre(buildInitiales(m.getPrenom(), m.getNom()));
      response.setTypeMembre(getTypeMembreLibelle(m.getStatutCompte()));
    }

    if (cotisation.getOrganisation() != null) {
      dev.lions.unionflow.server.entity.Organisation o = cotisation.getOrganisation();
      response.setOrganisationId(o.getId());
      response.setNomOrganisation(o.getNom());
      response.setRegionOrganisation(o.getRegion());
      response.setIconeOrganisation(getIconeOrganisation(o.getTypeOrganisation()));
    }

    response.setTypeCotisation(cotisation.getTypeCotisation());
    response.setType(cotisation.getTypeCotisation());
    response.setTypeCotisationLibelle(getTypeCotisationLibelle(cotisation.getTypeCotisation()));
    response.setTypeLibelle(getTypeCotisationLibelle(cotisation.getTypeCotisation()));
    response.setTypeSeverity(getTypeCotisationSeverity(cotisation.getTypeCotisation()));
    response.setTypeIcon(getTypeCotisationIcon(cotisation.getTypeCotisation()));
    response.setLibelle(cotisation.getLibelle());
    response.setDescription(cotisation.getDescription());
    response.setMontantDu(cotisation.getMontantDu());
    response.setMontant(cotisation.getMontantDu());
    response.setMontantFormatte(formatMontant(cotisation.getMontantDu()));
    response.setMontantPaye(cotisation.getMontantPaye());
    response.setMontantRestant(cotisation.getMontantRestant());
    response.setCodeDevise(cotisation.getCodeDevise());
    response.setStatut(cotisation.getStatut());
    response.setStatutLibelle(getStatutLibelle(cotisation.getStatut()));
    response.setStatutSeverity(getStatutSeverity(cotisation.getStatut()));
    response.setStatutIcon(getStatutIcon(cotisation.getStatut()));
    response.setDateEcheance(cotisation.getDateEcheance());
    response.setDateEcheanceFormattee(cotisation.getDateEcheance() != null
        ? cotisation.getDateEcheance().format(DateTimeFormatter.ofPattern("dd/MM/yyyy", Locale.FRANCE))
        : null);
    response.setDatePaiement(cotisation.getDatePaiement());
    response.setDatePaiementFormattee(cotisation.getDatePaiement() != null
        ? cotisation.getDatePaiement().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", Locale.FRANCE))
        : null);
    if (cotisation.isEnRetard()) {
      long jours = java.time.temporal.ChronoUnit.DAYS.between(cotisation.getDateEcheance(), LocalDate.now());
      response.setRetardCouleur("text-red-500");
      response.setRetardTexte(jours + " jour" + (jours > 1 ? "s" : "") + " de retard");
    } else {
      response.setRetardCouleur("text-green-600");
      response.setRetardTexte(cotisation.getStatut() != null && "PAYEE".equals(cotisation.getStatut()) ? "Payée" : "À jour");
    }
    response.setModePaiementIcon(getModePaiementIcon(null));
    response.setModePaiementLibelle(getModePaiementLibelle(null));
    response.setPeriode(cotisation.getPeriode());
    response.setAnnee(cotisation.getAnnee());
    response.setMois(cotisation.getMois());
    response.setObservations(cotisation.getObservations());
    response.setRecurrente(cotisation.getRecurrente());
    response.setNombreRappels(cotisation.getNombreRappels());
    response.setDateDernierRappel(cotisation.getDateDernierRappel());
    response.setValideParId(cotisation.getValideParId());
    response.setNomValidateur(cotisation.getNomValidateur());
    response.setDateValidation(cotisation.getDateValidation());

    if (cotisation.getMontantDu() != null && cotisation.getMontantDu().compareTo(BigDecimal.ZERO) > 0) {
      BigDecimal paye = cotisation.getMontantPaye() != null ? cotisation.getMontantPaye() : BigDecimal.ZERO;
      response.setPourcentagePaiement(paye.multiply(BigDecimal.valueOf(100))
          .divide(cotisation.getMontantDu(), 0, java.math.RoundingMode.HALF_UP).intValue());
    } else {
      response.setPourcentagePaiement(0);
    }

    response.setEnRetard(cotisation.isEnRetard());
    if (cotisation.isEnRetard()) {
      response
          .setJoursRetard(java.time.temporal.ChronoUnit.DAYS.between(cotisation.getDateEcheance(), LocalDate.now()));
    } else {
      response.setJoursRetard(0L);
    }

    response.setDateCreation(cotisation.getDateCreation());
    response.setDateModification(cotisation.getDateModification());
    response.setCreePar(cotisation.getCreePar());
    response.setModifiePar(cotisation.getModifiePar());
    response.setVersion(cotisation.getVersion());
    response.setActif(cotisation.getActif());

    return response;
  }

  /**
   * Convertit une entité Cotisation en Summary Response.
   */
  private CotisationSummaryResponse convertToSummaryResponse(Cotisation cotisation) {
    if (cotisation == null)
      return null;
    return new CotisationSummaryResponse(
        cotisation.getId(),
        cotisation.getNumeroReference(),
        cotisation.getMembre() != null ? cotisation.getMembre().getNomComplet() : "Inconnu",
        cotisation.getMontantDu(),
        cotisation.getMontantPaye(),
        cotisation.getStatut(),
        getStatutLibelle(cotisation.getStatut()),
        cotisation.getDateEcheance(),
        cotisation.getAnnee(),
        cotisation.getActif());
  }

  private String getTypeCotisationLibelle(String code) {
    if (code == null)
      return "Non défini";
    return switch (code) {
      case "MENSUELLE" -> "Mensuelle";
      case "TRIMESTRIELLE" -> "Trimestrielle";
      case "SEMESTRIELLE" -> "Semestrielle";
      case "ANNUELLE" -> "Annuelle";
      case "EXCEPTIONNELLE" -> "Exceptionnelle";
      case "ADHESION" -> "Adhésion";
      default -> code;
    };
  }

  private String getStatutLibelle(String code) {
    if (code == null)
      return "Non défini";
    return switch (code) {
      case "EN_ATTENTE" -> "En attente";
      case "PAYEE" -> "Payée";
      case "PARTIELLEMENT_PAYEE" -> "Partiellement payée";
      case "EN_RETARD" -> "En retard";
      case "ANNULEE" -> "Annulée";
      default -> code;
    };
  }

  private static String buildInitiales(String prenom, String nom) {
    if (prenom == null && nom == null)
      return "—";
    String p = prenom != null && !prenom.isEmpty() ? prenom.substring(0, 1).toUpperCase() : "";
    String n = nom != null && !nom.isEmpty() ? nom.substring(0, 1).toUpperCase() : "";
    return (p + n).isEmpty() ? "—" : p + n;
  }

  private static String getTypeMembreLibelle(String statutCompte) {
    if (statutCompte == null)
      return "";
    return switch (statutCompte) {
      case "ACTIF" -> "Actif";
      case "EN_ATTENTE_VALIDATION" -> "En attente";
      case "SUSPENDU" -> "Suspendu";
      case "RADIE" -> "Radié";
      case "INACTIF" -> "Inactif";
      default -> statutCompte;
    };
  }

  private static String getIconeOrganisation(String typeOrganisation) {
    if (typeOrganisation == null)
      return "pi-building";
    return switch (typeOrganisation.toUpperCase()) {
      case "ASSOCIATION", "ONG" -> "pi-users";
      case "CLUB" -> "pi-star";
      case "COOPERATIVE" -> "pi-briefcase";
      default -> "pi-building";
    };
  }

  /** Sévérité PrimeFaces pour le type de cotisation (p:tag). */
  private static String getTypeCotisationSeverity(String typeCotisation) {
    if (typeCotisation == null)
      return "secondary";
    return switch (typeCotisation.toUpperCase()) {
      case "ANNUELLE", "ADHESION" -> "success";
      case "MENSUELLE", "TRIMESTRIELLE" -> "info";
      case "EXCEPTIONNELLE" -> "warn";
      default -> "secondary";
    };
  }

  /** Icône PrimeFaces pour le type de cotisation. */
  private static String getTypeCotisationIcon(String typeCotisation) {
    if (typeCotisation == null)
      return "pi-tag";
    return switch (typeCotisation.toUpperCase()) {
      case "MENSUELLE" -> "pi-calendar";
      case "ANNUELLE" -> "pi-star";
      case "ADHESION" -> "pi-user-plus";
      default -> "pi-tag";
    };
  }

  /** Sévérité PrimeFaces pour le statut (p:tag). */
  private static String getStatutSeverity(String statut) {
    if (statut == null)
      return "secondary";
    return switch (statut.toUpperCase()) {
      case "PAYEE" -> "success";
      case "EN_ATTENTE", "PARTIELLEMENT_PAYEE" -> "info";
      case "EN_RETARD" -> "error";
      case "ANNULEE" -> "secondary";
      default -> "secondary";
    };
  }

  /** Icône PrimeFaces pour le statut. */
  private static String getStatutIcon(String statut) {
    if (statut == null)
      return "pi-circle";
    return switch (statut.toUpperCase()) {
      case "PAYEE" -> "pi-check";
      case "EN_ATTENTE" -> "pi-clock";
      case "EN_RETARD" -> "pi-exclamation-triangle";
      case "PARTIELLEMENT_PAYEE" -> "pi-percentage";
      case "ANNULEE" -> "pi-times";
      default -> "pi-circle";
    };
  }

  private static String formatMontant(BigDecimal montant) {
    if (montant == null)
      return "";
    return NumberFormat.getNumberInstance(Locale.FRANCE).format(montant.longValue());
  }

  private static String getModePaiementIcon(String methode) {
    if (methode == null)
      return "pi-wallet";
    return switch (methode.toUpperCase()) {
      case "WAVE_MONEY", "MOBILE_MONEY" -> "pi-mobile";
      case "VIREMENT" -> "pi-arrow-right-arrow-left";
      case "ESPECES" -> "pi-money-bill";
      case "CARTE" -> "pi-credit-card";
      default -> "pi-wallet";
    };
  }

  private static String getModePaiementLibelle(String methode) {
    if (methode == null)
      return "—";
    return switch (methode.toUpperCase()) {
      case "WAVE_MONEY" -> "Wave Money";
      case "MOBILE_MONEY" -> "Mobile Money";
      case "VIREMENT" -> "Virement";
      case "ESPECES" -> "Espèces";
      case "CARTE" -> "Carte";
      default -> methode;
    };
  }

  /**
   * Valide les règles métier pour une cotisation.
   */
  private void validateCotisationRules(Cotisation cotisation) {
    if (cotisation.getMontantDu().compareTo(BigDecimal.ZERO) <= 0) {
      throw new IllegalArgumentException("Le montant dû doit être positif");
    }
    if (cotisation.getDateEcheance().isBefore(LocalDate.now().minusYears(1))) {
      throw new IllegalArgumentException("La date d'échéance ne peut pas être antérieure à un an");
    }
    if (cotisation.getMontantPaye().compareTo(cotisation.getMontantDu()) > 0) {
      throw new IllegalArgumentException("Le montant payé ne peut pas dépasser le montant dû");
    }
    if ("PAYEE".equals(cotisation.getStatut())
        && cotisation.getMontantPaye().compareTo(cotisation.getMontantDu()) < 0) {
      throw new IllegalArgumentException(
          "Une cotisation marquée comme payée doit avoir un montant payé égal au montant dû");
    }
  }

  /**
   * Envoie des rappels de cotisations groupés.
   */
  @Transactional
  public int envoyerRappelsCotisationsGroupes(List<UUID> membreIds) {
    if (membreIds == null || membreIds.isEmpty()) {
      throw new IllegalArgumentException("La liste des membres ne peut pas être vide");
    }
    log.info("Envoi de rappels de cotisations groupés à {} membres", membreIds.size());

    int rappelsEnvoyes = 0;
    for (UUID membreId : membreIds) {
      try {
        List<Cotisation> cotisationsEnRetard = cotisationRepository.findCotisationsAuRappel(7, 3).stream()
            .filter(c -> c.getMembre() != null && c.getMembre().getId().equals(membreId))
            .collect(Collectors.toList());

        for (Cotisation cotisation : cotisationsEnRetard) {
          cotisationRepository.incrementerNombreRappels(cotisation.getId());
          rappelsEnvoyes++;
        }
      } catch (Exception e) {
        log.warn("Erreur lors de l'envoi du rappel pour le membre {}: {}", membreId, e.getMessage());
      }
    }
    return rappelsEnvoyes;
  }

  /**
   * Récupère le membre connecté via SecurityIdentity.
   * Méthode helper réutilisable (Pattern DRY).
   *
   * @return Membre connecté
   * @throws NotFoundException si le membre n'est pas trouvé
   */
  private Membre getMembreConnecte() {
    String email = securiteHelper.resolveEmail();
    log.debug("Récupération du membre connecté: {}", email);

    return membreRepository.findByEmail(email)
        .orElseThrow(() -> new NotFoundException(
            "Membre non trouvé pour l'email: " + email + ". Veuillez contacter l'administrateur."));
  }

  /**
   * Toutes les cotisations du membre connecté (tous statuts), ou des organisations gérées si ADMIN/ADMIN_ORGANISATION.
   * Utilisé pour les onglets Toutes / Payées / Dues / Retard.
   */
  public List<CotisationResponse> getMesCotisations(int page, int size) {
    String email = securiteHelper.resolveEmail();
    if (email == null || email.isBlank()) {
      return Collections.emptyList();
    }
    Set<String> roles = securiteHelper.getRoles();
    if (roles != null && (roles.contains("ADMIN") || roles.contains("ADMIN_ORGANISATION"))) {
      List<Organisation> orgs = organisationService.listerOrganisationsPourUtilisateur(email);
      if (orgs == null || orgs.isEmpty()) {
        log.info("Admin/Admin org: aucune organisation pour {}. Retour liste vide.", email);
        return Collections.emptyList();
      }
      Set<UUID> orgIds = orgs.stream().map(Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new));
      List<Cotisation> cotisations = cotisationRepository.findByOrganisationIdIn(
          orgIds, Page.of(page, size), Sort.by("dateEcheance").descending());
      return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
    }
    Membre membreConnecte = membreRepository.findByEmail(email).orElse(null);
    if (membreConnecte == null) {
      log.info("Aucun membre trouvé pour l'email. Retour liste vide.");
      return Collections.emptyList();
    }
    return getCotisationsByMembre(membreConnecte.getId(), page, size);
  }

  /**
   * Liste les cotisations en attente du membre connecté, ou des organisations gérées si ADMIN/ADMIN_ORGANISATION.
   * Auto-détection du membre via SecurityIdentity (pas de membreId en paramètre).
   * Utilisé par la page personnelle "Payer mes Cotisations".
   *
   * @return Liste des cotisations en attente
   */
  public List<CotisationResponse> getMesCotisationsEnAttente() {
    String email = securiteHelper.resolveEmail();
    if (email == null || email.isBlank()) {
      return Collections.emptyList();
    }
    Set<String> roles = securiteHelper.getRoles();
    if (roles != null && (roles.contains("ADMIN") || roles.contains("ADMIN_ORGANISATION"))) {
      List<Organisation> orgs = organisationService.listerOrganisationsPourUtilisateur(email);
      if (orgs == null || orgs.isEmpty()) {
        return Collections.emptyList();
      }
      Set<UUID> orgIds = orgs.stream().map(Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new));
      List<Cotisation> cotisations = cotisationRepository.findEnAttenteByOrganisationIdIn(orgIds);
      log.info("Cotisations en attente (admin): {} pour {} organisations", cotisations.size(), orgIds.size());
      return cotisations.stream().map(this::convertToResponse).collect(Collectors.toList());
    }
    Membre membreConnecte = membreRepository.findByEmail(email).orElse(null);
    if (membreConnecte == null) {
      log.info("Aucun membre trouvé pour l'email: {}. Retour d'une liste vide.", email);
      return Collections.emptyList();
    }
    log.info("Récupération des cotisations en attente pour le membre: {} ({})",
        membreConnecte.getNumeroMembre(), membreConnecte.getId());
    int anneeEnCours = LocalDate.now().getYear();
    List<Cotisation> cotisations = cotisationRepository.getEntityManager()
        .createQuery(
            "SELECT c FROM Cotisation c " +
            "LEFT JOIN FETCH c.membre LEFT JOIN FETCH c.organisation " +
            "WHERE c.membre.id = :membreId " +
            "AND c.statut = 'EN_ATTENTE' " +
            "AND EXTRACT(YEAR FROM c.dateEcheance) = :annee " +
            "ORDER BY c.dateEcheance ASC",
            Cotisation.class)
        .setParameter("membreId", membreConnecte.getId())
        .setParameter("annee", anneeEnCours)
        .getResultList();
    log.info("Cotisations en attente trouvées: {} pour le membre {}",
        cotisations.size(), membreConnecte.getNumeroMembre());
    return cotisations.stream()
        .map(this::convertToResponse)
        .collect(Collectors.toList());
  }

  /**
   * Récupère la synthèse des cotisations du membre connecté, ou des organisations gérées si ADMIN/ADMIN_ORGANISATION.
   * KPI : cotisations en attente, montant dû, prochaine échéance, total payé année.
   *
   * @return Map avec les KPI
   */
  public Map<String, Object> getMesCotisationsSynthese() {
    String email = securiteHelper.resolveEmail();
    if (email == null || email.isBlank()) {
      return syntheseVide(LocalDate.now().getYear());
    }
    Set<String> roles = securiteHelper.getRoles();
    if (roles != null && (roles.contains("ADMIN") || roles.contains("ADMIN_ORGANISATION"))) {
      List<Organisation> orgs = organisationService.listerOrganisationsPourUtilisateur(email);
      if (orgs == null || orgs.isEmpty()) {
        return syntheseVide(LocalDate.now().getYear());
      }
      Set<UUID> orgIds = orgs.stream().map(Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new));
      int anneeEnCours = LocalDate.now().getYear();
      var em = cotisationRepository.getEntityManager();
      Long cotisationsEnAttente = em.createQuery(
              "SELECT COUNT(c) FROM Cotisation c WHERE c.organisation.id IN :orgIds AND c.statut = 'EN_ATTENTE'",
              Long.class)
          .setParameter("orgIds", orgIds)
          .getSingleResult();
      BigDecimal montantDu = em.createQuery(
              "SELECT COALESCE(SUM(c.montantDu), 0) FROM Cotisation c WHERE c.organisation.id IN :orgIds AND c.statut = 'EN_ATTENTE'",
              BigDecimal.class)
          .setParameter("orgIds", orgIds)
          .getSingleResult();
      LocalDate prochaineEcheance = em.createQuery(
              "SELECT MIN(c.dateEcheance) FROM Cotisation c WHERE c.organisation.id IN :orgIds AND c.statut = 'EN_ATTENTE'",
              LocalDate.class)
          .setParameter("orgIds", orgIds)
          .getSingleResult();
      BigDecimal totalPayeAnnee = em.createQuery(
              "SELECT COALESCE(SUM(c.montantPaye), 0) FROM Cotisation c WHERE c.organisation.id IN :orgIds AND c.statut = 'PAYEE' AND c.datePaiement IS NOT NULL AND EXTRACT(YEAR FROM c.datePaiement) = :annee",
              BigDecimal.class)
          .setParameter("orgIds", orgIds)
          .setParameter("annee", anneeEnCours)
          .getSingleResult();
      log.info("Synthèse (admin): {} cotisations en attente, {} FCFA dû, total payé {} FCFA", cotisationsEnAttente, montantDu, totalPayeAnnee);
      Map<String, Object> result = new java.util.LinkedHashMap<>();
      result.put("cotisationsEnAttente", cotisationsEnAttente != null ? cotisationsEnAttente.intValue() : 0);
      result.put("montantDu", montantDu != null ? montantDu : BigDecimal.ZERO);
      result.put("prochaineEcheance", prochaineEcheance);
      result.put("totalPayeAnnee", totalPayeAnnee != null ? totalPayeAnnee : BigDecimal.ZERO);
      result.put("anneeEnCours", anneeEnCours);
      return result;
    }
    Membre membreConnecte = getMembreConnecte();
    log.info("Récupération de la synthèse des cotisations pour le membre: {} ({})",
        membreConnecte.getNumeroMembre(), membreConnecte.getId());
    int anneeEnCours = LocalDate.now().getYear();
    Long cotisationsEnAttente = cotisationRepository.getEntityManager()
        .createQuery(
            "SELECT COUNT(c) FROM Cotisation c " +
            "WHERE c.membre.id = :membreId AND c.statut != 'PAYEE' AND c.statut != 'ANNULEE'",
            Long.class)
        .setParameter("membreId", membreConnecte.getId())
        .getSingleResult();
    BigDecimal montantDu = cotisationRepository.getEntityManager()
        .createQuery(
            "SELECT COALESCE(SUM(c.montantDu - COALESCE(c.montantPaye, 0)), 0) FROM Cotisation c " +
            "WHERE c.membre.id = :membreId AND c.statut != 'PAYEE' AND c.statut != 'ANNULEE'",
            BigDecimal.class)
        .setParameter("membreId", membreConnecte.getId())
        .getSingleResult();
    LocalDate prochaineEcheance = cotisationRepository.getEntityManager()
        .createQuery(
            "SELECT MIN(c.dateEcheance) FROM Cotisation c " +
            "WHERE c.membre.id = :membreId AND c.statut != 'PAYEE' AND c.statut != 'ANNULEE'",
            LocalDate.class)
        .setParameter("membreId", membreConnecte.getId())
        .getSingleResult();
    BigDecimal totalPayeAnnee = cotisationRepository.getEntityManager()
        .createQuery(
            "SELECT COALESCE(SUM(c.montantPaye), 0) FROM Cotisation c " +
            "WHERE c.membre.id = :membreId " +
            "AND c.statut = 'PAYEE' " +
            "AND c.datePaiement IS NOT NULL " +
            "AND EXTRACT(YEAR FROM c.datePaiement) = :annee",
            BigDecimal.class)
        .setParameter("membreId", membreConnecte.getId())
        .setParameter("annee", anneeEnCours)
        .getSingleResult();
    log.info("Synthèse calculée pour {}: {} cotisations en attente, {} FCFA dû, total payé {} FCFA",
        membreConnecte.getNumeroMembre(), cotisationsEnAttente, montantDu, totalPayeAnnee);
    Map<String, Object> result = new java.util.LinkedHashMap<>();
    result.put("cotisationsEnAttente", cotisationsEnAttente != null ? cotisationsEnAttente.intValue() : 0);
    result.put("montantDu", montantDu != null ? montantDu : BigDecimal.ZERO);
    result.put("prochaineEcheance", prochaineEcheance);
    result.put("totalPayeAnnee", totalPayeAnnee != null ? totalPayeAnnee : BigDecimal.ZERO);
    result.put("anneeEnCours", anneeEnCours);
    return result;
  }

  private Map<String, Object> syntheseVide(int anneeEnCours) {
    Map<String, Object> result = new java.util.LinkedHashMap<>();
    result.put("cotisationsEnAttente", 0);
    result.put("montantDu", BigDecimal.ZERO);
    result.put("prochaineEcheance", (LocalDate) null);
    result.put("totalPayeAnnee", BigDecimal.ZERO);
    result.put("anneeEnCours", anneeEnCours);
    return result;
  }
}
