package com.tzld.piaoquan.recommend.server.service.rank;


import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
import com.tzld.piaoquan.recommend.server.common.enums.AppTypeEnum;
import com.tzld.piaoquan.recommend.server.model.RankVideoInfo;
import com.tzld.piaoquan.recommend.server.model.Video;
import com.tzld.piaoquan.recommend.server.service.flowpool.FlowPoolConstants;
import com.tzld.piaoquan.recommend.server.service.recall.RecallResult;
import com.tzld.piaoquan.recommend.server.service.recall.strategy.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
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.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author dyp
 */
@Service
@Slf4j
public abstract class RankService {
    @Autowired
    @Qualifier("redisTemplate")
    public RedisTemplate<String, String> redisTemplate;
    @ApolloJsonValue("${alg.recall.special.app&vid:{}}")
    protected Map<String, List<Long>> specialAppVid;

    public RankResult rank(RankParam param) {
        if (param == null
                || param.getRecallResult() == null
                || CollectionUtils.isEmpty(param.getRecallResult().getData())) {
            return null;
        }

        if (2 == param.getRecommendType()) {
            tagDuplicateVideos(param);
            List<Video> rovRecallRank = mergeAndRankRovRecall(param);
            return new RankResult(rovRecallRank);
        }

        if (param.isSpecialRecommend()) {
            Optional<RecallResult.RecallData> data = param.getRecallResult().getData().stream()
                    .filter(d -> d.getPushFrom().equals(SpecialRecallStrategy.PUSH_FROM))
                    .findFirst();
            if (data.isPresent()
                    && data.get() != null) {
                return new RankResult(data.get().getVideos());
            }
            return null;
        }

        // 1 通过 apptype 判断该小程序走怎样的排序策略。
        if (param.getAppType() == AppTypeEnum.PIAO_QUAN_MEIHAO_ZHUFU.getCode() && Objects.equals(param.getCategoryId(), 100)) {
            List<Video> results = new ArrayList<>(extractAndSort(param, BlessRecallStrategy.PUSH_FORM));
            removeDuplicate(results);
            List<String> videoIdKeys = results.stream()
                    .map(t -> param.getRankKeyPrefix() + t.getVideoId())
                    .collect(Collectors.toList());
            List<String> videoScores = redisTemplate.opsForValue().multiGet(videoIdKeys);
            if (CollectionUtils.isNotEmpty(videoScores)
                    && videoScores.size() == results.size()) {
                for (int i = 0; i < videoScores.size(); i++) {
                    results.get(i).setSortScore(NumberUtils.toDouble(videoScores.get(i), 0.0));
                }
                Collections.sort(results, Comparator.comparingDouble(o -> -o.getSortScore()));
            }
            return new RankResult(results);
        }
        if (this.matchSpecialApp(param.getAppType())) {
            // 特殊app固定列表，只有一路召回，且不走排序。
            List<Video> results = new ArrayList<>(extractAndSort(param, RegionRealtimeRecallStrategyV5Hand.PUSH_FORM));
            removeDuplicate(results);
            results = results.subList(0, Math.min(results.size(), param.getSize()));
            return new RankResult(results);
        }
        if (param.getAppType() == AppTypeEnum.APP_SPEED.getCode()) {
            List<Video> results = new ArrayList<>(extractAndSort(param, AppFallbackRecallStrategy.PUSH_FORM));
            removeDuplicate(results);
            return new RankResult(results);
        }

        // 给所有重复视频打上多个召回源标记
        tagDuplicateVideos(param);

        // 2 正常走分发 排序+冷启动
        List<Video> rovRecallRank = mergeAndRankRovRecall(param);
        List<Video> flowPoolRank = mergeAndRankFlowPoolRecall(param);

        List<Video> douHotFlowPoolRank = extractAndSort(param, DouHotFlowPoolRecallStrategy.PUSH_FROM);

        removeDuplicate(param, rovRecallRank, flowPoolRank, douHotFlowPoolRank);

        // 融合排序
        return mergeAndSort(param, rovRecallRank, flowPoolRank, douHotFlowPoolRank);
    }

