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


import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
import com.tzld.piaoquan.recommend.feature.domain.video.base.ItemFeature;
import com.tzld.piaoquan.recommend.feature.domain.video.base.RequestContext;
import com.tzld.piaoquan.recommend.feature.domain.video.base.UserFeature;
import com.tzld.piaoquan.recommend.server.common.base.RankItem;
import com.tzld.piaoquan.recommend.server.common.enums.AppTypeEnum;
import com.tzld.piaoquan.recommend.server.model.MachineInfo;
import com.tzld.piaoquan.recommend.server.model.Video;
import com.tzld.piaoquan.recommend.server.remote.FeatureRemoteService;
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 com.tzld.piaoquan.recommend.server.service.score.ScoreParam;
import com.tzld.piaoquan.recommend.server.service.score.ScorerUtils;
import com.tzld.piaoquan.recommend.server.util.CommonCollectionUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

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

/**
 * @author dyp
 */
@Service
@Slf4j
public class RankService {
    @Autowired
    @Qualifier("redisTemplate")
    public RedisTemplate<String, String> redisTemplate;
    @Autowired
    private FeatureRemoteService featureRemoteService;
    @Value("${newRankSwitch:false}")
    private boolean newRankSwitch;
    @Value("${newRankAbExpCode:528}")
    private String newRankAbExpCode;
    @ApolloJsonValue("${alg.recall.special.app&vid:{}}")
    protected Map<String, List<Long>> specialAppVid;
    @ApolloJsonValue("${region.recall.return.size:{}}")
    protected Map<String, Map<String, Integer>> regionRecallReturnSize;

