package com.tzld.piaoquan.recommend.server.service.filter.strategy;

import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
import com.tzld.piaoquan.recommend.server.common.ThreadPoolFactory;
import com.tzld.piaoquan.recommend.server.service.filter.FilterParam;
import com.tzld.piaoquan.recommend.server.service.filter.FilterStrategy;
import com.tzld.piaoquan.recommend.server.util.CommonCollectionUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author dyp
 */
@Component
@Slf4j
public class ViewedStrategy implements FilterStrategy {
    @Autowired
    @Qualifier("filterRedisTemplate")
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private MongoTemplate mongoTemplate;

    private String keyFormat = "user:exclude:videoidset:%s";
    private String newKeyFormat = "user:exclude:videoidset:new:%s";

    @ApolloJsonValue("${view.exp.config:{}}")
    private Map<String, Integer> viewExpConfig;
    @Value("${video.filter.cache.expire.new:600}")
    private Long videoFilterCacheNewExpire;
    @Value("${filter.view.cache.mongo.size:4000}")
    private Integer filterViewCacheMongoSize;

    @Override
    public List<Long> filter(FilterParam param) {
        boolean hit = MapUtils.isNotEmpty(viewExpConfig)
                && CollectionUtils.isNotEmpty(param.getAbExpCodes())
                && CollectionUtils.containsAny(viewExpConfig.keySet(), param.getAbExpCodes());
        if (hit) {
            return filterNew(param);
        } else {
            return filterOld(param);
        }
    }

    private List<Long> filterOld(FilterParam param) {
        String user = StringUtils.isNotBlank(param.getUid()) ? param.getUid() : param.getMid();
        if (StringUtils.isBlank(user)) {
            return param.getVideoIds();
        }
        String key = String.format(keyFormat, user);
        Set<String> viewedVideoIds = redisTemplate.opsForSet().members(key);
        if (CollectionUtils.isEmpty(viewedVideoIds)) {
            // 从mongo取曝光数据
            Criteria criteria = new Criteria();
            criteria.and("uid").is(user);
            // criteria.and("create_time").gte();
            Query query = new Query();
            query.addCriteria(criteria);
            query.limit(filterViewCacheMongoSize);
            List<VideoView> list = mongoTemplate.find(query, VideoView.class);
            //TODO 为什么限制最多10000条？是不是限制近几天更合适？
            if (CollectionUtils.isNotEmpty(list)) {
                int limit = filterViewCacheMongoSize;
                for (int i = list.size() - 1; i >= 0 && limit-- > 0; i--) {
                    viewedVideoIds.add(String.valueOf(list.get(i).getVideoId()));
                }
            } else {
                // 避免穿透
                viewedVideoIds.add("-1");
            }
            // 异步写Redis
            ThreadPoolFactory.defaultPool().execute(() -> {
                redisTemplate.executePipelined(new SessionCallback<String>() {
                    @Override
                    public <A, B> String execute(RedisOperations<A, B> redisOperations) throws DataAccessException {
                        SetOperations<String, String> operations =
                                (SetOperations<String, String>) redisOperations.opsForSet();
                        operations.add(key, viewedVideoIds.toArray(new String[viewedVideoIds.size()]));
                        redisTemplate.expire(key, 360 * 3600, TimeUnit.SECONDS);
                        return null;
                    }
                });
            });
        }

        if (CollectionUtils.isEmpty(viewedVideoIds)) {
            return param.getVideoIds();
        }
        return param.getVideoIds().stream()
                .filter(vid -> !viewedVideoIds.contains(String.valueOf(vid)))
                .collect(Collectors.toList());
    }

    private List<Long> filterNew(FilterParam param) {
        String user = StringUtils.isNotBlank(param.getUid()) ? param.getUid() : param.getMid();
        if (StringUtils.isBlank(user)) {
            return param.getVideoIds();
        }
        String key = String.format(newKeyFormat, user);
        Set<String> viewedVideoIds = redisTemplate.opsForSet().members(key);
        if (CollectionUtils.isEmpty(viewedVideoIds)) {
            // 从mongo取曝光数据
            Criteria criteria = new Criteria();
            criteria.and("uid").is(user);
            long videoViewedFilterTime = 30 * 24 * 3600;
            if (MapUtils.isNotEmpty(viewExpConfig)) {
                for (Map.Entry<String, Integer> entry : viewExpConfig.entrySet()) {
                    if (CommonCollectionUtils.contains(param.getAbExpCodes(), entry.getKey())) {
                        videoViewedFilterTime = entry.getValue() != null
                                ? entry.getValue()
                                : videoViewedFilterTime;
                        break;
                    }
                }
            }
            criteria.and("create_time").gte(System.currentTimeMillis() - videoViewedFilterTime * 1000);
            Query query = new Query();
            query.addCriteria(criteria);
            query.limit(filterViewCacheMongoSize);
            List<VideoView> list = mongoTemplate.find(query, VideoView.class);
            //TODO 为什么限制最多10000条？是不是限制近几天更合适？
            if (CollectionUtils.isNotEmpty(list)) {
                int limit = filterViewCacheMongoSize;
                for (int i = list.size() - 1; i >= 0 && limit-- > 0; i--) {
                    viewedVideoIds.add(String.valueOf(list.get(i).getVideoId()));
                }
            } else {
                // 避免穿透
                viewedVideoIds.add("-1");
            }
            // 异步写Redis
            ThreadPoolFactory.defaultPool().execute(() -> {
                redisTemplate.executePipelined(new SessionCallback<String>() {
                    @Override
                    public <A, B> String execute(RedisOperations<A, B> redisOperations) throws DataAccessException {
                        SetOperations<String, String> operations =
                                (SetOperations<String, String>) redisOperations.opsForSet();
                        operations.add(key, viewedVideoIds.toArray(new String[viewedVideoIds.size()]));
                        redisTemplate.expire(key, videoFilterCacheNewExpire, TimeUnit.SECONDS);
                        return null;
                    }
                });
            });
        }

        if (CollectionUtils.isEmpty(viewedVideoIds)) {
            return param.getVideoIds();
        }
        return param.getVideoIds().stream()
                .filter(vid -> !viewedVideoIds.contains(String.valueOf(vid)))
                .collect(Collectors.toList());
    }

}
