package cn.lihongjie.coal.common;

import static org.springframework.data.jpa.repository.query.QueryUtils.toOrders;

import com.google.common.base.CaseFormat;

import io.vavr.control.Try;

import jakarta.persistence.*;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.formula.functions.T;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.support.PageableUtils;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

@Slf4j
public class JpaUtils {

    public static Function<String, String> underscoreToCamelCase =
            (String name) -> CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name);

    public static List<Map> convertTuplesToRawMap(List<Tuple> tuples) {
        return convertTuplesToMap(tuples, Function.identity(), Function.identity()).stream()
                .map(x -> (Map) x)
                .collect(Collectors.toList());
    }

    public static List<Map<String, Object>> convertTuplesToMap(List<Tuple> tuples) {
        return convertTuplesToMap(tuples, Function.identity(), Function.identity());
    }

    public static Function<Object, Object> handleArray =
            (Object value) -> {
                if (value == null) {
                    return null;
                }
                if (value.getClass().isArray()) {

                    Object[] objects = (Object[]) value;

                    if (objects.length == 0 || (objects.length == 1 && objects[0] == null)) {
                        return new ArrayList<>();
                    }

                    return new ArrayList<>(Arrays.asList(objects));
                } else {
                    return value;
                }
            };

    public static <T> void mergeMapToPojo(
            Iterable<T> pojos, Iterable<Map> maps, ConversionService conversionService) {

        mergeMapToPojo(
                pojos,
                x -> ((Function<Object, String>) ReflectUtils::getId).apply(x),
                maps,
                "id",
                conversionService);
    }

    @SneakyThrows
    public static <T, ID> void mergeMapToPojo(
            Iterable<T> pojos,
            Function<T, ID> pojoIdGetter,
            Iterable<Map> maps,
            String mapKey,
            ConversionService conversionService) {

        if (ObjectUtils.isEmpty(maps)) {
            return;
        }

        if (ObjectUtils.isEmpty(pojos)) {
            return;
        }

        Map<ID, T> pojoMap =
                StreamSupport.stream(pojos.spliterator(), false)
                        .collect(Collectors.toMap(pojoIdGetter, Function.identity()));

        Map<ID, Map> mapMap =
                StreamSupport.stream(maps.spliterator(), false)
                        .collect(Collectors.toMap(e -> (ID) e.get(mapKey), Function.identity()));

        for (ID id : mapMap.keySet()) {

            T pojo = pojoMap.get(id);

            if (pojo == null) {
                continue;
            }

            Map map = mapMap.get(id);

            for (Object key : map.keySet()) {

                if (StringUtils.equals(key.toString(), mapKey)) {
                    continue;
                }

                String fieldName = key.toString();
                Object value = map.get(key);
                if (!ReflectUtils.hasField(pojo.getClass(), fieldName)) {

                    fieldName = underscoreToCamelCase.apply(fieldName);
                }

                if (!ReflectUtils.hasField(pojo.getClass(), fieldName)) {

                    log.info(
                            "field {}/{} not found in class {}",
                            key,
                            fieldName,
                            pojo.getClass().getName());
                    continue;
                }

                Field field = ReflectUtils.getField(pojo.getClass(), fieldName).get();

                ReflectUtils.writeField(
                        pojo, fieldName, conversionService.convert(value, field.getType()));
            }
        }
    }

    public static <T> List<T> execNativeQuery(
            EntityManager entityManager, String sql, Object parameter, Class<T> clazz) {

        if (clazz.isAssignableFrom(Map.class)) {
            Query query = entityManager.createNativeQuery(sql, Tuple.class);
            setQueryParameter(query, parameter);
            return convertTuplesToMap(query.getResultList(), underscoreToCamelCase);
        } else {

            Query query = entityManager.createNativeQuery(sql, clazz);

            setQueryParameter(query, parameter);

            return query.getResultList();
        }
    }

    public static void setQueryParameter(Query query, Object parameter) {

        if (query == null || parameter == null) {
            return;
        }

        if (parameter instanceof Map) {
            Map<String, Object> map = (Map<String, Object>) parameter;
            for (String key : map.keySet()) {
                Try.run(() -> query.setParameter(key, map.get(key)));
            }
        } else if (parameter instanceof Iterable<?> it) {

            Try.run(() -> query.setParameter("param", it));

            int index = 0;
            for (Object o : it) {

                Try.run(() -> query.setParameter("param" + index, o));
            }
        } else {

            ReflectUtils.getAllFieldsList(parameter.getClass()).stream()
                    .filter(x -> !Modifier.isStatic(x.getModifiers()))
                    .forEach(
                            x -> {
                                Object value = ReflectUtils.getFieldValue(parameter, x.getName());
                                Try.run(() -> query.setParameter(x.getName(), value));
                            });
        }
    }

    public static <S extends T> TypedQuery<S> getQuery(
            EntityManager entityManager, Specification<S> spec, Class<S> domainClass, Sort sort) {

        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<S> query = builder.createQuery(domainClass);

        Root<S> root = applySpecificationToCriteria(entityManager, spec, domainClass, query);
        query.select(root);

        if (sort.isSorted()) {
            query.orderBy(toOrders(sort, root, builder));
        }

        return (entityManager.createQuery(query));
    }

    public static boolean hasDuplicateInCurrentOrg(
            EntityManager entityManager,
            Class clazz,
            String fieldName,
            Object value) {
        return hasDuplicate(
                entityManager,
                clazz,
                fieldName,
                value,
                Ctx.currentUser().getOrganizationId(),
                null);
    }

    public static boolean hasDuplicateInCurrentOrg(
            EntityManager entityManager,
            Class clazz,
            String fieldName,
            Object value,
            String selfId) {
        return hasDuplicate(entityManager, clazz, fieldName, value, Ctx.currentUser().getOrganizationId(), selfId);
    }

    public static boolean hasDuplicate(
            EntityManager entityManager,
            Class clazz,
            String fieldName,
            Object value) {
        return hasDuplicate(entityManager, clazz, fieldName, value, null, null);
    }

    public static boolean hasDuplicate(
            EntityManager entityManager,
            Class clazz,
            String fieldName,
            Object value,
            String selfId) {
        return hasDuplicate(entityManager, clazz, fieldName, value, null, selfId);
    }

    public static boolean hasDuplicate(
            EntityManager entityManager,
            Class clazz,
            String fieldName,
            Object value,
            String organizationId,
            String selfId) {
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Long> query = builder.createQuery(Long.class);
        Root root = query.from(clazz);
        query.select(builder.count(root));

        ArrayList<Predicate> wheres = new ArrayList<>();

        wheres.add(builder.equal(root.get(fieldName), value));

        if (StringUtils.isNotEmpty(organizationId)) {
            wheres.add(builder.equal(root.get("organizationId"), organizationId));
        }

        if (StringUtils.isNotEmpty(selfId)) {
            wheres.add(builder.notEqual(root.get("id"), selfId));
        }

        query.where(wheres.toArray(new Predicate[0]));
        return entityManager.createQuery(query).getSingleResult() > 0;
    }

    public static <S extends T> Page<S> readPage(
            EntityManager entityManager,
            TypedQuery<S> query,
            final Class<S> domainClass,
            Pageable pageable,
            @Nullable Specification<S> spec) {

        if (pageable.isPaged()) {
            query.setFirstResult(PageableUtils.getOffsetAsInteger(pageable));
            query.setMaxResults(pageable.getPageSize());
        }

        return PageableExecutionUtils.getPage(
                query.getResultList(),
                pageable,
                () -> executeCountQuery(getCountQuery(entityManager, spec, domainClass)));
    }

    private static long executeCountQuery(TypedQuery<Long> query) {

        Assert.notNull(query, "TypedQuery must not be null");

        List<Long> totals = query.getResultList();
        long total = 0L;

        for (Long element : totals) {
            total += element == null ? 0 : element;
        }

        return total;
    }

    public static <S extends T> TypedQuery<Long> getCountQuery(
            EntityManager entityManager, @Nullable Specification<S> spec, Class<S> domainClass) {

        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Long> query = builder.createQuery(Long.class);

        Root<S> root = applySpecificationToCriteria(entityManager, spec, domainClass, query);

        if (query.isDistinct()) {
            query.select(builder.countDistinct(root));
        } else {
            query.select(builder.count(root));
        }

        // Remove all Orders the Specifications might have applied
        query.orderBy(Collections.emptyList());

        return (entityManager.createQuery(query));
    }

    private static <S, U extends T> Root<U> applySpecificationToCriteria(
            EntityManager entityManager,
            @Nullable Specification<U> spec,
            Class<U> domainClass,
            CriteriaQuery<S> query) {

        Assert.notNull(domainClass, "Domain class must not be null");
        Assert.notNull(query, "CriteriaQuery must not be null");

        Root<U> root = query.from(domainClass);

        if (spec == null) {
            return root;
        }

        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        Predicate predicate = spec.toPredicate(root, query, builder);

        if (predicate != null) {
            query.where(predicate);
        }

        return root;
    }

    public static List<Map<String, Object>> convertTuplesToMap(
            List<Tuple> tuples, Function<String, String> keyMapper) {
        return convertTuplesToMap(tuples, keyMapper, Function.identity());
    }

    public static List<Map<String, Object>> convertTuplesToMap(
            List<Tuple> tuples,
            Function<String, String> keyMapper,
            Function<Object, Object> valueMapper) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (Tuple single : tuples) {
            Map<String, Object> tempMap = new HashMap<>();
            for (TupleElement<?> key : single.getElements()) {
                tempMap.put(keyMapper.apply(key.getAlias()), valueMapper.apply(single.get(key)));
            }
            result.add(tempMap);
        }
        return result;
    }
}
