package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.souscription.FormuleAbonnementResponse;
import dev.lions.unionflow.server.api.dto.souscription.SouscriptionDemandeRequest;
import dev.lions.unionflow.server.api.dto.souscription.SouscriptionStatutResponse;
import dev.lions.unionflow.server.api.enums.abonnement.PlageMembres;
import dev.lions.unionflow.server.api.enums.abonnement.StatutSouscription;
import dev.lions.unionflow.server.api.enums.abonnement.StatutValidationSouscription;
import dev.lions.unionflow.server.api.enums.abonnement.TypeFormule;
import dev.lions.unionflow.server.api.enums.abonnement.TypePeriodeAbonnement;
import dev.lions.unionflow.server.api.enums.abonnement.TypeOrganisationFacturation;
import dev.lions.unionflow.server.entity.FormuleAbonnement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.SouscriptionOrganisation;
import dev.lions.unionflow.server.repository.FormuleAbonnementRepository;
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.SouscriptionOrganisationRepository;
import dev.lions.unionflow.server.service.support.SecuriteHelper;
import dev.lions.unionflow.server.service.MembreKeycloakSyncService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.NotFoundException;
import org.jboss.logging.Logger;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * Service orchestrant le workflow de souscription/onboarding UnionFlow.
 *
 * <p>Cycle de vie d'une souscription :
 * <pre>
 *   creerDemande() → EN_ATTENTE_PAIEMENT
 *   initierPaiementWave() → PAIEMENT_INITIE (session Wave ouverte)
 *   confirmerPaiement() → PAIEMENT_CONFIRME (en attente SuperAdmin)
 *   approuver() → VALIDEE + activation du compte ADMIN_ORGANISATION
 *   rejeter() → REJETEE
 * </pre>
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2026-03-30
 */
@ApplicationScoped
public class SouscriptionService {

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

    /** Prix de base XOF/mois par (TypeFormule, PlageMembres). */
    private static final java.util.Map<String, BigDecimal> PRIX_BASE = buildPrixBase();

    @Inject
    SouscriptionOrganisationRepository souscriptionRepo;

    @Inject
    FormuleAbonnementRepository formuleRepo;

    @Inject
    WaveCheckoutService waveService;

    @Inject
    SecuriteHelper securiteHelper;

    @Inject
    MembreRepository membreRepository;

    @Inject
    OrganisationRepository organisationRepo;

    @Inject
    MembreService membreService;

    @Inject
    MembreOrganisationRepository membreOrganisationRepository;

    @Inject
    NotificationService notificationService;

    @Inject
    MembreKeycloakSyncService keycloakSyncService;

    @Inject
    EmailTemplateService emailTemplateService;

    // ── Catalogue ─────────────────────────────────────────────────────────────

    /**
     * Retourne toutes les formules du catalogue, triées par ordre d'affichage.
     *
     * <p>Endpoint public — PermitAll.
     */
    public List<FormuleAbonnementResponse> getFormules() {
        return formuleRepo.findAllActifOrderByOrdre()
                .stream()
                .map(this::toFormuleResponse)
                .collect(Collectors.toList());
    }

    // ── Création de la demande ─────────────────────────────────────────────────