    @Value("${word2vec.exp:691}")
    protected String word2vecExp;

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

        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);

        rankFilter(param, rovRecallRank, flowPoolRank);

        removeDuplicate(param, rovRecallRank, flowPoolRank);


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

    private void tagDuplicateVideos(RankParam param) {
        if (CollectionUtils.isEmpty(param.getRecallResult().getData())) {
            return;
        }
        // pushFromIndex
        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
        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.size() > 0) {
                    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 void rankFilter(RankParam param, List<Video> rovRecallRank, List<Video> flowPoolRank) {
    }

    public List<Video> mergeAndRankRovRecall(RankParam param) {
        // TODO ab test
        // TODO 抽象成Strategy

        return mergeAndRankRovRecallOld(param);
    }

    public List<Video> mergeAndRankRovRecallOld(RankParam param) {
        List<Video> rovRecallRank = new ArrayList<>();
        rovRecallRank.addAll(extractAndSort(param, RegionHRecallStrategy.PUSH_FORM));
        rovRecallRank.addAll(extractAndSort(param, RegionHDupRecallStrategy.PUSH_FORM));
        rovRecallRank.addAll(extractAndSort(param, Region24HRecallStrategy.PUSH_FORM));
        rovRecallRank.addAll(extractAndSort(param, RegionRelative24HRecallStrategy.PUSH_FORM));
        rovRecallRank.addAll(extractAndSort(param, RegionRelative24HDupRecallStrategy.PUSH_FORM));

        // @zhangbo 增加不同召回子策略的返回数量-阿波罗：region.recall.return.size
        String abCode = param.getAbCode();
        int sizeReturn = param.getSize();
        if (abCode != null && this.regionRecallReturnSize != null &&
                this.regionRecallReturnSize.containsKey(abCode) &&
                this.regionRecallReturnSize.get(abCode) != null) {
            sizeReturn = this.regionRecallReturnSize.get(abCode).getOrDefault("result_size", sizeReturn);
        }

        if (param.getAbCode().equals("60054")
                || param.getAbCode().equals("60068")
                || param.getAbCode().equals("60081")
                || param.getAbCode().equals("60084")
                || param.getAbCode().equals("60087")
                || param.getAbCode().equals("60088")
                || param.getAbCode().equals("60089")
                || param.getAbCode().equals("60090")
                || param.getAbCode().equals("60091")
                || param.getAbCode().equals("60092")
                || param.getAbCode().equals("60093")
                || param.getAbCode().equals("60094")
                || param.getAbCode().equals("60095")
                || param.getAbCode().equals("60096")
                || param.getAbCode().equals("60097")
                || param.getAbCode().equals("60098")
                || param.getAbCode().equals("60111")
                || param.getAbCode().equals("60103")
                || param.getAbCode().equals("60104")
                || param.getAbCode().equals("60110")
                || param.getAbCode().equals("60150")
        ) {
            // 地域召回要做截取，再做融合排序
            removeDuplicate(rovRecallRank);
            rovRecallRank = rovRecallRank.size() <= sizeReturn
                    ? rovRecallRank
                    : rovRecallRank.subList(0, sizeReturn);


            // 补充不分地域小时数据
            rovRecallRank.addAll(extractAndSort(param, RegionHWithoutDupRecallStrategy.PUSH_FORM));
            // merge sim recall 和 return recall
            rovRecallRank.addAll(extractAndSort(param, SimHotVideoRecallStrategy.PUSH_FORM));
            rovRecallRank.addAll(extractAndSort(param, ReturnVideoRecallStrategy.PUSH_FORM));
            if (param.getAbCode().equals("60150")) {
                rovRecallRank.addAll(extractAndSort(param, ShareDeepRecallStrategy.PUSH_FORM));
                rovRecallRank.addAll(extractAndSort(param, ShareWidthRecallStrategy.PUSH_FORM));
            }
            removeDuplicate(rovRecallRank);

            // 融合排序
            List<String> videoIdKeys = rovRecallRank.stream()
                    .map(t -> param.getRankKeyPrefix() + t.getVideoId())
                    .collect(Collectors.toList());
            List<String> videoScores = redisTemplate.opsForValue().multiGet(videoIdKeys);
            if (CollectionUtils.isNotEmpty(videoScores)
                    && videoScores.size() == rovRecallRank.size()) {
                for (int i = 0; i < videoScores.size(); i++) {
                    rovRecallRank.get(i).setSortScore(NumberUtils.toDouble(videoScores.get(i), 0.0));
                }
                Collections.sort(rovRecallRank, Comparator.comparingDouble(o -> -o.getSortScore()));
            }
        } else {
            rovRecallRank.addAll(extractAndSort(param, SimHotVideoRecallStrategy.PUSH_FORM));
            rovRecallRank.addAll(extractAndSort(param, ReturnVideoRecallStrategy.PUSH_FORM));
            removeDuplicate(rovRecallRank);

            // 融合排序
            List<String> videoIdKeys = rovRecallRank.stream()
                    .map(t -> param.getRankKeyPrefix() + t.getVideoId())
                    .collect(Collectors.toList());
            List<String> videoScores = redisTemplate.opsForValue().multiGet(videoIdKeys);
            if (CollectionUtils.isNotEmpty(videoScores)
                    && videoScores.size() == rovRecallRank.size()) {
                for (int i = 0; i < videoScores.size(); i++) {
                    rovRecallRank.get(i).setSortScore(NumberUtils.toDouble(videoScores.get(i), 0.0));
                }
                rovRecallRank.sort(Comparator.comparingDouble(o -> -o.getSortScore()));
            }
        }
        return rovRecallRank;
    }

    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());
            }
        }
    }

    private List<Video> mergeAndRankRovRecallNew(RankParam param) {
        UserFeature userFeature = featureRemoteService.getUserFeature(param.getMid());

        List<Video> recallVideos = param.getRecallResult().mergeRecallVideos();
        List<RankItem> rankItems = CommonCollectionUtils.toList(recallVideos, RankItem::new);

        List<Long> videoIds = CommonCollectionUtils.toListDistinct(recallVideos, Video::getVideoId);
        Map<Long, ItemFeature> videoFeatureMap = featureRemoteService.getVideoFeatureMap(videoIds);

        for (RankItem rankItem : rankItems) {
            rankItem.setItemFeature(videoFeatureMap.get(rankItem.getVideoId()));
        }

        // TODO
        ScoreParam scoreParam = convert(param);
        List<RankItem> rovRecallScore = ScorerUtils.getScorerPipeline(ScorerUtils.BASE_CONF)
                .scoring(scoreParam, userFeature, rankItems);

        return CommonCollectionUtils.toList(rovRecallScore, i -> {
            // hard code 将排序分数 赋值给video的sortScore
            Video v = i.getVideo();
            v.setSortScore(i.getScore());
            return v;
        });
    }

    protected ScoreParam convert(RankParam param) {
        ScoreParam scoreParam = new ScoreParam();

        scoreParam.setMid(param.getMid());

        // TODO hardcode 为了兼容写入逻辑
        RequestContext context = new RequestContext();
        context.setApptype(param.getAppType() + "");

        // TODO 地域转换
        context.setRegion(param.getProvince());
        context.setCity(param.getCity());

        Calendar calendar = Calendar.getInstance();
        context.setWeek((calendar.get(Calendar.DAY_OF_WEEK) + 6) % 7 + "");
        context.setDay(new SimpleDateFormat("yyyyMMdd").format(calendar.getTime()));
        context.setHour(new SimpleDateFormat("HH").format(calendar.getTime()));

        MachineInfo machineInfo = param.getMachineInfo();
        if (machineInfo != null) {
            context.setMachineinfo_brand(machineInfo.getBrand());
            context.setMachineinfo_model(machineInfo.getModel());
            context.setMachineinfo_platform(machineInfo.getPlatform());
            context.setMachineinfo_sdkversion(machineInfo.getSdkVersion());
            context.setMachineinfo_system(machineInfo.getSystem());
            context.setMachineinfo_wechatversion(machineInfo.getWechatVersion());
        }

        scoreParam.setRequestContext(context);
        return scoreParam;
    }

    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) {
        // 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());
            }
        }

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

    public RankResult mergeAndSort(RankParam param, List<Video> rovRecallRank, List<Video> flowPoolRank) {
        if (CollectionUtils.isEmpty(rovRecallRank)) {
            if (param.getSize() < flowPoolRank.size()) {
                return new RankResult(flowPoolRank.subList(0, param.getSize()));
            } else {
                return new RankResult(flowPoolRank);
            }
        }

        List<Video> result = new ArrayList<>();
        for (int i = 0; i < param.getTopK() && i < rovRecallRank.size(); i++) {
            result.add(rovRecallRank.get(i));
        }

        double flowPoolP = getFlowPoolP(param);
        int flowPoolIndex = 0;
        int rovPoolIndex = param.getTopK();

        for (int i = 0; i < param.getSize() - param.getTopK(); i++) {
            double rand = RandomUtils.nextDouble(0, 1);
            if (rand < flowPoolP) {
                if (flowPoolIndex < flowPoolRank.size()) {
                    result.add(flowPoolRank.get(flowPoolIndex++));
                } else {
                    break;
                }
            } else {
                if (rovPoolIndex < rovRecallRank.size()) {
                    result.add(rovRecallRank.get(rovPoolIndex++));
                } else {
                    break;
                }
            }
        }
        if (rovPoolIndex >= rovRecallRank.size()) {
            for (int i = flowPoolIndex; i < flowPoolRank.size() && result.size() < param.getSize(); i++) {
                result.add(flowPoolRank.get(i));
            }
        }
        if (flowPoolIndex >= flowPoolRank.size()) {
            for (int i = rovPoolIndex; i < rovRecallRank.size() && result.size() < param.getSize(); i++) {
                result.add(rovRecallRank.get(i));
            }
        }
        return new RankResult(result);
    }

    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;
    }

}
