package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.membre.request.CreateMembreRequest;
import dev.lions.unionflow.server.api.dto.membre.request.UpdateMembreRequest;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria;
import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO;

import dev.lions.unionflow.server.entity.FormuleAbonnement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.MembreRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;

/** Service métier pour les membres */
@ApplicationScoped
public class MembreService {

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

  @Inject
  MembreRepository membreRepository;
  @Inject
  dev.lions.unionflow.server.repository.MembreRoleRepository membreRoleRepository;
  @Inject
  dev.lions.unionflow.server.repository.RoleRepository roleRepository;
  @Inject
  dev.lions.unionflow.server.repository.MembreOrganisationRepository membreOrganisationRepository;

  @Inject
  dev.lions.unionflow.server.repository.TypeReferenceRepository typeReferenceRepository;

  @Inject
  MembreImportExportService membreImportExportService;

  @PersistenceContext
  EntityManager entityManager;

  @Inject
  dev.lions.unionflow.server.service.OrganisationService organisationService;

  @Inject
  io.quarkus.security.identity.SecurityIdentity securityIdentity;

  @Inject
  dev.lions.unionflow.server.repository.InscriptionEvenementRepository inscriptionEvenementRepository;

  @Inject
  dev.lions.unionflow.server.messaging.KafkaEventProducer kafkaEventProducer;

  @Inject
  MembreKeycloakSyncService keycloakSyncService;

  @Inject
  AuditService auditService;

  @Inject
  dev.lions.unionflow.server.repository.NotificationRepository notificationRepository;

  /** Crée un nouveau membre en attente de validation admin */
  @Transactional
  public Membre creerMembre(Membre membre) {
    LOG.infof("Création d'un nouveau membre: %s", membre.getEmail());

    // Générer un numéro de membre unique
    if (membre.getNumeroMembre() == null || membre.getNumeroMembre().isEmpty()) {
      membre.setNumeroMembre(genererNumeroMembre());
    }

    // Définir la date de naissance par défaut si non fournie (pour éviter @NotNull)
    if (membre.getDateNaissance() == null) {
      membre.setDateNaissance(LocalDate.now().minusYears(18));
      LOG.warn("Date de naissance non fournie, définie par défaut à il y a 18 ans");
    }

    // Vérifier l'unicité de l'email
    if (membreRepository.findByEmail(membre.getEmail()).isPresent()) {
      throw new IllegalArgumentException("Un membre avec cet email existe déjà");
    }

    // Vérifier l'unicité du numéro de membre
    if (membreRepository.findByNumeroMembre(membre.getNumeroMembre()).isPresent()) {
      throw new IllegalArgumentException("Un membre avec ce numéro existe déjà");
    }

    // Statut initial : en attente de validation admin
    // L'activation (ACTIF + Keycloak MEMBRE_ACTIF) se fait via PUT /api/membres/{id}/activer
    membre.setStatutCompte("EN_ATTENTE_VALIDATION");
    membre.setActif(false);

    membreRepository.persist(membre);
    LOG.infof("Membre créé en attente de validation: %s (ID: %s)", membre.getNomComplet(), membre.getId());

    // Publier l'événement Kafka pour mise à jour temps réel
    try {
      Map<String, Object> memberData = new HashMap<>();
      memberData.put("memberId", membre.getId().toString());
      memberData.put("nomComplet", membre.getNomComplet());
      memberData.put("email", membre.getEmail());
      memberData.put("numeroMembre", membre.getNumeroMembre());
      memberData.put("statutCompte", membre.getStatutCompte());
      kafkaEventProducer.publishMemberCreated(membre.getId(), null, memberData);
    } catch (Exception e) {
      LOG.warnf("Kafka event publication failed (non-blocking): %s", e.getMessage());
    }

    return membre;
  }

  /**
   * Active un membre : passe son statut à ACTIF et son flag actif à true.
   * Doit être suivi d'un appel à MembreKeycloakSyncService.activerMembreDansKeycloak()
   * pour que le rôle MEMBRE_ACTIF soit assigné dans Keycloak.
   *
   * @param membreId UUID du membre à activer
   * @return Le membre mis à jour
   * @throws jakarta.ws.rs.NotFoundException si le membre est introuvable
   */
  @Transactional
  public Membre activerMembre(UUID membreId) {
    LOG.infof("Activation du membre ID: %s", membreId);

    Membre membre = membreRepository.findByIdOptional(membreId)
        .orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Membre non trouvé avec l'ID: " + membreId));

    membre.setStatutCompte("ACTIF");
    membre.setActif(true);
    membreRepository.persist(membre);

    LOG.infof("Membre activé avec succès: %s (ID: %s)", membre.getNomComplet(), membreId);

    try {
      Map<String, Object> memberData = new HashMap<>();
      memberData.put("memberId", membre.getId().toString());
      memberData.put("nomComplet", membre.getNomComplet());
      memberData.put("statutCompte", "ACTIF");
      kafkaEventProducer.publishMemberUpdated(membre.getId(), null, memberData);
    } catch (Exception e) {
      LOG.warnf("Kafka event publication failed (non-blocking): %s", e.getMessage());
    }

    return membre;
  }

  /**
   * Affecte un membre existant à une organisation.
   * Crée le lien MembreOrganisation (statut EN_ATTENTE_VALIDATION) si inexistant.
   * Si le lien existe déjà, la méthode est idempotente.
   *
   * @param membreId       UUID du membre
   * @param organisationId UUID de l'organisation cible
   * @return Le membre mis à jour
   */
  @Transactional
  public Membre affecterOrganisation(UUID membreId, UUID organisationId) {
    LOG.infof("Affectation du membre %s à l'organisation %s", membreId, organisationId);

    Membre membre = membreRepository.findByIdOptional(membreId)
        .orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Membre non trouvé: " + membreId));

    boolean dejaLie = membreOrganisationRepository.findFirstByMembreId(membreId).isPresent();
    if (dejaLie) {
      LOG.infof("Membre %s déjà lié à une organisation — opération ignorée", membreId);
      return membre;
    }

    lierMembreOrganisationEtIncrementerQuota(membre, organisationId, "EN_ATTENTE_VALIDATION");

    LOG.infof("Membre %s affecté à l'organisation %s", membre.getNumeroMembre(), organisationId);
    return membre;
  }