    /**
     * Crée une demande de souscription pour une organisation.
     *
     * <p>Calcule le montant total selon la matrice tarifaire :
     * {@code montantTotal = prixBase × coeffOrg × coeffPeriode × nombreMois}
     *
     * @param request données de la demande (formule, plage, période, type org, orgId)
     * @return réponse avec le statut EN_ATTENTE_PAIEMENT et le montant calculé
     * @throws NotFoundException   si l'organisation ou la formule n'existent pas
     * @throws BadRequestException si une souscription active existe déjà pour l'org
     */
    @Transactional
    public SouscriptionStatutResponse creerDemande(SouscriptionDemandeRequest request) {
        LOG.infof("Création demande souscription — org=%s formule=%s plage=%s période=%s type=%s",
                request.getOrganisationId(), request.getTypeFormule(),
                request.getPlageMembres(), request.getTypePeriode(), request.getTypeOrganisation());

        UUID orgId = parseUuid(request.getOrganisationId(), "organisationId");
        Organisation org = organisationRepo.findByIdOptional(orgId)
                .orElseThrow(() -> new NotFoundException("Organisation introuvable: " + orgId));

        // Vérifier qu'il n'existe pas déjà une souscription non-rejetée pour cette org
        souscriptionRepo.find(
                "organisation.id = ?1 and statutValidation != ?2",
                orgId, StatutValidationSouscription.REJETEE)
                .firstResultOptional()
                .ifPresent(s -> {
                    throw new BadRequestException(
                            "Une souscription en cours existe déjà pour cette organisation (statut: "
                            + s.getStatutValidation() + ")");
                });

        TypeFormule typeFormule = parseEnum(TypeFormule.class, request.getTypeFormule(), "typeFormule");
        PlageMembres plage = parseEnum(PlageMembres.class, request.getPlageMembres(), "plageMembres");
        TypePeriodeAbonnement periode = parseEnum(TypePeriodeAbonnement.class, request.getTypePeriode(), "typePeriode");

        // typeOrganisation est optionnel : si absent, on le dérive depuis l'entité Organisation
        String typeOrgStr = (request.getTypeOrganisation() != null && !request.getTypeOrganisation().isBlank())
                ? request.getTypeOrganisation()
                : (org.getTypeOrganisation() != null ? org.getTypeOrganisation() : "ASSOCIATION");
        TypeOrganisationFacturation typeOrg = parseEnum(TypeOrganisationFacturation.class, typeOrgStr, "typeOrganisation");

        FormuleAbonnement formule = formuleRepo.findByCodeAndPlage(typeFormule, plage)
                .orElseThrow(() -> new NotFoundException(
                        "Formule introuvable pour code=" + typeFormule + " et plage=" + plage));

        // Calcul du montant total
        BigDecimal prixBase = formule.getPrixMensuel();
        BigDecimal coeffOrg = typeOrg.getCoefficient(typeFormule.name());
        BigDecimal coeffPeriode = periode.getCoefficient();
        int nombreMois = periode.getNombreMois();
        BigDecimal coeffTotal = coeffOrg.multiply(coeffPeriode);
        BigDecimal montantTotal = prixBase
                .multiply(coeffTotal)
                .multiply(BigDecimal.valueOf(nombreMois))
                .setScale(0, RoundingMode.HALF_UP);

        LOG.debugf("Calcul: prixBase=%s × coeffOrg=%s × coeffPériode=%s × %d mois = %s XOF",
                prixBase, coeffOrg, coeffPeriode, nombreMois, montantTotal);

        LocalDate dateDebut = LocalDate.now();
        LocalDate dateFin = dateDebut.plusMonths(nombreMois);

        SouscriptionOrganisation souscription = SouscriptionOrganisation.builder()
                .organisation(org)
                .formule(formule)
                .typePeriode(periode)
                .plage(plage)
                .typeOrganisationSouscription(typeOrg)
                .coefficientApplique(coeffTotal)
                .montantTotal(montantTotal)
                .statutValidation(StatutValidationSouscription.EN_ATTENTE_PAIEMENT)
                .statut(StatutSouscription.EN_ATTENTE)
                .dateDebut(dateDebut)
                .dateFin(dateFin)
                .quotaMax(formule.getMaxMembres())
                .build();

        souscriptionRepo.persist(souscription);
        LOG.infof("Souscription créée id=%s montant=%s XOF", souscription.getId(), montantTotal);

        return toStatutResponse(souscription, null);
    }

    // ── Consultation ──────────────────────────────────────────────────────────

