package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.messagerie.request.BloquerMembreRequest;
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationDirecteRequest;
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationRoleRequest;
import dev.lions.unionflow.server.api.dto.messagerie.request.EnvoyerMessageRequest;
import dev.lions.unionflow.server.api.dto.messagerie.request.MettreAJourPolitiqueRequest;
import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse;
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse;
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse;
import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse;
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication;
import dev.lions.unionflow.server.entity.ContactPolicy;
import dev.lions.unionflow.server.entity.Conversation;
import dev.lions.unionflow.server.entity.ConversationParticipant;
import dev.lions.unionflow.server.entity.MemberBlock;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.MembreOrganisation;
import dev.lions.unionflow.server.entity.Message;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.ContactPolicyRepository;
import dev.lions.unionflow.server.repository.ConversationParticipantRepository;
import dev.lions.unionflow.server.repository.ConversationRepository;
import dev.lions.unionflow.server.repository.MemberBlockRepository;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.MessageRepository;
import dev.lions.unionflow.server.messaging.KafkaEventProducer;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.NotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;

/**
 * Service métier pour la messagerie instantanée.
 *
 * <p>Gère les conversations (directes et canaux-rôle), les messages (texte,
 * vocal, image), les blocages et les politiques de communication.
 *
 * <p>Politique par appartenance : deux membres de la même organisation
 * peuvent se contacter sans demande d'amitié préalable.
 * L'adhésion est la relation de confiance.
 *
 * @author UnionFlow Team
 * @version 4.0
 * @since 2026-04-13
 */
@ApplicationScoped
public class MessagingService {

    private static final Logger LOG = Logger.getLogger(MessagingService.class);
    private static final int PAGE_SIZE_DEFAULT = 30;

    @Inject ConversationRepository conversationRepository;
    @Inject ConversationParticipantRepository participantRepository;
    @Inject MessageRepository messageRepository;
    @Inject ContactPolicyRepository contactPolicyRepository;
    @Inject MemberBlockRepository memberBlockRepository;
    @Inject MembreRepository membreRepository;
    @Inject MembreOrganisationRepository membreOrganisationRepository;
    @Inject KafkaEventProducer kafkaEventProducer;
    @Inject io.quarkus.security.identity.SecurityIdentity securityIdentity;

    // ── Conversations ─────────────────────────────────────────────────────────