  /**
   * Promeut un membre au rôle d'administrateur d'organisation.
   * Passe immédiatement le statut à ACTIF — les admins sont opérationnels sans
   * validation intermédiaire.
   * Doit être suivi d'un appel à
   * MembreKeycloakSyncService.promouvoirAdminOrganisationDansKeycloak()
   * pour que le rôle ADMIN_ORGANISATION soit assigné dans Keycloak.
   *
   * @param membreId UUID du membre à promouvoir
   * @return Le membre mis à jour
   * @throws jakarta.ws.rs.NotFoundException si le membre est introuvable
   */
  @Transactional
  public Membre promouvoirAdminOrganisation(UUID membreId) {
    LOG.infof("Promotion admin d'organisation pour le membre ID: %s", membreId);

    Membre membre = membreRepository.findByIdOptional(membreId)
        .orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Membre non trouvé avec l'ID: " + membreId));

    // Vérifier le quota d'administrateurs selon la formule souscrite
    membreOrganisationRepository.findFirstByMembreId(membreId).ifPresent(mo -> {
      UUID orgId = mo.getOrganisation().getId();
      entityManager.createQuery(
          "SELECT s FROM SouscriptionOrganisation s WHERE s.organisation.id = :orgId AND s.statut = 'ACTIVE'",
          dev.lions.unionflow.server.entity.SouscriptionOrganisation.class)
          .setParameter("orgId", orgId)
          .getResultStream().findFirst().ifPresent(souscription -> {
            FormuleAbonnement formule = souscription.getFormule();
            if (formule != null && formule.getMaxAdmins() != null) {
              long adminCount = entityManager.createQuery(
                  "SELECT COUNT(mr) FROM MembreRole mr WHERE mr.organisation.id = :orgId " +
                  "AND mr.role.code = 'ORGADMIN' AND mr.actif = true", Long.class)
                  .setParameter("orgId", orgId).getSingleResult();
              if (adminCount >= formule.getMaxAdmins()) {
                throw new jakarta.ws.rs.ForbiddenException(
                    "Le quota d'administrateurs de votre plan (" + formule.getMaxAdmins() +
                    ") est atteint. Mettez à niveau votre abonnement pour ajouter plus d'administrateurs.");
              }
            }
          });
    });

    membre.setStatutCompte("ACTIF");
    membre.setActif(true);
    membreRepository.persist(membre);

    // Mettre à jour le rôle BDD vers ORGADMIN
    membreOrganisationRepository.findFirstByMembreId(membreId).ifPresent(mo -> {
      membreRoleRepository.findActifsByMembreId(membreId)
          .forEach(mr -> { mr.setActif(false); entityManager.persist(mr); });
      assignerRoleDefaut(mo, "ORGADMIN");
    });

    LOG.infof("Membre promu admin d'organisation: %s (ID: %s)", membre.getNomComplet(), membreId);
    return membre;
  }

  /** Met à jour un membre existant */
  @Transactional
  public Membre mettreAJourMembre(UUID id, Membre membreModifie) {
    LOG.infof("Mise à jour du membre ID: %s", id);

    Membre membre = membreRepository.findById(id);
    if (membre == null) {
      throw new IllegalArgumentException("Membre non trouvé avec l'ID: " + id);
    }

    // Vérifier l'unicité de l'email si modifié
    if (!membre.getEmail().equals(membreModifie.getEmail())) {
      if (membreRepository.findByEmail(membreModifie.getEmail()).isPresent()) {
        throw new IllegalArgumentException("Un membre avec cet email existe déjà");
      }
    }

    // Mettre à jour les champs
    membre.setPrenom(membreModifie.getPrenom());
    membre.setNom(membreModifie.getNom());
    membre.setEmail(membreModifie.getEmail());
    membre.setTelephone(membreModifie.getTelephone());
    membre.setDateNaissance(membreModifie.getDateNaissance());
    membre.setActif(membreModifie.getActif());

    LOG.infof("Membre mis à jour avec succès: %s", membre.getNomComplet());
    return membre;
  }

  /** Trouve un membre par son ID */
  public Optional<Membre> trouverParId(UUID id) {
    return Optional.ofNullable(membreRepository.findById(id));
  }

  /** Trouve un membre par son email */
  public Optional<Membre> trouverParEmail(String email) {
    return membreRepository.findByEmail(email);
  }

  /** Trouve un membre par son numéro de membre (ex: MBR-0001) */
  public Optional<Membre> trouverParNumeroMembre(String numeroMembre) {
    return membreRepository.findByNumeroMembre(numeroMembre);
  }

  /** Liste tous les membres actifs */
  public List<Membre> listerMembresActifs() {
    return membreRepository.findAllActifs();
  }

  /** Recherche des membres par nom ou prénom */
  public List<Membre> rechercherMembres(String recherche) {
    return membreRepository.findByNomOrPrenom(recherche);
  }

  /**
   * Désactive un membre avec propagation complète des cascades métier.
   *
   * <p>Garde-fous et effets :
   * <ol>
   *   <li><b>Check mono-admin</b> : si le membre est le seul ORGADMIN d'une org,
   *       lève {@link jakarta.ws.rs.WebApplicationException} 409 Conflict — l'appelant
   *       doit d'abord assigner un autre admin pour éviter l'orphelinage.</li>
   *   <li>DB : {@code actif=false}, {@code statutCompte='DESACTIVE'}</li>
   *   <li>Toutes les adhésions actives → {@code SUSPENDU}, {@code nombreMembres} décrémenté</li>
   *   <li>Tous les {@link dev.lions.unionflow.server.entity.MembreRole} → {@code actif=false}
   *       (perte immédiate des droits fonctionnels)</li>
   *   <li>Keycloak (lions-user-manager) : {@code user.enabled=false} → login impossible</li>
   *   <li>Kafka : événement {@code member.deactivated} émis pour les consommateurs externes</li>
   * </ol>
   *
   * <p>Non couvert (laissé à des services spécialisés) : comptes épargne, cotisations,
   * inscriptions événements, approbations en attente — à traiter via workflow dédié.
   */
  @Transactional
  public void desactiverMembre(UUID id) {
    LOG.infof("Désactivation du membre ID: %s", id);

    Membre membre = membreRepository.findById(id);
    if (membre == null) {
      throw new IllegalArgumentException("Membre non trouvé avec l'ID: " + id);
    }

    // ── 1. GARDE-FOU mono-admin : refuser si l'orphelinage créerait une org sans admin ──
    List<String> orgsOrphelines = verifierOrgsOrphelinees(id);
    if (!orgsOrphelines.isEmpty()) {
      final String msg = "Suppression impossible : ce membre est le seul administrateur de "
          + orgsOrphelines.size() + " organisation(s) ("
          + String.join(", ", orgsOrphelines)
          + "). Veuillez d'abord désigner un autre administrateur avant de supprimer ce compte.";
      LOG.warnf("Refus désactivation %s (mono-admin de %s)", id, orgsOrphelines);
      throw new jakarta.ws.rs.WebApplicationException(msg, jakarta.ws.rs.core.Response.Status.CONFLICT);
    }

    // ── 2. DB : flags principaux du membre ───────────────────────────────────────────
    membre.setActif(false);
    membre.setStatutCompte("DESACTIVE");

    // ── 3. Adhésions actives → SUSPENDU + décrément compteur org ─────────────────────
    final var adhesionsActives = membreOrganisationRepository.findOrganisationsActivesParMembre(id);
    for (var mo : adhesionsActives) {
      mo.getOrganisation().retirerMembre();
      mo.setStatutMembre(dev.lions.unionflow.server.api.enums.membre.StatutMembre.SUSPENDU);
    }
    final int nbAdhesionsSuspendues = adhesionsActives.size();

    // ── 4. Désactivation des rôles fonctionnels (ORGADMIN, TRESORIER, etc.) ─────────
    final int rolesDesactives = (int) membreRoleRepository.update(
        "actif = false, dateFin = ?1, modifiePar = ?2 "
            + "WHERE membreOrganisation.membre.id = ?3 AND actif = true",
        LocalDate.now(), "system", id);
    LOG.infof("%d MembreRole désactivés pour le membre %s", rolesDesactives, id);

    // ── 5. Annulation des notifications pending pour ce membre ──────────────────────
    try {
      final long notifsAnnulees = notificationRepository.update(
          "statut = ?1, dateModification = ?2 "
              + "WHERE membre.id = ?3 AND statut IN (?4, ?5) AND actif = true",
          "ANNULEE", java.time.LocalDateTime.now(), id,
          "EN_ATTENTE", "ECHEC_TEMPORAIRE");
      if (notifsAnnulees > 0) {
        LOG.infof("%d notifications pending annulées pour membre %s", notifsAnnulees, id);
      }
    } catch (Exception e) {
      LOG.warnf("Annulation notifications pending échouée pour %s : %s", id, e.getMessage());
    }

    // ── 6. Propagation Keycloak (non bloquant) ───────────────────────────────────────
    try {
      keycloakSyncService.syncMembreToKeycloak(id);
      LOG.infof("Compte Keycloak désactivé pour membre %s", id);
    } catch (Exception e) {
      LOG.warnf("Sync Keycloak échouée pour membre %s : %s (DB reste cohérente)",
          id, e.getMessage());
    }

    // ── 7. Événement Kafka pour les autres modules/services ─────────────────────────
    try {
      kafkaEventProducer.publishMemberDeactivated(membre);
    } catch (Exception e) {
      LOG.warnf("Publication Kafka member.deactivated échouée pour %s : %s", id, e.getMessage());
    }

    // ── 8. Audit log (traçabilité RGPD/compliance) ──────────────────────────────────
    try {
      String operateur = securityIdentity != null && !securityIdentity.isAnonymous()
          ? securityIdentity.getPrincipal().getName()
          : "system";
      auditService.logMembreDesactive(id, membre.getEmail(), operateur,
          nbAdhesionsSuspendues, rolesDesactives);
    } catch (Exception e) {
      LOG.warnf("Audit log MEMBRE_DESACTIVE échoué pour %s : %s", id, e.getMessage());
    }

    LOG.infof("Membre désactivé avec cascade complète : %s (adhésions=%d, rôles=%d)",
        membre.getNomComplet(), nbAdhesionsSuspendues, rolesDesactives);
  }

