package com.imprimelibros.erp.datatables;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;

public class DataTable<T> {

    public interface FilterHook<T> extends BiConsumer<SpecBuilder<T>, DataTablesRequest> {
    }

    public interface SpecBuilder<T> {
        void add(Specification<T> extra);
    }

    private final JpaSpecificationExecutor<T> repo;
    private final Class<T> entityClass;
    private final DataTablesRequest dt;
    private final List<String> searchable;
    private final List<Function<T, Map<String, Object>>> adders = new ArrayList<>();
    private final List<Function<Map<String, Object>, Map<String, Object>>> editors = new ArrayList<>();
    private final List<FilterHook<T>> filters = new ArrayList<>();
    private Specification<T> baseSpec = (root, q, cb) -> cb.conjunction();
    private final ObjectMapper om = new ObjectMapper();
    private List<String> orderable = null;

    private DataTable(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
            List<String> searchable) {
        this.repo = repo;
        this.entityClass = entityClass;
        this.dt = dt;
        this.searchable = searchable;
    }

    public static <T> DataTable<T> of(JpaSpecificationExecutor<T> repo, Class<T> entityClass, DataTablesRequest dt,
            List<String> searchable) {
        return new DataTable<>(repo, entityClass, dt, searchable);
    }

    /** Equivalente a tu $q->where(...): establece condición base */
    public DataTable<T> where(Specification<T> spec) {
        this.baseSpec = this.baseSpec.and(spec);
        return this;
    }

    /** add("campo", fn(entity)->valor|Map) */
    public DataTable<T> add(String field, Function<T, Object> fn) {
        adders.add(entity -> {
            Map<String, Object> m = new HashMap<>();
            m.put(field, fn.apply(entity));
            return m;
        });
        return this;
    }

    /**
     * add(fn(entity)->Map<String,Object>) para devolver objetos anidados como tu
     * "logo"
     */
    public DataTable<T> add(Function<T, Map<String, Object>> fn) {
        adders.add(fn);
        return this;
    }

    /**
     * edit("campo", fn(entity)->valor) sobreescribe un campo existente o lo crea si
     * no existe
     */
    public DataTable<T> edit(String field, Function<T, Object> fn) {
        editors.add(row -> {
            row.put(field, fn.apply((T) row.get("__entity")));
            return row;
        });
        return this;
    }

    public DataTable<T> orderable(List<String> fields) {
        this.orderable = fields;
        return this;
    }

    private List<String> getOrderable() {
        return (orderable == null || orderable.isEmpty()) ? this.searchable : this.orderable;
    }

    /** filter((builder, req) -> builder.add(miExtraSpec(req))) */
    public DataTable<T> filter(FilterHook<T> hook) {
        filters.add(hook);
        return this;
    }

    public DataTablesResponse<Map<String, Object>> toJson(long totalCount) {
        // Construye spec con búsqueda global + base + filtros custom
        Specification<T> spec = baseSpec.and(DataTablesSpecification.build(dt, searchable));
        final Specification<T>[] holder = new Specification[] { spec };
        filters.forEach(h -> h.accept(extra -> holder[0] = holder[0].and(extra), dt));
        spec = holder[0];

        // Sort
        // Sort
        Sort sort = Sort.unsorted();
        if (!dt.order.isEmpty() && !dt.columns.isEmpty()) {
            List<Sort.Order> orders = new ArrayList<>();
            for (var o : dt.order) {
                var col = dt.columns.get(o.column);
                String field = col != null ? col.name : null;

                if (field == null || field.isBlank())
                    continue;
                if (!col.orderable)
                    continue;
                if (!getOrderable().contains(field))
                    continue; // << usa tu whitelist

                orders.add(new Sort.Order(
                        "desc".equalsIgnoreCase(o.dir) ? Sort.Direction.DESC : Sort.Direction.ASC,
                        field));
            }
            if (!orders.isEmpty()) {
                sort = Sort.by(orders);
            } else {
                for (var c : dt.columns) {
                    if (c != null && c.orderable && c.name != null && !c.name.isBlank()
                            && getOrderable().contains(c.name)) {
                        sort = Sort.by(c.name);
                        break;
                    }
                }
            }
        }

        // Page
        int page = dt.length > 0 ? dt.start / dt.length : 0;
        Pageable pageable = dt.length > 0 ? PageRequest.of(page, dt.length, sort) : Pageable.unpaged();

        var p = repo.findAll(holder[0], pageable);
        long filtered = p.getTotalElements();

        // Mapear entidad -> Map base (via Jackson) + add/edit
        List<Map<String, Object>> data = new ArrayList<>();
        for (T e : p.getContent()) {
            Map<String, Object> row = om.convertValue(e, Map.class);
            row.put("__entity", e); // para editores que necesiten la entidad
            for (var ad : adders)
                row.putAll(ad.apply(e));
            for (var ed : editors)
                ed.apply(row);
            row.remove("__entity");
            data.add(row);
        }
        return new DataTablesResponse<>(dt.draw, totalCount, filtered, data);
    }
}
