package com.izouma.jmrh.web;

import cn.licoy.encryptbody.annotation.decrypt.DecryptBody;
import cn.licoy.encryptbody.annotation.encrypt.EncryptBody;
import cn.licoy.encryptbody.enums.DecryptBodyMethod;
import cn.licoy.encryptbody.enums.EncryptBodyMethod;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.izouma.jmrh.annotations.Searchable;
import com.izouma.jmrh.domain.*;
import com.izouma.jmrh.dto.PageQuery;
import com.izouma.jmrh.utils.DateTimeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;

@SuppressWarnings("ALL")
@Slf4j
@EncryptBody(EncryptBodyMethod.AES)
@DecryptBody(DecryptBodyMethod.AES)
public class BaseController {
    public static PageRequest toPageRequest(PageQuery pageQuery) {
        PageRequest pageRequest;
        if (StringUtils.isNotEmpty(pageQuery.getSort())) {
            List<Sort.Order> orders = new ArrayList<>();
            for (String sortStr : pageQuery.getSort().split(";")) {
                String direction = "asc";
                String prop = sortStr;
                if (sortStr.contains(",asc") || sortStr.contains(",desc")) {
                    prop = sortStr.split(",")[0];
                    direction = sortStr.split(",")[1];
                }
                orders.add("asc".equals(direction) ? Sort.Order.asc(prop) : Sort.Order.desc(prop));
            }
            pageRequest = PageRequest.of(pageQuery.getPage(), pageQuery.getSize(), Sort.by(orders));
        } else {
            pageRequest = PageRequest.of(pageQuery.getPage(), pageQuery.getSize());
        }
        return pageRequest;
    }

    public <T> Specification<T> getResAll(PageQuery pageQuery, Class<?> queryClass) {
        return (Specification<T>) (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> or = new ArrayList<>();
            List<Predicate> and = new ArrayList<>();
            or.add(root.get("tresource").isNotNull());
            or.add(root.get("product").isNotNull());
            or.add(root.get("artProduct").isNotNull());
            if (pageQuery.getQuery().get("user") != null) {
                User user = (User) pageQuery.getQuery().get("user");
                Join<User, Collect> join = root.join("user", JoinType.LEFT);
                ObjectMapper objectMapper = new ObjectMapper();
                if (user.getId() != null && user.getId() > 0) {
                    and.add(criteriaBuilder.equal(join.get("id"), user.getId()));
                }
            }
            Predicate[] array_and = new Predicate[and.size()];
            Predicate Pre_And = criteriaBuilder.and(and.toArray(array_and));

            Predicate[] arrayOr = new Predicate[or.size()];
            Predicate Pre_Or = criteriaBuilder.or(or.toArray(arrayOr));

            /*criteriaBuilder.or(or.toArray(new Predicate[0]));*/
            return criteriaQuery.where(Pre_And, Pre_Or).getRestriction();
        };
    }