    /**
     * Démarre ou récupère une conversation directe 1-1.
     * Idempotent : si la conversation existe déjà, elle est retournée.
     */
    @Transactional
    public ConversationResponse demarrerConversationDirecte(DemarrerConversationDirecteRequest request) {
        Membre moi = getMembreConnecte();
        Membre destinataire = membreRepository.findById(request.destinataireId());
        if (destinataire == null) {
            throw new NotFoundException("Membre destinataire non trouvé : " + request.destinataireId());
        }

        UUID orgId = request.organisationId();
        verifierAppartenance(moi.getId(), orgId);
        verifierAppartenance(request.destinataireId(), orgId);
        verifierPolitique(moi.getId(), request.destinataireId(), orgId, false);

        // Idempotence : chercher une conversation directe existante
        return conversationRepository
            .findConversationDirecte(moi.getId(), request.destinataireId(), orgId)
            .map(c -> {
                // Envoyer le message initial si fourni
                if (request.contenuInitial() != null && !request.contenuInitial().isBlank()) {
                    envoyerMessageDansConversation(c, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
                }
                return toConversationResponse(c, moi.getId());
            })
            .orElseGet(() -> {
                Organisation org = getOrganisation(orgId);
                Conversation conv = Conversation.builder()
                    .organisation(org)
                    .typeConversation(TypeConversation.DIRECTE)
                    .statut(StatutConversation.ACTIVE)
                    .build();
                conversationRepository.persist(conv);

                ajouterParticipant(conv, moi, "INITIATEUR");
                ajouterParticipant(conv, destinataire, "PARTICIPANT");

                if (request.contenuInitial() != null && !request.contenuInitial().isBlank()) {
                    envoyerMessageDansConversation(conv, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
                }

                LOG.infof("Conversation directe créée: %s ↔ %s dans org %s",
                    moi.getEmail(), destinataire.getEmail(), orgId);
                return toConversationResponse(conv, moi.getId());
            });
    }

    /**
     * Démarre ou récupère un canal de rôle.
     * Le canal est partagé : tous les membres qui contactent "Le Trésorier"
     * aboutissent dans le même canal.
     */
    @Transactional
    public ConversationResponse demarrerConversationRole(DemarrerConversationRoleRequest request) {
        Membre moi = getMembreConnecte();
        UUID orgId = request.organisationId();
        verifierAppartenance(moi.getId(), orgId);
        verifierPolitique(moi.getId(), null, orgId, true);

        String roleCible = request.roleCible();
        List<Membre> porteurs = trouverPorteursDuRole(orgId, roleCible);
        if (porteurs.isEmpty()) {
            throw new NotFoundException("Aucun membre avec le rôle " + roleCible + " dans cette organisation");
        }

        Organisation org = getOrganisation(orgId);
        String titreCanal = libelleDuRole(roleCible);

        return conversationRepository.findCanalRole(orgId, roleCible)
            .map(c -> {
                // Ajouter l'initiateur s'il n'est pas encore participant
                if (!participantRepository.estParticipant(c.getId(), moi.getId())) {
                    ajouterParticipant(c, moi, "PARTICIPANT");
                }
                envoyerMessageDansConversation(c, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
                return toConversationResponse(c, moi.getId());
            })
            .orElseGet(() -> {
                Conversation conv = Conversation.builder()
                    .organisation(org)
                    .typeConversation(TypeConversation.ROLE_CANAL)
                    .roleCible(roleCible)
                    .titre(titreCanal)
                    .statut(StatutConversation.ACTIVE)
                    .build();
                conversationRepository.persist(conv);

                ajouterParticipant(conv, moi, "INITIATEUR");
                porteurs.forEach(p -> ajouterParticipant(conv, p, "MODERATEUR"));

                envoyerMessageDansConversation(conv, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);

                LOG.infof("Canal rôle créé: %s dans org %s", roleCible, orgId);
                return toConversationResponse(conv, moi.getId());
            });
    }

    /**
     * Retourne la liste des conversations du membre connecté.
     */
    public List<ConversationSummaryResponse> getMesConversations() {
        Membre moi = getMembreConnecte();
        return conversationRepository.findByMembreId(moi.getId()).stream()
            .map(c -> toConversationSummary(c, moi.getId()))
            .collect(Collectors.toList());
    }

    /**
     * Retourne le détail d'une conversation (avec les derniers messages).
     */
    public ConversationResponse getConversation(UUID conversationId) {
        Membre moi = getMembreConnecte();
        Conversation conv = conversationRepository.findConversationById(conversationId)
            .orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
        verifierParticipant(conv, moi.getId());
        return toConversationResponse(conv, moi.getId());
    }

    /**
     * Archive une conversation.
     */
    @Transactional
    public ConversationResponse archiverConversation(UUID conversationId) {
        Membre moi = getMembreConnecte();
        Conversation conv = conversationRepository.findConversationById(conversationId)
            .orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
        verifierParticipant(conv, moi.getId());
        conv.archiver();
        return toConversationResponse(conv, moi.getId());
    }

    // ── Messages ──────────────────────────────────────────────────────────────

    /**
     * Envoie un message dans une conversation existante.
     */
    @Transactional
    public MessageResponse envoyerMessage(UUID conversationId, EnvoyerMessageRequest request) {
        Membre moi = getMembreConnecte();
        Conversation conv = conversationRepository.findConversationById(conversationId)
            .orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));

        verifierParticipant(conv, moi.getId());
        if (!conv.estActive()) {
            throw new BadRequestException("Cette conversation est archivée");
        }

        TypeContenu type = parseTypeContenu(request.typeMessage());
        validerContenuMessage(type, request);

        Message message = envoyerMessageDansConversation(
            conv, moi,
            request.contenu(), type,
            request.urlFichier(), request.dureeAudio(),
            request.messageParentId()
        );
        return toMessageResponse(message);
    }

    /**
     * Récupère les messages d'une conversation (paginés).
     */
    public List<MessageResponse> getMessages(UUID conversationId, int page) {
        Membre moi = getMembreConnecte();
        Conversation conv = conversationRepository.findConversationById(conversationId)
            .orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
        verifierParticipant(conv, moi.getId());

        return messageRepository.findByConversationPagine(conversationId, page, PAGE_SIZE_DEFAULT)
            .stream()
            .map(this::toMessageResponse)
            .collect(Collectors.toList());
    }

    /**
     * Marque tous les messages d'une conversation comme lus.
     */
    @Transactional
    public void marquerConversationLue(UUID conversationId) {
        Membre moi = getMembreConnecte();
        participantRepository.findParticipant(conversationId, moi.getId())
            .ifPresent(p -> {
                p.marquerLu();
                LOG.debugf("Conversation %s marquée lue par %s", conversationId, moi.getEmail());
            });
    }

    /**
     * Supprime un message (soft delete — contenu remplacé par "[Message supprimé]").
     */
    @Transactional
    public void supprimerMessage(UUID conversationId, UUID messageId) {
        Membre moi = getMembreConnecte();
        Message message = messageRepository.findMessageById(messageId)
            .orElseThrow(() -> new NotFoundException("Message non trouvé : " + messageId));

        if (!message.getConversation().getId().equals(conversationId)) {
            throw new NotFoundException("Message non trouvé dans cette conversation");
        }
        if (!message.getExpediteur().getId().equals(moi.getId())) {
            throw new ForbiddenException("Vous ne pouvez supprimer que vos propres messages");
        }
        message.supprimer();
    }

    // ── Blocages ──────────────────────────────────────────────────────────────

    /**
     * Bloque un membre dans une organisation.
     */
    @Transactional
    public void bloquerMembre(BloquerMembreRequest request) {
        Membre moi = getMembreConnecte();
        UUID membreABloquerId = request.membreABloquerId();
        UUID orgId = request.organisationId();

        if (moi.getId().equals(membreABloquerId)) {
            throw new BadRequestException("Vous ne pouvez pas vous bloquer vous-même");
        }

        Membre aBloquer = membreRepository.findById(membreABloquerId);
        if (aBloquer == null) {
            throw new NotFoundException("Membre non trouvé : " + membreABloquerId);
        }

        if (memberBlockRepository.estBloque(moi.getId(), membreABloquerId, orgId)) {
            throw new BadRequestException("Ce membre est déjà bloqué");
        }

        Organisation org = getOrganisation(orgId);
        MemberBlock block = MemberBlock.builder()
            .bloqueur(moi)
            .bloque(aBloquer)
            .organisation(org)
            .build();
        memberBlockRepository.persist(block);
        LOG.infof("%s a bloqué %s dans org %s", moi.getEmail(), aBloquer.getEmail(), orgId);
    }

    /**
     * Débloque un membre dans une organisation.
     */
    @Transactional
    public void debloquerMembre(UUID membreId, UUID organisationId) {
        Membre moi = getMembreConnecte();
        MemberBlock block = memberBlockRepository.findBlocage(moi.getId(), membreId, organisationId)
            .orElseThrow(() -> new NotFoundException("Aucun blocage trouvé pour ce membre"));
        block.setActif(false);
    }

    /**
     * Retourne la liste des membres bloqués par le membre connecté.
     */
    public List<MemberBlock> getMesBlocages() {
        Membre moi = getMembreConnecte();
        return memberBlockRepository.findByBloqueur(moi.getId());
    }

    // ── Politique de communication ────────────────────────────────────────────

    /**
     * Retourne la politique de communication d'une organisation.
     * Crée une politique par défaut si elle n'existe pas encore.
     */
    @Transactional
    public ContactPolicyResponse getPolitique(UUID organisationId) {
        ContactPolicy policy = contactPolicyRepository.findByOrganisationId(organisationId)
            .orElseGet(() -> creerPolitiqueParDefaut(organisationId));
        return toContactPolicyResponse(policy);
    }

    /**
     * Met à jour la politique de communication.
     * Réservé aux ADMIN et ADMIN_ORGANISATION.
     */
    @Transactional
    public ContactPolicyResponse mettreAJourPolitique(UUID organisationId, MettreAJourPolitiqueRequest request) {
        ContactPolicy policy = contactPolicyRepository.findByOrganisationId(organisationId)
            .orElseGet(() -> creerPolitiqueParDefaut(organisationId));

        if (request.typePolitique() != null) {
            policy.setTypePolitique(TypePolitiqueCommunication.valueOf(request.typePolitique()));
        }
        if (request.autoriserMembreVersMembre() != null) {
            policy.setAutoriserMembreVersMembre(request.autoriserMembreVersMembre());
        }
        if (request.autoriserMembreVersRole() != null) {
            policy.setAutoriserMembreVersRole(request.autoriserMembreVersRole());
        }
        if (request.autoriserNotesVocales() != null) {
            policy.setAutoriserNotesVocales(request.autoriserNotesVocales());
        }
        return toContactPolicyResponse(policy);
    }

    // ── Méthodes privées ──────────────────────────────────────────────────────

    private Message envoyerMessageDansConversation(
            Conversation conv, Membre expediteur,
            String contenu, TypeContenu type,
            String urlFichier, Integer dureeAudio,
            UUID messageParentId) {

        Message.MessageBuilder builder = Message.builder()
            .conversation(conv)
            .expediteur(expediteur)
            .typeMessage(type)
            .contenu(contenu)
            .urlFichier(urlFichier)
            .dureeAudio(dureeAudio);

        if (messageParentId != null) {
            messageRepository.findMessageById(messageParentId)
                .ifPresent(builder::messageParent);
        }

        Message message = builder.build();
        messageRepository.persist(message);
        conv.enregistrerNouveauMessage();

        // Notifier via Kafka → WebSocket
        try {
            java.util.Map<String, Object> data = new java.util.HashMap<>();
            data.put("conversationId", conv.getId().toString());
            data.put("messageId", message.getId() != null ? message.getId().toString() : "");
            data.put("expediteurId", expediteur.getId().toString());
            data.put("typeMessage", type.name());
            kafkaEventProducer.publishNouveauMessage(conv.getId(), conv.getOrganisation().getId().toString(), data);
        } catch (Exception e) {
            LOG.warnf("Impossible de publier l'event Kafka pour le message: %s", e.getMessage());
        }

        return message;
    }

    private void ajouterParticipant(Conversation conv, Membre membre, String role) {
        if (!participantRepository.estParticipant(conv.getId(), membre.getId())) {
            ConversationParticipant participant = ConversationParticipant.builder()
                .conversation(conv)
                .membre(membre)
                .roleDansConversation(role)
                .notifier(true)
                .build();
            participantRepository.persist(participant);
        }
    }

    private void verifierAppartenance(UUID membreId, UUID organisationId) {
        boolean appartient = membreOrganisationRepository
            .count("membre.id = ?1 AND organisation.id = ?2 AND actif = true", membreId, organisationId) > 0;
        if (!appartient) {
            throw new ForbiddenException("Le membre n'appartient pas à cette organisation");
        }
    }

    private void verifierPolitique(UUID expediteurId, UUID destinataireId, UUID orgId, boolean versRole) {
        contactPolicyRepository.findByOrganisationId(orgId).ifPresent(policy -> {
            if (versRole && !policy.getAutoriserMembreVersRole()) {
                throw new ForbiddenException("La politique de cette organisation n'autorise pas les contacts vers les rôles");
            }
            if (!versRole && !policy.getAutoriserMembreVersMembre()) {
                throw new ForbiddenException("La politique de cette organisation n'autorise pas les contacts entre membres");
            }
        });

        // Vérifier le blocage
        if (destinataireId != null && memberBlockRepository.estBloque(destinataireId, expediteurId, orgId)) {
            throw new ForbiddenException("Vous ne pouvez pas contacter ce membre");
        }
    }

    private void verifierParticipant(Conversation conv, UUID membreId) {
        if (!participantRepository.estParticipant(conv.getId(), membreId)) {
            throw new ForbiddenException("Vous n'êtes pas participant à cette conversation");
        }
    }

    private List<Membre> trouverPorteursDuRole(UUID orgId, String role) {
        List<MembreOrganisation> membresOrg = membreOrganisationRepository
            .find("organisation.id = ?1 AND roleOrg = ?2 AND actif = true", orgId, role)
            .list();
        return membresOrg.stream()
            .map(MembreOrganisation::getMembre)
            .collect(Collectors.toList());
    }

    private ContactPolicy creerPolitiqueParDefaut(UUID organisationId) {
        Organisation org = getOrganisation(organisationId);
        ContactPolicy policy = ContactPolicy.builder()
            .organisation(org)
            .typePolitique(TypePolitiqueCommunication.OUVERT)
            .autoriserMembreVersMembre(true)
            .autoriserMembreVersRole(true)
            .autoriserNotesVocales(true)
            .build();
        contactPolicyRepository.persist(policy);
        return policy;
    }

    private Membre getMembreConnecte() {
        String email = securityIdentity.getPrincipal().getName();
        return membreRepository.find("email", email).firstResult();
    }

    private Organisation getOrganisation(UUID orgId) {
        return (Organisation) dev.lions.unionflow.server.entity.Organisation.findById(orgId);
    }

    private TypeContenu parseTypeContenu(String type) {
        if (type == null || type.isBlank()) return TypeContenu.TEXTE;
        try {
            return TypeContenu.valueOf(type.toUpperCase());
        } catch (IllegalArgumentException e) {
            return TypeContenu.TEXTE;
        }
    }

    private void validerContenuMessage(TypeContenu type, EnvoyerMessageRequest req) {
        switch (type) {
            case TEXTE:
                if (req.contenu() == null || req.contenu().isBlank()) {
                    throw new BadRequestException("Le contenu est obligatoire pour un message texte");
                }
                break;
            case VOCAL:
                if (req.urlFichier() == null || req.urlFichier().isBlank()) {
                    throw new BadRequestException("L'URL du fichier audio est obligatoire pour une note vocale");
                }
                if (req.dureeAudio() == null) {
                    throw new BadRequestException("La durée audio est obligatoire pour une note vocale");
                }
                break;
            case IMAGE:
                if (req.urlFichier() == null || req.urlFichier().isBlank()) {
                    throw new BadRequestException("L'URL de l'image est obligatoire");
                }
                break;
            default:
                break;
        }
    }

    private String libelleDuRole(String role) {
        return switch (role) {
            case "PRESIDENT"        -> "Président";
            case "TRESORIER"        -> "Trésorier";
            case "SECRETAIRE"       -> "Secrétaire";
            case "VICE_PRESIDENT"   -> "Vice-Président";
            case "ADMIN"            -> "Administrateur";
            case "ADMIN_ORGANISATION" -> "Administrateur";
            default                 -> role;
        };
    }

    // ── Conversions DTO ───────────────────────────────────────────────────────

    private ConversationResponse toConversationResponse(Conversation conv, UUID membreConnecteId) {
        List<MessageResponse> msgs = messageRepository
            .findByConversationPagine(conv.getId(), 0, PAGE_SIZE_DEFAULT)
            .stream().map(this::toMessageResponse).collect(Collectors.toList());

        List<ConversationResponse.ParticipantResponse> parts =
            participantRepository.findByConversation(conv.getId()).stream()
                .map(p -> ConversationResponse.ParticipantResponse.builder()
                    .membreId(p.getMembre().getId())
                    .prenom(p.getMembre().getPrenom())
                    .nom(p.getMembre().getNom())
                    .roleDansConversation(p.getRoleDansConversation())
                    .luJusqua(p.getLuJusqua())
                    .build())
                .collect(Collectors.toList());

        long nonLus = messageRepository.countNonLus(conv.getId(), membreConnecteId);

        return ConversationResponse.builder()
            .id(conv.getId())
            .typeConversation(conv.getTypeConversation().name())
            .titre(resolverTitre(conv, membreConnecteId))
            .statut(conv.getStatut().name())
            .roleCible(conv.getRoleCible())
            .organisationId(conv.getOrganisation().getId())
            .organisationNom(conv.getOrganisation().getNom())
            .dateCreation(conv.getDateCreation())
            .dernierMessageAt(conv.getDernierMessageAt())
            .nombreMessages(conv.getNombreMessages())
            .participants(parts)
            .messages(msgs)
            .nonLus(nonLus)
            .build();
    }

    private ConversationSummaryResponse toConversationSummary(Conversation conv, UUID membreConnecteId) {
        String apercu = messageRepository.findDernierMessage(conv.getId())
            .map(m -> {
                if (TypeContenu.VOCAL.equals(m.getTypeMessage())) return "🎤 Note vocale";
                if (TypeContenu.IMAGE.equals(m.getTypeMessage())) return "📷 Image";
                String c = m.getContenu();
                return c != null && c.length() > 100 ? c.substring(0, 97) + "..." : c;
            })
            .orElse(null);

        String dernierType = messageRepository.findDernierMessage(conv.getId())
            .map(m -> m.getTypeMessage().name()).orElse(null);

        long nonLus = messageRepository.countNonLus(conv.getId(), membreConnecteId);

        return ConversationSummaryResponse.builder()
            .id(conv.getId())
            .typeConversation(conv.getTypeConversation().name())
            .titre(resolverTitre(conv, membreConnecteId))
            .statut(conv.getStatut().name())
            .dernierMessageApercu(apercu)
            .dernierMessageType(dernierType)
            .dernierMessageAt(conv.getDernierMessageAt())
            .nonLus(nonLus)
            .organisationId(conv.getOrganisation().getId())
            .build();
    }

    private MessageResponse toMessageResponse(Message message) {
        String contenuAffiche = message.estSupprime()
            ? "[Message supprimé]"
            : message.getContenu();

        String parentApercu = null;
        UUID parentId = null;
        if (message.getMessageParent() != null) {
            parentId = message.getMessageParent().getId();
            String pc = message.getMessageParent().getContenu();
            parentApercu = pc != null && pc.length() > 100 ? pc.substring(0, 97) + "..." : pc;
        }

        return MessageResponse.builder()
            .id(message.getId())
            .typeMessage(message.getTypeMessage().name())
            .contenu(contenuAffiche)
            .urlFichier(message.estSupprime() ? null : message.getUrlFichier())
            .dureeAudio(message.getDureeAudio())
            .supprime(message.estSupprime())
            .expediteurId(message.getExpediteur().getId())
            .expediteurNom(message.getExpediteur().getNom())
            .expediteurPrenom(message.getExpediteur().getPrenom())
            .messageParentId(parentId)
            .messageParentApercu(parentApercu)
            .dateEnvoi(message.getDateCreation())
            .build();
    }

    private ContactPolicyResponse toContactPolicyResponse(ContactPolicy policy) {
        return ContactPolicyResponse.builder()
            .id(policy.getId())
            .organisationId(policy.getOrganisation().getId())
            .typePolitique(policy.getTypePolitique().name())
            .autoriserMembreVersMembre(Boolean.TRUE.equals(policy.getAutoriserMembreVersMembre()))
            .autoriserMembreVersRole(Boolean.TRUE.equals(policy.getAutoriserMembreVersRole()))
            .autoriserNotesVocales(Boolean.TRUE.equals(policy.getAutoriserNotesVocales()))
            .build();
    }

    /**
     * Résout le titre affiché pour une conversation.
     * Pour DIRECTE : "Prénom Nom" de l'autre participant.
     * Pour ROLE_CANAL : le titre du canal.
     */
    private String resolverTitre(Conversation conv, UUID membreConnecteId) {
        if (conv.getTitre() != null) return conv.getTitre();
        if (TypeConversation.DIRECTE.equals(conv.getTypeConversation())) {
            return participantRepository.findByConversation(conv.getId()).stream()
                .filter(p -> !p.getMembre().getId().equals(membreConnecteId))
                .findFirst()
                .map(p -> p.getMembre().getPrenom() + " " + p.getMembre().getNom())
                .orElse("Conversation");
        }
        return conv.getRoleCible();
    }
}