  /**
   * Vérifie si la désactivation d'un membre entraînerait l'orphelinage d'organisations
   * (i.e. le membre est le seul ORGADMIN actif d'au moins une org).
   *
   * @return liste des noms d'organisations qui deviendraient orphelines (vide si OK)
   */
  private List<String> verifierOrgsOrphelinees(UUID membreId) {
    List<String> orphelines = new ArrayList<>();
    // Toutes les orgs où ce membre est ORGADMIN actif
    final LocalDate today = LocalDate.now();
    List<dev.lions.unionflow.server.entity.MembreRole> rolesAdmin = membreRoleRepository.list(
        "membreOrganisation.membre.id = ?1 AND role.code = ?2 AND actif = true "
            + "AND (dateDebut IS NULL OR dateDebut <= ?3) "
            + "AND (dateFin IS NULL OR dateFin >= ?3)",
        membreId, "ORGADMIN", today);

    for (var role : rolesAdmin) {
      if (role.getOrganisation() == null) continue;
      UUID orgId = role.getOrganisation().getId();
      long totalAdmins = membreRoleRepository.countAdminsByOrganisationId(orgId);
      // Si ce membre est le seul admin (total=1) et qu'on le désactive → org orpheline
      if (totalAdmins <= 1) {
        orphelines.add(role.getOrganisation().getNom());
      }
    }
    return orphelines;
  }

  /** Génère un numéro de membre unique */
  private String genererNumeroMembre() {
    String prefix = "UF" + LocalDate.now().getYear();
    String suffix = UUID.randomUUID().toString().substring(0, 8).toUpperCase();
    return prefix + "-" + suffix;
  }

  /** Compte le nombre total de membres actifs */
  public long compterMembresActifs() {
    return membreRepository.countActifs();
  }

  /** Liste tous les membres actifs avec pagination */
  public List<Membre> listerMembresActifs(Page page, Sort sort) {
    return membreRepository.findAllActifs(page, sort);
  }

  /** Liste tous les membres avec pagination. Pour ADMIN_ORGANISATION, limite aux membres de ses organisations. */
  public List<Membre> listerMembres(Page page, Sort sort) {
    Optional<Set<UUID>> orgIds = getOrganisationIdsForCurrentUserIfAdminOrg();
    if (orgIds.isPresent()) {
      Set<UUID> ids = orgIds.get();
      if (ids.isEmpty()) return List.of();
      return membreRepository.findDistinctByOrganisationIdIn(ids, page, sort);
    }
    // SuperAdmin : filtre les désactivés par défaut (ne pas polluer les listes UI)
    return membreRepository.findAllActifs(page, sort);
  }

  /** Compte les membres actifs. Pour ADMIN_ORGANISATION, compte uniquement les membres de ses organisations. */
  public long compterMembres() {
    Optional<Set<UUID>> orgIds = getOrganisationIdsForCurrentUserIfAdminOrg();
    if (orgIds.isPresent()) {
      Set<UUID> ids = orgIds.get();
      if (ids.isEmpty()) return 0L;
      return membreRepository.countDistinctByOrganisationIdIn(ids);
    }
    return membreRepository.countActifs();
  }

  /** Recherche des membres avec pagination. Pour ADMIN_ORGANISATION, limite aux membres de ses organisations. */
  public List<Membre> rechercherMembres(String recherche, Page page, Sort sort) {
    Optional<Set<UUID>> orgIds = getOrganisationIdsForCurrentUserIfAdminOrg();
    if (orgIds.isPresent()) {
      Set<UUID> ids = orgIds.get();
      if (ids.isEmpty()) return List.of();
      return membreRepository.findByNomOrPrenomAndOrganisationIdIn(recherche, ids, page, sort);
    }
    return membreRepository.findByNomOrPrenom(recherche, page, sort);
  }

  /**
   * Si l'utilisateur connecté est ADMIN_ORGANISATION (et pas ADMIN/SUPER_ADMIN), retourne les IDs de ses organisations.
   * Sinon retourne Optional.empty() pour indiquer "tous les membres".
   */
  private Optional<Set<UUID>> getOrganisationIdsForCurrentUserIfAdminOrg() {
    if (securityIdentity.getPrincipal() == null) return Optional.empty();
    Set<String> roles = securityIdentity.getRoles();
    if (roles == null) return Optional.empty();
    boolean adminOrg = roles.contains("ADMIN_ORGANISATION");
    boolean adminOrSuper = roles.contains("ADMIN") || roles.contains("SUPER_ADMIN");
    if (!adminOrg || adminOrSuper) return Optional.empty();
    String email = securityIdentity.getPrincipal().getName();
    if (email == null || email.isBlank()) return Optional.empty();
    List<dev.lions.unionflow.server.entity.Organisation> orgs = organisationService.listerOrganisationsPourUtilisateur(email);
    if (orgs == null || orgs.isEmpty()) return Optional.of(Set.of());
    Set<UUID> ids = orgs.stream().map(dev.lions.unionflow.server.entity.Organisation::getId).collect(Collectors.toCollection(LinkedHashSet::new));
    return Optional.of(ids);
  }

  /** Obtient les statistiques avancées des membres */
  public Map<String, Object> obtenirStatistiquesAvancees() {
    LOG.info("Calcul des statistiques avancées des membres");

    long totalMembres = membreRepository.count();
    long membresActifs = membreRepository.countActifs();
    long membresInactifs = totalMembres - membresActifs;
    long nouveauxMembres30Jours = membreRepository.countNouveauxMembres(LocalDate.now().minusDays(30));
    long totalOrganisations = organisationService.rechercherOrganisationsCount("");

    Map<String, Object> stats = new java.util.HashMap<>();
    stats.put("totalMembres", totalMembres);
    stats.put("total", totalMembres); // alias pour compatibilité mobile
    stats.put("membresActifs", membresActifs);
    stats.put("membresInactifs", membresInactifs);
    stats.put("nouveauxMembres30Jours", nouveauxMembres30Jours);
    stats.put("tauxActivite", totalMembres > 0 ? (membresActifs * 100.0 / totalMembres) : 0.0);
    stats.put("totalOrganisations", totalOrganisations);
    stats.put("timestamp", LocalDateTime.now());
    return stats;
  }

