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

import com.google.common.collect.Lists;
import com.google.common.reflect.TypeToken;
import com.tzld.piaoquan.recommend.server.model.Video;
import com.tzld.piaoquan.recommend.server.service.filter.FilterParam;
import com.tzld.piaoquan.recommend.server.service.filter.FilterResult;
import com.tzld.piaoquan.recommend.server.service.filter.FilterService;
import com.tzld.piaoquan.recommend.server.service.recall.FilterParamFactory;
import com.tzld.piaoquan.recommend.server.service.recall.RecallParam;
import com.tzld.piaoquan.recommend.server.service.recall.RecallStrategy;
import com.tzld.piaoquan.recommend.server.util.DateUtils;
import com.tzld.piaoquan.recommend.server.util.JSONUtils;
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.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author dyp
 */
@Slf4j
public abstract class AbstractRegionRecallStrategy implements RecallStrategy {
    @Autowired
    @Qualifier("redisTemplate")
    protected RedisTemplate<String, String> redisTemplate;

    @Autowired
    protected FilterService filterService;

    private final String recordKeyFormat = "{\"date\":\"%s\", \"h\":\"%d\"}";

    @Override
    public List<Video> recall(RecallParam param) {

        String recordKey = recordKey(param);
        String lastVideoKey = lastVideoKey(param);
        int idx = 0;
        String poolKey = "";

        // TODO 整段逻辑很奇怪，需要重新理解
        /**
         *  TODO 问题：召回时按照降序获取idx，再降序取数据，但是融合排序实验会按照score重新排序，可能导致数据被跳过
         *  比如：地域 [[1, 0.1],[2, 0.2],[3, 0.3][4, 0.4]]
         *  排序实验 [[1, 0.9],[2, 0.8],[3, 0.7][4, 0.6]]
         *
         *  返回2个视频是 1和2，但是会记录最后一个视频的id是2，那么当地域再次召回时，会召回1，不会召回3、4
         *
         */


        // recordKey: 每小时生成一个召回源，用来决定用哪个召回源
        // poolKey: 召回源的key
        // lastVideoLKey: 上次返回的最后一个视频
        /**
         * 1、如果当前小时数据已生成，那么召回源用当前小时数据
         * 2、如果当前小时数据未生成，那么召回源用前一小时数据
         *
         * 1、如果recordKey是当前小时数据，用lastVideo作为cursor
         * 2、如果recordKey是前一小时数据，重置lastVideo
         *
         *
         */

        String record = redisTemplate.opsForValue().get(recordKey);
        if (StringUtils.isNotBlank(record)) {
            Map<String, String> recordMap = JSONUtils.fromJson(record, new TypeToken<Map<String, String>>() {
            }, Collections.emptyMap());
            if (MapUtils.isNotEmpty(recordMap)) {
                String record_dt = recordMap.get("date");
                int record_h = NumberUtils.toInt(recordMap.get("h"), 0);
                String now_dt = DateUtils.getCurrentDateStr("yyyyMMdd");
                int h = DateUtils.getCurrentHour();
                if (record_dt.equals(now_dt) && record_h == h) {
                    poolKey = poolKey(param, record_dt, record_h);
                    idx = getIdx(lastVideoKey, poolKey);
                } else if ((record_dt.equals(now_dt) && h - record_h == 1)
                        || (h == 0 && record_h == 23)) {
                    poolKey = poolKey(param, now_dt, h);
                    if (redisTemplate.hasKey(poolKey)) {
                        redisTemplate.opsForValue().set(recordKey, String.format(recordKeyFormat, now_dt,
                                h), 2, TimeUnit.HOURS);
                        redisTemplate.delete(lastVideoKey);
                    } else {
                        poolKey = poolKey(param, record_dt, record_h);
                        idx = getIdx(lastVideoKey, poolKey);
                    }
                } else {
                    redisTemplate.delete(lastVideoKey);
                    poolKey = updateLastVideoRecord(recordKey, param);
                }
            }
        } else {
            redisTemplate.delete(lastVideoKey);
            poolKey = updateLastVideoRecord(recordKey, param);
        }
        if (StringUtils.isBlank(poolKey)) {
            return null;
        }

        // 召回
        // 风险过滤 扩大因子
        int getSize = param.getSize() * 5;
        int freq = 0;
        String lastVideoId = "";
        List<Video> results = new ArrayList<>();
        while (results.size() < param.getSize()) {
            freq += 1;
            if (freq > 2) {
                break;
            }
            Set<ZSetOperations.TypedTuple<String>> data = redisTemplate.opsForZSet().reverseRangeWithScores(poolKey, idx,
                    idx + getSize - 1);
            if (CollectionUtils.isEmpty(data)) {
                break;
            }
            idx += getSize;

            Map<Long, Double> videoMap = new LinkedHashMap<>();
            for (ZSetOperations.TypedTuple<String> t : data) {
                lastVideoId = t.getValue();
                videoMap.put(NumberUtils.toLong(t.getValue(), 0), t.getScore());
            }
            FilterParam filterParam = FilterParamFactory.create(param, Lists.newArrayList(videoMap.keySet()));

            FilterResult filterResult = filterService.filter(filterParam);

            if (filterResult != null && CollectionUtils.isNotEmpty(filterResult.getVideoIds())) {
                filterResult.getVideoIds().stream().forEach(vid -> {
                    Video video = new Video();
                    video.setVideoId(vid);
                    video.setRovScore(videoMap.get(vid));
                    video.setPushFrom(pushFrom());
                    video.setLastVideoKey(lastVideoKey);
                    results.add(video);
                });
            }
        }

        // TODO 如果不限制返回size个数据，可以迁移到排序阶段
        Collections.sort(results, Comparator.comparingDouble(o -> -o.getRovScore()));

        // 这里是一个优化：如果所有数据都被过滤，说明下一次召回可以直接从lastVideo开始
        // TODO 可以优化成异步更新
        if (StringUtils.isNotBlank(lastVideoId)
                && CollectionUtils.isEmpty(results)
                && StringUtils.isNotBlank(param.getMid())) {
            // 召回数据不为空 & 过滤后结果为空 &  mid不为空时，更新召回获取的末位视频id记录到定位的key中
            redisTemplate.opsForValue().set(lastVideoKey, lastVideoId, 2, TimeUnit.HOURS);
        }

        int sizeReturn = param.getSize();
        return results.subList(0, results.size() < sizeReturn ? results.size() : sizeReturn);
    }

