package dev.lions.unionflow.server.resource;

import dev.lions.unionflow.server.api.dto.common.PagedResponse;
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.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.MembreOrganisation;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.MembreRoleRepository;
import dev.lions.unionflow.server.service.MemberLifecycleService;
import dev.lions.unionflow.server.service.MembreKeycloakSyncService;
import dev.lions.unionflow.server.service.MembreService;
import dev.lions.unionflow.server.service.MembreSuiviService;
import dev.lions.unionflow.server.service.OrganisationService;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.InputStream;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.ExampleObject;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.RestForm;

/** Resource REST pour la gestion des membres */
@Path("/api/membres")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ApplicationScoped
@Tag(name = "Membres", description = "API de gestion des membres")
@Transactional
public class MembreResource {

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

  @Inject
  MembreService membreService;

  @Inject
  MembreKeycloakSyncService keycloakSyncService;

  @Inject
  MembreSuiviService membreSuiviService;

  @Inject
  OrganisationService organisationService;

  @Inject
  MemberLifecycleService memberLifecycleService;

  @Inject
  MembreOrganisationRepository membreOrgRepository;

  @Inject
  MembreRepository membreRepository;

  @Inject
  MembreRoleRepository membreRoleRepository;

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

  @Inject
  JsonWebToken jwt;

  @GET
  @RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR" })
  @Operation(summary = "Lister les membres")
  @APIResponse(responseCode = "200", description = "Liste des membres avec pagination")
  public PagedResponse<MembreSummaryResponse> listerMembres(
      @Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0") int page,
      @Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") int size,
      @Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom") String sortField,
      @Parameter(description = "Direction du tri (asc/desc)") @QueryParam("direction") @DefaultValue("asc") String sortDirection) {

    LOG.infof("Récupération de la liste des membres - page: %d, size: %d", page, size);

    Sort sort = "desc".equalsIgnoreCase(sortDirection)
        ? Sort.by(sortField).descending()
        : Sort.by(sortField).ascending();

    // Filtrage par rôle : ADMIN_ORGANISATION ne voit que ses organisations
    java.util.Set<String> roles = securityIdentity.getRoles();
    boolean onlyOrgAdmin = roles != null && roles.contains("ADMIN_ORGANISATION")
        && !roles.contains("ADMIN")
        && !roles.contains("SUPER_ADMIN");

    List<Membre> membres;
    long totalElements;

    if (onlyOrgAdmin) {
      java.security.Principal p = securityIdentity.getPrincipal();
      String email = p != null ? p.getName() : null;

      if (email == null || email.isEmpty()) {
        LOG.warn("ADMIN_ORGANISATION sans email identifié - retour liste vide");
        membres = List.of();
        totalElements = 0;
      } else {
        List<UUID> orgIds = organisationService.listerOrganisationsPourUtilisateur(email)
            .stream()
            .map(dev.lions.unionflow.server.entity.Organisation::getId)
            .collect(java.util.stream.Collectors.toList());

        LOG.infof("ADMIN_ORGANISATION %s : accès à %d organisations", email, orgIds.size());

        membres = membreService.listerMembresParOrganisations(orgIds, Page.of(page, size), sort);
        totalElements = membreService.compterMembresParOrganisations(orgIds);
      }
    } else {
      // ADMIN / SUPER_ADMIN : accès à tous les membres
      membres = membreService.listerMembres(Page.of(page, size), sort);
      totalElements = membreService.compterMembres();
    }

    List<MembreSummaryResponse> membresDTO = membreService.convertToSummaryResponseList(membres);

    return new PagedResponse<>(membresDTO, totalElements, page, size);
  }

