package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.dashboard.DashboardDataResponse;
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
import dev.lions.unionflow.server.api.dto.dashboard.DashboardStatsResponse;
import dev.lions.unionflow.server.api.dto.dashboard.RecentActivityResponse;
import dev.lions.unionflow.server.api.dto.dashboard.UpcomingEventResponse;
import dev.lions.unionflow.server.api.dto.dashboard.MonthlyStatDTO;

import dev.lions.unionflow.server.api.service.dashboard.DashboardService;
import dev.lions.unionflow.server.entity.Cotisation;
import dev.lions.unionflow.server.entity.DemandeAide;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.CotisationRepository;
import dev.lions.unionflow.server.repository.DemandeAideRepository;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional;
import org.jboss.logging.Logger;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Implémentation du service Dashboard pour Quarkus
 *
 * <p>
 * Cette implémentation récupère les données réelles depuis la base de données
 * via les repositories.
 *
 * @author UnionFlow Team
 * @version 2.0
 * @since 2025-01-17
 */
@ApplicationScoped
public class DashboardServiceImpl implements DashboardService {

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

    @Inject
    MembreRepository membreRepository;

    @Inject
    EvenementRepository evenementRepository;

    @Inject
    CotisationRepository cotisationRepository;

    @Inject
    DemandeAideRepository demandeAideRepository;

    @Inject
    OrganisationRepository organisationRepository;

    @Override
    @Transactional(Transactional.TxType.REQUIRED)
    public DashboardDataResponse getDashboardData(String organizationId, String userId) {
        LOG.infof("Récupération des données dashboard pour org: %s et user: %s", organizationId, userId);

        return DashboardDataResponse.builder()
                .stats(getDashboardStats(organizationId, userId))
                .recentActivities(getRecentActivities(organizationId, userId, 10))
                .upcomingEvents(getUpcomingEvents(organizationId, userId, 5))
                .userPreferences(getUserPreferences(userId))
                .organizationId(organizationId)
                .userId(userId)
                .build();
    }

    @Override
    @Transactional(Transactional.TxType.REQUIRED)
    public DashboardStatsResponse getDashboardStats(String organizationId, String userId) {
        LOG.infof("Récupération des stats dashboard pour org: %s et user: %s", organizationId, userId);

        // Gérer le cas où organizationId est vide ou invalide
        UUID orgId = parseOrganizationId(organizationId);
        java.util.Set<UUID> orgIds = orgId != null ? java.util.Set.of(orgId) : java.util.Set.of();

        // Compter les membres (par organisation si orgId fourni, sinon global)
        long totalMembers;
        long activeMembers;
        if (!orgIds.isEmpty()) {
            totalMembers = membreRepository.countDistinctByOrganisationIdIn(orgIds);
            activeMembers = membreRepository.countActifsDistinctByOrganisationIdIn(orgIds);
        } else {
            totalMembers = membreRepository.count();
            activeMembers = membreRepository.countActifs();
        }

        // Compter les événements (par organisation si orgId fourni)
        long totalEvents;
        long upcomingEvents;
        if (orgId != null) {
            totalEvents = evenementRepository.countByOrganisationId(orgId);
            upcomingEvents = evenementRepository.findEvenementsAVenirByOrganisationId(orgId, Page.of(0, 500), Sort.by("dateDebut", Sort.Direction.Ascending)).size();
        } else {
            totalEvents = evenementRepository.count();
            upcomingEvents = evenementRepository.findEvenementsAVenir().size();
        }

        // Compter les cotisations (par organisation si orgId fourni)
        long totalContributions;
        if (orgId != null) {
            totalContributions = cotisationRepository.countByOrganisationId(orgId);
        } else {
            totalContributions = cotisationRepository.count();
        }
        BigDecimal totalContributionAmount = calculateTotalContributionAmount(orgId);

        // Compter les demandes en attente (déjà filtré par org dans la boucle si orgId non null)
        List<DemandeAide> pendingRequests = demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE);
        long pendingRequestsCount = pendingRequests.stream()
                .filter(d -> orgId == null || (d.getOrganisation() != null && d.getOrganisation().getId().equals(orgId)))
                .count();

        // Calculer la croissance mensuelle (membres ajoutés ce mois, dans l'org ou global)
        LocalDate debutMois = LocalDate.now().withDayOfMonth(1);
        long nouveauxMembresMois = orgId != null
                ? membreRepository.countNouveauxMembresByOrganisationId(debutMois, orgId)
                : membreRepository.countNouveauxMembres(debutMois);
        long totalMembresAvant = totalMembers - nouveauxMembresMois;
        double monthlyGrowth = totalMembresAvant > 0
                ? (double) nouveauxMembresMois / totalMembresAvant * 100.0
                : 0.0;