    public static <T> Specification<T> toSpecification(PageQuery pageQuery, Class<?> queryClass) {
        return (Specification<T>) (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> and = new ArrayList<>();
            pageQuery.getQuery().forEach((property, value) -> {
                if (value == null) {
                    return;
                }
                if (String.class.equals(value.getClass())) {
                    if (StringUtils.isEmpty((String) value)) {
                        return;
                    }
                }
                Field field = getDeclaredField(queryClass, property);
                if (field == null) return;
                Class fieldType = field.getType();
                if (Enum.class.isAssignableFrom(fieldType)) {
                    if (value instanceof Collection) {
                        if (!((Collection) value).isEmpty()) {
                            List list = new ArrayList();
                            for (Object o : ((Collection) value)) {
                                list.add(Enum.valueOf(fieldType, String.valueOf(o)));
                            }
                            and.add(root.get(property).in(list));
                        }
                    } else if (value instanceof String && StringUtils.isNotEmpty((String) value)) {
                        if (((String) value).contains(",")) {
                            String[] arr = ((String) value).split(",");
                            List list = new ArrayList();
                            for (String s : arr) {
                                list.add(Enum.valueOf(fieldType, s));
                            }
                            and.add(root.get(property).in(list));
                        } else {
                            and.add(criteriaBuilder.and(criteriaBuilder
                                    .equal(root.get(property), Enum.valueOf(fieldType, String.valueOf(value)))));
                        }
                    }
                } else if (LocalDateTime.class == fieldType) {
                    if (value instanceof List) {
                        List list = (List) value;
                        if (list.size() == 1) {
                            LocalDateTime start = DateTimeUtils
                                    .toLocalDateTime((String) list.get(0), "yyyy-MM-dd HH:mm:ss");
                            and.add(criteriaBuilder.greaterThanOrEqualTo(root.get(property), start));
                        } else if (list.size() == 2) {
                            LocalDateTime end = DateTimeUtils
                                    .toLocalDateTime((String) list.get(1), "yyyy-MM-dd HH:mm:ss");
                            and.add(criteriaBuilder.lessThanOrEqualTo(root.get(property), end));
                        }
                    } else if (value instanceof String && Pattern
                            .matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$", (String) value)) {
                        String[] arr = ((String) value).split(",");
                        LocalDateTime start = DateTimeUtils.toLocalDateTime(arr[0], "yyyy-MM-dd HH:mm:ss");
                        and.add(criteriaBuilder.greaterThanOrEqualTo(root.get(property), start));
                        LocalDateTime end = DateTimeUtils.toLocalDateTime(arr[1], "yyyy-MM-dd HH:mm:ss");
                        and.add(criteriaBuilder.lessThanOrEqualTo(root.get(property), end));
                    } else {
                        and.add(criteriaBuilder.and(criteriaBuilder.equal(root.get(property), DateTimeUtils
                                .toLocalDateTime((String) value, "yyyy-MM-dd HH:mm:ss"))));
                    }
                } else if (LocalDate.class == fieldType) {
                    if (value instanceof List) {
                        List list = (List) value;
                        if (list.size() == 1) {
                            LocalDate start = DateTimeUtils
                                    .toLocalDate((String) list.get(0), "yyyy-MM-dd");
                            and.add(criteriaBuilder.greaterThanOrEqualTo(root.get(property), start));
                        } else if (list.size() == 2) {
                            LocalDate end = DateTimeUtils
                                    .toLocalDate((String) list.get(1), "yyyy-MM-dd");
                            and.add(criteriaBuilder.lessThanOrEqualTo(root.get(property), end));
                        }
                    } else if (value instanceof String && Pattern
                            .matches("^\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2}$", (String) value)) {
                        String[] arr = ((String) value).split(",");
                        LocalDate start = DateTimeUtils.toLocalDate(arr[0], "yyyy-MM-dd");
                        and.add(criteriaBuilder.greaterThanOrEqualTo(root.get(property), start));
                        LocalDate end = DateTimeUtils.toLocalDate(arr[1], "yyyy-MM-dd");
                        and.add(criteriaBuilder.lessThanOrEqualTo(root.get(property), end));
                    } else {
                        and.add(criteriaBuilder.and(criteriaBuilder.equal(root.get(property), DateTimeUtils
                                .toLocalDateTime((String) value, "yyyy-MM-dd"))));
                    }
                } else if (fieldType == DataType.class) {
                    Class cla = queryClass;
                    Join<DataType, Class> join = root.join(property, JoinType.LEFT);
                    ObjectMapper objectMapper = new ObjectMapper();
                    DataType articleType = objectMapper.convertValue(value, DataType.class);
                    if (StringUtils.isNotEmpty(articleType.getKeyType())) {
                        and.add(criteriaBuilder.equal(join.get("keyType"), articleType.getKeyType()));
                    }
                    if (articleType.getId() != null && articleType.getId() > 0) {
                        and.add(criteriaBuilder.equal(join.get("id"), articleType.getId()));
                    }
                } else if (fieldType == OrgInfo.class) {
                    Join<OrgInfo, Class> join = root.join(property, JoinType.LEFT);
                    ObjectMapper objectMapper = new ObjectMapper();
                    OrgInfo articleType = objectMapper.convertValue(value, OrgInfo.class);
                    if (articleType.getId() != null && articleType.getId() > 0) {
                        and.add(criteriaBuilder.equal(join.get("id"), articleType.getId()));
                    }
                } else if (property.equals("state")) {
                    String[] split = String.valueOf(value).split(",");
                    if (split.length > 1) {
                        List<Predicate> or = new ArrayList<>();
                        or.add(criteriaBuilder.equal(root.get("state"), Long.parseLong(split[0])));
                        or.add(criteriaBuilder.equal(root.get("state"), Long.parseLong(split[1])));
                        and.add(criteriaBuilder.or(or.toArray(new Predicate[0])));
                    } else {
                        and.add(criteriaBuilder.and(criteriaBuilder.equal(root.get(property), value)));
                    }
                } else {
                    and.add(criteriaBuilder.and(criteriaBuilder.equal(root.get(property), value)));
                }
            });

            if (StringUtils.isNotEmpty(pageQuery.getSearch())) {
                Field[] fields = queryClass.getDeclaredFields();
                List<Predicate> or = new ArrayList<>();
                try {
                    if (StringUtils.isNumeric(pageQuery.getSearch())) {
                        or.add(criteriaBuilder.equal(root.get("id"), Long.parseLong(pageQuery.getSearch())));
                    }
                } catch (Exception ignored) {
                }
                for (Field field : fields) {
                    Searchable annotation = field.getAnnotation(Searchable.class);
                    if (annotation == null) {
                        continue;
                    }
                    if (!annotation.value()) {
                        continue;
                    }
                    or.add(criteriaBuilder.like(root.get(field.getName()), "%" + pageQuery.getSearch() + "%"));
                }
                and.add(criteriaBuilder.or(or.toArray(new Predicate[0])));
            }

            return criteriaBuilder.and(and.toArray(new Predicate[0]));
        };
    }