  // ========================================
  // MÉTHODES DE CONVERSION DTO
  // ========================================

  /** Convertit une entité Membre en MembreResponse */
  public MembreResponse convertToResponse(Membre membre) {
    if (membre == null) {
      return null;
    }

    MembreResponse dto = new MembreResponse();
    dto.setId(membre.getId());
    dto.setNumeroMembre(membre.getNumeroMembre());
    dto.setKeycloakId(membre.getKeycloakId());
    dto.setPrenom(membre.getPrenom());
    dto.setNom(membre.getNom());
    dto.setNomComplet(membre.getNomComplet());
    dto.setEmail(membre.getEmail());
    dto.setTelephone(membre.getTelephone());
    dto.setTelephoneWave(membre.getTelephoneWave());
    dto.setDateNaissance(membre.getDateNaissance());
    dto.setAge(membre.getAge());
    dto.setProfession(membre.getProfession());
    dto.setPhotoUrl(membre.getPhotoUrl());

    dto.setStatutMatrimonial(membre.getStatutMatrimonial());
    if (membre.getStatutMatrimonial() != null) {
      dto.setStatutMatrimonialLibelle(
          typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_MATRIMONIAL", membre.getStatutMatrimonial()));
    }

    dto.setNationalite(membre.getNationalite());
    dto.setTypeIdentite(membre.getTypeIdentite());
    if (membre.getTypeIdentite() != null) {
      dto.setTypeIdentiteLibelle(
          typeReferenceRepository.findLibelleByDomaineAndCode("TYPE_IDENTITE", membre.getTypeIdentite()));
    }
    dto.setNumeroIdentite(membre.getNumeroIdentite());

    dto.setNiveauVigilanceKyc(membre.getNiveauVigilanceKyc());
    dto.setStatutKyc(membre.getStatutKyc());
    dto.setDateVerificationIdentite(membre.getDateVerificationIdentite());

    dto.setStatutCompte(membre.getStatutCompte());
    if (membre.getStatutCompte() != null) {
      dto.setStatutCompteLibelle(
          typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_COMPTE", membre.getStatutCompte()));
      dto.setStatutCompteSeverity(
          typeReferenceRepository.findSeverityByDomaineAndCode("STATUT_COMPTE", membre.getStatutCompte()));
    }

    // Chargement de tous les rôles actifs via MembreOrganisation → MembreRole
    List<dev.lions.unionflow.server.entity.MembreRole> roles = membreRoleRepository
        .findActifsByMembreId(membre.getId());
    if (!roles.isEmpty()) {
      List<String> roleCodes = roles.stream()
          .filter(r -> r.getRole() != null)
          .map(r -> r.getRole().getCode())
          .collect(Collectors.toList());
      dto.setRoles(roleCodes);
    } else {
      dto.setRoles(new ArrayList<>());
    }
    if (membre.getMembresOrganisations() != null && !membre.getMembresOrganisations().isEmpty()) {
      dev.lions.unionflow.server.entity.MembreOrganisation mo = membre.getMembresOrganisations().get(0);
      if (mo.getOrganisation() != null) {
        dto.setOrganisationId(mo.getOrganisation().getId());
        dto.setOrganisationNom(mo.getOrganisation().getNom());
      }
      dto.setDateAdhesion(mo.getDateAdhesion());
    } else if (membre.getDateCreation() != null) {
      // Fallback : date de création du compte comme date d'adhésion (membres sans organisation)
      dto.setDateAdhesion(membre.getDateCreation().toLocalDate());
    }

    // Nombre d'événements auxquels le membre a participé
    dto.setNombreEvenementsParticipes(
        (int) inscriptionEvenementRepository.countByMembre(membre.getId()));

    // Adresse principale (principale=true en priorité, sinon première adresse active)
    if (membre.getAdresses() != null && !membre.getAdresses().isEmpty()) {
      dev.lions.unionflow.server.entity.Adresse adressePrincipale = membre.getAdresses().stream()
          .filter(a -> Boolean.TRUE.equals(a.getPrincipale()) && Boolean.TRUE.equals(a.getActif()))
          .findFirst()
          .orElseGet(() -> membre.getAdresses().stream()
              .filter(a -> Boolean.TRUE.equals(a.getActif()))
              .findFirst()
              .orElse(null));
      if (adressePrincipale != null) {
        dto.setAdresse(adressePrincipale.getAdresse());
        dto.setVille(adressePrincipale.getVille());
        dto.setCodePostal(adressePrincipale.getCodePostal());
      }
    }

    // Notes / biographie
    dto.setNotes(membre.getNotes());

    // Champs de base DTO
    dto.setDateCreation(membre.getDateCreation());
    dto.setDateModification(membre.getDateModification());
    dto.setCreePar(membre.getCreePar());
    dto.setModifiePar(membre.getModifiePar());
    dto.setActif(membre.getActif());
    dto.setVersion(membre.getVersion() != null ? membre.getVersion() : 0L);

    return dto;
  }

  /** Convertit une entité Membre en MembreSummaryResponse */
  public MembreSummaryResponse convertToSummaryResponse(Membre membre) {
    if (membre == null) {
      return null;
    }

    List<String> rolesNames = new ArrayList<>();
    List<dev.lions.unionflow.server.entity.MembreRole> roles = membreRoleRepository
        .findActifsByMembreId(membre.getId());
    if (!roles.isEmpty()) {
      rolesNames = roles.stream()
          .filter(r -> r.getRole() != null)
          .map(r -> r.getRole().getCode())
          .collect(Collectors.toList());
    }

    String libelle = null;
    String severity = null;
    if (membre.getStatutCompte() != null) {
      libelle = typeReferenceRepository.findLibelleByDomaineAndCode("STATUT_COMPTE", membre.getStatutCompte());
      severity = typeReferenceRepository.findSeverityByDomaineAndCode("STATUT_COMPTE", membre.getStatutCompte());
    }

    UUID organisationId = null;
    String organisationNom = null;
    java.time.LocalDate dateAdhesion = null;
    if (membre.getMembresOrganisations() != null && !membre.getMembresOrganisations().isEmpty()) {
      dev.lions.unionflow.server.entity.MembreOrganisation mo = membre.getMembresOrganisations().get(0);
      if (mo.getOrganisation() != null) {
        organisationId = mo.getOrganisation().getId();
        organisationNom = mo.getOrganisation().getNom();
      }
      dateAdhesion = mo.getDateAdhesion();
    }

    return new MembreSummaryResponse(
        membre.getId(),
        membre.getNumeroMembre(),
        membre.getPrenom(),
        membre.getNom(),
        membre.getEmail(),
        membre.getTelephone(),
        membre.getProfession(),
        membre.getStatutCompte(),
        libelle,
        severity,
        membre.getActif(),
        rolesNames,
        organisationId,
        organisationNom,
        dateAdhesion);
  }

  /** Convertit un CreateMembreRequest en entité Membre */
  public Membre convertFromCreateRequest(CreateMembreRequest dto) {
    if (dto == null) {
      return null;
    }

    Membre membre = new Membre();

    // Copie des champs
    membre.setNom(dto.nom());
    membre.setPrenom(dto.prenom());
    membre.setEmail(dto.email());
    membre.setTelephone(dto.telephone());
    membre.setTelephoneWave(dto.telephoneWave());
    membre.setDateNaissance(dto.dateNaissance());
    membre.setProfession(dto.profession());
    membre.setPhotoUrl(dto.photoUrl());
    membre.setStatutMatrimonial(dto.statutMatrimonial());
    membre.setNationalite(dto.nationalite());
    membre.setTypeIdentite(dto.typeIdentite());
    membre.setNumeroIdentite(dto.numeroIdentite());

    return membre;
  }

  /** Convertit une liste d'entités en liste de MembreSummaryResponse */
  public List<MembreSummaryResponse> convertToSummaryResponseList(List<Membre> membres) {
    if (membres == null)
      return new ArrayList<>();
    return membres.stream().map(this::convertToSummaryResponse).collect(Collectors.toList());
  }

  /** Convertit une liste d'entités en liste de MembreResponse */
  public List<MembreResponse> convertToResponseList(List<Membre> membres) {
    if (membres == null)
      return new ArrayList<>();
    return membres.stream().map(this::convertToResponse).collect(Collectors.toList());
  }

  /** Met à jour une entité Membre à partir d'un UpdateMembreRequest */
  public void updateFromRequest(Membre membre, UpdateMembreRequest dto) {
    if (membre == null || dto == null) {
      return;
    }

    // Mise à jour des champs modifiables
    membre.setPrenom(dto.prenom());
    membre.setNom(dto.nom());
    membre.setEmail(dto.email());
    membre.setTelephone(dto.telephone());
    membre.setTelephoneWave(dto.telephoneWave());
    membre.setDateNaissance(dto.dateNaissance());
    membre.setProfession(dto.profession());
    membre.setPhotoUrl(dto.photoUrl());
    membre.setStatutMatrimonial(dto.statutMatrimonial());
    membre.setNationalite(dto.nationalite());
    membre.setTypeIdentite(dto.typeIdentite());
    membre.setNumeroIdentite(dto.numeroIdentite());
    if (dto.actif() != null) {
      membre.setActif(dto.actif());
    }
    membre.setDateModification(LocalDateTime.now());
  }

  /** Recherche avancée de membres avec filtres multiples (DEPRECATED) */
  public List<Membre> rechercheAvancee(
      String recherche,
      Boolean actif,
      LocalDate dateAdhesionMin,
      LocalDate dateAdhesionMax,
      Page page,
      Sort sort) {
    LOG.infof(
        "Recherche avancée (DEPRECATED) - recherche: %s, actif: %s, dateMin: %s, dateMax: %s",
        recherche, actif, dateAdhesionMin, dateAdhesionMax);

    return membreRepository.rechercheAvancee(
        recherche, actif, dateAdhesionMin, dateAdhesionMax, page, sort);
  }

  /**
   * Nouvelle recherche avancée de membres avec critères complets Retourne des
   * résultats paginés
   * avec statistiques
   *
   * @param criteria Critères de recherche
   * @param page     Pagination
   * @param sort     Tri
   * @return Résultats de recherche avec métadonnées
   */
  public MembreSearchResultDTO searchMembresAdvanced(
      MembreSearchCriteria criteria, Page page, Sort sort) {
    LOG.infof("Recherche avancée de membres - critères: %s", criteria.getDescription());

    // Pour ADMIN_ORGANISATION : restreindre aux organisations gérées par l'utilisateur
    Optional<Set<UUID>> allowedOrgIds = getOrganisationIdsForCurrentUserIfAdminOrg();
    if (allowedOrgIds.isPresent()) {
      Set<UUID> ids = allowedOrgIds.get();
      if (ids.isEmpty()) {
        return MembreSearchResultDTO.empty(criteria, page.size, page.index);
      }
      if (criteria.getOrganisationIds() == null || criteria.getOrganisationIds().isEmpty()) {
        criteria.setOrganisationIds(new ArrayList<>(ids));
      } else {
        List<UUID> intersection = criteria.getOrganisationIds().stream()
            .filter(ids::contains)
            .collect(Collectors.toList());
        criteria.setOrganisationIds(intersection);
      }
    }

    // Construction de la requête dynamique
    StringBuilder queryBuilder = new StringBuilder("SELECT m FROM Membre m WHERE 1=1");
    Map<String, Object> parameters = new HashMap<>();

    // Ajout des critères de recherche
    addSearchCriteria(queryBuilder, parameters, criteria);

    // Requête pour compter le total
    String countQuery = queryBuilder
        .toString()
        .replace("SELECT m FROM Membre m", "SELECT COUNT(m) FROM Membre m");

    // Exécution de la requête de comptage
    TypedQuery<Long> countQueryTyped = entityManager.createQuery(countQuery, Long.class);
    for (Map.Entry<String, Object> param : parameters.entrySet()) {
      countQueryTyped.setParameter(param.getKey(), param.getValue());
    }
    long totalElements = countQueryTyped.getSingleResult();

    if (totalElements == 0) {
      return MembreSearchResultDTO.empty(criteria, page.size, page.index);
    }

    // Ajout du tri et pagination
    String finalQuery = queryBuilder.toString();
    if (sort != null) {
      finalQuery += " ORDER BY " + buildOrderByClause(sort);
    }

    // Exécution de la requête principale
    TypedQuery<Membre> queryTyped = entityManager.createQuery(finalQuery, Membre.class);
    for (Map.Entry<String, Object> param : parameters.entrySet()) {
      queryTyped.setParameter(param.getKey(), param.getValue());
    }
    queryTyped.setFirstResult(page.index * page.size);
    queryTyped.setMaxResults(page.size);
    List<Membre> membres = queryTyped.getResultList();

    // Conversion en SummaryResponses
    List<MembreSummaryResponse> membresDTO = convertToSummaryResponseList(membres);

    // Calcul des statistiques
    MembreSearchResultDTO.SearchStatistics statistics = calculateSearchStatistics(membres);

    // Construction du résultat
    MembreSearchResultDTO result = MembreSearchResultDTO.builder()
        .membres(membresDTO)
        .totalElements(totalElements)
        .totalPages((int) Math.ceil((double) totalElements / page.size))
        .currentPage(page.index)
        .pageSize(page.size)
        .criteria(criteria)
        .statistics(statistics)
        .build();

    // Calcul des indicateurs de pagination
    result.calculatePaginationFlags();

    return result;
  }

  /** Ajoute les critères de recherche à la requête */
  private void addSearchCriteria(
      StringBuilder queryBuilder, Map<String, Object> parameters, MembreSearchCriteria criteria) {

    // Recherche générale dans nom, prénom, email
    if (criteria.getQuery() != null) {
      queryBuilder.append(
          " AND (LOWER(m.nom) LIKE LOWER(:query) OR LOWER(m.prenom) LIKE LOWER(:query) OR"
              + " LOWER(m.email) LIKE LOWER(:query))");
      parameters.put("query", "%" + criteria.getQuery() + "%");
    }

    // Recherche par nom
    if (criteria.getNom() != null) {
      queryBuilder.append(" AND LOWER(m.nom) LIKE LOWER(:nom)");
      parameters.put("nom", "%" + criteria.getNom() + "%");
    }

    // Recherche par prénom
    if (criteria.getPrenom() != null) {
      queryBuilder.append(" AND LOWER(m.prenom) LIKE LOWER(:prenom)");
      parameters.put("prenom", "%" + criteria.getPrenom() + "%");
    }

    // Recherche par email
    if (criteria.getEmail() != null) {
      queryBuilder.append(" AND LOWER(m.email) LIKE LOWER(:email)");
      parameters.put("email", "%" + criteria.getEmail() + "%");
    }

    // Recherche par téléphone
    if (criteria.getTelephone() != null) {
      queryBuilder.append(" AND m.telephone LIKE :telephone");
      parameters.put("telephone", "%" + criteria.getTelephone() + "%");
    }

    // Filtre par statut
    if (criteria.getStatut() != null) {
      boolean isActif = "ACTIF".equals(criteria.getStatut());
      queryBuilder.append(" AND m.actif = :actif");
      parameters.put("actif", isActif);
    } else if (!Boolean.TRUE.equals(criteria.getIncludeInactifs())) {
      // Par défaut, exclure les inactifs
      queryBuilder.append(" AND m.actif = true");
    }

    // Filtre par dates d'adhésion (via MembreOrganisation)
    if (criteria.getDateAdhesionMin() != null) {
      queryBuilder.append(
          " AND EXISTS (SELECT 1 FROM MembreOrganisation mo2 WHERE mo2.membre = m AND mo2.dateAdhesion >= :dateAdhesionMin)");
      parameters.put("dateAdhesionMin", criteria.getDateAdhesionMin());
    }

    if (criteria.getDateAdhesionMax() != null) {
      queryBuilder.append(
          " AND EXISTS (SELECT 1 FROM MembreOrganisation mo3 WHERE mo3.membre = m AND mo3.dateAdhesion <= :dateAdhesionMax)");
      parameters.put("dateAdhesionMax", criteria.getDateAdhesionMax());
    }

    // Filtre par âge (calculé à partir de la date de naissance)
    if (criteria.getAgeMin() != null) {
      LocalDate maxBirthDate = LocalDate.now().minusYears(criteria.getAgeMin());
      queryBuilder.append(" AND m.dateNaissance <= :maxBirthDateForMinAge");
      parameters.put("maxBirthDateForMinAge", maxBirthDate);
    }

    if (criteria.getAgeMax() != null) {
      LocalDate minBirthDate = LocalDate.now().minusYears(criteria.getAgeMax() + 1).plusDays(1);
      queryBuilder.append(" AND m.dateNaissance >= :minBirthDateForMaxAge");
      parameters.put("minBirthDateForMaxAge", minBirthDate);
    }

    // Filtre par organisations (via MembreOrganisation)
    if (criteria.getOrganisationIds() != null && !criteria.getOrganisationIds().isEmpty()) {
      queryBuilder.append(
          " AND EXISTS (SELECT 1 FROM MembreOrganisation mo WHERE mo.membre = m AND mo.organisation.id IN :organisationIds)");
      parameters.put("organisationIds", criteria.getOrganisationIds());
    }

    // Filtre par rôles (via MembreOrganisation -> MembreRole)
    if (criteria.getRoles() != null && !criteria.getRoles().isEmpty()) {
      queryBuilder.append(" AND EXISTS (");
      queryBuilder.append("  SELECT 1 FROM MembreRole mr WHERE mr.membreOrganisation.membre = m");
      queryBuilder.append("  AND mr.actif = true");
      queryBuilder.append("  AND mr.role.code IN :roleCodes");
      queryBuilder.append(")");
      parameters.put("roleCodes", criteria.getRoles());
    }
  }

  /** Construit la clause ORDER BY à partir du Sort */
  private String buildOrderByClause(Sort sort) {
    if (sort.getColumns().isEmpty()) {
      return "m.nom ASC";
    }

    return sort.getColumns().stream()
        .map(column -> {
          String direction = column.getDirection() == Sort.Direction.Descending ? "DESC" : "ASC";
          return "m." + column.getName() + " " + direction;
        })
        .collect(Collectors.joining(", "));
  }

  /** Calcule les statistiques sur les résultats de recherche */
  private MembreSearchResultDTO.SearchStatistics calculateSearchStatistics(List<Membre> membres) {
    if (membres.isEmpty()) {
      return MembreSearchResultDTO.SearchStatistics.builder()
          .membresActifs(0)
          .membresInactifs(0)
          .ageMoyen(0.0)
          .ageMin(0)
          .ageMax(0)
          .nombreOrganisations(0)
          .nombreRegions(0)
          .ancienneteMoyenne(0.0)
          .build();
    }

    long membresActifs = membres.stream().mapToLong(m -> Boolean.TRUE.equals(m.getActif()) ? 1 : 0).sum();
    long membresInactifs = membres.size() - membresActifs;

    // Calcul des âges
    List<Integer> ages = membres.stream()
        .filter(m -> m.getDateNaissance() != null)
        .map(m -> Period.between(m.getDateNaissance(), LocalDate.now()).getYears())
        .collect(Collectors.toList());

    double ageMoyen = ages.stream().mapToInt(Integer::intValue).average().orElse(0.0);
    int ageMin = ages.stream().mapToInt(Integer::intValue).min().orElse(0);
    int ageMax = ages.stream().mapToInt(Integer::intValue).max().orElse(0);

    // Calcul de l'ancienneté moyenne
    double ancienneteMoyenne = 0.0; // calculé via MembreOrganisation

    // Nombre d'organisations via les membresOrganisations
    long nombreOrganisations = membres.stream()
        .flatMap(m -> m.getMembresOrganisations() != null
            ? m.getMembresOrganisations().stream()
            : java.util.stream.Stream.empty())
        .map(mo -> mo.getOrganisation() != null ? mo.getOrganisation().getId() : null)
        .filter(java.util.Objects::nonNull)
        .distinct()
        .count();

    return MembreSearchResultDTO.SearchStatistics.builder()
        .membresActifs(membresActifs)
        .membresInactifs(membresInactifs)
        .ageMoyen(ageMoyen)
        .ageMin(ageMin)
        .ageMax(ageMax)
        .nombreOrganisations(nombreOrganisations)
        .nombreRegions(
            membres.stream()
                .flatMap(m -> m.getAdresses() != null ? m.getAdresses().stream() : java.util.stream.Stream.empty())
                .map(dev.lions.unionflow.server.entity.Adresse::getRegion)
                .filter(r -> r != null && !r.isEmpty())
                .distinct()
                .count())
        .ancienneteMoyenne(ancienneteMoyenne)
        .build();
  }

  // ========================================
  // MÉTHODES D'AUTOCOMPLÉTION (WOU/DRY)
  // ========================================

  /**
   * Obtient la liste des villes distinctes depuis les adresses des membres
   * Réutilisable pour autocomplétion (WOU/DRY)
   */
  public List<String> obtenirVillesDistinctes(String query) {
    LOG.infof("Récupération des villes distinctes - query: %s", query);

    String jpql = "SELECT DISTINCT a.ville FROM Adresse a WHERE a.ville IS NOT NULL AND a.ville != ''";
    if (query != null && !query.trim().isEmpty()) {
      jpql += " AND LOWER(a.ville) LIKE LOWER(:query)";
    }
    jpql += " ORDER BY a.ville ASC";

    TypedQuery<String> typedQuery = entityManager.createQuery(jpql, String.class);
    if (query != null && !query.trim().isEmpty()) {
      typedQuery.setParameter("query", "%" + query.trim() + "%");
    }
    typedQuery.setMaxResults(50); // Limiter à 50 résultats pour performance

    List<String> villes = typedQuery.getResultList();
    LOG.infof("Trouvé %d villes distinctes", villes.size());
    return villes;
  }

  /**
   * Obtient la liste des professions distinctes depuis les membres
   * (autocomplétion).
   */
  public List<String> obtenirProfessionsDistinctes(String query) {
    LOG.infof("Récupération des professions distinctes - query: %s", query);
    String jpql = "SELECT DISTINCT m.profession FROM Membre m WHERE m.profession IS NOT NULL AND m.profession != ''";
    if (query != null && !query.trim().isEmpty()) {
      jpql += " AND LOWER(m.profession) LIKE LOWER(:query)";
    }
    jpql += " ORDER BY m.profession ASC";
    TypedQuery<String> typedQuery = entityManager.createQuery(jpql, String.class);
    if (query != null && !query.trim().isEmpty()) {
      typedQuery.setParameter("query", "%" + query.trim() + "%");
    }
    typedQuery.setMaxResults(50);
    return typedQuery.getResultList();
  }

  /**
   * Exporte une sélection de membres en Excel (WOU/DRY - réutilise la logique
   * d'export)
   *
   * @param membreIds Liste des IDs des membres à exporter
   * @param format    Format d'export (EXCEL, CSV, etc.)
   * @return Données binaires du fichier Excel
   */
  public byte[] exporterMembresSelectionnes(List<UUID> membreIds, String format) {
    if (membreIds == null || membreIds.isEmpty()) {
      throw new IllegalArgumentException("La liste des membres ne peut pas être vide");
    }

    LOG.infof("Export de %d membres sélectionnés - format: %s", membreIds.size(), format);

    // Récupérer les membres
    List<Membre> membres = membreIds.stream()
        .map(id -> membreRepository.findByIdOptional(id))
        .filter(opt -> opt.isPresent())
        .map(java.util.Optional::get)
        .collect(Collectors.toList());

    // Convertir en DTOs
    List<MembreResponse> membresDTO = convertToResponseList(membres);

    // Générer le fichier Excel (simplifié - à améliorer avec Apache POI)
    // Pour l'instant, générer un CSV simple
    StringBuilder csv = new StringBuilder();
    csv.append("Numéro;Nom;Prénom;Email;Téléphone;Statut;Date Adhésion\n");
    for (MembreResponse m : membresDTO) {
      csv.append(
          String.format(
              "%s;%s;%s;%s;%s;%s;%s\n",
              m.getNumeroMembre() != null ? m.getNumeroMembre() : "",
              m.getNom() != null ? m.getNom() : "",
              m.getPrenom() != null ? m.getPrenom() : "",
              m.getEmail() != null ? m.getEmail() : "",
              m.getTelephone() != null ? m.getTelephone() : "",
              m.getStatutCompte() != null ? m.getStatutCompte() : "",
              m.getDateAdhesion() != null ? m.getDateAdhesion().toString() : ""));
    }

    return csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8);
  }

  /**
   * Importe des membres depuis un fichier Excel ou CSV
   */
  public MembreImportExportService.ResultatImport importerMembres(
      InputStream fileInputStream,
      String fileName,
      UUID organisationId,
      String typeMembreDefaut,
      boolean mettreAJourExistants,
      boolean ignorerErreurs) {
    return membreImportExportService.importerMembres(
        fileInputStream, fileName, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);
  }

  /**
   * Exporte des membres vers Excel
   */
  public byte[] exporterVersExcel(List<MembreResponse> membres, List<String> colonnesExport, boolean inclureHeaders,
      boolean formaterDates, boolean inclureStatistiques, String motDePasse) {
    try {
      return membreImportExportService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates,
          inclureStatistiques, motDePasse);
    } catch (Exception e) {
      LOG.errorf(e, "Erreur lors de l'export Excel");
      throw new RuntimeException("Erreur lors de l'export Excel: " + e.getMessage(), e);
    }
  }