    private void tagDuplicateVideos(RankParam param) {
        if (CollectionUtils.isEmpty(param.getRecallResult().getData())) {
            return;
        }
        // pushFromIndex, videoId -> pushFromList
        Map<Long, Set<String>> videoIdPushFromMap = new HashMap<>();
        for (RecallResult.RecallData data : param.getRecallResult().getData()) {
            if (CollectionUtils.isEmpty(data.getVideos())) {
                continue;
            }
            for (Video video : data.getVideos()) {
                Set<String> pushFromSet = videoIdPushFromMap.computeIfAbsent(video.getVideoId(), k -> new HashSet<>());
                pushFromSet.add(data.getPushFrom());
            }
        }

        // pushFromRank pushFrom -> videos
        Map<String, List<Video>> pushFromVideosMap = param.getRecallResult().getData().stream()
                .collect(Collectors.toMap(RecallResult.RecallData::getPushFrom, result -> result.getVideos() == null
                        ? new ArrayList<>() : result.getVideos(), (v1, v2) -> v2));

        for (RecallResult.RecallData data : param.getRecallResult().getData()) {
            if (CollectionUtils.isEmpty(data.getVideos())) {
                continue;
            }
            for (Video video : data.getVideos()) {
                Set<String> pushFromSet = videoIdPushFromMap.get(video.getVideoId());
                if (pushFromSet != null && !pushFromSet.isEmpty()) {
                    Map<String, List<String>> pushFromIndex = new HashMap<>();
                    Map<String, Integer> pushFromRank = new HashMap<>();
                    pushFromSet.forEach(p -> {

                        pushFromIndex.computeIfAbsent(p, k -> new ArrayList<>());

                        List<Video> videos = pushFromVideosMap.get(p);
                        if (CollectionUtils.isNotEmpty(videos)) {
                            for (int i = 0; i < videos.size(); i++) {
                                if (Objects.equals(videos.get(i).getVideoId(), video.getVideoId())) {
                                    pushFromRank.put(p, i + 1);
                                    break;
                                }
                            }
                        }

                    });
                    video.setPushFromIndex(pushFromIndex);
                    video.setPushFromRank(pushFromRank);
                }
            }
        }

    }

    public abstract List<Video> mergeAndRankRovRecall(RankParam param);


    public void removeDuplicate(List<Video> rovRecallRank) {
        if (CollectionUtils.isNotEmpty(rovRecallRank)) {
            Set<Long> videoIds = new HashSet<>();
            Iterator<Video> ite = rovRecallRank.iterator();
            while (ite.hasNext()) {
                Video v = ite.next();
                if (videoIds.contains(v.getVideoId())) {
                    ite.remove();
                    continue;
                }
                videoIds.add(v.getVideoId());
            }
        }
    }

    public List<Video> mergeAndRankFlowPoolRecall(RankParam param) {
        List<Video> quickFlowPoolVideos = extractAndSort(param, FlowPoolConstants.QUICK_PUSH_FORM);
        if (CollectionUtils.isNotEmpty(quickFlowPoolVideos)) {
            return quickFlowPoolVideos;
        } else {
            return extractAndSort(param, FlowPoolConstants.PUSH_FORM);
        }
    }

    public List<Video> extractAndSort(RankParam param, String pushFrom) {
        if (param == null
                || param.getRecallResult() == null
                || CollectionUtils.isEmpty(param.getRecallResult().getData())) {
            return Collections.emptyList();
        }
        Optional<RecallResult.RecallData> data = param.getRecallResult().getData().stream()
                .filter(d -> d.getPushFrom().equals(pushFrom))
                .findFirst();
        if (data.isPresent()
                && data.get() != null
                && CollectionUtils.isNotEmpty(data.get().getVideos())) {
            List<Video> result = data.get().getVideos();
            Collections.sort(result, Comparator.comparingDouble(o -> -o.getRovScore()));
            return result;
        }
        return Collections.emptyList();
    }

