package com.eshore.gringotts.web.domain.base.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.eshore.gringotts.web.domain.base.controller.query.Query;
import com.eshore.gringotts.web.domain.base.entity.IdOnlyEntity_;
import com.eshore.gringotts.web.domain.base.entity.SimpleEntity;
import com.eshore.gringotts.web.domain.base.entity.SimpleEntity_;
import com.eshore.gringotts.web.domain.base.repository.SimpleRepository;
import com.eshore.gringotts.web.domain.entity.User;
import com.eshore.gringotts.web.domain.service.UserService;
import java.util.Optional;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.list.ImmutableList;
import org.eclipse.collections.api.list.MutableList;
import org.eclipse.collections.api.set.ImmutableSet;
import org.springframework.data.domain.Sort;

/**
 * @author lanyuanxiaoyao
 * @date 2024-11-21
 */
@Slf4j
public abstract class SimpleServiceSupport<ENTITY extends SimpleEntity> implements SimpleService<ENTITY> {
    protected final SimpleRepository<ENTITY, Long> repository;
    protected final UserService userService;

    public SimpleServiceSupport(SimpleRepository<ENTITY, Long> repository, UserService userService) {
        this.repository = repository;
        this.userService = userService;
    }

    @Transactional(rollbackOn = Throwable.class)
    @Override
    public Long save(ENTITY entity) {
        if (ObjectUtil.isNotNull(entity.getId())) {
            Long id = entity.getId();
            ENTITY targetEntity = repository.findById(entity.getId()).orElseThrow(() -> new IdNotFoundException(id));
            BeanUtil.copyProperties(
                    entity,
                    targetEntity,
                    CopyOptions.create()
                            .setIgnoreProperties(
                                    IdOnlyEntity_.ID,
                                    SimpleEntity_.CREATED_USER,
                                    SimpleEntity_.CREATED_TIME,
                                    SimpleEntity_.MODIFIED_USER,
                                    SimpleEntity_.MODIFIED_TIME
                            )
            );
            entity = targetEntity;
        }
        entity = repository.save(entity);
        return entity.getId();
    }

    @Override
    public ImmutableList<ENTITY> list() throws Exception {
        return Lists.immutable.ofAll(repository.findAll(
                (root, query, builder) ->
                        builder.and(
                                listPredicate(root, query, builder)
                                        .reject(ObjectUtil::isNull)
                                        .toArray(new Predicate[]{})
                        )
        ));
    }

    @Override
    public ImmutableList<ENTITY> list(ImmutableSet<Long> ids) throws Exception {
        return Lists.immutable.ofAll(repository.findAll(
                (root, query, builder) -> {
                    MutableList<Predicate> predicates = Lists.mutable.ofAll(listPredicate(root, query, builder));
                    predicates.add(builder.in(root.get("id")).value(ids));
                    return builder.and(predicates.reject(ObjectUtil::isNull).toArray(new Predicate[predicates.size()]));
                }
        ));
    }