  @GET
  @Path("/{id}")
  @RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "MEMBRE", "USER" })
  @Operation(summary = "Récupérer un membre par son ID")
  @APIResponse(responseCode = "200", description = "Membre trouvé")
  @APIResponse(responseCode = "404", description = "Membre non trouvé")
  public Response obtenirMembre(@Parameter(description = "UUID du membre") @PathParam("id") UUID id) {
    LOG.infof("Récupération du membre ID: %s", id);
    Membre membre = membreService.trouverParId(id)
        .filter(m -> m.getActif() == null || m.getActif())
        .orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + id));

    return Response.ok(membreService.convertToResponse(membre)).build();
  }

  @GET
  @Path("/me/suivis")
  @RolesAllowed({ "USER", "MEMBRE", "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "CONSULTANT", "SECRETAIRE", "GESTIONNAIRE_RH" })
  @Operation(summary = "Liste des ids des membres suivis (réseau)")
  @APIResponse(responseCode = "200", description = "Liste des UUID suivis")
  public Response obtenirMesSuivis() {
    String email = securityIdentity.getPrincipal().getName();
    List<UUID> ids = membreSuiviService.getFollowedIds(email);
    return Response.ok(ids).build();
  }

  @GET
  @Path("/me")
  @RolesAllowed({ "USER", "MEMBRE", "ADMIN", "ADMIN_ORGANISATION", "SUPER_ADMIN" })
  @Operation(summary = "Récupérer le membre connecté")
  @APIResponse(responseCode = "200", description = "Membre connecté trouvé ou auto-provisionné")
  public Response obtenirMembreConnecte() {
    String email = securityIdentity.getPrincipal().getName();
    LOG.infof("Récupération du membre connecté: %s", email);

    Membre membre = membreService.trouverParEmail(email)
        .orElseGet(() -> {
          LOG.infof("Fiche membre inexistante pour %s — auto-provisionnement depuis JWT", email);
          return autoProvisionnerMembre(email);
        });

    // Si la fiche existe mais est inactive (provisionnement précédent incomplet), on l'active
    if (membre.getActif() != null && !membre.getActif()) {
      LOG.infof("Fiche inactive pour %s — activation automatique", email);
      membre = membreService.activerMembre(membre.getId());
    }

    MembreResponse response = membreService.convertToResponse(membre);

    // Enrichir avec les rôles Keycloak si la table membres_roles est vide
    if (response.getRoles() == null || response.getRoles().isEmpty()) {
      java.util.Set<String> keycloakRoles = securityIdentity.getRoles();
      // Filtrer les rôles internes Keycloak (offline_access, uma_authorization, etc.)
      java.util.List<String> rolesFiltres = keycloakRoles.stream()
          .filter(r -> !r.equals("offline_access") && !r.equals("uma_authorization")
              && !r.startsWith("default-roles-"))
          .collect(java.util.stream.Collectors.toList());
      response.setRoles(rolesFiltres);
    }

    return Response.ok(response).build();
  }

  /**
   * Retourne la liste des organisations du membre connecté (pour le sélecteur multi-org).
   * Inclut le type, la catégorie et les modules actifs de chaque organisation.
   */
  @GET
  @Path("/mes-organisations")
  @RolesAllowed({ "USER", "MEMBRE", "ADMIN", "ADMIN_ORGANISATION", "SUPER_ADMIN", "MODERATEUR" })
  @Operation(summary = "Organisations du membre connecté",
      description = "Retourne la liste des organisations auxquelles le membre connecté appartient (multi-org)")
  @APIResponse(responseCode = "200", description = "Liste des organisations")
  public Response getMesOrganisations() {
    String email = securityIdentity.getPrincipal().getName();
    try {
      var membre = membreService.trouverParEmail(email);
      if (membre.isEmpty()) {
        return Response.ok(java.util.List.of()).build();
      }
      // Charger les liens membre-organisation avec les infos d'org
      var liens = organisationService.listerOrganisationsParMembre(membre.get().getId());
      return Response.ok(liens).build();
    } catch (Exception e) {
      LOG.errorf(e, "Erreur lors de la récupération des organisations du membre %s", email);
      return Response.serverError()
          .entity(java.util.Map.of("error", "Erreur serveur"))
          .build();
    }
  }

  /** Crée et active une fiche membre depuis les claims JWT lors du premier accès. */
  private Membre autoProvisionnerMembre(String email) {
    String prenom = "Utilisateur";
    String nom = "UnionFlow";
    UUID keycloakId = null;

    if (jwt != null) {
      String givenName = jwt.getClaim("given_name");
      String familyName = jwt.getClaim("family_name");
      String sub = jwt.getSubject();
      if (givenName != null && !givenName.isBlank()) prenom = givenName;
      if (familyName != null && !familyName.isBlank()) nom = familyName;
      if (sub != null) {
        try { keycloakId = UUID.fromString(sub); } catch (Exception ignored) {}
      }
    }

    CreateMembreRequest req = CreateMembreRequest.builder()
        .prenom(prenom)
        .nom(nom)
        .email(email)
        .dateNaissance(LocalDate.of(1900, 1, 1))
        .build();

    Membre nouveau = membreService.convertFromCreateRequest(req);
    if (keycloakId != null) nouveau.setKeycloakId(keycloakId);

    // creerMembre() force actif=false + EN_ATTENTE_VALIDATION.
    // On active immédiatement car l'utilisateur est déjà authentifié via Keycloak.
    try {
      Membre cree = membreService.creerMembre(nouveau);
      return membreService.activerMembre(cree.getId());
    } catch (IllegalArgumentException e) {
      // Fiche déjà présente mais inactive — on l'active directement
      LOG.infof("Fiche existante pour %s — activation directe", email);
      Membre existant = membreService.trouverParEmail(email)
          .orElseThrow(() -> new jakarta.ws.rs.NotFoundException("Membre introuvable : " + email));
      return membreService.activerMembre(existant.getId());
    }
  }

  @POST
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
  @Operation(summary = "Créer un nouveau membre")
  @APIResponse(responseCode = "201", description = "Membre créé avec succès")
  @APIResponse(responseCode = "400", description = "Données invalides")
  public Response creerMembre(@Valid CreateMembreRequest membreDTO) {
    LOG.infof("Création d'un nouveau membre: %s", membreDTO.email());
    // Conversion DTO vers entité
    Membre membre = membreService.convertFromCreateRequest(membreDTO);

    // Création du membre — statut EN_ATTENTE_VALIDATION
    Membre nouveauMembre = membreService.creerMembre(membre);

    // Provisionner le compte Keycloak (non bloquant — l'admin peut activer manuellement)
    String motDePasseTemporaire = null;
    try {
      motDePasseTemporaire = keycloakSyncService.provisionKeycloakUser(nouveauMembre.getId());
    } catch (Exception e) {
      LOG.warnf("Provisionnement Keycloak échoué pour %s (non bloquant): %s", nouveauMembre.getEmail(), e.getMessage());
    }

    // Lier le membre à l'organisation si un organisationId est fourni
    java.util.Set<String> roles = securityIdentity.getRoles();
    boolean onlyOrgAdmin = roles != null && roles.contains("ADMIN_ORGANISATION")
        && !roles.contains("ADMIN")
        && !roles.contains("SUPER_ADMIN");

    if (membreDTO.organisationId() != null) {
      if (onlyOrgAdmin) {
        // Vérifier que l'ADMIN_ORGANISATION a accès à cette organisation
        String email = securityIdentity.getPrincipal().getName();
        List<UUID> userOrgIds = organisationService.listerOrganisationsPourUtilisateur(email)
            .stream()
            .map(org -> org.getId())
            .collect(java.util.stream.Collectors.toList());

        if (!userOrgIds.contains(membreDTO.organisationId())) {
          return Response.status(Response.Status.FORBIDDEN)
              .entity(Map.of("error", "Vous n'avez pas accès à cette organisation"))
              .build();
        }
      }

      // Auto-activer si l'organisation a une souscription active (l'admin a déjà payé)
      boolean orgActif = membreService.orgHasActiveSubscription(membreDTO.organisationId());

      // Lier le membre à l'organisation (SUPER_ADMIN ou ADMIN_ORGANISATION)
      membreService.lierMembreOrganisationEtIncrementerQuota(
          nouveauMembre,
          membreDTO.organisationId(),
          orgActif ? "ACTIF" : "EN_ATTENTE_VALIDATION");

      if (orgActif) {
        nouveauMembre = membreService.activerMembre(nouveauMembre.getId());
        try {
          keycloakSyncService.activerMembreDansKeycloak(nouveauMembre.getId());
        } catch (Exception e) {
          LOG.warnf("Activation Keycloak échouée pour %s (non bloquant): %s",
              nouveauMembre.getEmail(), e.getMessage());
        }
      }
    } else if (onlyOrgAdmin) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("error", "organisationId obligatoire pour ADMIN_ORGANISATION"))
          .build();
    }

    // Conversion de retour vers DTO
    MembreResponse nouveauMembreDTO = membreService.convertToResponse(nouveauMembre);
    nouveauMembreDTO.setMotDePasseTemporaire(motDePasseTemporaire);

    return Response.status(Response.Status.CREATED).entity(nouveauMembreDTO).build();
  }

  @PUT
  @Path("/{id}")
  @RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION" })
  @Operation(summary = "Mettre à jour un membre existant")
  @APIResponse(responseCode = "200", description = "Membre mis à jour avec succès")
  @APIResponse(responseCode = "404", description = "Membre non trouvé")
  @APIResponse(responseCode = "400", description = "Données invalides")
  public Response mettreAJourMembre(
      @Parameter(description = "UUID du membre") @PathParam("id") UUID id,
      @Valid UpdateMembreRequest membreDTO) {
    LOG.infof("Mise à jour du membre ID: %s", id);

    // Recupérer le membre
    Membre membreAModifier = membreService.trouverParId(id)
        .orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + id));

    // Mettre à jour depuis la requête
    membreService.updateFromRequest(membreAModifier, membreDTO);

    // Mise à jour en base
    Membre membreMisAJour = membreService.mettreAJourMembre(id, membreAModifier);

    // Conversion de retour
    MembreResponse membreMisAJourDTO = membreService.convertToResponse(membreMisAJour);

    return Response.ok(membreMisAJourDTO).build();
  }

  @DELETE
  @Path("/{id}")
  @RolesAllowed({ "ADMIN", "SUPER_ADMIN" })
  @Operation(summary = "Désactiver un membre")
  @APIResponse(responseCode = "204", description = "Membre désactivé avec succès")
  @APIResponse(responseCode = "404", description = "Membre non trouvé")
  public Response desactiverMembre(
      @Parameter(description = "UUID du membre") @PathParam("id") UUID id) {
    LOG.infof("Désactivation du membre ID: %s", id);
    membreService.desactiverMembre(id);
    return Response.noContent().build();
  }

  @POST
  @Path("/{id}/suivre")
  @RolesAllowed({ "USER", "MEMBRE", "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "CONSULTANT", "SECRETAIRE", "GESTIONNAIRE_RH" })
  @Operation(summary = "Suivre un membre (réseau)")
  @APIResponse(responseCode = "200", description = "Suivi activé")
  @APIResponse(responseCode = "400", description = "Requête invalide")
  @APIResponse(responseCode = "404", description = "Membre cible non trouvé")
  public Response suivreMembre(@Parameter(description = "UUID du membre à suivre") @PathParam("id") UUID id) {
    String email = securityIdentity.getPrincipal().getName();
    try {
      boolean following = membreSuiviService.follow(email, id);
      return Response.ok(Map.of("following", following)).build();
    } catch (IllegalArgumentException e) {
      if (e.getMessage().contains("introuvable")) {
        return Response.status(Response.Status.NOT_FOUND).entity(Map.of("message", e.getMessage())).build();
      }
      return Response.status(Response.Status.BAD_REQUEST).entity(Map.of("message", e.getMessage())).build();
    }
  }

  @DELETE
  @Path("/{id}/suivre")
  @RolesAllowed({ "USER", "MEMBRE", "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "CONSULTANT", "SECRETAIRE", "GESTIONNAIRE_RH" })
  @Operation(summary = "Ne plus suivre un membre (réseau)")
  @APIResponse(responseCode = "200", description = "Suivi désactivé")
  @APIResponse(responseCode = "400", description = "Requête invalide")
  public Response nePlusSuivreMembre(@Parameter(description = "UUID du membre à ne plus suivre") @PathParam("id") UUID id) {
    String email = securityIdentity.getPrincipal().getName();
    try {
      boolean following = membreSuiviService.unfollow(email, id);
      return Response.ok(Map.of("following", following)).build();
    } catch (IllegalArgumentException e) {
      return Response.status(Response.Status.BAD_REQUEST).entity(Map.of("message", e.getMessage())).build();
    }
  }

  /**
   * Liste tous les membres actifs (statut compte = ACTIF).
   * Utilisé notamment pour la création de campagnes de cotisations.
   */
  @GET
  @Path("/actifs")
  @RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "TRESORIER" })
  @Operation(summary = "Membres actifs", description = "Liste tous les membres dont le compte est actif")
  @APIResponse(responseCode = "200", description = "Liste des membres actifs")
  public Response getMembresActifs() {
    try {
      LOG.info("GET /api/membres/actifs");
      List<Membre> membres = membreService.listerMembresActifs();
      List<MembreResponse> membresDTO = membreService.convertToResponseList(membres);
      return Response.ok(membresDTO).build();
    } catch (Exception e) {
      LOG.errorf(e, "Erreur récupération membres actifs");
      return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
          .entity(Map.of("error", e.getMessage())).build();
    }
  }

  /**
   * Liste TOUS les membres (y compris EN_ATTENTE_VALIDATION) — réservé SUPER_ADMIN.
   * Utile pour les imports de données historiques et la gestion admin.
   */
  @GET
  @Path("/admin/tous")
  @RolesAllowed({ "SUPER_ADMIN" })
  @Operation(summary = "Tous les membres (admin)", description = "Liste tous les membres quelque soit leur statut, réservé SUPER_ADMIN")
  @APIResponse(responseCode = "200", description = "Liste complète des membres")
  public Response getTousMembres(
      @Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0") int page,
      @Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("100") int size) {
    try {
      LOG.infof("GET /api/membres/admin/tous - page=%d size=%d", page, size);
      List<Membre> membres = membreRepository.findAll(
          io.quarkus.panache.common.Sort.by("nom").ascending())
          .page(io.quarkus.panache.common.Page.of(page, size))
          .list();
      List<MembreResponse> membresDTO = membreService.convertToResponseList(membres);
      long total = membreRepository.count();
      return Response.ok(Map.of(
          "data", membresDTO,
          "totalElements", total,
          "page", page,
          "size", size,
          "totalPages", (int) Math.ceil((double) total / size)
      )).build();
    } catch (Exception e) {
      LOG.errorf(e, "Erreur récupération tous membres");
      return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
          .entity(Map.of("error", e.getMessage())).build();
    }
  }

  /**
   * Liste les membres d'une organisation spécifique (statut ACTIF dans l'organisation).
   * Utilisé pour la création de campagnes ciblées.
   */
  @GET
  @Path("/organisation/{organisationId}")
  @RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "TRESORIER" })
  @Operation(summary = "Membres d'une organisation", description = "Liste les membres actifs d'une organisation")
  @APIResponse(responseCode = "200", description = "Liste des membres")
  public Response getMembresParOrganisation(
      @Parameter(description = "UUID de l'organisation") @PathParam("organisationId") UUID organisationId) {
    try {
      LOG.infof("GET /api/membres/organisation/%s", organisationId);
      List<dev.lions.unionflow.server.entity.MembreOrganisation> liens =
          membreOrgRepository.findMembresActifsParOrganisation(organisationId);
      List<MembreResponse> membresDTO = liens.stream()
          .filter(mo -> mo.getMembre() != null)
          .map(mo -> membreService.convertToResponse(mo.getMembre()))
          .collect(java.util.stream.Collectors.toList());
      return Response.ok(membresDTO).build();
    } catch (Exception e) {
      LOG.errorf(e, "Erreur récupération membres organisation %s", organisationId);
      return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
          .entity(Map.of("error", e.getMessage())).build();
    }
  }

  @GET
  @Path("/recherche")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "SECRETAIRE"})
  @Operation(summary = "Rechercher des membres par nom ou prénom")
  @APIResponse(responseCode = "200", description = "Résultats de la recherche")
  public Response rechercherMembres(
      @Parameter(description = "Terme de recherche") @QueryParam("q") String recherche,
      @Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0") int page,
      @Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") int size,
      @Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom") String sortField,
      @Parameter(description = "Direction du tri (asc/desc)") @QueryParam("direction") @DefaultValue("asc") String sortDirection) {

    LOG.infof("Recherche de membres avec le terme: %s", recherche);
    if (recherche == null || recherche.trim().isEmpty()) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("message", "Le terme de recherche est requis"))
          .build();
    }

    Sort sort = "desc".equalsIgnoreCase(sortDirection)
        ? Sort.by(sortField).descending()
        : Sort.by(sortField).ascending();

    List<Membre> membres = membreService.rechercherMembres(recherche.trim(), Page.of(page, size), sort);
    List<MembreSummaryResponse> membresDTO = membreService.convertToSummaryResponseList(membres);

    return Response.ok(membresDTO).build();
  }

  @GET
  @Path("/stats")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "SECRETAIRE"})
  @Operation(summary = "Obtenir les statistiques avancées des membres")
  @APIResponse(responseCode = "200", description = "Statistiques complètes des membres")
  public Response obtenirStatistiques() {
    LOG.info("Récupération des statistiques avancées des membres");
    Map<String, Object> statistiques = membreService.obtenirStatistiquesAvancees();
    return Response.ok(statistiques).build();
  }

  @GET
  @Path("/autocomplete/villes")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "SECRETAIRE"})
  @Operation(summary = "Obtenir la liste des villes pour autocomplétion")
  @APIResponse(responseCode = "200", description = "Liste des villes distinctes")
  public Response obtenirVilles(
      @Parameter(description = "Terme de recherche (optionnel)") @QueryParam("query") String query) {
    LOG.infof("Récupération des villes pour autocomplétion - query: %s", query);
    List<String> villes = membreService.obtenirVillesDistinctes(query);
    return Response.ok(villes).build();
  }

  @GET
  @Path("/autocomplete/professions")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "SECRETAIRE"})
  @Operation(summary = "Obtenir la liste des professions pour autocomplétion")
  @APIResponse(responseCode = "200", description = "Liste des professions distinctes")
  public Response obtenirProfessions(
      @Parameter(description = "Terme de recherche (optionnel)") @QueryParam("query") String query) {
    LOG.infof("Récupération des professions pour autocomplétion - query: %s", query);
    List<String> professions = membreService.obtenirProfessionsDistinctes(query);
    return Response.ok(professions).build();
  }

  @GET
  @Path("/recherche-avancee")
  @Operation(summary = "Recherche avancée de membres avec filtres multiples (DEPRECATED)")
  @APIResponse(responseCode = "200", description = "Résultats de la recherche avancée")
  @Deprecated
  public Response rechercheAvancee(
      @Parameter(description = "Terme de recherche") @QueryParam("q") String recherche,
      @Parameter(description = "Statut actif (true/false)") @QueryParam("actif") Boolean actif,
      @Parameter(description = "Date d'adhésion minimum (YYYY-MM-DD)") @QueryParam("dateAdhesionMin") String dateAdhesionMin,
      @Parameter(description = "Date d'adhésion maximum (YYYY-MM-DD)") @QueryParam("dateAdhesionMax") String dateAdhesionMax,
      @Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0") int page,
      @Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") int size,
      @Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom") String sortField,
      @Parameter(description = "Direction du tri (asc/desc)") @QueryParam("direction") @DefaultValue("asc") String sortDirection) {

    LOG.infof(
        "Recherche avancée de membres (DEPRECATED) - recherche: %s, actif: %s", recherche, actif);

    Sort sort = "desc".equalsIgnoreCase(sortDirection)
        ? Sort.by(sortField).descending()
        : Sort.by(sortField).ascending();

    // Conversion des dates si fournies
    java.time.LocalDate dateMin = dateAdhesionMin != null ? java.time.LocalDate.parse(dateAdhesionMin) : null;
    java.time.LocalDate dateMax = dateAdhesionMax != null ? java.time.LocalDate.parse(dateAdhesionMax) : null;

    List<Membre> membres = membreService.rechercheAvancee(
        recherche, actif, dateMin, dateMax, Page.of(page, size), sort);
    List<MembreSummaryResponse> membresDTO = membreService.convertToSummaryResponseList(membres);

    return Response.ok(membresDTO).build();
  }

  /**
   * Nouvelle recherche avancée avec critères complets et résultats enrichis
   * Réservée aux super
   * administrateurs pour des recherches sophistiquées
   */
  @POST
  @Path("/search/advanced")
  @RolesAllowed({ "SUPER_ADMIN", "ADMIN", "ADMIN_ORGANISATION" })
  @Operation(summary = "Recherche avancée de membres avec critères multiples", description = """
      Recherche sophistiquée de membres avec de nombreux critères de filtrage :
      - Recherche textuelle dans nom, prénom, email
      - Filtres par organisation, rôles, statut
      - Filtres par âge, région, profession
      - Filtres par dates d'adhésion
      - Résultats paginés avec statistiques

      Réservée aux super administrateurs et administrateurs.
      """)
  @APIResponses({
      @APIResponse(responseCode = "200", description = "Recherche effectuée avec succès", content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = MembreSearchResultDTO.class), examples = @ExampleObject(name = "Exemple de résultats", value = """
          {
            "membres": [],
            "totalElements": 247,
            "totalPages": 13,
            "currentPage": 0,
            "pageSize": 20,
            "hasNext": true,
            "hasPrevious": false,
            "executionTimeMs": 45,
            "statistics": {
              "membresActifs": 230,
              "membresInactifs": 17,
              "ageMoyen": 34.5,
              "nombreOrganisations": 12
            }
          }
          """))),
      @APIResponse(responseCode = "400", description = "Critères de recherche invalides", content = @Content(mediaType = MediaType.APPLICATION_JSON, examples = @ExampleObject(value = """
          {
            "message": "Critères de recherche invalides",
            "details": "La date minimum ne peut pas être postérieure à la date maximum"
          }
          """))),
      @APIResponse(responseCode = "403", description = "Accès non autorisé - Rôle SUPER_ADMIN, ADMIN ou ADMIN_ORGANISATION requis"),
      @APIResponse(responseCode = "500", description = "Erreur interne du serveur")
  })
  @SecurityRequirement(name = "keycloak")
  public Response searchMembresAdvanced(
      @RequestBody(description = "Critères de recherche avancée", required = false, content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = MembreSearchCriteria.class), examples = @ExampleObject(name = "Exemple de critères", value = """
          {
            "query": "marie",
            "statut": "ACTIF",
            "ageMin": 25,
            "ageMax": 45,
            "region": "Dakar",
            "roles": ["PRESIDENT", "SECRETAIRE"],
            "dateAdhesionMin": "2020-01-01",
            "includeInactifs": false
          }
          """))) MembreSearchCriteria criteria,
      @Parameter(description = "Numéro de page (0-based)", example = "0") @QueryParam("page") @DefaultValue("0") int page,
      @Parameter(description = "Taille de la page", example = "20") @QueryParam("size") @DefaultValue("20") int size,
      @Parameter(description = "Champ de tri", example = "nom") @QueryParam("sort") @DefaultValue("nom") String sortField,
      @Parameter(description = "Direction du tri (asc/desc)", example = "asc") @QueryParam("direction") @DefaultValue("asc") String sortDirection) {

    long startTime = System.currentTimeMillis();

    // Validation des critères
    if (criteria == null) {
      LOG.warn("Recherche avancée de membres - critères null rejetés");
      throw new IllegalArgumentException("Les critères de recherche sont requis");
    }

    LOG.infof(
        "Recherche avancée de membres - critères: %s, page: %d, size: %d",
        criteria.getDescription(), page, size);

    // Nettoyage et validation des critères
    criteria.sanitize();

    if (!criteria.hasAnyCriteria()) {
      throw new IllegalArgumentException("Au moins un critère de recherche doit être spécifié");
    }

    if (!criteria.isValid()) {
      throw new IllegalArgumentException(
          "Critères de recherche invalides: Vérifiez la cohérence des dates et des âges");
    }

    // Construction du tri
    Sort sort = "desc".equalsIgnoreCase(sortDirection)
        ? Sort.by(sortField).descending()
        : Sort.by(sortField).ascending();

    // Exécution de la recherche
    MembreSearchResultDTO result = membreService.searchMembresAdvanced(criteria, Page.of(page, size), sort);

    // Calcul du temps d'exécution
    long executionTime = System.currentTimeMillis() - startTime;
    result.setExecutionTimeMs(executionTime);

    LOG.infof(
        "Recherche avancée terminée - %d résultats trouvés en %d ms",
        result.getTotalElements(), executionTime);

    return Response.ok(result).build();
  }

  @POST
  @Path("/export/selection")
  @RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION" })
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
  @Operation(summary = "Exporter une sélection de membres en Excel")
  @APIResponse(responseCode = "200", description = "Fichier Excel généré")
  public Response exporterSelectionMembres(
      @Parameter(description = "Liste des IDs des membres à exporter") List<UUID> membreIds,
      @Parameter(description = "Format d'export") @QueryParam("format") @DefaultValue("EXCEL") String format) {
    LOG.infof("Export de %d membres sélectionnés", membreIds.size());
    LOG.infof("Export de %d membres sélectionnés", membreIds.size());
    byte[] excelData = membreService.exporterMembresSelectionnes(membreIds, format);
    return Response.ok(excelData)
        .header("Content-Disposition", "attachment; filename=\"membres_selection_" +
            java.time.LocalDate.now() + "." + (format != null ? format.toLowerCase() : "xlsx") + "\"")
        .build();
  }

  @POST
  @Path("/import")
  @Consumes(MediaType.MULTIPART_FORM_DATA)
  @Produces(MediaType.APPLICATION_JSON)
  @RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION" })
  @Operation(summary = "Importer des membres depuis un fichier Excel ou CSV",
      description = "Format strict (colonnes obligatoires: nom, prenom, email, telephone). " +
          "Si organisationId est fourni, les membres sont rattachés à l'organisation et le quota souscription (tranche) est respecté.")
  @APIResponse(responseCode = "200", description = "Import terminé")
  public Response importerMembres(
      @RestForm("file") org.jboss.resteasy.reactive.multipart.FileUpload file,
      @RestForm("fileName") String fileName,
      @RestForm("organisationId") String organisationIdStr,
      @RestForm("typeMembreDefaut") String typeMembreDefaut,
      @RestForm("mettreAJourExistants") boolean mettreAJourExistants,
      @RestForm("ignorerErreurs") boolean ignorerErreurs) {

    if (file == null || file.size() == 0) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("error", "Aucun fichier fourni"))
          .build();
    }

    if (fileName == null || fileName.isEmpty()) {
      fileName = file.fileName();
    }

    if (typeMembreDefaut == null || typeMembreDefaut.isEmpty()) {
      typeMembreDefaut = "ACTIF";
    }

    UUID organisationId = (organisationIdStr != null && !organisationIdStr.isEmpty())
        ? UUID.fromString(organisationIdStr)
        : null;

    // Validation périmètre ADMIN_ORGANISATION : doit avoir accès à l'organisation
    java.util.Set<String> roles = securityIdentity.getRoles();
    boolean onlyOrgAdmin = roles != null && roles.contains("ADMIN_ORGANISATION")
        && !roles.contains("ADMIN")
        && !roles.contains("SUPER_ADMIN");

    if (onlyOrgAdmin) {
      if (organisationId == null) {
        return Response.status(Response.Status.BAD_REQUEST)
            .entity(Map.of("error", "organisationId obligatoire pour ADMIN_ORGANISATION"))
            .build();
      }

      java.security.Principal p2 = securityIdentity.getPrincipal();
      String email = p2 != null ? p2.getName() : null;

      if (email == null || email.isEmpty()) {
        return Response.status(Response.Status.UNAUTHORIZED)
            .entity(Map.of("error", "Utilisateur non identifié"))
            .build();
      }

      List<UUID> userOrgIds = organisationService.listerOrganisationsPourUtilisateur(email)
          .stream()
          .map(dev.lions.unionflow.server.entity.Organisation::getId)
          .collect(java.util.stream.Collectors.toList());

      if (!userOrgIds.contains(organisationId)) {
        return Response.status(Response.Status.FORBIDDEN)
            .entity(Map.of("error", "Vous n'avez pas accès à cette organisation"))
            .build();
      }

      LOG.infof("ADMIN_ORGANISATION %s : import autorisé pour organisation %s", email, organisationId);
    }

    InputStream fileInputStream;
    try {
      fileInputStream = java.nio.file.Files.newInputStream(file.uploadedFile());
    } catch (java.io.IOException e) {
      throw new RuntimeException("Erreur lors de la lecture du fichier: " + e.getMessage(), e);
    }
    dev.lions.unionflow.server.service.MembreImportExportService.ResultatImport resultat = membreService
        .importerMembres(
            fileInputStream, fileName, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs);

    Map<String, Object> response = new HashMap<>();
    response.put("totalLignes", resultat.totalLignes);
    response.put("lignesTraitees", resultat.lignesTraitees);
    response.put("lignesErreur", resultat.lignesErreur);
    response.put("erreurs", resultat.erreurs);
    response.put("membresImportes", resultat.membresImportes);

    return Response.ok(response).build();
  }

  @GET
  @Path("/export")
  @RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION" })
  @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
  @Operation(summary = "Exporter des membres en Excel, CSV ou PDF")
  @APIResponse(responseCode = "200", description = "Fichier exporté")
  public Response exporterMembres(
      @Parameter(description = "Format d'export") @QueryParam("format") @DefaultValue("EXCEL") String format,
      @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("associationId") UUID associationId,
      @Parameter(description = "Statut des membres") @QueryParam("statut") String statut,
      @Parameter(description = "Type de membre") @QueryParam("type") String type,
      @Parameter(description = "Date adhésion début") @QueryParam("dateAdhesionDebut") String dateAdhesionDebut,
      @Parameter(description = "Date adhésion fin") @QueryParam("dateAdhesionFin") String dateAdhesionFin,
      @Parameter(description = "Colonnes à exporter") @QueryParam("colonnes") List<String> colonnesExportList,
      @Parameter(description = "Inclure les en-têtes") @QueryParam("inclureHeaders") @DefaultValue("true") boolean inclureHeaders,
      @Parameter(description = "Formater les dates") @QueryParam("formaterDates") @DefaultValue("true") boolean formaterDates,
      @Parameter(description = "Inclure un onglet statistiques (Excel uniquement)") @QueryParam("inclureStatistiques") @DefaultValue("false") boolean inclureStatistiques,
      @Parameter(description = "Mot de passe pour chiffrer le fichier (optionnel)") @QueryParam("motDePasse") String motDePasse) {

    List<MembreResponse> membres = membreService.listerMembresPourExport(
        associationId, statut, type, dateAdhesionDebut, dateAdhesionFin);

    byte[] exportData;
    String contentType;
    String extension;

    List<String> colonnesExport = colonnesExportList != null ? colonnesExportList : new ArrayList<>();

    if ("CSV".equalsIgnoreCase(format)) {
      exportData = membreService.exporterVersCSV(membres, colonnesExport, inclureHeaders, formaterDates);
      contentType = "text/csv";
      extension = "csv";
    } else if ("PDF".equalsIgnoreCase(format)) {
      exportData = membreService.exporterVersPDF(membres, colonnesExport, inclureHeaders, formaterDates, inclureStatistiques);
      contentType = "application/pdf";
      extension = "pdf";
    } else {
      // Pour Excel, inclure les statistiques uniquement si demandé et si format Excel
      boolean stats = inclureStatistiques && "EXCEL".equalsIgnoreCase(format);
      exportData = membreService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates, stats,
          motDePasse);
      contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
      extension = "xlsx";
    }

    return Response.ok(exportData)
        .type(contentType)
        .header("Content-Disposition", "attachment; filename=\"membres_export_" +
            java.time.LocalDate.now() + "." + extension + "\"")
        .build();
  }

  @GET
  @Path("/import/modele")
  @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
  @Operation(summary = "Télécharger le modèle Excel pour l'import")
  @APIResponse(responseCode = "200", description = "Modèle Excel généré")
  public Response telechargerModeleImport() {
    byte[] modele = membreService.genererModeleImport();
    return Response.ok(modele)
        .header("Content-Disposition", "attachment; filename=\"modele_import_membres.xlsx\"")
        .build();
  }

  @PUT
  @Path("/{id}/affecter-organisation")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN"})
  @Operation(
      summary = "Affecter un membre à une organisation",
      description = "Crée le lien MembreOrganisation (statut EN_ATTENTE_VALIDATION) si le membre n'est pas encore rattaché à une organisation. Idempotent.")
  @APIResponse(responseCode = "200", description = "Membre affecté à l'organisation")
  @APIResponse(responseCode = "404", description = "Membre ou organisation non trouvé")
  @APIResponse(responseCode = "403", description = "Accès réservé aux ADMIN / SUPER_ADMIN")
  public Response affecterOrganisation(
      @Parameter(description = "UUID du membre") @PathParam("id") UUID id,
      @Parameter(description = "UUID de l'organisation") @QueryParam("organisationId") UUID organisationId) {
    LOG.infof("Affectation du membre %s à l'organisation %s", id, organisationId);

    if (organisationId == null) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("error", "organisationId est obligatoire"))
          .build();
    }

    Membre membre = membreService.affecterOrganisation(id, organisationId);
    return Response.ok(membreService.convertToResponse(membre)).build();
  }

  @PUT
  @Path("/{id}/promouvoir-admin-organisation")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN"})
  @Operation(
      summary = "Promouvoir un membre en administrateur d'organisation",
      description = "Passe le membre en statut ACTIF et lui assigne le rôle ADMIN_ORGANISATION dans Keycloak. "
          + "Réservé aux super administrateurs de la plateforme. "
          + "Le compte est immédiatement opérationnel sans validation intermédiaire.")
  @APIResponse(responseCode = "200", description = "Membre promu administrateur d'organisation")
  @APIResponse(responseCode = "404", description = "Membre non trouvé")
  @APIResponse(responseCode = "403", description = "Accès réservé aux ADMIN / SUPER_ADMIN")
  public Response promouvoirAdminOrganisation(
      @Parameter(description = "UUID du membre à promouvoir") @PathParam("id") UUID id) {
    LOG.infof("Promotion admin d'organisation pour le membre ID: %s", id);

    Membre membrePromu = membreService.promouvoirAdminOrganisation(id);

    // Assigner ADMIN_ORGANISATION dans Keycloak (non bloquant)
    try {
      keycloakSyncService.promouvoirAdminOrganisationDansKeycloak(id);
    } catch (Exception e) {
      LOG.warnf("Promotion Keycloak échouée pour %s (non bloquant): %s", membrePromu.getEmail(), e.getMessage());
    }

    return Response.ok(membreService.convertToResponse(membrePromu)).build();
  }

  @PUT
  @Path("/{id}/activer")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN"})
  @Operation(summary = "Activer un membre", description = "Passe le membre en statut ACTIF et lui assigne le rôle MEMBRE_ACTIF dans Keycloak.")
  @APIResponse(responseCode = "200", description = "Membre activé avec succès")
  @APIResponse(responseCode = "404", description = "Membre non trouvé")
  public Response activerMembre(@Parameter(description = "UUID du membre") @PathParam("id") UUID id) {
    LOG.infof("Activation du membre ID: %s", id);

    Membre membreActive = membreService.activerMembre(id);

    // Assigner le rôle MEMBRE_ACTIF dans Keycloak (non bloquant)
    try {
      keycloakSyncService.activerMembreDansKeycloak(id);
    } catch (Exception e) {
      LOG.warnf("Activation Keycloak échouée pour %s (non bloquant): %s", membreActive.getEmail(), e.getMessage());
    }

    return Response.ok(membreService.convertToResponse(membreActive)).build();
  }

  @PUT
  @Path("/{id}/reinitialiser-mot-de-passe")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
  @Operation(summary = "Réinitialiser le mot de passe d'un membre",
      description = "Génère un nouveau mot de passe temporaire et le définit dans Keycloak. Le nouveau mot de passe est retourné une seule fois dans la réponse.")
  @APIResponse(responseCode = "200", description = "Mot de passe réinitialisé, retourné dans motDePasseTemporaire")
  @APIResponse(responseCode = "404", description = "Membre non trouvé")
  @APIResponse(responseCode = "400", description = "Le membre n'a pas de compte Keycloak")
  public Response reinitialiserMotDePasse(
      @Parameter(description = "UUID du membre") @PathParam("id") UUID id) {
    LOG.infof("Réinitialisation mot de passe pour membre ID: %s", id);

    Membre membre = membreService.trouverParId(id)
        .orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + id));

    String newPassword;
    try {
      newPassword = keycloakSyncService.reinitialiserMotDePasse(id);
    } catch (IllegalStateException e) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("error", e.getMessage())).build();
    }

    dev.lions.unionflow.server.api.dto.membre.response.MembreResponse response =
        membreService.convertToResponse(membre);
    response.setMotDePasseTemporaire(newPassword);

    LOG.infof("Mot de passe réinitialisé pour %s", membre.getEmail());
    return Response.ok(response).build();
  }

  @GET
  @Path("/export/count")
  @Produces(MediaType.APPLICATION_JSON)
  @Operation(summary = "Compter les membres selon les filtres pour l'export")
  @APIResponse(responseCode = "200", description = "Nombre de membres correspondant aux critères")
  public Response compterMembresPourExport(
      @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("associationId") UUID associationId,
      @Parameter(description = "Statut des membres") @QueryParam("statut") String statut,
      @Parameter(description = "Type de membre") @QueryParam("type") String type,
      @Parameter(description = "Date adhésion début") @QueryParam("dateAdhesionDebut") String dateAdhesionDebut,
      @Parameter(description = "Date adhésion fin") @QueryParam("dateAdhesionFin") String dateAdhesionFin) {

    List<MembreResponse> membres = membreService.listerMembresPourExport(
        associationId, statut, type, dateAdhesionDebut, dateAdhesionFin);

    return Response.ok(Map.of("count", membres.size())).build();
  }

  // =========================================================================
  //  Endpoints cycle de vie des adhésions (MemberLifecycleService)
  // =========================================================================

  /**
   * Invite un membre existant à rejoindre une organisation.
   * Crée un lien MembreOrganisation au statut INVITE avec token + expiration 7j.
   */
  @PUT
  @Path("/{membreId}/inviter-organisation")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
  @Operation(summary = "Inviter un membre dans une organisation",
      description = "Crée une invitation (statut INVITE) pour un membre existant. Token valable 7 jours.")
  @APIResponse(responseCode = "200", description = "Invitation créée")
  @APIResponse(responseCode = "404", description = "Membre ou organisation introuvable")
  @APIResponse(responseCode = "409", description = "Membre déjà lié à cette organisation")
  public Response inviterMembre(
      @Parameter(description = "UUID du membre à inviter") @PathParam("membreId") UUID membreId,
      @Parameter(description = "UUID de l'organisation") @QueryParam("organisationId") UUID organisationId,
      @Parameter(description = "Rôle proposé (optionnel)") @QueryParam("roleOrg") String roleOrg) {

    if (organisationId == null) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("error", "organisationId est obligatoire")).build();
    }

    Membre membre = membreService.trouverParId(membreId)
        .orElseThrow(() -> new NotFoundException("Membre introuvable : " + membreId));
    Organisation organisation = organisationService.trouverParId(organisationId)
        .orElseThrow(() -> new NotFoundException("Organisation introuvable : " + organisationId));

    UUID adminId = resolveCurrentAdminId();
    try {
      var lien = memberLifecycleService.inviterMembre(membre, organisation, adminId, roleOrg);
      return Response.ok(Map.of(
          "membreOrgId", lien.getId(),
          "statut", lien.getStatutMembre(),
          "tokenInvitation", lien.getTokenInvitation(),
          "expiresAt", lien.getDateExpirationInvitation()
      )).build();
    } catch (IllegalStateException e) {
      return Response.status(Response.Status.CONFLICT)
          .entity(Map.of("error", e.getMessage())).build();
    }
  }

  /**
   * Accepte une invitation via son token (INVITE → EN_ATTENTE_VALIDATION).
   * Endpoint public — le membre clique sur le lien reçu par email.
   */
  @POST
  @Path("/accepter-invitation/{token}")
  @PermitAll
  @Operation(summary = "Accepter une invitation",
      description = "Valide le token d'invitation et passe l'adhésion en EN_ATTENTE_VALIDATION.")
  @APIResponse(responseCode = "200", description = "Invitation acceptée")
  @APIResponse(responseCode = "400", description = "Token invalide ou expiré")
  public Response accepterInvitation(
      @Parameter(description = "Token d'invitation") @PathParam("token") String token) {
    try {
      var lien = memberLifecycleService.accepterInvitation(token);
      return Response.ok(Map.of(
          "membreOrgId", lien.getId(),
          "statut", lien.getStatutMembre(),
          "organisation", lien.getOrganisation().getNom()
      )).build();
    } catch (IllegalArgumentException e) {
      return Response.status(Response.Status.NOT_FOUND)
          .entity(Map.of("error", e.getMessage())).build();
    } catch (IllegalStateException e) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("error", e.getMessage())).build();
    }
  }

  /**
   * Active une adhésion (EN_ATTENTE_VALIDATION / INVITE / SUSPENDU → ACTIF).
   */
  @PUT
  @Path("/{membreOrgId}/activer-adhesion")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
  @Operation(summary = "Activer une adhésion",
      description = "Transitions autorisées : EN_ATTENTE_VALIDATION, INVITE, SUSPENDU → ACTIF.")
  @APIResponse(responseCode = "200", description = "Adhésion activée")
  @APIResponse(responseCode = "404", description = "Lien membre-organisation introuvable")
  @APIResponse(responseCode = "409", description = "Transition non autorisée depuis le statut actuel")
  public Response activerAdhesion(
      @Parameter(description = "UUID du lien membre-organisation") @PathParam("membreOrgId") UUID membreOrgId,
      Map<String, String> body) {

    String motif = body != null ? body.get("motif") : null;
    UUID adminId = resolveCurrentAdminId();
    try {
      var lien = memberLifecycleService.activerMembre(membreOrgId, adminId, motif);
      Map<String, Object> result = new HashMap<>();
      result.put("membreOrgId", lien.getId());
      result.put("statut", lien.getStatutMembre());
      result.put("dateChangementStatut", lien.getDateChangementStatut());
      return Response.ok(result).build();
    } catch (IllegalArgumentException e) {
      return Response.status(Response.Status.NOT_FOUND)
          .entity(Map.of("error", e.getMessage())).build();
    } catch (IllegalStateException e) {
      return Response.status(Response.Status.CONFLICT)
          .entity(Map.of("error", e.getMessage())).build();
    }
  }

  /**
   * Suspend une adhésion (ACTIF → SUSPENDU).
   */
  @PUT
  @Path("/{membreOrgId}/suspendre-adhesion")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
  @Operation(summary = "Suspendre une adhésion", description = "Transition autorisée : ACTIF → SUSPENDU.")
  @APIResponse(responseCode = "200", description = "Adhésion suspendue")
  @APIResponse(responseCode = "404", description = "Lien membre-organisation introuvable")
  @APIResponse(responseCode = "409", description = "Transition non autorisée")
  public Response suspendrAdhesion(
      @Parameter(description = "UUID du lien membre-organisation") @PathParam("membreOrgId") UUID membreOrgId,
      Map<String, String> body) {

    String motif = body != null ? body.get("motif") : null;
    UUID adminId = resolveCurrentAdminId();
    try {
      var lien = memberLifecycleService.suspendreMembre(membreOrgId, adminId, motif);
      Map<String, Object> result = new HashMap<>();
      result.put("membreOrgId", lien.getId());
      result.put("statut", lien.getStatutMembre());
      result.put("dateChangementStatut", lien.getDateChangementStatut());
      return Response.ok(result).build();
    } catch (IllegalArgumentException e) {
      return Response.status(Response.Status.NOT_FOUND)
          .entity(Map.of("error", e.getMessage())).build();
    } catch (IllegalStateException e) {
      return Response.status(Response.Status.CONFLICT)
          .entity(Map.of("error", e.getMessage())).build();
    }
  }

  /**
   * Radie un membre d'une organisation (→ RADIE).
   */
  @PUT
  @Path("/{membreOrgId}/radier-adhesion")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
  @Operation(summary = "Radier un membre d'une organisation")
  @APIResponse(responseCode = "200", description = "Adhésion radiée")
  @APIResponse(responseCode = "404", description = "Lien membre-organisation introuvable")
  public Response radierAdhesion(
      @Parameter(description = "UUID du lien membre-organisation") @PathParam("membreOrgId") UUID membreOrgId,
      Map<String, String> body) {

    MembreOrganisation lienCheck = membreOrgRepository.findByIdOptional(membreOrgId)
        .orElseThrow(() -> new NotFoundException("Lien membre-organisation introuvable: " + membreOrgId));
    verifierOwnershipEtProtectionAdmin(lienCheck);

    String motif = body != null ? body.get("motif") : null;
    UUID adminId = resolveCurrentAdminId();
    try {
      var lien = memberLifecycleService.radierMembre(membreOrgId, adminId, motif);
      Map<String, Object> result = new HashMap<>();
      result.put("membreOrgId", lien.getId());
      result.put("statut", lien.getStatutMembre());
      result.put("dateChangementStatut", lien.getDateChangementStatut());
      return Response.ok(result).build();
    } catch (IllegalArgumentException e) {
      return Response.status(Response.Status.NOT_FOUND)
          .entity(Map.of("error", e.getMessage())).build();
    }
  }

  /**
   * Archive une adhésion (→ ARCHIVE) sans supprimer l'historique.
   */
  @PUT
  @Path("/{membreOrgId}/archiver-adhesion")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
  @Operation(summary = "Archiver une adhésion",
      description = "Conserve l'historique sans supprimer le lien membre-organisation.")
  @APIResponse(responseCode = "200", description = "Adhésion archivée")
  @APIResponse(responseCode = "404", description = "Lien membre-organisation introuvable")
  public Response archiverAdhesion(
      @Parameter(description = "UUID du lien membre-organisation") @PathParam("membreOrgId") UUID membreOrgId,
      Map<String, String> body) {

    MembreOrganisation lienCheck = membreOrgRepository.findByIdOptional(membreOrgId)
        .orElseThrow(() -> new NotFoundException("Lien membre-organisation introuvable: " + membreOrgId));
    verifierOwnershipEtProtectionAdmin(lienCheck);

    String motif = body != null ? body.get("motif") : null;
    try {
      var lien = memberLifecycleService.archiverMembre(membreOrgId, motif);
      Map<String, Object> result = new HashMap<>();
      result.put("membreOrgId", lien.getId());
      result.put("statut", lien.getStatutMembre());
      result.put("dateChangementStatut", lien.getDateChangementStatut());
      return Response.ok(result).build();
    } catch (IllegalArgumentException e) {
      return Response.status(Response.Status.NOT_FOUND)
          .entity(Map.of("error", e.getMessage())).build();
    }
  }

  // =========================================================================
  //  Endpoints lifecycle par membreId + organisationId (sans membreOrgId)
  // =========================================================================

  /**
   * Retourne le statut d'adhésion d'un membre dans une organisation.
   * Utilisé par le profil membre pour afficher les boutons d'action contextuels.
   */
  @GET
  @Path("/{membreId}/adhesion")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER"})
  @Operation(summary = "Statut d'adhésion d'un membre dans une organisation")
  @APIResponse(responseCode = "200", description = "Statut d'adhésion")
  @APIResponse(responseCode = "404", description = "Aucun lien membre-organisation trouvé")
  public Response getAdhesionStatut(
      @Parameter(description = "UUID du membre") @PathParam("membreId") UUID membreId,
      @Parameter(description = "UUID de l'organisation") @QueryParam("organisationId") UUID organisationId) {

    if (organisationId == null) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("error", "organisationId requis")).build();
    }
    return membreOrgRepository.findByMembreIdAndOrganisationId(membreId, organisationId)
        .map(lien -> Response.ok(Map.of(
            "membreOrgId", lien.getId(),
            "statut", lien.getStatutMembre(),
            "dateInvitation", lien.getDateInvitation() != null ? lien.getDateInvitation().toString() : "",
            "dateExpiration", lien.getDateExpirationInvitation() != null ? lien.getDateExpirationInvitation().toString() : "",
            "roleOrg", lien.getRoleOrg() != null ? lien.getRoleOrg() : "",
            "motifStatut", lien.getMotifStatut() != null ? lien.getMotifStatut() : ""
        )).build())
        .orElse(Response.status(Response.Status.NOT_FOUND)
            .entity(Map.of("error", "Aucune adhésion trouvée")).build());
  }

  /**
   * Active l'adhésion d'un membre (EN_ATTENTE/INVITE/SUSPENDU → ACTIF)
   * en passant par membreId + organisationId plutôt que membreOrgId.
   */
  @PUT
  @Path("/{membreId}/adhesion/activer")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
  @Operation(summary = "Activer l'adhésion par membreId + organisationId")
  @APIResponse(responseCode = "200", description = "Adhésion activée")
  @APIResponse(responseCode = "404", description = "Lien membre-organisation introuvable")
  public Response activerAdhesionParMembre(
      @Parameter(description = "UUID du membre") @PathParam("membreId") UUID membreId,
      @Parameter(description = "UUID de l'organisation") @QueryParam("organisationId") UUID organisationId,
      Map<String, String> body) {

    if (organisationId == null) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("error", "organisationId requis")).build();
    }
    MembreOrganisation lien = membreOrgRepository
        .findByMembreIdAndOrganisationId(membreId, organisationId)
        .orElse(null);
    if (lien == null) {
      return Response.status(Response.Status.NOT_FOUND)
          .entity(Map.of("error", "Aucune adhésion trouvée")).build();
    }
    verifierOwnershipEtProtectionAdmin(lien);
    String motif = body != null ? body.get("motif") : null;
    UUID adminId = resolveCurrentAdminId();
    try {
      var updated = memberLifecycleService.activerMembre(lien.getId(), adminId, motif);
      return Response.ok(Map.of("statut", updated.getStatutMembre())).build();
    } catch (IllegalStateException e) {
      return Response.status(Response.Status.CONFLICT).entity(Map.of("error", e.getMessage())).build();
    }
  }

  /**
   * Suspend l'adhésion d'un membre (ACTIF → SUSPENDU) par membreId + organisationId.
   */
  @PUT
  @Path("/{membreId}/adhesion/suspendre")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
  @Operation(summary = "Suspendre l'adhésion par membreId + organisationId")
  @APIResponse(responseCode = "200", description = "Adhésion suspendue")
  @APIResponse(responseCode = "404", description = "Lien membre-organisation introuvable")
  public Response suspendrAdhesionParMembre(
      @Parameter(description = "UUID du membre") @PathParam("membreId") UUID membreId,
      @Parameter(description = "UUID de l'organisation") @QueryParam("organisationId") UUID organisationId,
      Map<String, String> body) {

    if (organisationId == null) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("error", "organisationId requis")).build();
    }
    MembreOrganisation lien = membreOrgRepository
        .findByMembreIdAndOrganisationId(membreId, organisationId)
        .orElse(null);
    if (lien == null) {
      return Response.status(Response.Status.NOT_FOUND)
          .entity(Map.of("error", "Aucune adhésion trouvée")).build();
    }
    verifierOwnershipEtProtectionAdmin(lien);
    String motif = body != null ? body.get("motif") : null;
    UUID adminId = resolveCurrentAdminId();
    try {
      var updated = memberLifecycleService.suspendreMembre(lien.getId(), adminId, motif);
      return Response.ok(Map.of("statut", updated.getStatutMembre())).build();
    } catch (IllegalStateException e) {
      return Response.status(Response.Status.CONFLICT).entity(Map.of("error", e.getMessage())).build();
    }
  }

  /**
   * Radie un membre d'une organisation par membreId + organisationId.
   */
  @PUT
  @Path("/{membreId}/adhesion/radier")
  @RolesAllowed({"ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION"})
  @Operation(summary = "Radier par membreId + organisationId")
  @APIResponse(responseCode = "200", description = "Adhésion radiée")
  public Response radierAdhesionParMembre(
      @Parameter(description = "UUID du membre") @PathParam("membreId") UUID membreId,
      @Parameter(description = "UUID de l'organisation") @QueryParam("organisationId") UUID organisationId,
      Map<String, String> body) {

    if (organisationId == null) {
      return Response.status(Response.Status.BAD_REQUEST)
          .entity(Map.of("error", "organisationId requis")).build();
    }
    MembreOrganisation lien = membreOrgRepository
        .findByMembreIdAndOrganisationId(membreId, organisationId)
        .orElse(null);
    if (lien == null) {
      return Response.status(Response.Status.NOT_FOUND)
          .entity(Map.of("error", "Aucune adhésion trouvée")).build();
    }
    verifierOwnershipEtProtectionAdmin(lien);
    String motif = body != null ? body.get("motif") : null;
    var updated = memberLifecycleService.radierMembre(lien.getId(), resolveCurrentAdminId(), motif);
    return Response.ok(Map.of("statut", updated.getStatutMembre())).build();
  }

  /**
   * Trouve un membre par son numéro de membre (ex: MBR-0001).
   * Utilisé notamment pour la recherche de parrain lors de l'inscription.
   */
  @GET
  @Path("/numero/{numeroMembre}")
  @RolesAllowed({ "ADMIN", "SUPER_ADMIN", "ADMIN_ORGANISATION", "MODERATEUR", "MEMBRE", "USER" })
  @Operation(summary = "Trouver un membre par son numéro")
  @APIResponse(responseCode = "200", description = "Membre trouvé")
  @APIResponse(responseCode = "404", description = "Membre non trouvé")
  public Response obtenirParNumero(@PathParam("numeroMembre") String numeroMembre) {
    LOG.infof("GET /api/membres/numero/%s", numeroMembre);
    Membre membre = membreService.trouverParNumeroMembre(numeroMembre)
        .orElseThrow(() -> new NotFoundException("Membre non trouvé avec le numéro: " + numeroMembre));
    return Response.ok(membreService.convertToResponse(membre)).build();
  }

  /**
   * Vérifie qu'un ADMIN_ORGANISATION :
   * 1. agit bien sur une organisation dont il est responsable (ownership),
   * 2. ne tente pas d'agir sur un autre admin (ORGADMIN) ou super-admin (SUPERADMIN).
   *
   * Sans effet si l'appelant est SUPER_ADMIN ou ADMIN (ils ont accès total).
   *
   * @throws ForbiddenException si l'une des règles est violée
   */
  private void verifierOwnershipEtProtectionAdmin(MembreOrganisation lien) {
    java.util.Set<String> roles = securityIdentity.getRoles();
    boolean isOrgAdminOnly = roles != null
        && roles.contains("ADMIN_ORGANISATION")
        && !roles.contains("ADMIN")
        && !roles.contains("SUPER_ADMIN");

    if (!isOrgAdminOnly) {
      return; // SUPER_ADMIN / ADMIN : accès total, pas de vérification supplémentaire
    }

    // ── 1. Ownership : l'org cible doit appartenir à l'admin connecté ──────
    String email = securityIdentity.getPrincipal().getName();
    java.util.List<UUID> orgIds = organisationService
        .listerOrganisationsPourUtilisateur(email)
        .stream()
        .map(Organisation::getId)
        .collect(java.util.stream.Collectors.toList());

    if (lien.getOrganisation() == null || !orgIds.contains(lien.getOrganisation().getId())) {
      LOG.warnf("ADMIN_ORGANISATION %s tente d'agir sur org %s qui n'est pas la sienne",
          email, lien.getOrganisation() != null ? lien.getOrganisation().getId() : "null");
      throw new ForbiddenException("Vous n'êtes pas autorisé à gérer les membres de cette organisation.");
    }

    // ── 2. Protection : interdire d'agir sur un admin ou super-admin ────────
    Membre cible = lien.getMembre();
    if (cible != null && cible.getId() != null) {
      long adminCount = membreRoleRepository.count(
          "membreOrganisation.membre.id = ?1 AND role.code IN (?2) AND actif = true",
          cible.getId(), java.util.List.of("ORGADMIN", "SUPERADMIN"));
      if (adminCount > 0) {
        LOG.warnf("ADMIN_ORGANISATION %s tente d'agir sur l'admin/superadmin membre=%s",
            email, cible.getId());
        throw new ForbiddenException(
            "Vous ne pouvez pas modifier le statut d'un administrateur ou super-administrateur.");
      }
    }
  }

  /** Résout l'UUID de l'admin connecté depuis le JWT subject. */
  private UUID resolveCurrentAdminId() {
    try {
      String sub = jwt != null ? jwt.getSubject() : null;
      return sub != null ? UUID.fromString(sub) : null;
    } catch (Exception e) {
      return null;
    }
  }
}