  /**
   * Exporte des membres vers CSV
   */
  public byte[] exporterVersCSV(List<MembreResponse> membres, List<String> colonnesExport, boolean inclureHeaders,
      boolean formaterDates) {
    try {
      return membreImportExportService.exporterVersCSV(membres, colonnesExport, inclureHeaders, formaterDates);
    } catch (Exception e) {
      LOG.errorf(e, "Erreur lors de l'export CSV");
      throw new RuntimeException("Erreur lors de l'export CSV: " + e.getMessage(), e);
    }
  }

  /**
   * Exporte des membres vers PDF
   */
  public byte[] exporterVersPDF(List<MembreResponse> membres, List<String> colonnesExport, boolean inclureHeaders,
      boolean formaterDates, boolean inclureStatistiques) {
    try {
      return membreImportExportService.exporterVersPDF(membres, colonnesExport, inclureHeaders, formaterDates,
          inclureStatistiques);
    } catch (Exception e) {
      LOG.errorf(e, "Erreur lors de l'export PDF");
      throw new RuntimeException("Erreur lors de l'export PDF: " + e.getMessage(), e);
    }
  }

  /**
   * Génère un modèle Excel pour l'import
   */
  public byte[] genererModeleImport() {
    try {
      return membreImportExportService.genererModeleImport();
    } catch (Exception e) {
      LOG.errorf(e, "Erreur lors de la génération du modèle");
      throw new RuntimeException("Erreur lors de la génération du modèle: " + e.getMessage(), e);
    }
  }