    private <Y> Path<Y> column(Root<ENTITY> root, String column) {
        String[] columns = StrUtil.splitToArray(column, "/");
        Path<Y> path = root.get(columns[0]);
        for (int i = 1; i < columns.length; i++) {
            path = path.get(columns[i]);
        }
        return path;
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private <Y> Object value(Path<Y> column, Object value) {
        Class<?> javaType = column.getJavaType();
        if (EnumUtil.isEnum(javaType)) {
            return EnumUtil.fromString((Class<Enum>) javaType, (String) value);
        }
        return value;
    }

    @SuppressWarnings("unchecked")
    private ImmutableList<Predicate> queryPredicates(Query.Queryable queryable, Root<ENTITY> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        MutableList<Predicate> predicates = Lists.mutable.empty();
        if (ObjectUtil.isEmpty(queryable)) {
            return predicates.toImmutable();
        }
        if (ObjectUtil.isNotEmpty(queryable.getNullEqual())) {
            queryable.getNullEqual().forEach(column -> predicates.add(builder.isNull(column(root, column))));
        }
        if (ObjectUtil.isNotEmpty(queryable.getNotNullEqual())) {
            queryable.getNotNullEqual().forEach(column -> predicates.add(builder.isNull(column(root, column))));
        }
        if (ObjectUtil.isNotEmpty(queryable.getEmpty())) {
            queryable.getEmpty().forEach(column -> predicates.add(builder.isEmpty(column(root, column))));
        }
        if (ObjectUtil.isNotEmpty(queryable.getNotEmpty())) {
            queryable.getNotEmpty().forEach(column -> predicates.add(builder.isNotEmpty(column(root, column))));
        }
        if (ObjectUtil.isNotEmpty(queryable.getEqual())) {
            queryable.getEqual().forEachKeyValue((column, value) -> {
                Path<Object> path = column(root, column);
                predicates.add(builder.equal(path, value(path, value)));
            });
        }
        if (ObjectUtil.isNotEmpty(queryable.getNotEqual())) {
            queryable.getEqual().forEachKeyValue((column, value) -> {
                Path<Object> path = column(root, column);
                predicates.add(builder.notEqual(path, value(path, value)));
            });
        }
        if (ObjectUtil.isNotEmpty(queryable.getLike())) {
            queryable.getLike().forEachKeyValue((column, value) -> predicates.add(builder.like(column(root, column), value)));
        }
        if (ObjectUtil.isNotEmpty(queryable.getNotLike())) {
            queryable.getNotLike().forEachKeyValue((column, value) -> predicates.add(builder.notLike(column(root, column), value)));
        }
        if (ObjectUtil.isNotEmpty(queryable.getGreat())) {
            queryable.getGreat().forEachKeyValue((column, value) -> predicates.add(builder.greaterThan(column(root, column), (Comparable<Object>) value)));
        }
        if (ObjectUtil.isNotEmpty(queryable.getLess())) {
            queryable.getLess().forEachKeyValue((column, value) -> predicates.add(builder.lessThan(column(root, column), (Comparable<Object>) value)));
        }
        if (ObjectUtil.isNotEmpty(queryable.getGreatEqual())) {
            queryable.getGreatEqual().forEachKeyValue((column, value) -> predicates.add(builder.greaterThanOrEqualTo(column(root, column), (Comparable<Object>) value)));
        }
        if (ObjectUtil.isNotEmpty(queryable.getLessEqual())) {
            queryable.getLessEqual().forEachKeyValue((column, value) -> predicates.add(builder.lessThanOrEqualTo(column(root, column), (Comparable<Object>) value)));
        }
        if (ObjectUtil.isNotEmpty(queryable.getIn())) {
            queryable.getIn().forEachKeyValue((column, value) -> predicates.add(builder.in(column(root, column)).value(value)));
        }
        if (ObjectUtil.isNotEmpty(queryable.getNotIn())) {
            queryable.getNotIn().forEachKeyValue((column, value) -> predicates.add(builder.in(column(root, column)).value(value).not()));
        }
        if (ObjectUtil.isNotEmpty(queryable.getBetween())) {
            queryable.getBetween().forEachKeyValue((column, value) -> predicates.add(builder.between(column(root, column), value.getStart(), value.getEnd())));
        }
        if (ObjectUtil.isNotEmpty(queryable.getNotBetween())) {
            queryable.getNotBetween().forEachKeyValue((column, value) -> predicates.add(builder.between(column(root, column), value.getStart(), value.getEnd())));
        }
        return predicates.toImmutable();
    }

    protected ImmutableList<Predicate> listPredicate(Root<ENTITY> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        User user = userService.currentLoginUser();
        if (User.isNotAdministrator(user)) {
            return Lists.immutable.of(builder.equal(root.get(SimpleEntity_.createdUser), user));
        }
        return Lists.immutable.empty();
    }

    @Override
    public ImmutableList<ENTITY> list(Query listQuery) throws Exception {
        return Lists.immutable.ofAll(repository.findAll(
                (root, query, builder) -> {
                    MutableList<Predicate> predicates = Lists.mutable.ofAll(listPredicate(root, query, builder));
                    predicates.addAllIterable(queryPredicates(listQuery.getQuery(), root, query, builder));
                    return builder.and(predicates.reject(ObjectUtil::isNull).toArray(new Predicate[predicates.size()]));
                },
                Sort.by(SimpleEntity_.CREATED_TIME).descending()
        ));
    }

    @Override
    public Optional<ENTITY> detailOptional(Long id) {
        if (ObjectUtil.isNull(id)) {
            return Optional.empty();
        }
        return repository.findOne(
                (root, query, builder) -> {
                    MutableList<Predicate> predicates = Lists.mutable.ofAll(listPredicate(root, query, builder));
                    predicates.add(builder.equal(root.get(IdOnlyEntity_.id), id));
                    return builder.and(predicates.reject(ObjectUtil::isNull).toArray(new Predicate[predicates.size()]));
                }
        );
    }

    @Override
    public ENTITY detail(Long id) {
        return detailOrNull(id);
    }

    @Override
    public ENTITY detailOrThrow(Long id) {
        return detailOptional(id).orElseThrow(() -> new IdNotFoundException(id));
    }

    @Override
    public ENTITY detailOrNull(Long id) {
        return detailOptional(id).orElse(null);
    }

    @Transactional(rollbackOn = Throwable.class)
    @Override
    public void remove(Long id) {
        if (ObjectUtil.isNotNull(id)) {
            repository.deleteById(id);
        }
    }

    public static final class IdNotFoundException extends RuntimeException {
        public IdNotFoundException() {
            super("资源不存在");
        }

        public IdNotFoundException(Long id) {
            super(StrUtil.format("ID为{}的资源不存在", id));
        }
    }
}