    /**
     * Retourne la souscription de l'organisation du membre connecté.
     */
    public SouscriptionStatutResponse getMaSouscription() {
        UUID membreId = securiteHelper.resolveMembreId();
        Membre membre = membreRepository.findByIdOptional(membreId)
                .orElseThrow(() -> new NotFoundException("Membre introuvable"));

        // Trouver l'organisation du membre (on prend la première organisation admin trouvée)
        SouscriptionOrganisation souscription = souscriptionRepo
                .find("organisation.id IN (SELECT mo.organisation.id FROM MembreOrganisation mo WHERE mo.membre.id = ?1) ORDER BY dateCreation DESC",
                        membreId)
                .firstResultOptional()
                .orElseThrow(() -> new NotFoundException("Aucune souscription trouvée pour ce membre"));

        return toStatutResponse(souscription, null);
    }

    /**
     * Retourne une souscription par son ID (usage interne / admin).
     */
    public SouscriptionStatutResponse getSouscription(UUID souscriptionId) {
        SouscriptionOrganisation s = findSouscription(souscriptionId);
        return toStatutResponse(s, null);
    }

    // ── Paiement Wave ──────────────────────────────────────────────────────────

    /**
     * Initie une session de paiement Wave pour une souscription en attente.
     *
     * @param souscriptionId UUID de la souscription
     * @return réponse avec le statut PAIEMENT_INITIE et le waveLaunchUrl
     */
    @Transactional
    public SouscriptionStatutResponse initierPaiementWave(UUID souscriptionId) {
        LOG.infof("Initiation paiement Wave — souscriptionId=%s", souscriptionId);

        SouscriptionOrganisation souscription = findSouscription(souscriptionId);

        if (!souscription.getStatutValidation().peutInitierPaiement()) {
            throw new BadRequestException("Impossible d'initier le paiement depuis le statut: "
                    + souscription.getStatutValidation());
        }

        BigDecimal montant = souscription.getMontantTotal();
        if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) {
            throw new BadRequestException("Montant de souscription invalide: " + montant);
        }

        // Wave attend le montant en string sans décimales pour XOF
        String amountStr = montant.setScale(0, RoundingMode.HALF_UP).toPlainString();
        String clientRef = "SOUSCRIPTION-" + souscriptionId;
        String successUrl = "unionflow://payment/success?id=" + souscriptionId;
        String errorUrl = "unionflow://payment/error?id=" + souscriptionId;

        WaveCheckoutService.WaveCheckoutSessionResponse session;
        try {
            session = waveService.createSession(amountStr, "XOF", successUrl, errorUrl, clientRef, null);
        } catch (WaveCheckoutService.WaveCheckoutException e) {
            LOG.errorf("Erreur Wave Checkout pour souscription %s: %s", souscriptionId, e.getMessage());
            throw new BadRequestException("Erreur de création de session Wave: " + e.getMessage());
        }

        souscription.setWaveSessionId(session.id);
        souscription.setWaveCheckoutUrl(session.waveLaunchUrl);
        souscription.setStatutValidation(StatutValidationSouscription.PAIEMENT_INITIE);
        souscriptionRepo.persist(souscription);