    public double getFlowPoolP(RankParam param) {
        List<Video> quickFlowPoolVideos = extractAndSort(param, FlowPoolConstants.QUICK_PUSH_FORM);
        if (CollectionUtils.isNotEmpty(quickFlowPoolVideos)) {
            String quick_flow_pool_P = redisTemplate.opsForValue().get("flow:pool:quick:distribute:rate:3");
            return NumberUtils.toDouble(quick_flow_pool_P, 0);
        } else {
            return param.getFlowPoolP();
        }
    }

    public void removeDuplicate(RankParam param, List<Video> rovRecallRank, List<Video> flowPoolRank, List<Video> douHotFlowPoolRank) {
        // TODO 重构 rov和流量池 融合排序
        //    去重原则：
        //        如果视频在ROV召回池topK，则保留ROV召回池，否则保留流量池
        // 1 rov pool topK
        Set<Long> rovTopKVideoIds = new HashSet<>();
        for (int i = 0; i < param.getTopK() && i < rovRecallRank.size(); i++) {
            rovTopKVideoIds.add(rovRecallRank.get(i).getVideoId());
        }

        // 2 flow pool 移除topK视频
        Set<Long> flowPoolVideoIds = new HashSet<>();
        Iterator<Video> flowRecallRankIte = flowPoolRank.iterator();
        while (flowRecallRankIte.hasNext()) {
            Video data = flowRecallRankIte.next();
            if (rovTopKVideoIds.contains(data.getVideoId())) {
                flowRecallRankIte.remove();
            } else {
                flowPoolVideoIds.add(data.getVideoId());
            }
        }

        // dou hot flow pool 移除topK视频
        Iterator<Video> douHotFlowPoolIte = douHotFlowPoolRank.iterator();
        while (douHotFlowPoolIte.hasNext()) {
            Video data = douHotFlowPoolIte.next();
            if (rovTopKVideoIds.contains(data.getVideoId())) {
                douHotFlowPoolIte.remove();
            } else {
                flowPoolVideoIds.add(data.getVideoId());
            }
        }

        // rov pool 移除flow中的视频
        Iterator<Video> rovRecallRankIte = rovRecallRank.iterator();
        while (rovRecallRankIte.hasNext()) {
            Video data = rovRecallRankIte.next();
            if (flowPoolVideoIds.contains(data.getVideoId())) {
                rovRecallRankIte.remove();
            }
        }
    }

    public abstract RankResult mergeAndSort(RankParam param, List<Video> rovRecallRank, List<Video> flowPoolRank, List<Video> douHotFlowPoolRank);

    private boolean matchSpecialApp(int appId) {
        Set<Integer> notSpecialApp = new HashSet<>(Arrays.asList(0, 4, 5));
        if (notSpecialApp.contains(appId)) {
            // vlog 票圈视频 内容精选 不允许走特殊列表，即使配置了也无效。
            return false;
        }
        if (specialAppVid != null && specialAppVid.getOrDefault("app", new ArrayList<>()).contains((long) appId)) {
            // log.info("This request hits a special logic in matchSpecialApp with appId={}", appId);
            return true;
        }
        return false;
    }

    protected void rankBeforePostProcessor(List<Video> rankVideo) {
        try {
            Map<String, List<Long>> pushFromVideoRank = new LinkedHashMap<>();
            Map<Long, RankVideoInfo> rankVideoInfoMap = new LinkedHashMap<>();
            for (Video video : rankVideo) {
                video.setPushFromRankVideo(pushFromVideoRank);
                video.setRankVideoInfoMap(rankVideoInfoMap);

                String pushFrom = video.getPushFrom();
                List<Long> pushFromVideos = pushFromVideoRank.computeIfAbsent(pushFrom, k -> new ArrayList<>());
                pushFromVideos.add(video.getVideoId());

                RankVideoInfo rankVideoInfo = new RankVideoInfo();
                rankVideoInfo.setVideoId(video.getVideoId());
                rankVideoInfo.setPushFrom(pushFrom);
                rankVideoInfoMap.put(video.getVideoId(), rankVideoInfo);
            }
        } catch (Exception e) {
            log.error("addPushFromVideoRank ", e);
        }
    }
}
