package com.fdkankan.mongodb.service.impl;

import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ObjectUtil;
import com.fdkankan.mongodb.base.BaseRequestMongo;
import com.fdkankan.mongodb.base.MongoPageResult;
import com.fdkankan.mongodb.service.MongodbBaseService;
import com.mongodb.client.result.DeleteResult;
import org.apache.commons.lang3.StringUtils;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import javax.annotation.PostConstruct;
import java.lang.reflect.ParameterizedType;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

/**
 * @author Xiewj
 * @date 2021/11/15
 * Mongodb工具类
 */
@Component
public class MongodbBaseServiceImpl<T> implements MongodbBaseService<T> {

    public static MongodbBaseServiceImpl mongodbBaseService;


    @PostConstruct
    public void init() {
        mongodbBaseService = this;
        mongodbBaseService.mongoTemplate = this.mongoTemplate;
    }


    @Autowired
    private MongoTemplate mongoTemplate;


    private static final int FIRST_PAGE_NUM = 1;
    private static final String ID = "_id";




    /**
     * 分页查询.
     *
     * @param query Mongo Query对象，构造你自己的查询条件.
     * @param page#mapper 映射器，你从db查出来的list的元素类型是entityClass, 如果你想要转换成另一个对象，比如去掉敏感字段等，可以使用mapper来决定如何转换.
     * @param page#pageSize 分页的大小.
     * @param page#pageNum 当前页.
     * @param page#lastId 条件分页参数, 区别于skip-limit，采用find(_id>lastId).limit分页.
     * 如果不跳页，像朋友圈，微博这样下拉刷新的分页需求，需要传递上一页的最后一条记录的ObjectId。 如果是null，则返回pageNum那一页.
     * @return PageResult，一个封装page信息的对象.
     */
    @Override
    public MongoPageResult<T> pageQuery(BaseRequestMongo page, Query query) {
        //分页逻辑
        long total = mongoTemplate.count(query, getTClass());
        final Integer pages = (int) Math.ceil(total / (double) page.getPageSize());
        if (page.getPageNum() <= 0 || page.getPageNum() > pages) {
            page.setPageNum(FIRST_PAGE_NUM);
        }
        if (StringUtils.isNotBlank(page.getLastId())) {
            final Criteria criteria = new Criteria(UUID.randomUUID().toString());
            if (page.getPageNum() != FIRST_PAGE_NUM) {
                criteria.and(ID).gt(new ObjectId(page.getLastId()));
            }
            query.limit(page.getPageSize());
            query.addCriteria(criteria);
        } else {
            int skip = page.getPageSize() * (page.getPageNum() - 1);
            query.skip(skip).limit(page.getPageSize());
        }
        List<T> entityList=null;
        if (page.getSortBy().toUpperCase(Locale.ROOT).equals(Sort.Direction.DESC.name())){
            query.with(Sort.by(Collections.singletonList(new Sort.Order(Sort.Direction.DESC, page.getOrderBy()))));
        }else {
            query.with(Sort.by(Collections.singletonList(new Sort.Order(Sort.Direction.ASC, page.getOrderBy()))));
        }
        entityList=mongoTemplate.find(query, getTClass());

        MongoPageResult<T> mongoPageResult = new MongoPageResult<>();
        mongoPageResult.setTotal(total);
        mongoPageResult.setPages(pages);
        mongoPageResult.setPageSize(page.getPageSize());
        mongoPageResult.setPageNum(page.getPageNum());
        mongoPageResult.setList(entityList);
        return mongoPageResult;
    }

    @Override
    public long getCount(Query query) {
       return mongoTemplate.count(query, getTClass());
    }

    @Override
    public T save(T t) {
        return mongoTemplate.save(t);
    }

    @Override
    public T insert(T t) {
        return  mongoTemplate.insert(t);
    }

    @Override
    public DeleteResult delete(T t) {
        return mongoTemplate.remove(t);
    }

    @Override
    public DeleteResult deleteById(Object id) {
        T t = mongoTemplate.findById(id, getTClass());
        if (ObjectUtil.isNotNull(t)){
            return delete(t);
        }
        return null;
    }

    @Override
    public DeleteResult delete(Query query) {
        return mongoTemplate.remove(query, getTClass());
    }

    @Override
    public T update(T t) {
       return mongoTemplate.save(t);
    }

    @Override
    public T findById(Object id) {
        return mongoTemplate.findById(id, getTClass());
    }

    @Override
    public List<T> findAll() {
        return mongoTemplate.findAll(getTClass());
    }

    @Override
    public List<T> find(Query query) {
        return mongoTemplate.find(query, getTClass());
    }

    @Override
    public GeoResults<T> geoNear(NearQuery query) {
        GeoResults<T> geoResults = mongoTemplate.geoNear(query, getTClass());
        return geoResults;
    }

    @Override
    public T findOne(Query query) {
        return mongoTemplate.findOne(query,getTClass());
    }

    @Override
    public boolean exists(Query query) {
        return mongoTemplate.exists(query, getTClass());
    }

    @Override
    public boolean exists(T t) {
        return exists(getIdQueryFor(t));
    }

    @Override
    public boolean exitsById(Object id) {
        return exists(findById(id));
    }

    @Override
    public long count(Query query) {
        return mongoTemplate.count(query, getTClass());
    }

    @Override
    public Class<T> getTClass() {
        Class<T> tClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        return tClass;
    }

    @Override
    public Pair<String, Object> extractIdPropertyAndValue(Object object) {
        Assert.notNull(object, "Id cannot be extracted from 'null'.");
        Class<?> objectType = object.getClass();
        if (object instanceof Document) {
            return Pair.of("_id", ((Document) object).get("_id"));
        } else {
            MongoPersistentEntity<?> entity = (MongoPersistentEntity) mongoTemplate.getConverter().getMappingContext().getPersistentEntity(objectType);
            if (entity != null && entity.hasIdProperty()) {
                MongoPersistentProperty idProperty = (MongoPersistentProperty) entity.getIdProperty();
                return Pair.of(idProperty.getFieldName(), entity.getPropertyAccessor(object).getProperty(idProperty));
            } else {
                throw new MappingException("No id property found for object of type " + objectType);
            }
        }
    }

    @Override
    public Query getIdQueryFor(Object object) {
        Pair<String, Object> id = this.extractIdPropertyAndValue(object);
        return new Query(Criteria.where((String) id.getFirst()).is(id.getSecond()));
    }

}
