package dev.lions.unionflow.client.view;

import java.io.OutputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;

import dev.lions.unionflow.server.api.dto.cotisation.request.CreateCotisationRequest;
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
import dev.lions.unionflow.server.api.dto.paiement.request.InitierPaiementEnLigneRequest;
import dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse;
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse;
import dev.lions.unionflow.client.service.CotisationService;
import dev.lions.unionflow.client.service.ErrorHandlerService;
import dev.lions.unionflow.client.service.ExportClientService;
import dev.lions.unionflow.client.service.MembreService;
import dev.lions.unionflow.client.service.NotificationClientService;
import dev.lions.unionflow.client.service.PaiementClientService;
import dev.lions.unionflow.client.service.RetryService;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.faces.context.ExternalContext;
import jakarta.inject.Inject;
import jakarta.inject.Named;

/**
 * Bean JSF pour la gestion des cotisations
 * Refactorisé pour utiliser directement CotisationResponse et se connecter au backend
 * 
 * @author UnionFlow Team
 * @version 2.0
 */
@Named("cotisationsBean")
@ViewScoped
public class CotisationsBean implements Serializable {
    
    private static final long serialVersionUID = 1L;
    private static final Logger LOG = Logger.getLogger(CotisationsBean.class);
    
    @Inject
    @RestClient
    private CotisationService cotisationService;

    @Inject
    @RestClient
    private MembreService membreService;

    @Inject
    @RestClient
    private NotificationClientService notificationService;

    @Inject
    @RestClient
    private ExportClientService exportService;

    @Inject
    ErrorHandlerService errorHandler;

    @Inject
    RetryService retryService;

    @Inject
    @RestClient
    private PaiementClientService paiementClientService;

    @Inject
    SecurityIdentity securityIdentity;

    // null = vue admin (toutes les cotisations) ; non-null = vue membre (cotisations du membre)
    private UUID membreConnecteId;

    // ── État du paiement Wave QR ──────────────────────────────────────────────
    /** UUID de l'IntentionPaiement en cours (retourné par clientReference) */
    private UUID waveIntentionId;
    /** wave_launch_url à encoder en QR code (JavaScript côté client) */
    private String waveLaunchUrl;
    /** Statut de l'intention : INITIEE | EN_COURS | COMPLETEE | EXPIREE | ECHOUEE */
    private String waveStatut;
    /** Indique si le dialog QR Wave est ouvert (contrôle le p:poll) */
    private boolean waveDialogOuvert = false;

    // Données principales - Utilisation directe de CotisationResponse
    private List<CotisationResponse> toutesLesCotisations;
    private List<CotisationResponse> cotisationsFiltrees;
    private List<CotisationResponse> cotisationsSelectionnees;
    private CotisationResponse cotisationSelectionnee;
    
    // Champs du dialogue paiement (évite de polluer CotisationResponse)
    private String methodePaiementDialog;
    private String referencePaiementDialog;
    private String observationsDialog;
    private BigDecimal montantPaiementPartiel;

    // Formulaire nouvelle cotisation
    private NouvelleCotisation nouvelleCotisation;
    
    // Filtres
    private Filtres filtres;
    
    // Statistiques
    private StatistiquesFinancieres statistiques;
    
    // Analytics
    private List<EvolutionPaiement> evolutionPaiements;
    private List<RepartitionMethode> repartitionMethodes;
    private List<RappelCotisation> rappelsEnAttente;
    
    @PostConstruct
    public void init() {
        initializeFiltres();
        resoudreMembreConnecte();
        chargerCotisations();
        chargerStatistiques();
        initializeNouvelleCotisation();
        chargerEvolutionPaiements();
        chargerRepartitionMethodes();
        chargerRappels();
        appliquerFiltres();
    }
    
    private void initializeFiltres() {
        filtres = new Filtres();
        cotisationsSelectionnees = new ArrayList<>();
    }

    /**
     * Détermine si l'utilisateur est admin/trésorier (vue globale) ou un membre ordinaire (vue personnelle).
     * Positionne {@code membreConnecteId} : null = vue admin, UUID = vue membre.
     */
    private void resoudreMembreConnecte() {
        if (isAdminOuTresorier()) {
            membreConnecteId = null; // vue globale
            return;
        }
        try {
            var moi = retryService.executeWithRetrySupplier(
                () -> membreService.obtenirMembreConnecte(),
                "résolution du membre connecté"
            );
            membreConnecteId = (moi != null) ? moi.getId() : null;
            LOG.infof("Mode membre: chargement des cotisations pour le membre %s", membreConnecteId);
        } catch (Exception e) {
            LOG.warnf(e, "Impossible de récupérer le membre connecté — mode admin par défaut");
            membreConnecteId = null;
        }
    }

    private boolean isAdminOuTresorier() {
        if (securityIdentity == null || securityIdentity.isAnonymous()) return false;
        var roles = securityIdentity.getRoles();
        return roles != null && (roles.contains("SUPER_ADMIN")
                || roles.contains("ADMIN_ORGANISATION")
                || roles.contains("TRESORIER"));
    }

    /** Exposé en EL pour adapter l'affichage (titres, colonnes admin). */
    public boolean isVueAdmin() {
        return membreConnecteId == null;
    }
    
    /**
     * Charge les cotisations depuis le backend.
     * Mode admin : toutes les cotisations. Mode membre : uniquement les siennes.
     */
    private void chargerCotisations() {
        toutesLesCotisations = new ArrayList<>();
        try {
            if (membreConnecteId != null) {
                // Vue membre — uniquement ses propres cotisations
                toutesLesCotisations = retryService.executeWithRetrySupplier(
                    () -> cotisationService.obtenirParMembre(membreConnecteId, 0, 200),
                    "chargement des cotisations du membre"
                );
            } else {
                // Vue admin/trésorier — toutes les cotisations
                toutesLesCotisations = retryService.executeWithRetrySupplier(
                    () -> cotisationService.listerToutes(0, 1000),
                    "chargement de toutes les cotisations"
                );
            }
            LOG.infof("Cotisations chargées: %d cotisations", toutesLesCotisations.size());
        } catch (Exception e) {
            LOG.errorf(e, "Erreur lors du chargement des cotisations");
            errorHandler.handleException(e, "lors du chargement des cotisations", null);
        }
    }
    