    public <T> Specification<T> getResourceSupplyAndDemand(PageQuery pageQuery, Class<?> queryClass) {
        return (Specification<T>) (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> and = new ArrayList<>();
            pageQuery.getQuery().forEach((property, value) -> {
                if (value == null) {
                    return;
                }
                if (String.class.equals(value.getClass())) {
                    if (StringUtils.isEmpty((String) value)) {
                        return;
                    }
                }
                Field field = getDeclaredField(queryClass, property);
                if (field == null) return;
                Class fieldType = field.getType();
                if (Enum.class.isAssignableFrom(fieldType)) {
                    if (value instanceof Collection) {
                        if (!((Collection) value).isEmpty()) {
                            List list = new ArrayList();
                            for (Object o : ((Collection) value)) {
                                list.add(Enum.valueOf(fieldType, String.valueOf(o)));
                            }
                            and.add(root.get(property).in(list));
                        }
                    } else if (value instanceof String && StringUtils.isNotEmpty((String) value)) {
                        if (((String) value).contains(",")) {
                            String[] arr = ((String) value).split(",");
                            List list = new ArrayList();
                            for (String s : arr) {
                                list.add(Enum.valueOf(fieldType, s));
                            }
                            and.add(root.get(property).in(list));
                        } else {
                            and.add(criteriaBuilder.and(criteriaBuilder
                                    .equal(root.get(property), Enum.valueOf(fieldType, String.valueOf(value)))));
                        }
                    }
                } else if (LocalDateTime.class == fieldType) {
                    if (value instanceof List) {
                        List list = (List) value;
                        if (list.size() == 1) {
                            LocalDateTime start = DateTimeUtils
                                    .toLocalDateTime((String) list.get(0), "yyyy-MM-dd HH:mm:ss");
                            and.add(criteriaBuilder.greaterThanOrEqualTo(root.get(property), start));
                        } else if (list.size() == 2) {
                            LocalDateTime end = DateTimeUtils
                                    .toLocalDateTime((String) list.get(1), "yyyy-MM-dd HH:mm:ss");
                            and.add(criteriaBuilder.lessThanOrEqualTo(root.get(property), end));
                        }
                    } else if (value instanceof String && Pattern
                            .matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$", (String) value)) {
                        String[] arr = ((String) value).split(",");
                        LocalDateTime start = DateTimeUtils.toLocalDateTime(arr[0], "yyyy-MM-dd HH:mm:ss");
                        and.add(criteriaBuilder.greaterThanOrEqualTo(root.get(property), start));
                        LocalDateTime end = DateTimeUtils.toLocalDateTime(arr[1], "yyyy-MM-dd HH:mm:ss");
                        and.add(criteriaBuilder.lessThanOrEqualTo(root.get(property), end));
                    } else {
                        and.add(criteriaBuilder.and(criteriaBuilder.equal(root.get(property), DateTimeUtils
                                .toLocalDateTime((String) value, "yyyy-MM-dd HH:mm:ss"))));
                    }
                } else if (LocalDate.class == fieldType) {
                    if (value instanceof List) {
                        List list = (List) value;
                        if (list.size() == 1) {
                            LocalDate start = DateTimeUtils
                                    .toLocalDate((String) list.get(0), "yyyy-MM-dd");
                            and.add(criteriaBuilder.greaterThanOrEqualTo(root.get(property), start));
                        } else if (list.size() == 2) {
                            LocalDate end = DateTimeUtils
                                    .toLocalDate((String) list.get(1), "yyyy-MM-dd");
                            and.add(criteriaBuilder.lessThanOrEqualTo(root.get(property), end));
                        }
                    } else if (value instanceof String && Pattern
                            .matches("^\\d{4}-\\d{2}-\\d{2},\\d{4}-\\d{2}-\\d{2}$", (String) value)) {
                        String[] arr = ((String) value).split(",");
                        LocalDate start = DateTimeUtils.toLocalDate(arr[0], "yyyy-MM-dd");
                        and.add(criteriaBuilder.greaterThanOrEqualTo(root.get(property), start));
                        LocalDate end = DateTimeUtils.toLocalDate(arr[1], "yyyy-MM-dd");
                        and.add(criteriaBuilder.lessThanOrEqualTo(root.get(property), end));
                    } else {
                        and.add(criteriaBuilder.and(criteriaBuilder.equal(root.get(property), DateTimeUtils
                                .toLocalDateTime((String) value, "yyyy-MM-dd"))));
                    }
                } else {
                    and.add(criteriaBuilder.and(criteriaBuilder.equal(root.get(property), value)));
                }
            });

            if (StringUtils.isNotEmpty(pageQuery.getSearch())) {
                Field[] fields = queryClass.getDeclaredFields();
                List<Predicate> or = new ArrayList<>();
                try {
                    if (StringUtils.isNumeric(pageQuery.getSearch())) {
                        or.add(criteriaBuilder.equal(root.get("id"), Long.parseLong(pageQuery.getSearch())));
                    }
                } catch (Exception ignored) {
                }
                for (Field field : fields) {
                    Searchable annotation = field.getAnnotation(Searchable.class);
                    if (annotation == null) {
                        continue;
                    }
                    if (!annotation.value()) {
                        continue;
                    }
                    or.add(criteriaBuilder.like(root.get(field.getName()), "%" + pageQuery.getSearch() + "%"));
                }
                and.add(criteriaBuilder.or(or.toArray(new Predicate[0])));
            }

            return criteriaBuilder.and(and.toArray(new Predicate[0]));
        };
    }

    private static Field getDeclaredField(Class<?> clazz, String property) {
        String className = clazz.getName();
        while (clazz != null && clazz != Object.class) {
            try {
                return clazz.getDeclaredField(property);
            } catch (NoSuchFieldException ignored) {
            }
            clazz = clazz.getSuperclass();
        }
        log.error("no such field [{}] in class [{}]", property, className);
        return null;
    }
}