        LOG.infof("Session Wave créée id=%s pour souscription=%s", session.id, souscriptionId);
        return toStatutResponse(souscription, session.waveLaunchUrl);
    }

    /**
     * Confirme la réception d'un paiement Wave (appelé depuis le deep link ou webhook).
     *
     * <p>Passe la souscription en PAIEMENT_CONFIRME et notifie les SuperAdmins.
     *
     * @param souscriptionId UUID de la souscription
     * @param waveRef        identifiant de transaction Wave retourné par le deep link
     */
    @Transactional
    public void confirmerPaiement(UUID souscriptionId, String waveRef) {
        LOG.infof("Confirmation paiement — souscriptionId=%s waveRef=%s", souscriptionId, waveRef);

        SouscriptionOrganisation souscription = findSouscription(souscriptionId);

        if (souscription.getStatutValidation() != StatutValidationSouscription.PAIEMENT_INITIE
                && souscription.getStatutValidation() != StatutValidationSouscription.EN_ATTENTE_PAIEMENT) {
            throw new BadRequestException("Impossible de confirmer depuis le statut: "
                    + souscription.getStatutValidation());
        }

        LocalDate dateDebut = LocalDate.now();
        LocalDate dateFin = dateDebut.plusMonths(souscription.getTypePeriode().getNombreMois());

        souscription.setReferencePaiementWave(waveRef);
        souscription.setStatutValidation(StatutValidationSouscription.VALIDEE);
        souscription.setStatut(StatutSouscription.ACTIVE);
        souscription.setDateDernierPaiement(dateDebut);
        souscription.setDateDebut(dateDebut);
        souscription.setDateFin(dateFin);
        souscription.setDateProchainePaiement(dateFin);
        souscriptionRepo.persist(souscription);

        // Auto-activation du compte admin de l'organisation (non-bloquant : la souscription est déjà commitée)
        try {
            activerAdminOrganisation(souscription.getOrganisation().getId());
            LOG.infof("Paiement confirmé et compte activé pour souscription=%s", souscriptionId);
        } catch (Exception e) {
            LOG.errorf("Activation compte échouée après paiement souscription=%s: %s — la souscription reste VALIDEE", souscriptionId, e.getMessage());
        }

        // Email de confirmation de souscription (non bloquant)
        envoyerEmailSouscriptionActive(souscription, dateDebut, dateFin);
    }

    // ── Validation SuperAdmin ──────────────────────────────────────────────────

    /**
     * Liste toutes les souscriptions (SuperAdmin), avec filtre optionnel par organisation.
     */
    public List<SouscriptionStatutResponse> listerToutes(UUID organisationId, int page, int size) {
        if (organisationId != null) {
            return souscriptionRepo
                    .find("organisation.id = ?1 order by dateCreation desc", organisationId)
                    .page(page, size)
                    .list()
                    .stream()
                    .map(s -> toStatutResponse(s, null))
                    .collect(Collectors.toList());
        }
        return souscriptionRepo
                .findAll()
                .page(page, size)
                .list()
                .stream()
                .map(s -> toStatutResponse(s, null))
                .collect(Collectors.toList());
    }

    /**
     * Retourne la souscription active d'une organisation (SuperAdmin).
     */
    public SouscriptionStatutResponse obtenirActiveParOrganisation(UUID organisationId) {
        return souscriptionRepo
                .find("organisation.id = ?1 and statut = ?2 order by dateCreation desc",
                        organisationId, dev.lions.unionflow.server.api.enums.abonnement.StatutSouscription.ACTIVE)
                .firstResultOptional()
                .map(s -> toStatutResponse(s, null))
                .orElse(null);
    }

    /**
     * Liste les souscriptions en attente de validation SuperAdmin.
     */
    public List<SouscriptionStatutResponse> getSouscriptionsEnAttenteValidation() {
        return souscriptionRepo
                .find("statutValidation = ?1 order by dateCreation asc",
                        StatutValidationSouscription.PAIEMENT_CONFIRME)
                .list()
                .stream()
                .map(s -> toStatutResponse(s, null))
                .collect(Collectors.toList());
    }

    /**
     * Approuve une souscription et active le compte de l'administrateur d'organisation.
     *
     * <p>Actions effectuées :
     * <ol>
     *   <li>Passe {@code statutValidation} à VALIDEE</li>
     *   <li>Passe {@code statut} à ACTIVE</li>
     *   <li>Calcule et persiste les dates de début/fin</li>
     *   <li>Appelle {@link MembreService#activerMembre(UUID)} pour l'admin de l'org</li>
     * </ol>
     *
     * @param souscriptionId UUID de la souscription à approuver
     * @param superAdminId   UUID du SuperAdmin qui valide
     */
    @Transactional
    public void approuver(UUID souscriptionId, UUID superAdminId) {
        LOG.infof("Approbation souscription=%s par superAdmin=%s", souscriptionId, superAdminId);

        SouscriptionOrganisation souscription = findSouscription(souscriptionId);

        if (souscription.getStatutValidation() != StatutValidationSouscription.PAIEMENT_CONFIRME
                && souscription.getStatutValidation() != StatutValidationSouscription.VALIDEE) {
            throw new BadRequestException("Impossible d'approuver depuis le statut: "
                    + souscription.getStatutValidation());
        }
        if (souscription.getStatutValidation() == StatutValidationSouscription.VALIDEE) {
            LOG.infof("Souscription %s déjà validée automatiquement — skip", souscriptionId);
            return;
        }

        LocalDate dateDebut = LocalDate.now();
        LocalDate dateFin = dateDebut.plusMonths(souscription.getTypePeriode().getNombreMois());

        souscription.setStatutValidation(StatutValidationSouscription.VALIDEE);
        souscription.setStatut(StatutSouscription.ACTIVE);
        souscription.setDateValidation(dateDebut);
        souscription.setValidatedById(superAdminId);
        souscription.setDateDebut(dateDebut);
        souscription.setDateFin(dateFin);
        souscription.setDateDernierPaiement(dateDebut);
        souscription.setDateProchainePaiement(dateFin);
        souscriptionRepo.persist(souscription);

        // Activer le membre admin de l'organisation
        activerAdminOrganisation(souscription.getOrganisation().getId());

        // Email de confirmation de souscription (non bloquant)
        envoyerEmailSouscriptionActive(souscription, dateDebut, dateFin);

        LOG.infof("Souscription %s approuvée — compte actif jusqu'au %s", souscriptionId, dateFin);
    }

    /**
     * Rejette une souscription avec un commentaire obligatoire.
     *
     * @param souscriptionId UUID de la souscription à rejeter
     * @param superAdminId   UUID du SuperAdmin qui rejette
     * @param commentaire    motif de refus (obligatoire)
     */
    @Transactional
    public void rejeter(UUID souscriptionId, UUID superAdminId, String commentaire) {
        LOG.infof("Rejet souscription=%s par superAdmin=%s — motif: %s",
                souscriptionId, superAdminId, commentaire);

        if (commentaire == null || commentaire.isBlank()) {
            throw new BadRequestException("Le commentaire de rejet est obligatoire");
        }

        SouscriptionOrganisation souscription = findSouscription(souscriptionId);

        if (souscription.getStatutValidation().isTerminal()) {
            throw new BadRequestException("La souscription est déjà dans un état terminal: "
                    + souscription.getStatutValidation());
        }

        souscription.setStatutValidation(StatutValidationSouscription.REJETEE);
        souscription.setStatut(StatutSouscription.RESILIEE);
        souscription.setDateValidation(LocalDate.now());
        souscription.setValidatedById(superAdminId);
        souscription.setCommentaireRejet(
                commentaire.length() > 500 ? commentaire.substring(0, 500) : commentaire);
        souscriptionRepo.persist(souscription);

        LOG.infof("Souscription %s rejetée", souscriptionId);
    }

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

    private SouscriptionOrganisation findSouscription(UUID id) {
        return souscriptionRepo.findByIdOptional(id)
                .orElseThrow(() -> new NotFoundException("Souscription introuvable: " + id));
    }

    /**
     * Active le premier membre avec le rôle d'admin trouvé pour l'organisation.
     * Si aucun lien membre-organisation n'existe, tente de créer le lien pour le
     * caller courant (email JWT) avant d'activer.
     */
    /**
     * Active l'admin de l'organisation après paiement de la souscription.
     *
     * <p>Stratégie : identifier le membre qui a payé (via JWT) et le promouvoir
     * ORGADMIN. Si le membre n'a pas de lien MembreOrganisation, le créer.
     *
     * <p>Cascades : promouvoirAdminOrganisation (DB) + sync Keycloak ADMIN_ORGANISATION.
     */
    private void activerAdminOrganisation(UUID organisationId) {
        // ── 1. Identifier le caller (le membre qui a payé) ─────────────────────
        String email = securiteHelper.resolveEmail();
        if (email == null) {
            LOG.warnf("activerAdminOrganisation: impossible de résoudre l'email JWT pour org=%s", organisationId);
            return;
        }

        Membre caller = membreRepository.findByEmail(email).orElse(null);
        if (caller == null) {
            LOG.warnf("activerAdminOrganisation: aucun membre trouvé pour email=%s", email);
            return;
        }

        Organisation org = organisationRepo.findByIdOptional(organisationId).orElse(null);
        if (org == null) {
            LOG.warnf("activerAdminOrganisation: organisation introuvable org=%s", organisationId);
            return;
        }

        LOG.infof("activerAdminOrganisation: caller=%s (%s) pour org=%s (%s)",
                caller.getId(), email, organisationId, org.getNom());

        // ── 2. S'assurer que le caller a un lien MembreOrganisation ────────────
        var lienOpt = membreOrganisationRepository
                .findByMembreIdAndOrganisationId(caller.getId(), organisationId);

        if (lienOpt.isEmpty()) {
            LOG.infof("activerAdminOrganisation: pas de lien existant — création pour membre=%s org=%s", caller.getId(), organisationId);
            membreService.lierMembreOrganisationEtIncrementerQuota(caller, organisationId, "ACTIF");
        } else {
            // Activer le lien existant s'il est en attente
            var lien = lienOpt.get();
            if (lien.getStatutMembre() != dev.lions.unionflow.server.api.enums.membre.StatutMembre.ACTIF) {
                lien.setStatutMembre(dev.lions.unionflow.server.api.enums.membre.StatutMembre.ACTIF);
                LOG.infof("activerAdminOrganisation: lien existant activé (statut→ACTIF) pour membre=%s", caller.getId());
            }
        }

        // ── 3. Promouvoir en ORGADMIN (DB + Keycloak) ──────────────────────────
        try {
            membreService.promouvoirAdminOrganisation(caller.getId());
            LOG.infof("activerAdminOrganisation: membre %s promu ORGADMIN en DB", caller.getId());
        } catch (Exception e) {
            LOG.errorf("activerAdminOrganisation: échec promotion DB pour membre=%s : %s", caller.getId(), e.getMessage());
        }

        try {
            keycloakSyncService.promouvoirAdminOrganisationDansKeycloak(caller.getId());
            LOG.infof("activerAdminOrganisation: rôle Keycloak ADMIN_ORGANISATION assigné pour membre=%s", caller.getId());
        } catch (Exception e) {
            LOG.warnf("activerAdminOrganisation: sync Keycloak ORGADMIN échouée pour membre=%s (non-bloquant): %s",
                    caller.getId(), e.getMessage());
        }
    }

    private void notifierSuperAdmins(SouscriptionOrganisation souscription) {
        // Notification simple via NotificationService — non bloquant
        LOG.infof("Notification SuperAdmins: nouvelle souscription à valider — org=%s montant=%s XOF",
                souscription.getOrganisation().getNom(),
                souscription.getMontantTotal());
        // L'envoi réel d'email peut être ajouté ici via notificationService
    }

    // ── Mapping ───────────────────────────────────────────────────────────────

    private SouscriptionStatutResponse toStatutResponse(
            SouscriptionOrganisation s, String waveLaunchUrl) {

        SouscriptionStatutResponse r = new SouscriptionStatutResponse();
        r.setSouscriptionId(s.getId() != null ? s.getId().toString() : null);
        r.setStatutValidation(s.getStatutValidation() != null ? s.getStatutValidation().name() : null);
        r.setStatutLibelle(s.getStatutValidation() != null ? s.getStatutValidation().getLibelle() : null);
        r.setTypeFormule(s.getFormule() != null ? s.getFormule().getCode().name() : null);
        r.setPlageMembres(s.getPlage() != null ? s.getPlage().name() : null);
        r.setPlageLibelle(s.getPlage() != null ? s.getPlage().getLibelle() : null);
        r.setTypePeriode(s.getTypePeriode() != null ? s.getTypePeriode().name() : null);
        r.setTypeOrganisation(s.getTypeOrganisationSouscription() != null
                ? s.getTypeOrganisationSouscription().name() : null);
        r.setMontantTotal(s.getMontantTotal());
        r.setMontantMensuelBase(s.getFormule() != null ? s.getFormule().getPrixMensuel() : null);
        r.setCoefficientApplique(s.getCoefficientApplique());
        r.setWaveSessionId(s.getWaveSessionId());
        // Utilise le waveLaunchUrl passé en paramètre, sinon l'URL stockée en base (pour récupération PAYMENT_INITIATED)
        r.setWaveLaunchUrl(waveLaunchUrl != null ? waveLaunchUrl : s.getWaveCheckoutUrl());
        r.setDateDebut(s.getDateDebut());
        r.setDateFin(s.getDateFin());
        r.setDateValidation(s.getDateValidation());
        r.setCommentaireRejet(s.getCommentaireRejet());
        r.setStatut(s.getStatut() != null ? s.getStatut().name() : null);
        if (s.getOrganisation() != null) {
            r.setOrganisationId(s.getOrganisation().getId().toString());
            r.setOrganisationNom(s.getOrganisation().getNom());
        }
        // ── Quota & Option C ──────────────────────────────────────────────────────
        r.setQuotaMax(s.getQuotaMax());
        r.setQuotaUtilise(s.getQuotaUtilise() != null ? s.getQuotaUtilise() : 0);
        r.setQuotaDepasse(s.isQuotaDepasse());
        if (s.getQuotaMax() != null && s.getQuotaUtilise() != null) {
            r.setQuotaRestant(Math.max(0, s.getQuotaMax() - s.getQuotaUtilise()));
        }
        if (s.getDateFin() != null) {
            r.setJoursAvantExpiration(java.time.LocalDate.now().until(s.getDateFin(), java.time.temporal.ChronoUnit.DAYS));
        }
        if (s.getFormule() != null) {
            FormuleAbonnement f = s.getFormule();
            r.setPlanCommercial(f.getPlanCommercial());
            r.setApiAccess(Boolean.TRUE.equals(f.getApiAccess()));
            r.setFederationAccess(Boolean.TRUE.equals(f.getFederationAccess()));
            r.setSupportPrioritaire(Boolean.TRUE.equals(f.getSupportPrioritaire()));
            r.setSlaGaranti(f.getSlaGaranti());
            r.setMaxAdmins(f.getMaxAdmins());
            r.setNiveauReporting(f.getNiveauReporting());
        }
        return r;
    }

    private FormuleAbonnementResponse toFormuleResponse(FormuleAbonnement f) {
        FormuleAbonnementResponse r = new FormuleAbonnementResponse();
        r.setCode(f.getCode().name());
        r.setLibelle(f.getLibelle());
        r.setDescription(f.getDescription());
        r.setPlage(f.getPlage().name());
        r.setPlageLibelle(f.getPlage().getLibelle());
        r.setMinMembres(f.getPlage().getMin());
        r.setMaxMembres(f.getPlage().getMaxAffichage());
        r.setPrixMensuel(f.getPrixMensuel());
        r.setPrixAnnuel(f.getPrixAnnuel());
        r.setOrdreAffichage(f.getOrdreAffichage() != null ? f.getOrdreAffichage() : 0);
        // Champs Option C (V19)
        r.setPlanCommercial(f.getPlanCommercial());
        r.setNiveauReporting(f.getNiveauReporting());
        r.setApiAccess(Boolean.TRUE.equals(f.getApiAccess()));
        r.setFederationAccess(Boolean.TRUE.equals(f.getFederationAccess()));
        r.setSupportPrioritaire(Boolean.TRUE.equals(f.getSupportPrioritaire()));
        r.setSlaGaranti(f.getSlaGaranti());
        r.setMaxAdmins(f.getMaxAdmins() != null ? f.getMaxAdmins() : -1);
        return r;
    }

    // ── Utilitaires ───────────────────────────────────────────────────────────

    private UUID parseUuid(String value, String fieldName) {
        try {
            return UUID.fromString(value);
        } catch (Exception e) {
            throw new BadRequestException("Format UUID invalide pour " + fieldName + ": " + value);
        }
    }

    private <T extends Enum<T>> T parseEnum(Class<T> enumClass, String value, String fieldName) {
        try {
            return Enum.valueOf(enumClass, value.toUpperCase());
        } catch (Exception e) {
            throw new BadRequestException("Valeur invalide pour " + fieldName + ": " + value
                    + " — valeurs acceptées: " + java.util.Arrays.toString(enumClass.getEnumConstants()));
        }
    }

    // ── Email notifications ───────────────────────────────────────────────────

    private void envoyerEmailSouscriptionActive(SouscriptionOrganisation s,
                                                 LocalDate dateDebut, LocalDate dateFin) {
        try {
            String email = securiteHelper.resolveEmail();
            if (email == null) return;
            Membre admin = membreRepository.findByEmail(email).orElse(null);
            if (admin == null || admin.getEmail() == null) return;

            FormuleAbonnement f = s.getFormule();
            emailTemplateService.envoyerConfirmationSouscription(
                    admin.getEmail(),
                    (admin.getPrenom() != null ? admin.getPrenom() : "") + " " + (admin.getNom() != null ? admin.getNom() : ""),
                    s.getOrganisation() != null ? s.getOrganisation().getNom() : "",
                    f != null && f.getLibelle() != null ? f.getLibelle() : "",
                    s.getMontantTotal() != null ? s.getMontantTotal() : BigDecimal.ZERO,
                    s.getTypePeriode() != null ? s.getTypePeriode().name() : "MENSUEL",
                    dateDebut, dateFin,
                    f != null ? f.getMaxMembres() : null,
                    f != null ? f.getMaxStockageMo() : null,
                    f != null && Boolean.TRUE.equals(f.getApiAccess()),
                    f != null && Boolean.TRUE.equals(f.getSupportPrioritaire()));
        } catch (Exception e) {
            LOG.warnf("Email souscription ignoré (non bloquant) : %s", e.getMessage());
        }
    }

    // ── Matrice tarifaire de référence ────────────────────────────────────────

    /**
     * Construit la map de prix de base XOF/mois (TypeFormule × PlageMembres).
     * Ces valeurs sont une référence — les prix réels sont stockés en base via
     * la table formules_abonnement et le seed V11.
     */
    private static java.util.Map<String, BigDecimal> buildPrixBase() {
        java.util.Map<String, BigDecimal> m = new java.util.HashMap<>();
        // PETITE (1-100)
        m.put("BASIC_PETITE",    new BigDecimal("3000"));
        m.put("STANDARD_PETITE", new BigDecimal("6000"));
        m.put("PREMIUM_PETITE",  new BigDecimal("10000"));
        // MOYENNE (101-500)
        m.put("BASIC_MOYENNE",    new BigDecimal("8000"));
        m.put("STANDARD_MOYENNE", new BigDecimal("15000"));
        m.put("PREMIUM_MOYENNE",  new BigDecimal("25000"));
        // GRANDE (501-2000)
        m.put("BASIC_GRANDE",    new BigDecimal("20000"));
        m.put("STANDARD_GRANDE", new BigDecimal("35000"));
        m.put("PREMIUM_GRANDE",  new BigDecimal("60000"));
        // TRES_GRANDE (2000+)
        m.put("BASIC_TRES_GRANDE",    new BigDecimal("50000"));
        m.put("STANDARD_TRES_GRANDE", new BigDecimal("80000"));
        m.put("PREMIUM_TRES_GRANDE",  new BigDecimal("120000"));
        return java.util.Collections.unmodifiableMap(m);
    }
}