    /**
     * Charge les statistiques depuis le backend
     */
    private void chargerStatistiques() {
        statistiques = new StatistiquesFinancieres();
        try {
            Map<String, Object> statsBackend = cotisationService.obtenirStatistiques();
            
            // Extraction des statistiques du backend
            Long cotisationsEnRetard = ((Number) statsBackend.getOrDefault("cotisationsEnRetard", 0L)).longValue();
            Double tauxPaiement = ((Number) statsBackend.getOrDefault("tauxPaiement", 0.0)).doubleValue();
            
            // Calcul des montants depuis les cotisations réelles
            BigDecimal totalCollecte = toutesLesCotisations.stream()
                .filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut()))
                .map(c -> c.getMontantPaye() != null ? c.getMontantPaye() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
            
            BigDecimal montantRetard = toutesLesCotisations.stream()
                .filter(c -> "EN_RETARD".equals(c.getStatut()))
                .map(c -> c.getMontantDu() != null ? c.getMontantDu() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
            
            statistiques.setTotalCollecte(totalCollecte);
            statistiques.setObjectifAnnuel(totalCollecte.multiply(new BigDecimal("1.3")));
            statistiques.setTauxRecouvrement(tauxPaiement);
            statistiques.setCotisationsEnRetard(cotisationsEnRetard.intValue());
            statistiques.setMontantRetard(montantRetard);
            
            // Moyenne mensuelle basée sur les 12 derniers mois
            BigDecimal moyenneMensuelle = totalCollecte.divide(new BigDecimal("12"), 2, java.math.RoundingMode.HALF_UP);
            statistiques.setMoyenneMensuelle(moyenneMensuelle);
            
            LOG.infof("Statistiques chargées: Total=%s, Taux=%.2f%%", totalCollecte, tauxPaiement);
        } catch (Exception e) {
            LOG.errorf(e, "Erreur lors du chargement des statistiques");
            errorHandler.handleException(e, "lors du chargement des statistiques", null);
            statistiques.setTotalCollecte(BigDecimal.ZERO);
            statistiques.setObjectifAnnuel(BigDecimal.ZERO);
            statistiques.setTauxRecouvrement(0.0);
            statistiques.setCotisationsEnRetard(0);
            statistiques.setMontantRetard(BigDecimal.ZERO);
            statistiques.setMoyenneMensuelle(BigDecimal.ZERO);
        }
    }
    
