package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.client.AdminRoleServiceClient;
import dev.lions.unionflow.server.client.AdminUserServiceClient;
import dev.lions.unionflow.server.client.RoleServiceClient;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.user.manager.dto.user.UserDTO;
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.OidcClient;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;

/**
 * Service de synchronisation bidirectionnelle entre Membre (unionflow) et User (Keycloak).
 *
 * <p>Ce service garantit la cohérence entre:
 * <ul>
 *   <li>Membre (entité business) stockée dans PostgreSQL unionflow</li>
 *   <li>User (identité authentification) gérée par Keycloak via lions-user-manager</li>
 * </ul>
 *
 * <h2>Règles de cohérence:</h2>
 * <ul>
 *   <li>Un Membre peut exister sans User Keycloak (membre géré par admin, sans accès portail)</li>
 *   <li>Un User Keycloak peut exister sans Membre (super-admin, staff technique)</li>
 *   <li>L'email est la clé de rapprochement (UNIQUE des deux côtés)</li>
 *   <li>Le lien est matérialisé par Membre.keycloakUserId</li>
 * </ul>
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2025-12-24
 */
@ApplicationScoped
public class MembreKeycloakSyncService {

    /**
     * Résultat du traitement du premier login.
     */
    public enum PremierLoginResultat {
        /** Premier login complété avec succès (token à rafraîchir côté mobile). */
        COMPLETE,
        /** Réauthentification requise (UPDATE_PASSWORD assigné sur un ancien compte). */
        REAUTH_REQUIS,
        /** Non applicable (premiereConnexion déjà false ou membre inexistant). */
        NON_APPLICABLE
    }

    private static final Logger LOGGER = Logger.getLogger(MembreKeycloakSyncService.class.getName());
    private static final String DEFAULT_REALM = "unionflow";

    /** URL du serveur Keycloak. Ex : https://security.lions.dev/realms/unionflow */
    @ConfigProperty(name = "quarkus.oidc.auth-server-url")
    String oidcAuthServerUrl;

    @Inject
    @NamedOidcClient("admin-service")
    OidcClient adminOidcClient;

    @Inject
    MembreRepository membreRepository;

    @Inject
    MembreService membreService;

    @Inject
    @RestClient
    AdminUserServiceClient userServiceClient;

    @Inject
    @RestClient
    AdminRoleServiceClient roleServiceClient;

    @Inject
    EmailTemplateService emailTemplateService;