  /**
   * Liste les membres pour l'export selon les filtres
   */
  public List<MembreResponse> listerMembresPourExport(
      UUID associationId,
      String statut,
      String type,
      String dateAdhesionDebut,
      String dateAdhesionFin) {

    List<Membre> membres;

    if (associationId != null) {
      TypedQuery<Membre> query = entityManager.createQuery(
          "SELECT DISTINCT m FROM Membre m JOIN m.membresOrganisations mo WHERE mo.organisation.id = :associationId",
          Membre.class);
      query.setParameter("associationId", associationId);
      membres = query.getResultList();
    } else {
      membres = membreRepository.listAll();
    }

    // Filtrer par statut
    if (statut != null && !statut.isEmpty()) {
      boolean actif = "ACTIF".equals(statut);
      membres = membres.stream()
          .filter(m -> m.getActif() == actif)
          .collect(Collectors.toList());
    }

    return convertToResponseList(membres);
  }

  /**
   * Liste les membres appartenant aux organisations spécifiées (pour ADMIN_ORGANISATION)
   *
   * @param organisationIds Liste des IDs d'organisations
   * @param page Pagination
   * @param sort Tri
   * @return Liste des membres
   */
  public List<Membre> listerMembresParOrganisations(
      List<UUID> organisationIds,
      Page page,
      Sort sort) {

    if (organisationIds == null || organisationIds.isEmpty()) {
      LOG.warn("listerMembresParOrganisations appelé avec liste vide");
      return List.of();
    }

    LOG.infof("Listage des membres pour %d organisations", organisationIds.size());

    String jpql = "SELECT DISTINCT m FROM Membre m " +
                  "JOIN m.membresOrganisations mo " +
                  "WHERE mo.organisation.id IN :orgIds " +
                  "AND (m.actif IS NULL OR m.actif = true OR m.statutCompte = 'EN_ATTENTE_VALIDATION') " +
                  "ORDER BY m.nom ASC, m.prenom ASC";

    TypedQuery<Membre> query = entityManager.createQuery(jpql, Membre.class);
    query.setParameter("orgIds", organisationIds);

    if (page != null) {
      query.setFirstResult((int)page.index * page.size);
      query.setMaxResults(page.size);
    }

    List<Membre> membres = query.getResultList();
    LOG.infof("Trouvé %d membres pour les organisations spécifiées", membres.size());

    return membres;
  }

