package dev.lions.components;

import jakarta.enterprise.context.Dependent;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;

import java.io.Serial;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import org.primefaces.model.FilterMeta;
import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SortMeta;
import org.primefaces.event.data.PageEvent;

import dev.lions.utils.Column;
import dev.lions.utils.FilterCriteria;
import dev.lions.exceptions.DataTableException;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * Composant de tableau de données dynamique avec support de pagination, tri et filtrage.
 * Fournit une interface riche et performante pour l'affichage et la manipulation des données
 * tabulaires dans l'application.
 *
 * @author Lions Dev Team
 * @version 2.1
 */
@Named
@Dependent
@Slf4j
public class DynamicDataTable<T> implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;
    private static final int DEFAULT_PAGE_SIZE = 10;
    private static final int MAX_PAGE_SIZE = 100;
    private static final String DEFAULT_EMPTY_MESSAGE = "Aucune donnée disponible";

    @Getter @Setter
    private List<T> data;

    @Getter @Setter
    private List<Column> columns;

    @Getter @Setter
    private String emptyMessage = DEFAULT_EMPTY_MESSAGE;

    @Getter @Setter
    @Min(1)
    private int pageSize = DEFAULT_PAGE_SIZE;

    @Getter
    private LazyDataModel<T> lazyModel;

    @Getter
    private final Map<String, FilterCriteria> activeFilters = new ConcurrentHashMap<>();

    private final Map<String, Comparator<T>> customSorters = new HashMap<>();
    private final Map<String, PropertyAccessor<T>> propertyAccessors = new HashMap<>();

    /**
     * Initialise le tableau avec les données et les colonnes spécifiées.
     *
     * @param data Données à afficher
     * @param columns Configuration des colonnes
     */
    public void initialize(@NotNull List<T> data, @NotNull List<Column> columns) {
        log.info("Initialisation du tableau dynamique avec {} enregistrements", data.size());

        validateInitializationParameters(data, columns);

        this.data = new ArrayList<>(data);
        this.columns = new ArrayList<>(columns);

        initializePropertyAccessors();
        initializeLazyLoading();
    }

    /**
     * Configure le modèle de chargement paresseux des données.
     */
    private void initializeLazyLoading() {
        lazyModel = new LazyDataModel<T>() {
            @Override
            public List<T> load(int first, int pageSize, Map<String, SortMeta> sortBy, Map<String, FilterMeta> filterBy) {
                try {
                    return loadDataPage(first, pageSize, sortBy, filterBy);
                } catch (Exception e) {
                    log.error("Erreur lors du chargement des données", e);
                    throw new DataTableException("Échec du chargement des données", e);
                }
            }

            @Override
            public int count(Map<String, FilterMeta> filterBy) {
                return data == null ? 0 : data.size();
            }
        };
        lazyModel.setRowCount(data.size());
    }

    /**
     * Charge une page de données selon les critères spécifiés.
     */
    protected List<T> loadDataPage(int first, int pageSize, Map<String, SortMeta> sortBy, Map<String, FilterMeta> filterBy) {
        return data.stream()
                   .filter(item -> applyFilters(item, filterBy))
                   .sorted((a, b) -> applySorting(a, b, sortBy))
                   .skip(first)
                   .limit(pageSize)
                   .collect(Collectors.toList());
    }

    /**
     * Applique les filtres sur un élément.
     */
    private boolean applyFilters(T item, Map<String, FilterMeta> filterBy) {
        if (filterBy == null || filterBy.isEmpty()) return true;

        return filterBy.entrySet().stream().allMatch(entry -> {
            Object filterValue = entry.getValue().getFilterValue();
            if (filterValue == null) return true;

            try {
                Object value = getPropertyValue(item, entry.getKey());
                return value != null && value.toString().toLowerCase().contains(filterValue.toString().toLowerCase());
            } catch (Exception e) {
                log.warn("Erreur lors du filtrage", e);
                return false;
            }
        });
    }

    /**
     * Vérifie si un élément correspond à un critère de filtrage.
     */
    private boolean matchesFilter(T item, String property, Object filterValue) {
        if (filterValue == null) {
            return true;
        }

        try {
            Object value = getPropertyValue(item, property);
            return compareValues(value, filterValue);
        } catch (Exception e) {
            log.warn("Erreur lors du filtrage de la propriété: {}", property, e);
            return false;
        }
    }

    /**
     * Compare deux valeurs pour le filtrage.
     */
    private boolean compareValues(Object value, Object filterValue) {
        if (value == null) {
            return filterValue == null;
        }

        String valueStr = value.toString().toLowerCase();
        String filterStr = filterValue.toString().toLowerCase();
        return valueStr.contains(filterStr);
    }

    /**
     * Applique le tri sur les données.
     */
    private int applySorting(T a, T b, Map<String, SortMeta> sortBy) {
        for (Map.Entry<String, SortMeta> entry : sortBy.entrySet()) {
            String property = entry.getKey();
            try {
                Comparable valueA = (Comparable) getPropertyValue(a, property);
                Comparable valueB = (Comparable) getPropertyValue(b, property);

                int result = valueA.compareTo(valueB);
                return entry.getValue().getOrder().isAscending() ? result : -result;
            } catch (Exception e) {
                log.warn("Erreur lors du tri", e);
            }
        }
        return 0;
    }


    /**
     * Compare les valeurs de deux propriétés pour le tri.
     */
    @SuppressWarnings("unchecked")
    private int compareProperties(T a, T b, String property) {
        try {
            Comparable valueA = (Comparable) getPropertyValue(a, property);
            Comparable valueB = (Comparable) getPropertyValue(b, property);

            if (valueA == null && valueB == null) return 0;
            if (valueA == null) return -1;
            if (valueB == null) return 1;

            return valueA.compareTo(valueB);
        } catch (Exception e) {
            log.warn("Erreur lors de la comparaison de la propriété: {}", property, e);
            return 0;
        }
    }

    /**
     * Initialise les accesseurs de propriétés pour optimiser les performances.
     */
    private void initializePropertyAccessors() {
        columns.forEach(column -> {
            String property = column.getField();
            try {
                Method getter = findGetter(property);
                propertyAccessors.put(property, item -> getter.invoke(item));
            } catch (Exception e) {
                log.warn("Impossible de créer l'accesseur pour la propriété: {}", property, e);
            }
        });
    }

    /**
     * Trouve la méthode getter pour une propriété.
     */
    private Method findGetter(String property) throws NoSuchMethodException {
        String getterName = "get" + property.substring(0, 1).toUpperCase() + property.substring(1);
        return data.get(0).getClass().getMethod(getterName);
    }

    /**
     * Interface fonctionnelle pour l'accès aux propriétés.
     */
    @FunctionalInterface
    private interface PropertyAccessor<T> {
        Object access(T item) throws Exception;
    }

    /**
     * Ajoute un trieur personnalisé pour une colonne.
     */
    public void addCustomSorter(String property, Comparator<T> comparator) {
        customSorters.put(property, comparator);
    }

    /**
     * Met à jour le nombre total de lignes.
     */
    private void updateRowCount() {
        if (lazyModel != null) {
            lazyModel.setRowCount(data.size());
        }
    }

    /**
     * Gère l'événement de changement de page.
     */
    public void onPageChange(PageEvent event) {
        log.debug("Changement de page: {}", event.getPage());
    }

    /**
     * Valide les paramètres d'initialisation.
     */
    private void validateInitializationParameters(List<T> data, List<Column> columns) {
        if (data == null || data.isEmpty()) {
            throw new DataTableException("Les données ne peuvent pas être nulles ou vides");
        }
        if (columns == null || columns.isEmpty()) {
            throw new DataTableException("La configuration des colonnes est requise");
        }
    }

    /**
     * Rafraîchit les données du tableau.
     */
    public void refresh() {
        log.debug("Rafraîchissement du tableau");
        updateRowCount();
    }

    private Object getPropertyValue(T item, String property) throws Exception {
        PropertyAccessor<T> accessor = propertyAccessors.get(property);
        if (accessor != null) {
            return accessor.access(item);
        }
        throw new NoSuchFieldException("Propriété inaccessible : " + property);
    }

}