    /**
     * Provisionne un compte Keycloak pour un Membre existant qui n'en a pas encore.
     *
     * <p>Cette méthode:
     * <ol>
     *   <li>Vérifie que le Membre n'a pas déjà un compte Keycloak</li>
     *   <li>Vérifie qu'aucun User Keycloak n'existe avec le même email</li>
     *   <li>Crée le User dans Keycloak avec un mot de passe temporaire</li>
     *   <li>Configure les actions requises (vérification email + changement mot de passe)</li>
     *   <li>Lie le Membre au User créé via keycloakUserId</li>
     *   <li>Envoie l'email de bienvenue avec le lien de vérification</li>
     * </ol>
     *
     * <p><strong>Note transactionnelle :</strong> cette méthode est intentionnellement sans
     * {@code @Transactional} afin de ne pas marquer la transaction parente pour rollback en cas
     * d'échec du provisionnement Keycloak (non bloquant depuis {@code MembreResource.creerMembre}).
     * Elle participe à la transaction active du contexte appelant.
     *
     * @param membreId UUID du membre à provisionner
     * @throws IllegalStateException si le membre a déjà un compte Keycloak
     * @throws IllegalStateException si un user Keycloak existe déjà avec cet email
     * @throws NotFoundException si le membre n'existe pas
     */
    public String provisionKeycloakUser(java.util.UUID membreId) {
        LOGGER.info("Provisioning Keycloak user for Membre ID: " + membreId);

        // 1. Récupérer le Membre
        Membre membre = membreRepository.findByIdOptional(membreId)
            .orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + membreId));

        // 2. Vérifier qu'il n'a pas déjà un compte Keycloak
        if (membre.getKeycloakId() != null) {
            throw new IllegalStateException(
                "Le membre " + membre.getNomComplet() + " a déjà un compte Keycloak lié (ID: " + membre.getKeycloakId() + ")"
            );
        }

        // 3. Vérifier qu'aucun user Keycloak n'existe avec cet email
        try {
            UserSearchCriteriaDTO searchCriteria = new UserSearchCriteriaDTO();
            searchCriteria.setEmail(membre.getEmail());
            searchCriteria.setRealmName(DEFAULT_REALM);
            searchCriteria.setPageSize(1);

            var searchResult = userServiceClient.searchUsers(searchCriteria);
            if (searchResult != null && searchResult.getUsers() != null && !searchResult.getUsers().isEmpty()) {
                throw new IllegalStateException(
                    "Un compte Keycloak existe déjà avec l'email: " + membre.getEmail()
                );
            }
        } catch (IllegalStateException e) {
            throw e; // Re-throw pour propager l'erreur d'email en doublon
        } catch (Exception e) {
            LOGGER.warning("Impossible de vérifier l'existence du user Keycloak: " + e.getMessage());
            // On continue quand même - l'API Keycloak retournera une erreur si doublon
        }

        // 4. Créer le UserDTO à partir du Membre
        UserDTO newUser = createUserDTOFromMembre(membre);
        // Le mot de passe temporaire a été défini dans newUser, on le récupère avant envoi
        String temporaryPassword = newUser.getTemporaryPassword();

        try {
            // 5. Créer le user dans Keycloak
            UserDTO createdUser = userServiceClient.createUser(newUser, DEFAULT_REALM);

            // 6. Lier le Membre au User Keycloak
            if (createdUser.getId() != null) {
                try {
                    membre.setKeycloakId(UUID.fromString(createdUser.getId()));
                } catch (IllegalArgumentException e) {
                    LOGGER.warning("ID Keycloak invalide: " + createdUser.getId());
                }
            }
            membreRepository.persist(membre);

            LOGGER.info("✅ Compte Keycloak créé pour " + membre.getNomComplet() + " (Keycloak ID: " + createdUser.getId() + ")");

            // 7. Envoyer l'email de vérification
            try {
                userServiceClient.sendVerificationEmail(createdUser.getId(), DEFAULT_REALM);
                LOGGER.info("✅ Email de vérification envoyé à: " + membre.getEmail());
            } catch (Exception e) {
                LOGGER.warning("⚠️ Impossible d'envoyer l'email de vérification: " + e.getMessage());
            }

            return temporaryPassword;

        } catch (Exception e) {
            LOGGER.severe("❌ Erreur lors de la création du user Keycloak pour " + membre.getNomComplet() + ": " + e.getMessage());
            throw new RuntimeException("Impossible de créer le compte Keycloak: " + e.getMessage(), e);
        }
    }

    /**
     * Active un membre dans Keycloak en lui assignant le rôle MEMBRE_ACTIF.
     * Appelé après MembreService.activerMembre() lors de la validation admin.
     *
     * <p>Si le membre n'a pas encore de compte Keycloak, le provisionne d'abord
     * puis assigne le rôle MEMBRE_ACTIF.
     *
     * @param membreId UUID du membre à activer dans Keycloak
     * @throws NotFoundException si le membre n'existe pas en base
     */
    @Transactional(jakarta.transaction.Transactional.TxType.REQUIRES_NEW)
    public void activerMembreDansKeycloak(java.util.UUID membreId) {
        LOGGER.info("Activation Keycloak (rôle MEMBRE_ACTIF) pour Membre ID: " + membreId);

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

        // Lier le compte Keycloak si absent : chercher par email avant de tenter un provisionnement
        if (membre.getKeycloakId() == null) {
            try {
                UserSearchCriteriaDTO criteria = new UserSearchCriteriaDTO();
                criteria.setEmail(membre.getEmail());
                criteria.setRealmName(DEFAULT_REALM);
                criteria.setPageSize(1);
                var result = userServiceClient.searchUsers(criteria);
                if (result != null && result.getUsers() != null && !result.getUsers().isEmpty()) {
                    String kcId = result.getUsers().get(0).getId();
                    membre.setKeycloakId(UUID.fromString(kcId));
                    membreRepository.persist(membre);
                    LOGGER.info("Compte Keycloak existant lié pour " + membre.getEmail() + " → " + kcId);
                } else {
                    LOGGER.info("Compte Keycloak absent — provisionnement pour " + membre.getNomComplet());
                    provisionKeycloakUser(membreId);
                }
            } catch (Exception e) {
                LOGGER.warning("Recherche Keycloak par email échouée, tentative provisionnement : " + e.getMessage());
                provisionKeycloakUser(membreId);
            }
            // Recharger après liaison/provisionnement
            membre = membreRepository.findByIdOptional(membreId)
                .orElseThrow(() -> new NotFoundException("Membre non trouvé après liaison Keycloak: " + membreId));
        }

        String keycloakUserId = membre.getKeycloakId().toString();

        try {
            // Récupérer l'utilisateur Keycloak
            UserDTO user = userServiceClient.getUserById(keycloakUserId, DEFAULT_REALM);

            // S'assurer que le compte est activé
            if (Boolean.FALSE.equals(user.getEnabled())) {
                user.setEnabled(true);
                userServiceClient.updateUser(keycloakUserId, user, DEFAULT_REALM);
            }

            // Assigner MEMBRE + MEMBRE_ACTIF — MEMBRE est requis par les @RolesAllowed du backend
            roleServiceClient.assignRealmRoles(
                keycloakUserId,
                DEFAULT_REALM,
                new RoleServiceClient.RoleNamesRequest(List.of("MEMBRE", "MEMBRE_ACTIF"))
            );

            LOGGER.info("✅ Rôles MEMBRE + MEMBRE_ACTIF assignés dans Keycloak pour " + membre.getNomComplet());

        } catch (Exception e) {
            LOGGER.severe("❌ Erreur lors de l'activation Keycloak pour " + membre.getNomComplet() + ": " + e.getMessage());
            throw new RuntimeException("Impossible d'activer le compte Keycloak: " + e.getMessage(), e);
        }
    }

    /**
     * Promeut un membre au rôle ADMIN_ORGANISATION dans Keycloak.
     * Appelé après MembreService.promouvoirAdminOrganisation().
     *
     * <p>Si le membre n'a pas encore de compte Keycloak, le provisionne d'abord.
     * Assigne ensuite ADMIN_ORGANISATION et retire les rôles MEMBRE / MEMBRE_ACTIF
     * (un admin gère l'organisation — il n'est pas un membre ordinaire).
     *
     * @param membreId UUID du membre à promouvoir dans Keycloak
     * @throws NotFoundException si le membre n'existe pas en base
     */
    @Transactional(jakarta.transaction.Transactional.TxType.REQUIRES_NEW)
    public void promouvoirAdminOrganisationDansKeycloak(java.util.UUID membreId) {
        LOGGER.info("Promotion Keycloak (rôle ADMIN_ORGANISATION) pour Membre ID: " + membreId);

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

        // Lier le compte Keycloak si absent : chercher par email avant de tenter un provisionnement
        if (membre.getKeycloakId() == null) {
            try {
                UserSearchCriteriaDTO criteria = new UserSearchCriteriaDTO();
                criteria.setEmail(membre.getEmail());
                criteria.setRealmName(DEFAULT_REALM);
                criteria.setPageSize(1);
                var result = userServiceClient.searchUsers(criteria);
                if (result != null && result.getUsers() != null && !result.getUsers().isEmpty()) {
                    String kcId = result.getUsers().get(0).getId();
                    membre.setKeycloakId(UUID.fromString(kcId));
                    membreRepository.persist(membre);
                    LOGGER.info("Compte Keycloak existant lié pour " + membre.getEmail() + " → " + kcId);
                } else {
                    LOGGER.info("Compte Keycloak absent — provisionnement pour " + membre.getNomComplet());
                    provisionKeycloakUser(membreId);
                }
            } catch (Exception e) {
                LOGGER.warning("Recherche Keycloak par email échouée, tentative provisionnement : " + e.getMessage());
                provisionKeycloakUser(membreId);
            }
            membre = membreRepository.findByIdOptional(membreId)
                .orElseThrow(() -> new NotFoundException("Membre non trouvé après liaison Keycloak: " + membreId));
        }

        String keycloakUserId = membre.getKeycloakId().toString();

        try {
            UserDTO user = userServiceClient.getUserById(keycloakUserId, DEFAULT_REALM);

            // S'assurer que le compte est activé
            if (Boolean.FALSE.equals(user.getEnabled())) {
                user.setEnabled(true);
                userServiceClient.updateUser(keycloakUserId, user, DEFAULT_REALM);
            }

            // Révoquer MEMBRE et MEMBRE_ACTIF (non bloquant — peuvent ne pas exister)
            try {
                roleServiceClient.revokeRealmRoles(
                    keycloakUserId,
                    DEFAULT_REALM,
                    new RoleServiceClient.RoleNamesRequest(List.of("MEMBRE", "MEMBRE_ACTIF"))
                );
            } catch (Exception e) {
                LOGGER.warning("⚠️ Révocation MEMBRE/MEMBRE_ACTIF non bloquante: " + e.getMessage());
            }

            // Assigner ADMIN_ORGANISATION via l'endpoint dédié
            roleServiceClient.assignRealmRoles(
                keycloakUserId,
                DEFAULT_REALM,
                new RoleServiceClient.RoleNamesRequest(List.of("ADMIN_ORGANISATION"))
            );

            LOGGER.info("✅ Rôle ADMIN_ORGANISATION assigné dans Keycloak pour " + membre.getNomComplet());

        } catch (Exception e) {
            LOGGER.severe("❌ Erreur promotion Keycloak pour " + membre.getNomComplet() + ": " + e.getMessage());
            throw new RuntimeException("Impossible de promouvoir le compte Keycloak: " + e.getMessage(), e);
        }
    }

    /**
     * Synchronise les données du Membre vers le User Keycloak.
     * Si le membre n'a pas de compte Keycloak, le provisionne automatiquement.
     *
     * @param membreId UUID du membre à synchroniser
     */
    @Transactional
    public void syncMembreToKeycloak(java.util.UUID membreId) {
        LOGGER.info("Synchronizing Membre to Keycloak: " + membreId);

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

        // Si pas de compte Keycloak, le créer
        if (membre.getKeycloakId() == null) {
            LOGGER.info("Membre n'a pas de compte Keycloak - provisioning automatique");
            provisionKeycloakUser(membreId);
            return;
        }

        try {
            // Récupérer le user Keycloak
            UserDTO user = userServiceClient.getUserById(
                membre.getKeycloakId().toString(),
                DEFAULT_REALM
            );

            // Mettre à jour avec les données du Membre
            user.setPrenom(membre.getPrenom());
            user.setNom(membre.getNom());
            user.setEmail(membre.getEmail());
            user.setEnabled(membre.getActif() != null ? membre.getActif() : true);

            // Persister les changements
            userServiceClient.updateUser(user.getId(), user, user.getRealmName());

            LOGGER.info("✅ User Keycloak synchronisé avec succès: " + membre.getNomComplet());

        } catch (Exception e) {
            LOGGER.severe("❌ Erreur lors de la synchronisation vers Keycloak: " + e.getMessage());
            throw new RuntimeException("Impossible de synchroniser le user Keycloak: " + e.getMessage(), e);
        }
    }

    /**
     * Synchronise les données du User Keycloak vers le Membre.
     * Utile pour des mises à jour faites directement dans la console Keycloak.
     *
     * @param keycloakUserId UUID du user Keycloak
     * @param realm Realm Keycloak (ex: "unionflow")
     */
    @Transactional
    public void syncKeycloakToMembre(String keycloakUserId, String realm) {
        LOGGER.info("Synchronizing Keycloak User to Membre: " + keycloakUserId);

        // Trouver le Membre lié
        Optional<Membre> membreOpt = membreRepository.findByKeycloakUserId(keycloakUserId);

        if (membreOpt.isEmpty()) {
            LOGGER.warning("⚠️ Aucun Membre lié au user Keycloak: " + keycloakUserId);
            return;
        }

        Membre membre = membreOpt.get();

        try {
            // Récupérer les données Keycloak
            UserDTO user = userServiceClient.getUserById(keycloakUserId, realm != null ? realm : DEFAULT_REALM);

            // Mettre à jour le Membre
            membre.setPrenom(user.getPrenom());
            membre.setNom(user.getNom());
            membre.setEmail(user.getEmail());
            membre.setActif(user.getEnabled());

            membreRepository.persist(membre);

            LOGGER.info("✅ Membre synchronisé avec succès depuis Keycloak: " + membre.getNomComplet());

        } catch (Exception e) {
            LOGGER.severe("❌ Erreur lors de la synchronisation depuis Keycloak: " + e.getMessage());
            throw new RuntimeException("Impossible de synchroniser depuis Keycloak: " + e.getMessage(), e);
        }
    }

    /**
     * Trouve un Membre à partir de son ID user Keycloak.
     *
     * @param keycloakUserId UUID du user Keycloak
     * @return Optional contenant le Membre s'il existe
     */
    public Optional<Membre> findMembreByKeycloakUserId(String keycloakUserId) {
        return membreRepository.findByKeycloakUserId(keycloakUserId);
    }

    /**
     * Supprime le lien entre un Membre et son compte Keycloak.
     * Le compte Keycloak n'est PAS supprimé, seulement le lien.
     *
     * @param membreId UUID du membre
     */
    @Transactional
    public void unlinkKeycloakUser(java.util.UUID membreId) {
        LOGGER.info("Unlinking Keycloak user from Membre: " + membreId);

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

        if (membre.getKeycloakId() == null) {
            LOGGER.warning("⚠️ Membre n'a pas de compte Keycloak lié");
            return;
        }

        UUID oldKeycloakId = membre.getKeycloakId();
        membre.setKeycloakId(null);

        membreRepository.persist(membre);

        LOGGER.info("✅ Compte Keycloak " + oldKeycloakId + " délié du membre " + membre.getNomComplet());
    }

    /**
     * Crée un UserDTO Keycloak à partir d'un Membre.
     * Configure les paramètres par défaut et les actions requises.
     *
     * @param membre Le membre source
     * @return UserDTO prêt à être créé dans Keycloak
     */
    private UserDTO createUserDTOFromMembre(Membre membre) {
        UserDTO user = new UserDTO();

        // Informations de base
        // Dériver le username depuis la partie locale de l'email (avant @)
        // UserDTO exige le pattern ^[a-zA-Z0-9._-]+$ (pas de @)
        String emailLocalPart = membre.getEmail().contains("@")
            ? membre.getEmail().split("@")[0]
            : membre.getEmail();
        // Remplacer tout caractère hors pattern par '_'
        String username = emailLocalPart.replaceAll("[^a-zA-Z0-9._-]", "_");
        // Garantir la longueur minimale de 3 caractères
        if (username.length() < 3) {
            username = username + "_uf";
        }
        user.setUsername(username);
        user.setEmail(membre.getEmail());
        user.setPrenom(membre.getPrenom());
        user.setNom(membre.getNom());

        // Configuration du compte
        user.setEnabled(true);
        user.setEmailVerified(true); // Validé par l'admin qui crée le compte

        // Realm
        user.setRealmName(DEFAULT_REALM);

        // Mot de passe temporaire (généré aléatoirement)
        String temporaryPassword = generateTemporaryPassword();
        user.setTemporaryPassword(temporaryPassword);

        // UPDATE_PASSWORD : Keycloak affiche son écran de changement dans le Chrome Custom Tab
        // lors du premier login via AppAuth (Authorization Code + PKCE).
        user.setRequiredActions(List.of("UPDATE_PASSWORD"));

        // Marqueur permettant à completerPremierLogin de distinguer un nouveau compte
        // (attendant le changement de mot de passe) d'un ancien compte sans UPDATE_PASSWORD.
        user.setAttributes(new HashMap<>(Map.of("premiere_password_pending", List.of("true"))));

        // Rôles par défaut pour un nouveau membre
        user.setRealmRoles(List.of("MEMBRE")); // Rôle de base

        LOGGER.info("UserDTO créé pour " + membre.getNomComplet() + " (username: " + user.getUsername() + ")");

        return user;
    }

    /**
     * Réinitialise le mot de passe d'un membre existant dans Keycloak.
     * Génère un nouveau mot de passe temporaire et le définit via lions-user-manager.
     *
     * @param membreId UUID du membre
     * @return Le nouveau mot de passe temporaire généré
     * @throws NotFoundException si le membre n'existe pas
     * @throws IllegalStateException si le membre n'a pas de compte Keycloak
     */
    public String reinitialiserMotDePasse(java.util.UUID membreId) {
        LOGGER.info("Réinitialisation mot de passe pour membre ID: " + membreId);

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

        if (membre.getKeycloakId() == null) {
            throw new IllegalStateException("Le membre " + membre.getEmail() + " n'a pas de compte Keycloak");
        }

        String keycloakUserId = membre.getKeycloakId().toString();
        String newPassword = generateTemporaryPassword();

        dev.lions.user.manager.dto.user.PasswordResetRequestDTO resetRequest =
                dev.lions.user.manager.dto.user.PasswordResetRequestDTO.builder()
                        .password(newPassword)
                        .temporary(false)
                        .build();

        userServiceClient.resetPassword(keycloakUserId, DEFAULT_REALM, resetRequest);

        LOGGER.info("Mot de passe réinitialisé pour " + membre.getEmail());
        return newPassword;
    }

    /**
     * Change le mot de passe d'un membre lors de son premier login.
     * Met également à jour le flag {@code premiereConnexion} à {@code false}.
     *
     * @param membreId         UUID du membre
     * @param nouveauMotDePasse Nouveau mot de passe choisi par le membre
     */
    @Transactional
    public void changerMotDePassePremierLogin(UUID membreId, String nouveauMotDePasse) {
        LOGGER.info("Changement de mot de passe premier login pour membre ID: " + membreId);

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

        if (membre.getKeycloakId() == null) {
            throw new IllegalStateException("Le membre " + membre.getEmail() + " n'a pas de compte Keycloak");
        }

        String keycloakUserId = membre.getKeycloakId().toString();
        dev.lions.user.manager.dto.user.PasswordResetRequestDTO resetRequest =
                dev.lions.user.manager.dto.user.PasswordResetRequestDTO.builder()
                        .password(nouveauMotDePasse)
                        .temporary(false)
                        .build();

        // Tentative via lions-user-manager ; fallback sur l'API Admin Keycloak directe si 403/503
        // Note : le REST client MicroProfile peut lever WebApplicationException (pas nécessairement
        // ForbiddenException) selon la configuration du mapper de réponse — on capture la classe mère.
        try {
            userServiceClient.resetPassword(keycloakUserId, DEFAULT_REALM, resetRequest);
        } catch (jakarta.ws.rs.WebApplicationException e) {
            int status = e.getResponse() != null ? e.getResponse().getStatus() : 0;
            if (status == 403 || status == 503) {
                LOGGER.warning("lions-user-manager reset-password échoué (HTTP " + status
                        + "), fallback sur API Admin Keycloak directe.");
                changerMotDePasseDirectKeycloak(membre.getId(), nouveauMotDePasse);
                return; // changerMotDePasseDirectKeycloak persiste déjà les flags
            }
            throw e; // Statuts inattendus (400, 500…) : propager
        }

        membre.setPremiereConnexion(false);
        // Auto-activation : le membre prouve son identité en changeant son mot de passe temporaire
        boolean doitActiver = "EN_ATTENTE_VALIDATION".equals(membre.getStatutCompte());
        if (doitActiver) {
            membre.setStatutCompte("ACTIF");
            membre.setActif(true);
            LOGGER.info("Compte auto-activé lors du premier login pour: " + membre.getEmail());
        }
        membreRepository.persist(membre);

        if (doitActiver) {
            try {
                activerMembreDansKeycloak(membreId);
            } catch (Exception e) {
                LOGGER.warning("Activation Keycloak au premier login échouée (non bloquant): " + e.getMessage());
            }
        }

        LOGGER.info("Mot de passe premier login changé pour: " + membre.getEmail());
    }

    /**
     * Change le mot de passe d'un membre en appelant directement l'API Admin Keycloak.
     * Bypass lions-user-manager (évite les erreurs 403 de service account).
     *
     * @param membreId          UUID du membre UnionFlow
     * @param nouveauMotDePasse Nouveau mot de passe (en clair, transmis en HTTPS)
     */
    @Transactional
    public void changerMotDePasseDirectKeycloak(UUID membreId, String nouveauMotDePasse) {
        LOGGER.info("Changement de mot de passe (direct Keycloak) pour membre ID: " + membreId);

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

        if (membre.getKeycloakId() == null) {
            throw new IllegalStateException("Le membre n'a pas de compte Keycloak: " + membre.getEmail());
        }

        String keycloakUserId = membre.getKeycloakId().toString();

        // Obtenir le token admin via OIDC client credentials
        String adminToken;
        try {
            adminToken = adminOidcClient.getTokens().await().indefinitely().getAccessToken();
        } catch (Exception e) {
            throw new RuntimeException("Impossible d'obtenir le token admin Keycloak: " + e.getMessage(), e);
        }

        // Dériver l'URL Admin Keycloak depuis l'URL OIDC
        // Ex: https://security.lions.dev/realms/unionflow → https://security.lions.dev/admin/realms/unionflow
        String adminUrl = oidcAuthServerUrl.replace("/realms/", "/admin/realms/")
                + "/users/" + keycloakUserId + "/reset-password";

        String body = String.format(
                "{\"type\":\"password\",\"value\":\"%s\",\"temporary\":false}",
                nouveauMotDePasse.replace("\"", "\\\""));

        try {
            HttpClient client = HttpClient.newHttpClient();
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(adminUrl))
                    .header("Authorization", "Bearer " + adminToken)
                    .header("Content-Type", "application/json")
                    .PUT(HttpRequest.BodyPublishers.ofString(body))
                    .build();

            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() != 204 && response.statusCode() != 200) {
                throw new RuntimeException("Keycloak Admin API retourné " + response.statusCode()
                        + ": " + response.body());
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("Erreur lors de la réinitialisation du mot de passe Keycloak: " + e.getMessage(), e);
        }

        // Mettre à jour les flags
        membre.setPremiereConnexion(false);
        if ("EN_ATTENTE_VALIDATION".equals(membre.getStatutCompte())) {
            membre.setStatutCompte("ACTIF");
            membre.setActif(true);
        }
        membreRepository.persist(membre);

        LOGGER.info("Mot de passe changé (direct Keycloak) pour: " + membre.getEmail());
    }

    /**
     * Marque le premier login comme complété.
     *
     * <p>Keycloak ayant déjà forcé le changement de mot de passe via sa required action
     * {@code UPDATE_PASSWORD} dans le Chrome Custom Tab, il suffit de mettre à jour la DB
     * et d'assigner les rôles MEMBRE + MEMBRE_ACTIF si le compte était EN_ATTENTE_VALIDATION.
     *
     * <p><strong>Remédiation automatique des anciens comptes :</strong> si le compte Keycloak
     * ne possède ni l'attribut {@code premiere_password_pending} ni la required action
     * {@code UPDATE_PASSWORD}, c'est un ancien compte créé avant la mise en place du flux AppAuth.
     * Le service assigne automatiquement {@code UPDATE_PASSWORD} + le marqueur dans Keycloak
     * et retourne {@link PremierLoginResultat#REAUTH_REQUIS} pour que Flutter déclenche une
     * nouvelle authentification (qui affichera l'écran de changement de mot de passe).
     *
     * @param membreId UUID du membre
     * @return résultat du traitement (COMPLETE, REAUTH_REQUIS ou NON_APPLICABLE)
     */
    @Transactional
    public PremierLoginResultat completerPremierLogin(UUID membreId) {
        Membre membre = membreRepository.findByIdOptional(membreId).orElse(null);
        if (membre == null || !Boolean.TRUE.equals(membre.getPremiereConnexion())) {
            return PremierLoginResultat.NON_APPLICABLE; // Idempotent
        }

        // Vérifier l'état du compte Keycloak pour détecter et remédier les anciens comptes
        if (membre.getKeycloakId() != null) {
            try {
                UserDTO kcUser = userServiceClient.getUserById(
                        membre.getKeycloakId().toString(), DEFAULT_REALM);

                boolean hasPendingMarker = kcUser.getAttributes() != null
                        && kcUser.getAttributes().containsKey("premiere_password_pending");
                boolean hasUpdatePassword = kcUser.getRequiredActions() != null
                        && kcUser.getRequiredActions().contains("UPDATE_PASSWORD");

                if (!hasPendingMarker && !hasUpdatePassword) {
                    // Ancien compte créé sans UPDATE_PASSWORD (avant le fix AppAuth).
                    // Assigner la required action + le marqueur, puis demander une réauthentification.
                    Map<String, List<String>> attrs = new HashMap<>(
                            kcUser.getAttributes() != null ? kcUser.getAttributes() : new HashMap<>());
                    attrs.put("premiere_password_pending", List.of("true"));
                    kcUser.setAttributes(attrs);

                    List<String> actions = new ArrayList<>(
                            kcUser.getRequiredActions() != null ? kcUser.getRequiredActions() : List.of());
                    if (!actions.contains("UPDATE_PASSWORD")) {
                        actions.add("UPDATE_PASSWORD");
                    }
                    kcUser.setRequiredActions(actions);
                    userServiceClient.updateUser(kcUser.getId(), kcUser, DEFAULT_REALM);
                    LOGGER.info("Ancien compte remédié — UPDATE_PASSWORD assigné dans Keycloak : " + membre.getEmail());
                    return PremierLoginResultat.REAUTH_REQUIS;
                }

                if (hasUpdatePassword) {
                    // UPDATE_PASSWORD encore présent (session pré-existante avant assignation)
                    // → réauthentification pour afficher l'écran de changement de mot de passe
                    LOGGER.info("UPDATE_PASSWORD toujours présent (session antérieure) → réauth requise : " + membre.getEmail());
                    return PremierLoginResultat.REAUTH_REQUIS;
                }

                if (hasPendingMarker) {
                    // Marqueur présent + UPDATE_PASSWORD absent → mot de passe bien changé → nettoyer
                    Map<String, List<String>> attrs = new HashMap<>(kcUser.getAttributes());
                    attrs.remove("premiere_password_pending");
                    kcUser.setAttributes(attrs);
                    userServiceClient.updateUser(kcUser.getId(), kcUser, DEFAULT_REALM);
                    LOGGER.info("Marqueur premiere_password_pending supprimé pour : " + membre.getEmail());
                }
                // hasPendingMarker=false, hasUpdatePassword=false : état inattendu mais non bloquant
                // → traiter comme complété (fall-through vers activation)

            } catch (Exception e) {
                LOGGER.warning("Vérification état Keycloak échouée pour " + membre.getEmail()
                        + " (non bloquant) : " + e.getMessage());
                // Non bloquant : continuer avec l'activation normale
            }
        }

        boolean doitActiver = "EN_ATTENTE_VALIDATION".equals(membre.getStatutCompte());
        membre.setPremiereConnexion(false);
        if (doitActiver) {
            membre.setStatutCompte("ACTIF");
            membre.setActif(true);
            LOGGER.info("Compte auto-activé au premier login pour : " + membre.getEmail());
        }
        membreRepository.persist(membre);

        if (doitActiver) {
            try {
                activerMembreDansKeycloak(membreId);
            } catch (Exception e) {
                LOGGER.warning("Activation Keycloak au premier login échouée (non bloquant) : " + e.getMessage());
            }
        }
        LOGGER.info("Premier login complété pour : " + membre.getEmail());

        // Email de bienvenue (non bloquant)
        if (doitActiver && membre.getEmail() != null) {
            try {
                String orgNom = "";
                try {
                    var memberships = membre.getMembresOrganisations();
                    if (memberships != null && !memberships.isEmpty()) {
                        orgNom = memberships.iterator().next().getOrganisation().getNom();
                    }
                } catch (Exception ignore) {}
                emailTemplateService.envoyerBienvenue(
                        membre.getEmail(),
                        membre.getPrenom() != null ? membre.getPrenom() : "",
                        membre.getNom() != null ? membre.getNom() : "",
                        orgNom,
                        null);
            } catch (Exception e) {
                LOGGER.warning("Email bienvenue ignoré (non bloquant) : " + e.getMessage());
            }
        }

        return PremierLoginResultat.COMPLETE;
    }

    /**
     * Génère un mot de passe temporaire sécurisé.
     * Le user sera forcé de le changer à la première connexion.
     *
     * @return Mot de passe temporaire
     */
    private String generateTemporaryPassword() {
        // Générer un mot de passe aléatoire de 16 caractères
        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%";
        StringBuilder password = new StringBuilder();
        java.security.SecureRandom random = new java.security.SecureRandom();

        for (int i = 0; i < 16; i++) {
            password.append(chars.charAt(random.nextInt(chars.length())));
        }

        return password.toString();
    }
}