    private int getIdx(String lastVideoKey, String poolKey) {
        int idx = 0;
        String lastVideo = redisTemplate.opsForValue().get(lastVideoKey);
        if (StringUtils.isNotBlank(lastVideo)) {
            Long idxLong = redisTemplate.opsForZSet().reverseRank(poolKey, lastVideo);
            if (idxLong != null) {
                idx = idxLong.intValue();
                idx += 1;
            }
        }
        return idx;
    }

    private String updateLastVideoRecord(String recordKey, RecallParam param) {
        String currentDateStr = DateUtils.getCurrentDateStr("yyyyMMdd");
        int h = DateUtils.getCurrentHour();

        String poolKey = poolKey(param, currentDateStr, h);
        if (!redisTemplate.hasKey(poolKey)) {
            if (h == 0) {
                currentDateStr = DateUtils.getBeforeDaysDateStr("yyyyMMdd", 1);
                h = 23;
            } else {
                h = h - 1;
            }
            poolKey = poolKey(param, currentDateStr, h);
        }
        redisTemplate.opsForValue().set(recordKey, String.format(recordKeyFormat, currentDateStr, h), 2, TimeUnit.HOURS);
        return poolKey;
    }

    abstract String recordKey(RecallParam param);

    abstract String poolKey(RecallParam param, String now_dt, int h);

    abstract String lastVideoKey(RecallParam param);
}