  /** Compte le nombre total de membres pour les organisations données (même filtre que listerMembresParOrganisations). */
  public long compterMembresParOrganisations(List<UUID> organisationIds) {
    if (organisationIds == null || organisationIds.isEmpty()) return 0L;
    String jpql = "SELECT COUNT(DISTINCT m) FROM Membre m " +
                  "JOIN m.membresOrganisations mo " +
                  "WHERE mo.organisation.id IN :orgIds " +
                  "AND (m.actif IS NULL OR m.actif = true OR m.statutCompte = 'EN_ATTENTE_VALIDATION')";
    TypedQuery<Long> query = entityManager.createQuery(jpql, Long.class);
    query.setParameter("orgIds", organisationIds);
    return query.getSingleResult();
  }

  /**
   * Vérifie si une organisation possède une souscription active.
   * Utilisé pour déterminer si un membre créé par un admin doit être auto-activé.
   *
   * @param orgId UUID de l'organisation
   * @return true si une souscription ACTIVE existe pour cette organisation
   */
  public boolean orgHasActiveSubscription(UUID orgId) {
    if (orgId == null) return false;
    return entityManager.createQuery(
        "SELECT COUNT(s) FROM SouscriptionOrganisation s " +
        "WHERE s.organisation.id = :orgId AND s.statut = 'ACTIVE'",
        Long.class)
      .setParameter("orgId", orgId)
      .getSingleResult() > 0;
  }