        // Calculer le taux d'engagement (membres actifs / total)
        double engagementRate = totalMembers > 0
                ? (double) activeMembers / totalMembers
                : 0.0;

        // Compter les organisations et répartition par type (pour une org : 1 et son type uniquement)
        long totalOrganizations;
        Map<String, Integer> orgTypeDistribution;
        if (orgId != null) {
            totalOrganizations = 1;
            orgTypeDistribution = organisationRepository.findByIdOptional(orgId)
                    .map(org -> java.util.Map.<String, Integer>of(org.getTypeOrganisation() != null && !org.getTypeOrganisation().isBlank() ? org.getTypeOrganisation() : "Autre", 1))
                    .orElse(java.util.Map.of());
        } else {
            totalOrganizations = organisationRepository.count();
            orgTypeDistribution = calculateOrganizationTypeDistribution();
        }

        // Calculer les données historiques mensuelles (12 derniers mois)
        List<MonthlyStatDTO> monthlyData = calculateMonthlyHistoricalData(orgId, 12);

        return DashboardStatsResponse.builder()
                .totalMembers((int) totalMembers)
                .activeMembers((int) activeMembers)
                .totalEvents((int) totalEvents)
                .upcomingEvents((int) upcomingEvents)
                .totalContributions((int) totalContributions)
                .totalContributionAmount(totalContributionAmount.doubleValue())
                .pendingRequests((int) pendingRequestsCount)
                .completedProjects(0) // À implémenter si nécessaire
                .monthlyGrowth(monthlyGrowth)
                .engagementRate(engagementRate)
                .lastUpdated(LocalDateTime.now())
                .totalOrganizations((int) totalOrganizations)
                .organizationTypeDistribution(orgTypeDistribution)
                .monthlyHistoricalData(monthlyData)
                .build();
    }

    @Override
    @Transactional(Transactional.TxType.REQUIRED)
    public List<RecentActivityResponse> getRecentActivities(String organizationId, String userId, int limit) {
        LOG.infof("Récupération de %d activités récentes pour org: %s et user: %s", limit, organizationId, userId);

        // Gérer le cas où organizationId est vide ou invalide
        UUID orgId = parseOrganizationId(organizationId);
        List<RecentActivityResponse> activities = new ArrayList<>();

        // Récupérer les membres récemment créés
        List<Membre> nouveauxMembres = membreRepository.rechercheAvancee(
                null, true, null, null, Page.of(0, limit), Sort.by("dateCreation", Sort.Direction.Descending));

        for (Membre membre : nouveauxMembres) {
            boolean appartiendAOrg = membre.getMembresOrganisations() != null
                    && membre.getMembresOrganisations().stream()
                            .anyMatch(mo -> mo.getOrganisation() != null
                                    && mo.getOrganisation().getId().equals(orgId));
            if (appartiendAOrg) {
                activities.add(RecentActivityResponse.builder()
                        .id(membre.getId().toString())
                        .type("member")
                        .title("Nouveau membre inscrit")
                        .description(membre.getNomComplet() + " a rejoint l'organisation")
                        .userName(membre.getNomComplet())
                        .timestamp(membre.getDateCreation())
                        .userAvatar(null)
                        .actionUrl("/members/" + membre.getId())
                        .build());
            }
        }

        // Récupérer les événements récemment créés
        List<Evenement> tousEvenements = evenementRepository.listAll();
        List<Evenement> nouveauxEvenements = tousEvenements.stream()
                .filter(e -> e.getOrganisation() != null && e.getOrganisation().getId().equals(orgId))
                .sorted(Comparator.comparing(Evenement::getDateCreation).reversed())
                .limit(limit)
                .collect(Collectors.toList());

        for (Evenement evenement : nouveauxEvenements) {
            activities.add(RecentActivityResponse.builder()
                    .id(evenement.getId().toString())
                    .type("event")
                    .title("Événement créé")
                    .description(evenement.getTitre() + " a été programmé")
                    .userName(evenement.getOrganisation().getNom())
                    .timestamp(evenement.getDateCreation())
                    .userAvatar(null)
                    .actionUrl("/events/" + evenement.getId())
                    .build());
        }

        // Récupérer les cotisations récentes
        List<Cotisation> cotisationsRecentes = cotisationRepository.rechercheAvancee(
                null, "PAYEE", null, null, null, Page.of(0, limit));

        for (Cotisation cotisation : cotisationsRecentes) {
            if (cotisation.getMembre() != null &&
                    cotisation.getOrganisation() != null &&
                    cotisation.getOrganisation().getId().equals(orgId)) {
                activities.add(RecentActivityResponse.builder()
                        .id(cotisation.getId().toString())
                        .type("contribution")
                        .title("Cotisation reçue")
                        .description("Paiement de " + cotisation.getMontantPaye() + " " + cotisation.getCodeDevise()
                                + " reçu")
                        .userName(cotisation.getMembre().getNomComplet())
                        .timestamp(cotisation.getDatePaiement() != null ? cotisation.getDatePaiement()
                                : cotisation.getDateCreation())
                        .userAvatar(null)
                        .actionUrl("/contributions/" + cotisation.getId())
                        .build());
            }
        }

        // Trier par timestamp décroissant et limiter
        return activities.stream()
                .sorted(Comparator.comparing(RecentActivityResponse::getTimestamp).reversed())
                .limit(limit)
                .collect(Collectors.toList());
    }

    @Override
    @Transactional(Transactional.TxType.REQUIRED)
    public List<UpcomingEventResponse> getUpcomingEvents(String organizationId, String userId, int limit) {
        LOG.infof("Récupération de %d événements à venir pour org: %s et user: %s", limit, organizationId, userId);

        UUID orgId = parseOrganizationId(organizationId);
        List<Evenement> evenements = orgId != null
                ? evenementRepository.findEvenementsAVenirByOrganisationId(orgId, Page.of(0, limit), Sort.by("dateDebut", Sort.Direction.Ascending))
                : evenementRepository.findEvenementsAVenir(Page.of(0, limit), Sort.by("dateDebut", Sort.Direction.Ascending));

        return evenements.stream()
                .filter(e -> orgId == null || (e.getOrganisation() != null && e.getOrganisation().getId().equals(orgId)))
                .map(this::convertToUpcomingEventResponse)
                .limit(limit)
                .collect(Collectors.toList());
    }

    private UpcomingEventResponse convertToUpcomingEventResponse(Evenement evenement) {
        return UpcomingEventResponse.builder()
                .id(evenement.getId().toString())
                .title(evenement.getTitre())
                .description(evenement.getDescription())
                .startDate(evenement.getDateDebut())
                .endDate(evenement.getDateFin())
                .location(evenement.getLieu())
                .maxParticipants(evenement.getCapaciteMax())
                .currentParticipants(evenement.getNombreInscrits())
                .status(evenement.getStatut() != null ? evenement.getStatut() : "PLANIFIE")
                .imageUrl(null)
                .tags(Collections.emptyList())
                .build();
    }

    private BigDecimal calculateTotalContributionAmount(UUID organisationId) {
        String jpql = organisationId != null
                ? "SELECT COALESCE(SUM(c.montantDu), 0) FROM Cotisation c WHERE c.organisation.id = :organisationId"
                : "SELECT COALESCE(SUM(c.montantDu), 0) FROM Cotisation c";
        TypedQuery<BigDecimal> query = cotisationRepository.getEntityManager()
                .createQuery(jpql, BigDecimal.class);
        if (organisationId != null) {
            query.setParameter("organisationId", organisationId);
        }
        BigDecimal result = query.getSingleResult();
        return result != null ? result : BigDecimal.ZERO;
    }

    private Map<String, Object> getUserPreferences(String userId) {
        Map<String, Object> preferences = new HashMap<>();
        preferences.put("theme", "royal_teal");
        preferences.put("language", "fr");
        preferences.put("notifications", true);
        preferences.put("autoRefresh", true);
        preferences.put("refreshInterval", 300);
        return preferences;
    }

    /**
     * Parse l'organizationId de manière sûre
     * Retourne null si la chaîne est vide ou invalide
     */
    private UUID parseOrganizationId(String organizationId) {
        if (organizationId == null || organizationId.trim().isEmpty()) {
            return null;
        }
        try {
            return UUID.fromString(organizationId);
        } catch (IllegalArgumentException e) {
            LOG.warnf("Invalid UUID for organizationId: %s, using null", organizationId);
            return null;
        }
    }

    /**
     * Calcule la répartition des organisations par type
     * @return Map avec le type d'organisation en clé et le nombre en valeur
     */
    private Map<String, Integer> calculateOrganizationTypeDistribution() {
        Map<String, Integer> distribution = new HashMap<>();

        List<Organisation> allOrgs = organisationRepository.listAll();

        for (Organisation org : allOrgs) {
            String type = org.getTypeOrganisation();
            if (type == null || type.trim().isEmpty()) {
                type = "Autre";
            }
            distribution.put(type, distribution.getOrDefault(type, 0) + 1);
        }

        return distribution;
    }

    /**
     * Calcule les données historiques mensuelles pour les graphiques
     * @param organizationId ID de l'organisation (peut être null pour toutes les orgs)
     * @param monthsBack Nombre de mois à remonter dans l'historique
     * @return Liste des statistiques mensuelles
     */
    private List<MonthlyStatDTO> calculateMonthlyHistoricalData(UUID organizationId, int monthsBack) {
        List<MonthlyStatDTO> monthlyData = new ArrayList<>();
        java.util.Set<UUID> orgIds = organizationId != null ? java.util.Set.of(organizationId) : null;
        long currentOrgTotalMembers = orgIds != null ? membreRepository.countDistinctByOrganisationIdIn(orgIds) : 0;
        long currentOrgActiveMembers = orgIds != null ? membreRepository.countActifsDistinctByOrganisationIdIn(orgIds) : 0;

        for (int i = monthsBack - 1; i >= 0; i--) {
            LocalDate monthStart = LocalDate.now().minusMonths(i).withDayOfMonth(1);
            LocalDate monthEnd = monthStart.plusMonths(1).minusDays(1);
            String monthLabel = monthStart.toString().substring(0, 7);

            long membersCount;
            long activeMembersCount;
            long newMembersThisMonth;
            long contributionsThisMonth;
            long eventsCount;

            if (organizationId != null) {
                membersCount = currentOrgTotalMembers;
                activeMembersCount = currentOrgActiveMembers;
                newMembersThisMonth = membreRepository.countNouveauxMembresByOrganisationIdInPeriod(monthStart, monthEnd, organizationId);
                contributionsThisMonth = cotisationRepository.countByOrganisationIdAndDatePaiementBetween(
                    organizationId, monthStart.atStartOfDay(), monthEnd.atTime(23, 59, 59));
                eventsCount = evenementRepository.countEvenements(organizationId, monthStart.atStartOfDay(), monthEnd.atTime(23, 59, 59));
            } else {
                membersCount = membreRepository.count("dateCreation <= ?1", monthEnd.atStartOfDay());
                activeMembersCount = membreRepository.count("dateCreation <= ?1 and actif = true", monthEnd.atStartOfDay());
                newMembersThisMonth = membreRepository.count(
                    "dateCreation >= ?1 and dateCreation <= ?2",
                    monthStart.atStartOfDay(),
                    monthEnd.atTime(23, 59, 59)
                );
                contributionsThisMonth = cotisationRepository.count(
                    "datePaiement >= ?1 and datePaiement <= ?2",
                    monthStart.atStartOfDay(),
                    monthEnd.atTime(23, 59, 59)
                );
                eventsCount = evenementRepository.count(
                    "dateDebut >= ?1 and dateDebut <= ?2",
                    monthStart.atStartOfDay(),
                    monthEnd.atTime(23, 59, 59)
                );
            }

            BigDecimal contributionAmount = calculateMonthlyContributionAmount(monthStart, monthEnd, organizationId);
            double engagementRate = membersCount > 0 ? (double) activeMembersCount / membersCount : 0.0;

            monthlyData.add(MonthlyStatDTO.builder()
                .month(monthLabel)
                .totalMembers((int) membersCount)
                .activeMembers((int) activeMembersCount)
                .contributionAmount(contributionAmount.doubleValue())
                .eventsCount((int) eventsCount)
                .engagementRate(engagementRate)
                .newMembers((int) newMembersThisMonth)
                .contributionsCount((int) contributionsThisMonth)
                .build());
        }

        return monthlyData;
    }

    /**
     * Calcule le montant total des contributions pour un mois donné
     */
    private BigDecimal calculateMonthlyContributionAmount(LocalDate monthStart, LocalDate monthEnd, UUID organizationId) {
        String jpql = "SELECT COALESCE(SUM(c.montantPaye), 0) FROM Cotisation c " +
                     "WHERE c.datePaiement >= :start AND c.datePaiement <= :end";

        if (organizationId != null) {
            jpql += " AND c.organisation.id = :orgId";
        }

        TypedQuery<BigDecimal> query = cotisationRepository.getEntityManager().createQuery(jpql, BigDecimal.class);
        query.setParameter("start", monthStart.atStartOfDay());
        query.setParameter("end", monthEnd.atTime(23, 59, 59));

        if (organizationId != null) {
            query.setParameter("orgId", organizationId);
        }

        BigDecimal result = query.getSingleResult();
        return result != null ? result : BigDecimal.ZERO;
    }
}