    /**
     * Calcule l'évolution des paiements depuis les données réelles
     */
    private void chargerEvolutionPaiements() {
        evolutionPaiements = new ArrayList<>();
        try {
            // Récupérer les cotisations payées des 12 derniers mois
            LocalDate maintenant = LocalDate.now();
            String[] moisNoms = {"Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Jul", "Aoû", "Sep", "Oct", "Nov", "Déc"};
            
            for (int i = 11; i >= 0; i--) {
                LocalDate moisDate = maintenant.minusMonths(i);
                Month mois = moisDate.getMonth();
                int annee = moisDate.getYear();
                
                BigDecimal montantMois = toutesLesCotisations.stream()
                    .filter(c -> c.getDatePaiement() != null 
                        && c.getDatePaiement().getYear() == annee
                        && c.getDatePaiement().getMonth() == mois
                        && ("PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut())))
                    .map(c -> c.getMontantPaye() != null ? c.getMontantPaye() : BigDecimal.ZERO)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
                
                EvolutionPaiement evolution = new EvolutionPaiement();
                evolution.setMois(moisNoms[mois.getValue() - 1]);
                evolution.setMontant(montantMois);
                evolution.setHauteur(montantMois.compareTo(BigDecimal.ZERO) > 0 
                    ? (int) (montantMois.divide(new BigDecimal("50000"), 0, java.math.RoundingMode.HALF_UP).intValue())
                    : 0);
                evolutionPaiements.add(evolution);
            }
        } catch (Exception e) {
            LOG.errorf(e, "Erreur lors du calcul de l'évolution des paiements");
        }
    }
    
    /**
     * Calcule la répartition par méthode de paiement depuis les données réelles
     */
    private void chargerRepartitionMethodes() {
        repartitionMethodes = new ArrayList<>();
        try {
            BigDecimal totalPaiements = toutesLesCotisations.stream()
                .filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut()))
                .map(c -> c.getMontantPaye() != null ? c.getMontantPaye() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

            if (totalPaiements.compareTo(BigDecimal.ZERO) == 0) return;

            // Grouper par modePaiementLibelle (champ disponible dans CotisationResponse)
            Map<String, BigDecimal> parMethode = toutesLesCotisations.stream()
                .filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut()))
                .collect(Collectors.groupingBy(
                    c -> c.getModePaiementLibelle() != null ? c.getModePaiementLibelle() : "Non défini",
                    Collectors.reducing(BigDecimal.ZERO,
                        c -> c.getMontantPaye() != null ? c.getMontantPaye() : BigDecimal.ZERO,
                        BigDecimal::add)));

            for (Map.Entry<String, BigDecimal> entry : parMethode.entrySet()) {
                BigDecimal montant = entry.getValue();
                double pourcentage = montant.multiply(BigDecimal.valueOf(100))
                    .divide(totalPaiements, 2, java.math.RoundingMode.HALF_UP)
                    .doubleValue();
                RepartitionMethode r = new RepartitionMethode();
                r.setMethode(entry.getKey());
                r.setMontant(montant);
                r.setPourcentage(pourcentage);
                r.setCouleur("primary-500");
                r.setIcon("pi pi-credit-card");
                repartitionMethodes.add(r);
            }
            repartitionMethodes.sort((a, b) -> b.getMontant().compareTo(a.getMontant()));
        } catch (Exception e) {
            LOG.warnf("Impossible de calculer la répartition des méthodes: %s", e.getMessage());
        }
    }
    
    /**
     * Charge les rappels depuis les cotisations en retard
     */
    private void chargerRappels() {
        rappelsEnAttente = new ArrayList<>();
        try {
            List<CotisationResponse> enRetard = retryService.executeWithRetrySupplier(
                () -> cotisationService.obtenirEnRetard(0, 100),
                "chargement des cotisations en retard"
            );
            
            for (CotisationResponse cotisation : enRetard) {
                RappelCotisation rappel = new RappelCotisation();
                rappel.setMembreId(cotisation.getMembreId());
                rappel.setNomMembre(cotisation.getNomMembre());
                rappel.setClub(cotisation.getNomOrganisation()); // Corrigé: getNomOrganisation au lieu de getNomAssociation
                rappel.setMontantDu(cotisation.getMontantDu());
                rappel.setJoursRetard(cotisation.getJoursRetard() != null ? cotisation.getJoursRetard().intValue() : 0);
                rappel.setPriorite(determinerPriorite(cotisation.getJoursRetard() != null ? cotisation.getJoursRetard() : 0L));
                rappelsEnAttente.add(rappel);
            }
            
            // Trier par priorité (HAUTE > MOYENNE > FAIBLE)
            rappelsEnAttente.sort((a, b) -> {
                int prioriteA = getPrioriteValue(a.getPriorite());
                int prioriteB = getPrioriteValue(b.getPriorite());
                if (prioriteA != prioriteB) return prioriteB - prioriteA;
                return b.getJoursRetard() - a.getJoursRetard();
            });
        } catch (Exception e) {
            LOG.errorf(e, "Erreur lors du chargement des rappels");
            errorHandler.handleException(e, "lors du chargement des rappels", null);
        }
    }
    
    private String determinerPriorite(long joursRetard) {
        if (joursRetard >= 30) return "HAUTE";
        if (joursRetard >= 15) return "MOYENNE";
        return "FAIBLE";
    }
    
    private int getPrioriteValue(String priorite) {
        return switch (priorite) {
            case "HAUTE" -> 3;
            case "MOYENNE" -> 2;
            case "FAIBLE" -> 1;
            default -> 0;
        };
    }
    
    private String getMethodeLibelle(String methode) {
        if (methode == null) return "Non défini";
        return switch (methode) {
            case "WAVE_MONEY" -> "Wave Money";
            case "ESPECES" -> "Espèces";
            case "VIREMENT" -> "Virement";
            case "CHEQUE" -> "Chèque";
            case "ORANGE_MONEY" -> "Orange Money";
            case "FREE_MONEY" -> "Free Money";
            case "CARTE_BANCAIRE" -> "Carte bancaire";
            default -> methode;
        };
    }
    
    private String getCouleurMethode(String methode) {
        if (methode == null) return "bg-gray-500";
        return switch (methode) {
            case "WAVE_MONEY" -> "bg-blue-500";
            case "ESPECES" -> "bg-green-500";
            case "VIREMENT" -> "bg-purple-500";
            case "CHEQUE" -> "bg-orange-500";
            case "ORANGE_MONEY" -> "bg-orange-400";
            case "FREE_MONEY" -> "bg-yellow-500";
            case "CARTE_BANCAIRE" -> "bg-indigo-500";
            default -> "bg-gray-500";
        };
    }
    
    private String getIconMethode(String methode) {
        if (methode == null) return "pi-circle";
        return switch (methode) {
            case "WAVE_MONEY", "ORANGE_MONEY", "FREE_MONEY" -> "pi-mobile";
            case "ESPECES" -> "pi-money-bill";
            case "VIREMENT" -> "pi-send";
            case "CHEQUE", "CARTE_BANCAIRE" -> "pi-credit-card";
            default -> "pi-circle";
        };
    }
    
    private void initializeNouvelleCotisation() {
        nouvelleCotisation = new NouvelleCotisation();
    }
    
    /**
     * Applique les filtres en utilisant la recherche backend
     */
    private void appliquerFiltres() {
        try {
            // Utiliser la recherche backend ; restreindre au membre connecté si vue membre
            final UUID filtreMembreId = membreConnecteId; // effectively final pour lambda
            cotisationsFiltrees = retryService.executeWithRetrySupplier(
                () -> cotisationService.rechercher(
                    filtreMembreId,
                    filtres.getStatut(),
                    filtres.getTypeCotisation(),
                    null, // annee
                    null, // mois
                    0,
                    1000
                ),
                "recherche de cotisations avec filtres"
            );
            
            // Appliquer les filtres supplémentaires côté client si nécessaire
            if (filtres.getNomMembre() != null && !filtres.getNomMembre().trim().isEmpty()) {
                cotisationsFiltrees = cotisationsFiltrees.stream()
                    .filter(c -> c.getNomMembre() != null 
                        && c.getNomMembre().toLowerCase().contains(filtres.getNomMembre().toLowerCase()))
                    .collect(Collectors.toList());
            }
            
            if (filtres.getClub() != null && !filtres.getClub().trim().isEmpty()) {
                cotisationsFiltrees = cotisationsFiltrees.stream()
                    .filter(c -> c.getNomOrganisation() != null
                        && c.getNomOrganisation().toLowerCase().contains(filtres.getClub().toLowerCase()))
                    .collect(Collectors.toList());
            }
            
            if (filtres.getDateDebut() != null) {
                cotisationsFiltrees = cotisationsFiltrees.stream()
                    .filter(c -> c.getDateEcheance() != null 
                        && !c.getDateEcheance().isBefore(filtres.getDateDebut()))
                    .collect(Collectors.toList());
            }
            
            if (filtres.getDateFin() != null) {
                cotisationsFiltrees = cotisationsFiltrees.stream()
                    .filter(c -> c.getDateEcheance() != null 
                        && !c.getDateEcheance().isAfter(filtres.getDateFin()))
                    .collect(Collectors.toList());
            }
        } catch (Exception e) {
            LOG.errorf(e, "Erreur lors de l'application des filtres");
            errorHandler.handleException(e, "lors de l'application des filtres", null);
            cotisationsFiltrees = new ArrayList<>();
        }
    }
    
    // Actions
    
    /**
     * Recherche avec filtres
     */
    public void rechercher() {
        appliquerFiltres();
        errorHandler.showInfo("Recherche", cotisationsFiltrees.size() + " cotisation(s) trouvée(s)");
    }
    
    /**
     * Réinitialise les filtres
     */
    public void reinitialiserFiltres() {
        filtres = new Filtres();
        chargerCotisations();
        appliquerFiltres();
    }
    
    /**
     * Enregistre une nouvelle cotisation via le backend
     */
    public void enregistrerCotisation() {
        try {
            CreateCotisationRequest request = CreateCotisationRequest.builder()
                    .membreId(nouvelleCotisation.getMembreId())
                    .organisationId(nouvelleCotisation.getOrganisationId())
                    .typeCotisation(nouvelleCotisation.getTypeCotisation())
                    .libelle(nouvelleCotisation.getLibelle())
                    .description(nouvelleCotisation.getDescription())
                    .montantDu(nouvelleCotisation.getMontantDu())
                    .codeDevise("XOF")
                    .dateEcheance(nouvelleCotisation.getDateEcheance())
                    .observations(nouvelleCotisation.getObservations())
                    .recurrente(false)
                    .build();

            CotisationResponse cotisationCreee = retryService.executeWithRetrySupplier(
                () -> cotisationService.creer(request),
                "création d'une cotisation"
            );

            // Recharger les données
            chargerCotisations();
            chargerStatistiques();
            appliquerFiltres();
            initializeNouvelleCotisation();

            LOG.infof("Nouvelle cotisation créée: %s", cotisationCreee.getId());
            errorHandler.showSuccess("Succès", "Cotisation créée avec succès");
        } catch (Exception e) {
            LOG.errorf(e, "Erreur lors de la création de la cotisation");
            errorHandler.handleException(e, "lors de la création d'une cotisation", null);
        }
    }
    
    /**
     * Enregistre le paiement complet de la cotisation sélectionnée via PUT /{id}/payer.
     */
    public void marquerCommePaye(CotisationResponse cotisation) {
        this.cotisationSelectionnee = cotisation;
        marquerCommePaye();
    }

    public void marquerCommePaye() {
        if (cotisationSelectionnee == null || cotisationSelectionnee.getId() == null) {
            errorHandler.showWarning("Attention", "Aucune cotisation sélectionnée");
            return;
        }
        try {
            BigDecimal montant = cotisationSelectionnee.getMontantDu() != null
                    ? cotisationSelectionnee.getMontantDu() : BigDecimal.ZERO;
            Map<String, Object> paiementData = new java.util.HashMap<>();
            paiementData.put("montantPaye", montant.toPlainString());
            paiementData.put("datePaiement", LocalDate.now().toString());
            if (methodePaiementDialog != null) paiementData.put("modePaiement", methodePaiementDialog);
            if (referencePaiementDialog != null) paiementData.put("reference", referencePaiementDialog);

            retryService.executeWithRetrySupplier(
                () -> cotisationService.payer(cotisationSelectionnee.getId(), paiementData),
                "marquage comme payée"
            );

            chargerCotisations();
            chargerStatistiques();
            chargerRepartitionMethodes();
            appliquerFiltres();
            LOG.infof("Cotisation payée: %s", cotisationSelectionnee.getId());
            errorHandler.showSuccess("Succès", "Paiement enregistré avec succès");
        } catch (Exception e) {
            LOG.errorf(e, "Erreur lors du paiement");
            errorHandler.handleException(e, "lors de l'enregistrement du paiement", null);
        }
    }

    /**
     * Enregistre un paiement partiel via PUT /{id}/payer.
     */
    public void enregistrerPaiementPartiel() {
        if (cotisationSelectionnee == null || cotisationSelectionnee.getId() == null) {
            errorHandler.showWarning("Attention", "Aucune cotisation sélectionnée");
            return;
        }
        if (montantPaiementPartiel == null || montantPaiementPartiel.compareTo(BigDecimal.ZERO) <= 0) {
            errorHandler.showWarning("Attention", "Le montant doit être supérieur à zéro");
            return;
        }
        BigDecimal montantRestant = cotisationSelectionnee.getMontantRestant() != null
                ? cotisationSelectionnee.getMontantRestant()
                : cotisationSelectionnee.getMontantDu();
        if (montantPaiementPartiel.compareTo(montantRestant) > 0) {
            errorHandler.showWarning("Attention", "Le montant ne peut pas dépasser le montant restant");
            return;
        }
        try {
            Map<String, Object> paiementData = new java.util.HashMap<>();
            paiementData.put("montantPaye", montantPaiementPartiel.toPlainString());
            paiementData.put("datePaiement", LocalDate.now().toString());
            if (methodePaiementDialog != null) paiementData.put("modePaiement", methodePaiementDialog);
            if (referencePaiementDialog != null) paiementData.put("reference", referencePaiementDialog);

            retryService.executeWithRetrySupplier(
                () -> cotisationService.payer(cotisationSelectionnee.getId(), paiementData),
                "paiement partiel"
            );

            chargerCotisations();
            chargerStatistiques();
            chargerRepartitionMethodes();
            appliquerFiltres();
            LOG.infof("Paiement partiel: %s — %s", cotisationSelectionnee.getId(), montantPaiementPartiel);
            errorHandler.showSuccess("Succès", "Paiement partiel enregistré");
        } catch (Exception e) {
            LOG.errorf(e, "Erreur paiement partiel");
            errorHandler.handleException(e, "lors de l'enregistrement du paiement partiel", null);
        }
    }
    
    /**
     * Sélectionne une cotisation et réinitialise les champs du dialogue de paiement.
     */
    public void selectionnerCotisation(CotisationResponse cotisation) {
        this.cotisationSelectionnee = cotisation;
        this.methodePaiementDialog = null;
        this.referencePaiementDialog = null;
        this.observationsDialog = null;
        this.montantPaiementPartiel = cotisation != null ? cotisation.getMontantRestant() : null;
        // Réinitialiser l'état Wave
        this.waveIntentionId = null;
        this.waveLaunchUrl = null;
        this.waveStatut = null;
        this.waveDialogOuvert = false;
    }

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

    /**
     * Initie un paiement Wave pour la cotisation sélectionnée.
     * Appelle POST /api/paiements/initier-paiement-en-ligne sans numéro de téléphone (QR web).
     * Le backend crée une session Wave Checkout et retourne le wave_launch_url.
     * Le dialog QR s'ouvre ensuite via oncomplete JS.
     */
    /**
     * Sélectionne la cotisation ET initie le paiement Wave en une seule action
     * (pour le bouton "Wave QR" de la DataTable).
     */
    public void initierPaiementWavePour(CotisationResponse cotisation) {
        selectionnerCotisation(cotisation);
        initierPaiementWave();
    }

    public void initierPaiementWave() {
        if (cotisationSelectionnee == null || cotisationSelectionnee.getId() == null) {
            errorHandler.showWarning("Attention", "Sélectionnez d'abord une cotisation");
            return;
        }
        try {
            // Pas de numéro de téléphone → QR non restreint (tout abonné Wave peut payer)
            InitierPaiementEnLigneRequest request = new InitierPaiementEnLigneRequest(
                    cotisationSelectionnee.getId(), "WAVE", null);

            PaiementGatewayResponse response = retryService.executeWithRetrySupplier(
                    () -> paiementClientService.initierPaiementEnLigne(request),
                    "initiation paiement Wave"
            );

            this.waveLaunchUrl = response.getWaveLaunchUrl();
            this.waveIntentionId = response.getClientReference() != null
                    ? UUID.fromString(response.getClientReference()) : null;
            this.waveStatut = "EN_COURS";
            this.waveDialogOuvert = true;

            LOG.infof("Wave QR initié: intention=%s, url=%s", waveIntentionId, waveLaunchUrl);

        } catch (Exception e) {
            LOG.errorf(e, "Erreur initiation Wave QR");
            errorHandler.handleException(e, "lors de l'initiation du paiement Wave", null);
        }
    }

    /**
     * Polling Wave — appelé par p:poll toutes les 3 secondes tant que le dialog est ouvert.
     * Vérifie le statut côté backend ; si COMPLETEE, recharge les cotisations et ferme le dialog.
     */
    public void pollStatutWave() {
        if (!waveDialogOuvert || waveIntentionId == null) return;
        if ("COMPLETEE".equals(waveStatut) || "EXPIREE".equals(waveStatut) || "ECHOUEE".equals(waveStatut)) return;

        try {
            IntentionStatutResponse statut = paiementClientService.getStatutIntention(waveIntentionId);
            this.waveStatut = statut.getStatut();

            if ("COMPLETEE".equals(waveStatut)) {
                waveDialogOuvert = false;
                chargerCotisations();
                chargerStatistiques();
                chargerRepartitionMethodes();
                appliquerFiltres();
                errorHandler.showSuccess("Paiement confirmé !",
                        "Votre paiement Wave a été enregistré avec succès.");
                LOG.infof("Wave QR complété: intention=%s", waveIntentionId);

            } else if ("EXPIREE".equals(waveStatut) || "ECHOUEE".equals(waveStatut)) {
                waveDialogOuvert = false;
                errorHandler.showWarning("Paiement annulé",
                        "La session Wave a expiré. Veuillez recommencer.");
            }
        } catch (Exception e) {
            LOG.warnf(e, "Erreur polling Wave — sera retentée au prochain tick");
        }
    }

    /** Ferme le dialog Wave QR et stoppe le polling. */
    public void annulerPaiementWave() {
        this.waveDialogOuvert = false;
        this.waveIntentionId = null;
        this.waveLaunchUrl = null;
        this.waveStatut = null;
    }

    // Getters Wave
    public String getWaveLaunchUrl()    { return waveLaunchUrl; }
    public String getWaveStatut()       { return waveStatut; }
    public boolean isWaveDialogOuvert() { return waveDialogOuvert; }
    public UUID getWaveIntentionId()    { return waveIntentionId; }
    
    /**
     * Envoie un rappel pour une cotisation (DRY/WOU - réutilise NotificationClientService)
     * @param cotisation La cotisation pour laquelle envoyer un rappel
     */
    public void envoyerRappel(CotisationResponse cotisation) {
        if (cotisation == null || cotisation.getMembreId() == null) {
            errorHandler.showWarning("Attention", "Aucune cotisation sélectionnée ou membre invalide");
            return;
        }
        
        try {
            String message = "Rappel: Votre cotisation de " + formatMontant(cotisation.getMontantDu()) 
                + " est en attente de paiement.";
            
            retryService.executeWithRetrySupplier(
                () -> {
                    notificationService.envoyerNotificationGroupe(buildNotifRequest(
                        "Rappel de cotisation", message,
                        List.of(cotisation.getMembreId().toString())));
                    return null;
                },
                "envoi d'un rappel de cotisation"
            );
            
            LOG.infof("Rappel envoyé à: %s", cotisation.getNomMembre());
            errorHandler.showSuccess("Rappel", "Rappel envoyé à " + cotisation.getNomMembre());
        } catch (Exception e) {
            LOG.errorf(e, "Erreur envoi rappel");
            errorHandler.handleException(e, "lors de l'envoi d'un rappel", null);
        }
    }
    
    /**
     * Envoie un rappel depuis un objet RappelCotisation (vue relances)
     */
    public void envoyerRappelPourRappel(RappelCotisation rappel) {
        if (rappel == null || rappel.getMembreId() == null) {
            errorHandler.showWarning("Attention", "Membre introuvable pour ce rappel");
            return;
        }
        try {
            String message = "Rappel: Votre cotisation de " + formatMontant(rappel.getMontantDu())
                + " est en attente de paiement.";
            retryService.executeWithRetrySupplier(
                () -> {
                    notificationService.envoyerNotificationGroupe(buildNotifRequest(
                        "Rappel de cotisation", message,
                        List.of(rappel.getMembreId().toString())));
                    return null;
                },
                "envoi d'un rappel de cotisation"
            );
            LOG.infof("Rappel envoyé à: %s", rappel.getNomMembre());
            errorHandler.showSuccess("Rappel", "Rappel envoyé à " + rappel.getNomMembre());
        } catch (Exception e) {
            LOG.errorf(e, "Erreur envoi rappel");
            errorHandler.handleException(e, "lors de l'envoi d'un rappel", null);
        }
    }

    /**
     * Envoie des rappels groupés
     */
    public void envoyerRappelsGroupes() {
        if (cotisationsSelectionnees == null || cotisationsSelectionnees.isEmpty()) {
            errorHandler.showWarning("Attention", "Aucune cotisation sélectionnée");
            return;
        }
        
        try {
            List<String> destinataires = cotisationsSelectionnees.stream()
                .filter(c -> c.getMembreId() != null)
                .map(c -> c.getMembreId().toString())
                .distinct()
                .collect(Collectors.toList());
            
            BigDecimal montantTotal = cotisationsSelectionnees.stream()
                .map(c -> c.getMontantDu() != null ? c.getMontantDu() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
            
            retryService.executeWithRetrySupplier(
                () -> {
                    notificationService.envoyerNotificationGroupe(buildNotifRequest(
                        "Rappel de paiement",
                        "Vous avez des cotisations en attente. Montant total: " + formatMontant(montantTotal),
                        destinataires));
                    return null;
                },
                "envoi de rappels groupés de cotisation"
            );
            
            LOG.infof("Rappels envoyés à %d membres", destinataires.size());
            errorHandler.showSuccess("Rappels", destinataires.size() + " rappel(s) envoyé(s)");
        } catch (Exception e) {
            LOG.errorf(e, "Erreur envoi rappels groupés");
            errorHandler.handleException(e, "lors de l'envoi de rappels groupés", null);
        }
    }
    
    /**
     * Exporte les cotisations en CSV
     */
    public void exporterCotisations() {
        try {
            LOG.infof("Export de %d cotisations", cotisationsFiltrees.size());
            
            if (cotisationsFiltrees.isEmpty()) {
                errorHandler.showWarning("Attention", "Aucune cotisation à exporter");
                return;
            }
            
            List<UUID> ids = cotisationsFiltrees.stream()
                .map(CotisationResponse::getId)
                .filter(id -> id != null)
                .collect(Collectors.toList());
            
            byte[] csvData = retryService.executeWithRetrySupplier(
                () -> exportService.exporterCotisationsSelectionneesCSV(ids),
                "export de cotisations en CSV"
            );
            
            telechargerFichier(csvData, "cotisations-export.csv", "text/csv");
            
            errorHandler.showSuccess("Export", "Export de " + cotisationsFiltrees.size() + " cotisation(s) terminé");
        } catch (Exception e) {
            LOG.errorf(e, "Erreur export cotisations");
            errorHandler.handleException(e, "lors de l'export de cotisations", null);
        }
    }
    
    /**
     * Génère un rapport financier mensuel
     */
    public void genererRapportFinancier() {
        try {
            LOG.info("Rapport financier généré");
            
            int annee = LocalDate.now().getYear();
            int mois = LocalDate.now().getMonthValue();
            
            byte[] rapport = retryService.executeWithRetrySupplier(
                () -> exportService.genererRapportMensuel(annee, mois, null),
                "génération d'un rapport financier mensuel"
            );
            
            telechargerFichier(rapport, "rapport-financier-" + annee + "-" + String.format("%02d", mois) + ".txt", "text/plain");
            
            errorHandler.showSuccess("Rapport", "Rapport financier généré avec succès");
        } catch (Exception e) {
            LOG.errorf(e, "Erreur génération rapport");
            errorHandler.handleException(e, "lors de la génération d'un rapport financier", null);
        }
    }
    
    /**
     * Télécharge un fichier via le navigateur
     */
    private void telechargerFichier(byte[] data, String nomFichier, String contentType) {
        try {
            jakarta.faces.context.FacesContext fc = jakarta.faces.context.FacesContext.getCurrentInstance();
            ExternalContext ec = fc.getExternalContext();
            
            ec.responseReset();
            ec.setResponseContentType(contentType + "; charset=UTF-8");
            ec.setResponseContentLength(data.length);
            ec.setResponseHeader("Content-Disposition", "attachment; filename=\"" + nomFichier + "\"");
            
            OutputStream output = ec.getResponseOutputStream();
            output.write(data);
            output.flush();
            
            fc.responseComplete();
        } catch (Exception e) {
            LOG.errorf(e, "Erreur téléchargement fichier");
            throw new RuntimeException("Erreur lors du téléchargement", e);
        }
    }
    
    /**
     * Formate un montant en FCFA
     */
    private String formatMontant(BigDecimal montant) {
        if (montant == null) return "0 FCFA";
        return String.format("%,.0f FCFA", montant.doubleValue());
    }

    private Map<String, Object> buildNotifRequest(String sujet, String corps, List<String> ids) {
        Map<String, Object> req = new HashMap<>();
        req.put("sujet", sujet);
        req.put("corps", corps);
        req.put("membreIds", ids.stream().map(UUID::fromString).collect(Collectors.toList()));
        return req;
    }
    
    /**
     * Actualise les données depuis le backend
     */
    public void actualiser() {
        chargerCotisations();
        chargerStatistiques();
        chargerEvolutionPaiements();
        chargerRepartitionMethodes();
        chargerRappels();
        appliquerFiltres();
        errorHandler.showSuccess("Actualisation", "Données actualisées");
    }
    
    /**
     * Compte les cotisations par statut
     */
    public long compterParStatut(String statut) {
        if (cotisationsFiltrees == null) return 0;
        return cotisationsFiltrees.stream()
            .filter(c -> statut.equals(c.getStatut()))
            .count();
    }
    
    /**
     * Compte les cotisations par type
     */
    public long compterParType(String type) {
        if (cotisationsFiltrees == null) return 0;
        return cotisationsFiltrees.stream()
            .filter(c -> type.equals(c.getTypeCotisation()))
            .count();
    }
    
    // Getters et Setters
    
    public List<CotisationResponse> getToutesLesCotisations() { 
        return toutesLesCotisations; 
    }
    
    public void setToutesLesCotisations(List<CotisationResponse> toutesLesCotisations) { 
        this.toutesLesCotisations = toutesLesCotisations; 
    }
    
    public List<CotisationResponse> getCotisationsFiltrees() { 
        return cotisationsFiltrees; 
    }
    
    public void setCotisationsFiltrees(List<CotisationResponse> cotisationsFiltrees) { 
        this.cotisationsFiltrees = cotisationsFiltrees; 
    }
    
    public List<CotisationResponse> getCotisationsSelectionnees() { 
        return cotisationsSelectionnees; 
    }
    
    public void setCotisationsSelectionnees(List<CotisationResponse> cotisationsSelectionnees) { 
        this.cotisationsSelectionnees = cotisationsSelectionnees; 
    }
    
    public CotisationResponse getCotisationSelectionnee() {
        return cotisationSelectionnee;
    }

    public void setCotisationSelectionnee(CotisationResponse cotisationSelectionnee) {
        this.cotisationSelectionnee = cotisationSelectionnee;
    }

    public String getMethodePaiementDialog() { return methodePaiementDialog; }
    public void setMethodePaiementDialog(String v) { this.methodePaiementDialog = v; }

    public String getReferencePaiementDialog() { return referencePaiementDialog; }
    public void setReferencePaiementDialog(String v) { this.referencePaiementDialog = v; }

    public String getObservationsDialog() { return observationsDialog; }
    public void setObservationsDialog(String v) { this.observationsDialog = v; }

    public BigDecimal getMontantPaiementPartiel() { return montantPaiementPartiel; }
    public void setMontantPaiementPartiel(BigDecimal v) { this.montantPaiementPartiel = v; }
    
    public NouvelleCotisation getNouvelleCotisation() { 
        return nouvelleCotisation; 
    }
    
    public void setNouvelleCotisation(NouvelleCotisation nouvelleCotisation) { 
        this.nouvelleCotisation = nouvelleCotisation; 
    }
    
    public Filtres getFiltres() { 
        return filtres; 
    }
    
    public void setFiltres(Filtres filtres) { 
        this.filtres = filtres; 
    }
    
    public StatistiquesFinancieres getStatistiques() { 
        return statistiques; 
    }
    
    public void setStatistiques(StatistiquesFinancieres statistiques) { 
        this.statistiques = statistiques; 
    }
    
    public List<EvolutionPaiement> getEvolutionPaiements() { 
        return evolutionPaiements; 
    }
    
    public void setEvolutionPaiements(List<EvolutionPaiement> evolutionPaiements) { 
        this.evolutionPaiements = evolutionPaiements; 
    }
    
    public List<RepartitionMethode> getRepartitionMethodes() { 
        return repartitionMethodes; 
    }
    
    public void setRepartitionMethodes(List<RepartitionMethode> repartitionMethodes) { 
        this.repartitionMethodes = repartitionMethodes; 
    }
    
    public List<RappelCotisation> getRappelsEnAttente() { 
        return rappelsEnAttente; 
    }
    
    public void setRappelsEnAttente(List<RappelCotisation> rappelsEnAttente) { 
        this.rappelsEnAttente = rappelsEnAttente; 
    }
    
    // Classes internes pour les formulaires et données d'affichage
    
    /**
     * Classe pour le formulaire de nouvelle cotisation
     */
    public static class NouvelleCotisation implements Serializable {
        private static final long serialVersionUID = 1L;

        private UUID membreId;
        private UUID organisationId;
        private String typeCotisation;
        private String libelle;
        private String description;
        private BigDecimal montantDu;
        private LocalDate dateEcheance;
        private String observations;

        // Getters et setters
        public UUID getMembreId() { return membreId; }
        public void setMembreId(UUID membreId) { this.membreId = membreId; }

        public UUID getOrganisationId() { return organisationId; }
        public void setOrganisationId(UUID organisationId) { this.organisationId = organisationId; }
        
        public String getTypeCotisation() { return typeCotisation; }
        public void setTypeCotisation(String typeCotisation) { this.typeCotisation = typeCotisation; }
        
        public String getLibelle() { return libelle; }
        public void setLibelle(String libelle) { this.libelle = libelle; }
        
        public String getDescription() { return description; }
        public void setDescription(String description) { this.description = description; }
        
        public BigDecimal getMontantDu() { return montantDu; }
        public void setMontantDu(BigDecimal montantDu) { this.montantDu = montantDu; }
        
        public LocalDate getDateEcheance() { return dateEcheance; }
        public void setDateEcheance(LocalDate dateEcheance) { this.dateEcheance = dateEcheance; }
        
        public String getObservations() { return observations; }
        public void setObservations(String observations) { this.observations = observations; }
    }
    
    /**
     * Classe pour les filtres de recherche
     */
    public static class Filtres implements Serializable {
        private static final long serialVersionUID = 1L;
        
        private String nomMembre;
        private String club;
        private String statut;
        private String typeCotisation;
        private String methodePaiement;
        private LocalDate dateDebut;
        private LocalDate dateFin;
        
        // Getters et setters
        public String getNomMembre() { return nomMembre; }
        public void setNomMembre(String nomMembre) { this.nomMembre = nomMembre; }
        
        public String getClub() { return club; }
        public void setClub(String club) { this.club = club; }
        
        public String getStatut() { return statut; }
        public void setStatut(String statut) { this.statut = statut; }
        
        public String getTypeCotisation() { return typeCotisation; }
        public void setTypeCotisation(String typeCotisation) { this.typeCotisation = typeCotisation; }
        
        public String getMethodePaiement() { return methodePaiement; }
        public void setMethodePaiement(String methodePaiement) { this.methodePaiement = methodePaiement; }
        
        public LocalDate getDateDebut() { return dateDebut; }
        public void setDateDebut(LocalDate dateDebut) { this.dateDebut = dateDebut; }
        
        public LocalDate getDateFin() { return dateFin; }
        public void setDateFin(LocalDate dateFin) { this.dateFin = dateFin; }
    }
    
    /**
     * Classe pour les statistiques financières
     */
    public static class StatistiquesFinancieres implements Serializable {
        private static final long serialVersionUID = 1L;
        
        private BigDecimal totalCollecte;
        private BigDecimal objectifAnnuel;
        private double tauxRecouvrement;
        private int cotisationsEnRetard;
        private BigDecimal montantRetard;
        private BigDecimal moyenneMensuelle;
        
        // Getters et setters
        public BigDecimal getTotalCollecte() { return totalCollecte; }
        public void setTotalCollecte(BigDecimal totalCollecte) { this.totalCollecte = totalCollecte; }
        
        public BigDecimal getObjectifAnnuel() { return objectifAnnuel; }
        public void setObjectifAnnuel(BigDecimal objectifAnnuel) { this.objectifAnnuel = objectifAnnuel; }
        
        public double getTauxRecouvrement() { return tauxRecouvrement; }
        public void setTauxRecouvrement(double tauxRecouvrement) { this.tauxRecouvrement = tauxRecouvrement; }
        
        public int getCotisationsEnRetard() { return cotisationsEnRetard; }
        public void setCotisationsEnRetard(int cotisationsEnRetard) { this.cotisationsEnRetard = cotisationsEnRetard; }
        
        public BigDecimal getMontantRetard() { return montantRetard; }
        public void setMontantRetard(BigDecimal montantRetard) { this.montantRetard = montantRetard; }
        
        public BigDecimal getMoyenneMensuelle() { return moyenneMensuelle; }
        public void setMoyenneMensuelle(BigDecimal moyenneMensuelle) { this.moyenneMensuelle = moyenneMensuelle; }
        
        // Méthodes de formatage
        public String getTotalCollecteFormatte() {
            if (totalCollecte == null) return "0 FCFA";
            return String.format("%,.0f FCFA", totalCollecte.doubleValue());
        }
        
        public String getObjectifAnnuelFormatte() {
            if (objectifAnnuel == null) return "0 FCFA";
            return String.format("%,.0f FCFA", objectifAnnuel.doubleValue());
        }
        
        public String getMontantRetardFormatte() {
            if (montantRetard == null) return "0 FCFA";
            return String.format("%,.0f FCFA", montantRetard.doubleValue());
        }
        
        public String getMoyenneMensuelleFormattee() {
            if (moyenneMensuelle == null) return "0 FCFA";
            return String.format("%,.0f FCFA", moyenneMensuelle.doubleValue());
        }
        
        public int getTauxRecouvrementInt() {
            return (int) tauxRecouvrement;
        }
    }
    
    /**
     * Classe pour l'évolution des paiements (graphique)
     */
    public static class EvolutionPaiement implements Serializable {
        private static final long serialVersionUID = 1L;
        
        private String mois;
        private BigDecimal montant;
        private int hauteur;
        
        // Getters et setters
        public String getMois() { return mois; }
        public void setMois(String mois) { this.mois = mois; }
        
        public BigDecimal getMontant() { return montant; }
        public void setMontant(BigDecimal montant) { this.montant = montant; }
        
        public int getHauteur() { return hauteur; }
        public void setHauteur(int hauteur) { this.hauteur = hauteur; }
        
        public String getMontantFormatte() {
            if (montant == null) return "0";
            return String.format("%.1fM", montant.divide(new BigDecimal("1000000"), 1, java.math.RoundingMode.HALF_UP).doubleValue());
        }
    }
    
    /**
     * Classe pour la répartition par méthode de paiement
     */
    public static class RepartitionMethode implements Serializable {
        private static final long serialVersionUID = 1L;
        
        private String methode;
        private double pourcentage;
        private BigDecimal montant;
        private String couleur;
        private String icon;
        
        // Getters et setters
        public String getMethode() { return methode; }
        public void setMethode(String methode) { this.methode = methode; }
        
        public double getPourcentage() { return pourcentage; }
        public void setPourcentage(double pourcentage) { this.pourcentage = pourcentage; }
        
        public BigDecimal getMontant() { return montant; }
        public void setMontant(BigDecimal montant) { this.montant = montant; }
        
        public String getCouleur() { return couleur; }
        public void setCouleur(String couleur) { this.couleur = couleur; }
        
        public String getIcon() { return icon; }
        public void setIcon(String icon) { this.icon = icon; }
        
        public String getMontantFormatte() {
            if (montant == null) return "0 FCFA";
            return String.format("%,.0f FCFA", montant.doubleValue());
        }
        
        public int getPourcentageInt() {
            return (int) pourcentage;
        }
        
        public int getLargeur() {
            return (int) (pourcentage * 2);
        }
    }
    
    /**
     * Classe pour les rappels de cotisation
     */
    public static class RappelCotisation implements Serializable {
        private static final long serialVersionUID = 1L;

        private UUID membreId;
        private String nomMembre;
        private String club;
        private BigDecimal montantDu;
        private int joursRetard;
        private String priorite;

        // Getters et setters
        public UUID getMembreId() { return membreId; }
        public void setMembreId(UUID membreId) { this.membreId = membreId; }

        public String getNomMembre() { return nomMembre; }
        public void setNomMembre(String nomMembre) { this.nomMembre = nomMembre; }
        
        public String getClub() { return club; }
        public void setClub(String club) { this.club = club; }
        
        public BigDecimal getMontantDu() { return montantDu; }
        public void setMontantDu(BigDecimal montantDu) { this.montantDu = montantDu; }
        
        public int getJoursRetard() { return joursRetard; }
        public void setJoursRetard(int joursRetard) { this.joursRetard = joursRetard; }
        
        public String getPriorite() { return priorite; }
        public void setPriorite(String priorite) { this.priorite = priorite; }
        
        public String getMontantDuFormatte() {
            if (montantDu == null) return "0 FCFA";
            return String.format("%,.0f FCFA", montantDu.doubleValue());
        }
        
        public String getPrioriteSeverity() {
            if (priorite == null) return "secondary";
            return switch (priorite) {
                case "HAUTE" -> "danger";
                case "MOYENNE" -> "warning";
                case "FAIBLE" -> "info";
                default -> "secondary";
            };
        }
    }
}