  /**
   * Vérifie si une organisation a reçu un paiement (confirmé ou validé).
   * Utilisé pour auto-activer l'admin dès que le paiement est reçu,
   * sans attendre la validation super admin.
   *
   * @param orgId UUID de l'organisation
   * @return true si la souscription est ACTIVE ou en PAIEMENT_CONFIRME/VALIDEE
   */
  public boolean orgHasPaidSubscription(UUID orgId) {
    if (orgId == null) return false;
    return entityManager.createQuery(
        "SELECT COUNT(s) FROM SouscriptionOrganisation s " +
        "WHERE s.organisation.id = :orgId " +
        "AND (s.statut = 'ACTIVE' OR s.statutValidation IN ('PAIEMENT_CONFIRME', 'VALIDEE'))",
        Long.class)
      .setParameter("orgId", orgId)
      .getSingleResult() > 0;
  }

  /**
   * Lie un membre à une organisation et incrémente le quota de la souscription.
   * Utilisé lors de la création unitaire ou de l'import massif.
   *
   * @param membre Membre à lier
   * @param organisationId ID de l'organisation
   * @param typeMembreDefaut Type de membre ("ACTIF", "EN_ATTENTE_VALIDATION", etc.)
   */
  @Transactional
  public void lierMembreOrganisationEtIncrementerQuota(
      dev.lions.unionflow.server.entity.Membre membre,
      UUID organisationId,
      String typeMembreDefaut) {

    if (membre == null || organisationId == null) {
      throw new IllegalArgumentException("Membre et organisationId obligatoires");
    }

    LOG.infof("Liaison membre %s à organisation %s", membre.getNumeroMembre(), organisationId);

    // Charger organisation
    dev.lions.unionflow.server.entity.Organisation organisation =
      entityManager.find(dev.lions.unionflow.server.entity.Organisation.class, organisationId);

    if (organisation == null) {
      throw new IllegalArgumentException("Organisation non trouvée: " + organisationId);
    }

    // Charger souscription active
    Optional<dev.lions.unionflow.server.entity.SouscriptionOrganisation> souscriptionOpt =
      entityManager.createQuery(
        "SELECT s FROM SouscriptionOrganisation s " +
        "WHERE s.organisation.id = :orgId AND s.statut = 'ACTIVE'",
        dev.lions.unionflow.server.entity.SouscriptionOrganisation.class)
      .setParameter("orgId", organisationId)
      .getResultStream()
      .findFirst();

    // Déterminer statut membre
    dev.lions.unionflow.server.api.enums.membre.StatutMembre statut =
      "ACTIF".equalsIgnoreCase(typeMembreDefaut)
        ? dev.lions.unionflow.server.api.enums.membre.StatutMembre.ACTIF
        : dev.lions.unionflow.server.api.enums.membre.StatutMembre.EN_ATTENTE_VALIDATION;

    // Créer lien MembreOrganisation
    dev.lions.unionflow.server.entity.MembreOrganisation membreOrganisation =
      new dev.lions.unionflow.server.entity.MembreOrganisation();
    membreOrganisation.setMembre(membre);
    membreOrganisation.setOrganisation(organisation);
    membreOrganisation.setStatutMembre(statut);
    membreOrganisation.setDateAdhesion(LocalDate.now());

    entityManager.persist(membreOrganisation);

    LOG.infof("MembreOrganisation créé (statut: %s)", statut);

    // Incrémenter le compteur nombreMembres de l'organisation
    organisation.ajouterMembre();
    entityManager.persist(organisation);

    // Assigner le rôle SIMPLEMEMBER par défaut
    assignerRoleDefaut(membreOrganisation, "SIMPLEMEMBER");

    // Vérifier quota et expiration avant d'incrémenter
    if (souscriptionOpt.isPresent()) {
      dev.lions.unionflow.server.entity.SouscriptionOrganisation souscription = souscriptionOpt.get();

      // Vérifier que la souscription n'est pas expirée
      if (!souscription.isActive()) {
        throw new jakarta.ws.rs.ForbiddenException(
            "La souscription de l'organisation est expirée ou inactive. " +
            "Veuillez renouveler votre abonnement avant d'ajouter de nouveaux membres.");
      }

      // Vérifier que le quota n'est pas dépassé
      if (souscription.isQuotaDepasse()) {
        Integer max = souscription.getQuotaMax();
        throw new jakarta.ws.rs.ForbiddenException(
            "Le quota de membres de votre plan est atteint (" + max + "/" + max + "). " +
            "Veuillez mettre à niveau votre formule d'abonnement.");
      }

      souscription.incrementerQuota();
      entityManager.persist(souscription);
      LOG.infof("Quota souscription incrémenté (utilise: %d/%s)",
                souscription.getQuotaUtilise(),
                souscription.getQuotaMax() != null ? souscription.getQuotaMax().toString() : "∞");
    } else {
      LOG.warn("Aucune souscription active trouvée pour organisation " + organisationId +
               " — ajout du membre sans vérification de quota");
    }
  }

  private void assignerRoleDefaut(dev.lions.unionflow.server.entity.MembreOrganisation mo, String roleCode) {
    roleRepository.findByCode(roleCode).ifPresent(role -> {
      dev.lions.unionflow.server.entity.MembreRole membreRole = new dev.lions.unionflow.server.entity.MembreRole();
      membreRole.setMembreOrganisation(mo);
      membreRole.setOrganisation(mo.getOrganisation());
      membreRole.setRole(role);
      membreRole.setActif(true);
      membreRole.setDateDebut(LocalDate.now());
      entityManager.persist(membreRole);
      LOG.infof("Rôle %s assigné au membre %s dans organisation %s",
          roleCode, mo.getMembre().getNumeroMembre(), mo.getOrganisation().getId());
    });
  }
}
