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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.tzld.piaoquan.recommend.server.common.RedisKeyConstants;
import com.tzld.piaoquan.recommend.server.feign.GrowthManagerFeign;
import com.tzld.piaoquan.recommend.server.feign.model.CommonResponse;
import com.tzld.piaoquan.recommend.server.feign.model.VideoCharacteristicInfo;
import com.tzld.piaoquan.recommend.server.gen.common.Result;
import com.tzld.piaoquan.recommend.server.gen.recommend.*;
import com.tzld.piaoquan.recommend.server.model.MachineInfo;
import com.tzld.piaoquan.recommend.server.model.RecommendParam;
import com.tzld.piaoquan.recommend.server.model.Video;
import com.tzld.piaoquan.recommend.server.repository.WxUserInfo;
import com.tzld.piaoquan.recommend.server.repository.WxUserInfoRepository;
import com.tzld.piaoquan.recommend.server.service.flowpool.FlowPoolConstants;
import com.tzld.piaoquan.recommend.server.service.flowpool.FlowPoolService;
import com.tzld.piaoquan.recommend.server.service.rank.RankParam;
import com.tzld.piaoquan.recommend.server.service.rank.RankResult;
import com.tzld.piaoquan.recommend.server.service.rank.RankRouter;
import com.tzld.piaoquan.recommend.server.service.rank.bo.UserSRBO;
import com.tzld.piaoquan.recommend.server.service.rank.bo.UserShareReturnProfile;
import com.tzld.piaoquan.recommend.server.service.recall.RecallParam;
import com.tzld.piaoquan.recommend.server.service.recall.RecallResult;
import com.tzld.piaoquan.recommend.server.service.recall.RecallService;
import com.tzld.piaoquan.recommend.server.service.recall.strategy.*;
import com.tzld.piaoquan.recommend.server.util.CommonCollectionUtils;
import com.tzld.piaoquan.recommend.server.util.FeatureUtils;
import com.tzld.piaoquan.recommend.server.util.JSONUtils;
import com.tzld.piaoquan.recommend.server.util.TraceUtils;
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.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.time.Duration;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * @author dyp
 */
@Service
@Slf4j
public class RecommendService {

    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate<String, String> redisTemplate;

    @ApolloJsonValue("${city_code:[]}")
    private Set<String> cityCodes;
    @Autowired
    private RecallService recallService;
    @Autowired
    private RankRouter rankRouter;
    @Autowired
    private PreViewedService preViewedService;
    @Autowired
    private FlowPoolService flowPoolService;
    @Autowired
    private StatisticsLogService statisticsLogService;
    @Autowired
    private TimerLogService timerLogService;
    @Autowired
    private FeatureService featureService;

    @Autowired
    private GrowthManagerFeign growthManagerFeign;

    private ThreadLocal<Map<String, Object>> timerLogMapTL = ThreadLocal.withInitial(HashMap::new);

    @Value("${risk.video.filter.switch:true}")
    private boolean riskVideoFilterSwitch;
    @ApolloJsonValue("${risk.scene:[]}")
    private Set<Long> riskScenes;
    @ApolloJsonValue("${creative.info.feature.channel.name:[]}")
    private Set<String> creativeInfoFeatureChannelNameSet;
    @ApolloJsonValue("${none.user.risk.exclude.apptype:[]}")
    private Set<Integer> noneUserRiskExcludeAppTypes;

    @ApolloJsonValue("${testing.risk.province:[]}")
    private Set<String> testingRiskProvince;
    @ApolloJsonValue("${testing.risk.city:[]}")
    private Set<String> testingRiskCity;

    @ApolloJsonValue("${log.upload.field.switch:{}}")
    private Map<String, String> logUploadFieldSwitch;

    public static final String channelGroupPrefix = "alg_recsys_user_channel_group";
    /**
     * wx_user_info 表信息
     * wx:user:info:%s
     */
    private static final String USER_INFO_CACHE_KEY = "wx:user:info:%s";
    private static final String MID_GENERATE_CACHE_KEY = "mid:generate:timestamp:risk:%s";

    @Autowired
    private RedisTemplate<String, String> userRedisTemplate;

    @Autowired
    private RedisTemplate<String, String> midRedisTemplate;

    @Autowired
    private WxUserInfoRepository wxUserInfoRepository;

    private LoadingCache<String, Set<String>> riskUserCache = CacheBuilder.newBuilder()
            .maximumSize(5)
            .refreshAfterWrite(300, TimeUnit.SECONDS)
            .expireAfterWrite(300, TimeUnit.SECONDS)
            .expireAfterAccess(300, TimeUnit.SECONDS)
            .build(new CacheLoader<String, Set<String>>() {
                @Override
                public Set<String> load(String key) {
                    Set<String> result = redisTemplate.opsForSet().members(key);
                    if (CollectionUtils.isEmpty(result)) {
                        return Collections.emptySet();
                    }
                    return result;
                }
            });

    @PostConstruct
    public void init() {
        Set<String> data1 = riskUserCache.getUnchecked(RedisKeyConstants.Recommend.riskUserUid);
        Set<String> data2 = riskUserCache.getUnchecked(RedisKeyConstants.Recommend.riskUserMid);
        log.info("riskUserCache uid size {}  mid size {}", data1.size(), data2.size());
    }

    public RecommendResponse homepageRecommend(RecommendRequest request) {
        return recommend(request, 0);
    }

    public RecommendResponse relevantRecommend(RecommendRequest request) {
        return recommend(request, 1);
    }

    public RecommendResponse immerseRecommend(RecommendRequest request) {
        return recommend(request, 2);
    }

    public RecommendResponse recommend(RecommendRequest request, int recommendType) {
        try {
            timerLogMapTL.get().put("traceId", TraceUtils.currentTraceId());

            if (request == null) {
                return RecommendResponse.newBuilder()
                        .setResult(Result.newBuilder().setCode(1).setMessage("success"))
                        .build();
            }

            timerLogMapTL.get().put("appType", request.getAppType());
            timerLogMapTL.get().put("recommendType", recommendType);

            if (request.getVersionAuditStatus() == 1) {
                timerLogMapTL.get().put("special", true);
                return specialMidRecommend(request);
            }
            Stopwatch stopwatch = Stopwatch.createStarted();

            if (StringUtils.isNotBlank(request.getMid())
                    && redisTemplate.opsForSet().isMember("special:mid", request.getMid())) {
                return specialMidRecommend(request);
            }

            stopwatch.reset().start();
            RecommendParam param = genRecommendParam(request, recommendType);
            long genRecommendParamTime = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS);
            timerLogMapTL.get().put("genRecommendParamTime", genRecommendParamTime);
            // log.info("genRecommendParam={},genRecommendParam cost={}", JSONUtils.toJson(param),
            //         genRecommendParamTime);
            List<Video> videos = videoRecommend(param);
            stopwatch.reset().start();
            updateCache(request, param, videos);
            long updateCacheTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
            timerLogMapTL.get().put("updateCacheTime", updateCacheTime);
            timerLogService.log(timerLogMapTL.get());
            logStatisticsInfo(request, param, videos);
            // 更新position
            List<VideoProto> vps = new ArrayList<>();
            for (int i = 0; i < videos.size(); i++) {
                Map<String, List<String>> pushFromIndex = videos.get(i).getPushFromIndex();
                List<PushFromIndex> pushFromIndexList = new ArrayList<>();
                if (MapUtils.isNotEmpty(pushFromIndex)) {
                    pushFromIndex.forEach((k, v) -> {
                        PushFromIndex.Builder builder = PushFromIndex.newBuilder().setPushFrom(k);
                        v.forEach(builder::addIndex);
                        pushFromIndexList.add(builder.build());
                    });
                }
                vps.add(VideoProto.newBuilder()
                        .setPosition(i + 1)
                        .setPushFrom(Strings.nullToEmpty(videos.get(i).getPushFrom()))
                        .setVideoId(videos.get(i).getVideoId())
                        .setRovScore(videos.get(i).getRovScore())
                        .setSortScore(videos.get(i).getSortScore())
                        .setFlowPool(Strings.nullToEmpty(videos.get(i).getFlowPool()))
                        .setIsInFlowPool(videos.get(i).isInFlowPool() ? 1 : 0)
                        .setRand(videos.get(i).getRand())
                        .addAllPushFromIndex(pushFromIndexList)
                        .build());
            }

            return RecommendResponse.newBuilder()
                    .setResult(Result.newBuilder().setCode(1).setMessage("success"))
                    .addAllVideo(vps)
                    .build();
        } finally {
            timerLogMapTL.remove();
        }

    }

    private void logStatisticsInfo(RecommendRequest request, RecommendParam param, List<Video> videos) {

        if (CollectionUtils.isEmpty(videos)) {
            return;
        }

        try {
            List<Map<String, String>> data = videos.stream().map(v -> {
                Map<String, String> map = new HashMap<>();

                map.put("traceId", String.valueOf(TraceUtils.currentTraceId()));
                map.put("recommendTraceId", Strings.nullToEmpty(request.getRecommendTraceId()));

                // TODO user
                map.put("sessionId", request.getSessionId());
                map.put("subsessionid", request.getSubSessionId());
                map.put("mid", param.getMid());

                // ab
                map.put("newexpgroup", request.getNewExpGroup());

                // scene
                map.put("apptype", String.valueOf(request.getAppType()));
                map.put("pagesource", request.getPageSource());

                // video
                map.put("videoId", String.valueOf(v.getVideoId()));
                map.put("relevantVideoId", String.valueOf(request.getVideoId()));
                map.put("rovScore", String.valueOf(v.getRovScore()));
                map.put("sortScore", String.valueOf(v.getSortScore()));
                map.put("pushFrom", String.valueOf(v.getPushFrom()));

                map.put("flowPool", String.valueOf(v.getFlowPool()));
                map.put("level", String.valueOf(v.getLevel()));
                map.put("inFlowPool", String.valueOf(v.isInFlowPool()));

                map.put("scoreRos", String.valueOf(v.getScoreRos()));
                map.put("scoreStr", String.valueOf(v.getScoreStr()));
                map.put("score", String.valueOf(v.getScore()));
                map.put("scoresMap", JSONUtils.toJson(v.getScoresMap()));
                map.put("metaFeatureMap", JSONUtils.toJson(v.getMetaFeatureMap()));

                if (MapUtils.isNotEmpty(logUploadFieldSwitch) && StringUtils.equals("1", logUploadFieldSwitch.get("allFeatureMap"))) {
                    map.put("allFeatureMap", JSONUtils.toJson(v.getAllFeatureMap()));
                } else {
                    map.put("allFeatureMap", JSONUtils.toJson(Collections.emptyMap()));
                }

                map.put("pushFromRank", JSONUtils.toJson(v.getPushFromRank()));
                // map.put("pushFromRankVideo", JSONUtils.toJson(v.getPushFromRankVideo()));
                map.put("abExpCode", JSONUtils.toJson(param.getAbExpCodes()));

                long featureTableSize = 0;
                if (MapUtils.isNotEmpty(v.getMetaFeatureMap())) {
                    featureTableSize = v.getMetaFeatureMap().entrySet()
                            .stream()
                            .filter(e -> MapUtils.isNotEmpty(e.getValue()))
                            .count();
                }
                map.put("featureTableSize", String.valueOf(featureTableSize));
                if (2 == param.getRecommendType()) {
                    Map<String, String> extend = new HashMap<>();
                    extend.put("reqVideo", String.valueOf(request.getRequestVideoId()));
                    extend.put("currentPage", request.getCurrentPageSource());
                    map.put("extend", JSONUtils.toJson(extend));
                }

                map.put("rootSessionId", request.getRootSessionId());

                JSONObject extJson = new JSONObject();
                if (MapUtils.isNotEmpty(logUploadFieldSwitch) && StringUtils.equals("1", logUploadFieldSwitch.get("ext"))) {
                    extJson.put("rankVideoInfoMap", v.getRankVideoInfoMap());
                }
                map.put("ext", JSONUtils.toJson(extJson));

                return map;

            }).collect(Collectors.toList());

            statisticsLogService.log(data);
        } catch (Exception e) {
            log.error("logStatisticsInfo error", e);
        }
    }

    public RecommendResponse specialMidRecommend(RecommendRequest request) {
        // log.info("hit special mid recommend request={}", JSONUtils.toJson(request));
        if (request == null) {
            return RecommendResponse.newBuilder()
                    .setResult(Result.newBuilder().setCode(1).setMessage("success"))
                    .build();
        }

        RecallParam recallParam = new RecallParam();
        recallParam.setAppType(request.getAppType());
        recallParam.setMid(request.getMid());
        recallParam.setSpecialRecommend(true);
        recallParam.setSize(request.getSize());
        recallParam.setProvince(request.getProvince());
        recallParam.setCityCode(request.getCityCode());

        RecallResult recallResult = recallService.recall(recallParam);

        RankParam rankParam = new RankParam();
        rankParam.setRecallResult(recallResult);
        rankParam.setSize(request.getSize());
        rankParam.setSpecialRecommend(true);

        RankResult rankResult = rankRouter.rank(rankParam);

        if (rankResult == null || CollectionUtils.isEmpty(rankResult.getVideos())) {
            return RecommendResponse.newBuilder()
                    .setResult(Result.newBuilder().setCode(1).setMessage("success"))
                    .build();
        }

        // 只返回size条数据
        List<Video> videos = rankResult.getVideos();
        if (recallParam.getSize() < rankResult.getVideos().size()) {
            videos = rankResult.getVideos().subList(0, recallParam.getSize());
        }

        if (CollectionUtils.isNotEmpty(videos)) {
            Video lastVideo = videos.get(videos.size() - 1);
            redisTemplate.opsForValue().set(lastVideo.getLastVideoKey(), String.valueOf(lastVideo.getVideoId()),
                    1, TimeUnit.DAYS);
        }

        return RecommendResponse.newBuilder()
                .setResult(Result.newBuilder().setCode(1).setMessage("success"))
                .addAllVideo(CommonCollectionUtils.toList(videos, v -> VideoProto.newBuilder()
                        .setPushFrom(Strings.nullToEmpty(v.getPushFrom()))
                        .setVideoId(v.getVideoId())
                        .setRovScore(v.getRovScore())
                        .build()))
                .build();
    }

    public RecommendParam genRecommendParam(RecommendRequest request, int recommendType) {
        RecommendParam param = new RecommendParam();
        param.setTopK(3);
        param.setFlowPoolP(0.3);
        param.setMid(request.getMid());
        param.setUid(request.getUid());
        param.setSize(request.getSize());
        param.setVideoId(request.getVideoId());
        param.setCityCode(request.getCityCode());
        param.setProvinceCode(request.getProvinceCode());
        param.setAppType(request.getAppType());
        param.setProvince(request.getProvince());
        param.setCity(request.getCity());


        param.setRuleKey("rule66");
        param.setDataKey("data66");
        param.setRankKeyPrefix("rank:score1:");

        String channelName = this.getChannelName(request.getRootSourceId());
        param.setChannelName(channelName);

        // 实验配置
        if (request.getAbExpCodeCount() > 0) {
            Set<String> abExpCodes = request.getAbExpCodeList().stream().collect(Collectors.toSet());
            param.setAbExpCodes(abExpCodes);
            // 流量池视频分发概率
            if (abExpCodes.contains("211")) {
                param.setFlowPoolP(0.9);
            } else if (abExpCodes.contains("221")) {
                param.setFlowPoolP(0.7);
            } else if (abExpCodes.contains("299")) {
                param.setFlowPoolP(0.5);
            } else if (abExpCodes.contains("300")) {
                param.setFlowPoolP(0.4);
            } else if (abExpCodes.contains("301")) {
                param.setFlowPoolP(0.6);
            } else if (abExpCodes.contains("339")) {
                param.setFlowPoolP(0);
            }
        }

        // 风险过滤
        if (riskVideoFilterSwitch) {
            //0 4产品之外，hotsencetype 1089进入，没有mid和uid时，推荐返回过滤掉风险视频
//            boolean isNoneUserRisk = (Objects.isNull(noneUserRiskExcludeAppTypes) || !noneUserRiskExcludeAppTypes.contains(param.getAppType()))
//                    && Objects.equals(1089L, request.getHotSceneType())
//                    && (Objects.isNull(param.getUid()) || Objects.equals("0", param.getUid().trim()) || Objects.equals("null", param.getUid().trim()))
//                    && (Objects.isNull(param.getMid()) || Objects.equals("", param.getMid().trim()));

            //0 4产品之外，hotsencetype 1089进入，没有mid和uid 或 mid/uid为新用户时，推荐返回过滤掉风险视频
            boolean isNoneUserRisk = isNoneUserRisk(request, param);
            boolean riskUser = riskScenes.contains(request.getHotSceneType())
                    || riskUserCache.getUnchecked(RedisKeyConstants.Recommend.riskUserUid).contains(param.getUid())
                    || riskUserCache.getUnchecked(RedisKeyConstants.Recommend.riskUserMid).contains(param.getMid())
                    || isNoneUserRisk;
            param.setRiskUser(riskUser);
        }
        //根据地域屏蔽视频
        boolean isTestingRiskProvince = Objects.nonNull(testingRiskProvince) && !testingRiskProvince.isEmpty()
                && Objects.nonNull(param.getProvince()) && !Objects.equals("", param.getProvince().trim())
                && testingRiskProvince.contains(param.getProvince());
        boolean isTestingRiskCity = Objects.nonNull(testingRiskCity) && !testingRiskCity.isEmpty()
                && Objects.nonNull(param.getCity()) && !Objects.equals("", param.getCity().trim())
                && testingRiskCity.contains(param.getCity());
        boolean isTestingRiskRegion = isTestingRiskProvince || isTestingRiskCity;
        param.setTestingRiskRegion(isTestingRiskRegion);

        if (request.hasMachineInfo()) {
            MachineInfoProto machineInfoProto = request.getMachineInfo();
            MachineInfo machineInfo = new MachineInfo();
            machineInfo.setBrand(machineInfoProto.getBrand());
            machineInfo.setModel(machineInfoProto.getModel());
            machineInfo.setPlatform(machineInfoProto.getPlatform());
            machineInfo.setSdkVersion(machineInfoProto.getSdkVersion());
            machineInfo.setSystem(machineInfoProto.getSystem());
            machineInfo.setWechatVersion(machineInfoProto.getWechatVersion());
            param.setMachineInfo(machineInfo);
        }

        if (StringUtils.isNotBlank(request.getCategoryId()) && StringUtils.isNumeric(request.getCategoryId())) {
            param.setCategoryId(Integer.parseInt(request.getCategoryId()));
        }

        param.setHotSceneType(request.getHotSceneType());
        param.setClientIp(request.getClientIp());
        param.setVersionCode(request.getVersionCode());
        param.setRootSourceId(request.getRootSourceId());
        param.setUserShareDepth(request.getUserShareDepth());
        param.setPageNum(request.getPageNum());
        param.setRootSessionId(request.getRootSessionId());
        param.setUserRTShareList(getUserRTShareList(request.getShareListList()));
        param.setRecommendType(recommendType);
        param.setRequestVideoId(request.getRequestVideoId());
        param.setCurrentPageSource(request.getCurrentPageSource());
        param.setOpenGId(request.getOpenGId());
        param.setUnionId(request.getUnionId());

        Map<String, String> creativeInfoFeature = this.getCreativeInfoFeature(request.getRootSourceId(), channelName, request.getUserShareDepth());
        param.setCreativeInfoFeature(creativeInfoFeature);

        return param;
    }

    private boolean isNoneUserRisk(RecommendRequest request, RecommendParam param) {
        boolean isNoneUserRisk = false;
        try {
            long startOfDay = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
            if ((Objects.isNull(noneUserRiskExcludeAppTypes) || !noneUserRiskExcludeAppTypes.contains(param.getAppType()))
                    && Objects.equals(1089L, request.getHotSceneType())) {
                if ((Objects.isNull(param.getUid()) || Objects.equals("0", param.getUid().trim()) || Objects.equals("null", param.getUid().trim()))
                        && (Objects.isNull(param.getMid()) || Objects.equals("", param.getMid().trim()))) {
                    //无mid/uid时
                    isNoneUserRisk = true;
                } else {
                    if (Objects.nonNull(param.getUid()) && !Objects.equals("0", param.getUid().trim()) && !Objects.equals("null", param.getUid().trim())) {
                        //有uid
                        WxUserInfo wxUserInfo = getWxUserInfo(param.getUid());
                        if (Objects.isNull(wxUserInfo) || (Objects.nonNull(wxUserInfo.getGmtCreateTimestamp()) && wxUserInfo.getGmtCreateTimestamp() > startOfDay)) {
                            //有uid，当天为入库则为新用户
                            isNoneUserRisk = true;
                        }
                    } else if (Objects.nonNull(param.getMid()) && !Objects.equals("", param.getMid().trim())) {
                        //有mid
                        String key = String.format(MID_GENERATE_CACHE_KEY, param.getMid());
                        String value = midRedisTemplate.opsForValue().get(key);
                        if (Objects.isNull(value) || Objects.equals("", value) || Long.parseLong(value) > startOfDay) {
                            //创建时间大于当天，则为新用户
                            isNoneUserRisk = true;
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.error("isNoneUserRisk error param = {} ", JSON.toJSONString(param), e);
        }
        return isNoneUserRisk;
    }

    private WxUserInfo getWxUserInfo(String uid) {
        try {
            String key = String.format(USER_INFO_CACHE_KEY, uid);
            String userInfo = userRedisTemplate.opsForValue().get(key);
            if (Objects.nonNull(userInfo)) {
                return JSON.parseObject(userInfo, WxUserInfo.class);
            }
            List<WxUserInfo> list = wxUserInfoRepository.findByUid(Long.parseLong(uid));
            if (Objects.nonNull(list) && !list.isEmpty()) {
                WxUserInfo wxUserInfo = list.get(0);
                userRedisTemplate.opsForValue().set(key, JSON.toJSONString(wxUserInfo), Duration.ofHours(2));
                return wxUserInfo;
            }
        } catch (Exception e) {
            log.error("getWxUserInfo error uid = {}", uid, e);
        }
        return null;
    }

    private List<Video> videoRecommend(RecommendParam param) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        String vid = String.valueOf(param.getVideoId());
        UserShareReturnProfile userProfile = featureService.getUserProfile(param.getUid(), param.getMid());
        Map<String, Map<String, String>> behaviorVideos = featureService.getBehaviorVideos(vid, userProfile);

        Map<String, String> userSocialRecallInfo = new HashMap<>();
        if (CollectionUtils.isNotEmpty(param.getAbExpCodes()) && param.getAbExpCodes().contains("566")) {
            userSocialRecallInfo = featureService.getUserSocialRecallInfo(param.getMid());
        }

        Map<String, String> headVideoInfo;
        if (null != behaviorVideos && behaviorVideos.containsKey(vid)) {
            headVideoInfo = behaviorVideos.get(vid);
        } else {
            headVideoInfo = new HashMap<>();
        }
        // long infoTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
        // timerLogMapTL.get().put("infoTime", infoTime);
        stopwatch.reset().start();

        RecallParam recallParam = convertToRecallParam(param);
        recallParam.setUserProfile(userProfile);
        recallParam.setHeadInfo(headVideoInfo);
        recallParam.setUserRTShareList(param.getUserRTShareList());
        recallParam.setBehaviorVideos(behaviorVideos);
        recallParam.setUserSocialRecallInfo(userSocialRecallInfo);
        RecallResult recallResult = recallService.recall(recallParam);

        long recallTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
        timerLogMapTL.get().put("recallTime", recallTime);
        // log.info("recallResult={}, videoRecommend recallResult cost={}", recallResult, recallTime);
        stopwatch.reset().start();

        RankParam rankParam = convertToRankParam(param, recallResult);
        rankParam.setUserProfile(userProfile);
        rankParam.setHeadInfo(headVideoInfo);
        rankParam.setUserRTShareList(param.getUserRTShareList());
        rankParam.setBehaviorVideos(behaviorVideos);
        RankResult rankResult = rankRouter.rank(rankParam);

        long rankTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
        timerLogMapTL.get().put("rankTime", rankTime);
        // log.info("rankResult={}, videoRecommend rank cost={}", rankResult, rankTime);


        if (rankResult == null || CollectionUtils.isEmpty(rankResult.getVideos())) {
            return Collections.emptyList();
        }

        // 只返回size条数据
        List<Video> videos = rankResult.getVideos();
        if (param.getSize() < rankResult.getVideos().size()) {
            videos = rankResult.getVideos().subList(0, param.getSize());
        }
        return videos;
    }

    public RecallParam convertToRecallParam(RecommendParam param) {
        RecallParam recallParam = new RecallParam();
        recallParam.setAppType(param.getAppType());
        // hard code 算法实验配置化之前，复用abcode做AB验证
        // note 避免非实验产品被覆盖
        recallParam.setRuleKey(param.getRuleKey());
        recallParam.setDataKey(param.getDataKey());

        recallParam.setVideoId(param.getVideoId());

        String provinceCode = StringUtils.isNotBlank(param.getProvinceCode())
                ? param.getProvinceCode()
                : "";
        String cityCode = StringUtils.isNotBlank(param.getCityCode())
                ? param.getCityCode()
                : "";
        recallParam.setRegionCode(cityCodes.contains(cityCode) ? cityCode : provinceCode);
        recallParam.setMid(param.getMid());
        recallParam.setSize(param.getSize());
        recallParam.setUid(param.getUid());
        // 风险过滤
        recallParam.setRiskUser(param.isRiskUser());
        recallParam.setTestingRiskRegion(param.isTestingRiskRegion());
        recallParam.setAbExpCodes(param.getAbExpCodes());

        recallParam.setProvince(param.getProvince());
        recallParam.setCity(param.getCity());
        recallParam.setCategoryId(param.getCategoryId());

        recallParam.setCityCode(param.getCityCode());

        recallParam.setHotSceneType(param.getHotSceneType());
        recallParam.setClientIp(param.getClientIp());
        recallParam.setRootSourceId(param.getRootSourceId());
        recallParam.setUserShareDepth(param.getUserShareDepth());
        recallParam.setChannelName(param.getChannelName());
        recallParam.setRootSessionId(param.getRootSessionId());
        recallParam.setRecommendType(param.getRecommendType());
        recallParam.setRequestVideoId(param.getRequestVideoId());
        recallParam.setCurrentPageSource(param.getCurrentPageSource());
        recallParam.setCreativeInfoFeature(param.getCreativeInfoFeature());
        recallParam.setOpenGId(param.getOpenGId());
        recallParam.setUnionId(param.getUnionId());

        return recallParam;
    }


    public RankParam convertToRankParam(RecommendParam param, RecallResult recallResult) {
        RankParam rankParam = new RankParam();
        rankParam.setRecallResult(recallResult);
        rankParam.setRankKeyPrefix(param.getRankKeyPrefix());

        rankParam.setSize(param.getSize());
        rankParam.setFlowPoolP(param.getFlowPoolP());
        rankParam.setTopK(param.getTopK());
        rankParam.setAppType(param.getAppType());
        rankParam.setMid(param.getMid());
        rankParam.setUid(param.getUid());
        rankParam.setProvince(param.getProvince());
        rankParam.setCity(param.getCity());
        rankParam.setMachineInfo(param.getMachineInfo());
        rankParam.setAbExpCodes(param.getAbExpCodes());
        rankParam.setCategoryId(param.getCategoryId());
        rankParam.setHeadVid(param.getVideoId());
        rankParam.setHotSceneType(param.getHotSceneType());
        rankParam.setVersionCode(param.getVersionCode());
        rankParam.setRootSourceId(param.getRootSourceId());
        rankParam.setUserShareDepth(param.getUserShareDepth());
        rankParam.setPageNum(param.getPageNum());
        rankParam.setChannelName(param.getChannelName());
        rankParam.setRootSessionId(param.getRootSessionId());
        rankParam.setRecommendType(param.getRecommendType());
        rankParam.setRequestVideoId(param.getRequestVideoId());
        rankParam.setCurrentPageSource(param.getCurrentPageSource());
        rankParam.setCreativeInfoFeature(param.getCreativeInfoFeature());
        rankParam.setOpenGId(param.getOpenGId());
        rankParam.setUnionId(param.getUnionId());
        return rankParam;
    }

    public void updateCache(RecommendRequest request, RecommendParam param, List<Video> videos) {
        if (StringUtils.isBlank(request.getMid())
                || CollectionUtils.isEmpty(videos)) {
            return;
        }

        preViewedService.updateCache(request.getAppType(), request.getMid(), videos, param.getAbExpCodes());
        updateLastVideoCache(videos);

        updateFlowPoolCache(request, param, videos);

        updateDouHotVideoCache(request, param, videos);
    }

    private void updateFlowPoolCache(RecommendRequest request, RecommendParam param,
                                     List<Video> videos) {
        if (request.getAppType() == 18
                || request.getAppType() == 19
                || CollectionUtils.isEmpty(videos)) {
            return;
        }

        List<Video> flowPoolVideos = videos.stream()
                .filter(v -> v.getPushFrom().equals(FlowPoolConstants.PUSH_FORM))
                .collect(Collectors.toList());


        if (CollectionUtils.isEmpty(flowPoolVideos)) {
            return;
        }
        flowPoolService.updateDistributeCountWithLevel(flowPoolVideos);
    }

    private void updateDouHotVideoCache(RecommendRequest request, RecommendParam param, List<Video> videos) {
        if (CollectionUtils.isEmpty(videos)) {
            return;
        }
        List<Video> douHotFlowPoolVideo = videos.stream()
                .filter(v -> v.getPushFrom().equals(DouHotFlowPoolRecallStrategy.PUSH_FROM))
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(douHotFlowPoolVideo)) {
            return;
        }
        flowPoolService.asyncHandleDouHotCache(douHotFlowPoolVideo, param.getProvince());
    }

    private void updateLastVideoCache(List<Video> videos) {

        if (CollectionUtils.isEmpty(videos)) {
            return;
        }

        Consumer<String> consumer = (p) -> {
            for (int i = videos.size() - 1; i >= 0; i--) {
                if (videos.get(i).getPushFrom().equals(p)) {
                    redisTemplate.opsForValue().set(videos.get(i).getLastVideoKey(), String.valueOf(videos.get(i).getVideoId()),
                            24, TimeUnit.HOURS);
                    break;
                }
            }
        };

        consumer.accept(RegionHRecallStrategy.PUSH_FORM);
        consumer.accept(RegionHDupRecallStrategy.PUSH_FORM);
        consumer.accept(Region24HRecallStrategy.PUSH_FORM);
        consumer.accept(RegionRelative24HRecallStrategy.PUSH_FORM);
        consumer.accept(RegionRelative24HDupRecallStrategy.PUSH_FORM);
    }

    private String getChannelName(String s) {
        String channel = FeatureUtils.getUserChannel(s);
        if (channel.equals("公众号mix")) {
            return getChannelType(s);
        } else {
            return channel;
        }
    }

    private String getChannelType(String s) {
        String key = String.format("%s:%s", channelGroupPrefix, s);
        String value = redisTemplate.opsForValue().get(key);
        if (null != value && value.equals("公众号买号")) {
            return value;
        }
        return "公众号代运营-Daily";
    }

    private List<UserSRBO> getUserRTShareList(List<VideoActionInfoProto> list) {
        List<UserSRBO> rtShareList = new ArrayList<>();
        if (null != list && !list.isEmpty()) {
            long startTs = System.currentTimeMillis() / 1000L - 2 * 3600L;
            for (VideoActionInfoProto videoAction : list) {
                if (null != videoAction) {
                    long id = videoAction.getVideoId();
                    long ts = videoAction.getTs();
                    if (ts > startTs && rtShareList.size() < 4) {
                        UserSRBO userSRBO = new UserSRBO();
                        userSRBO.setId(id);
                        userSRBO.setTs(ts);
                        rtShareList.add(userSRBO);
                    } else {
                        break;
                    }
                }
            }
        }
        return rtShareList;
    }

    private Map<String, String> getCreativeInfoFeature(String rootSourceId, String channelName, int userShareDepth) {
        Map<String, String> feature = new HashMap<>();
        if (CollectionUtils.isEmpty(creativeInfoFeatureChannelNameSet) || StringUtils.isEmpty(channelName) || userShareDepth != 0) {
            return feature;
        }
        if (!creativeInfoFeatureChannelNameSet.contains(channelName)) {
            return feature;
        }
        try {
            CommonResponse<VideoCharacteristicInfo> videoCharacteristic = growthManagerFeign.getVideoCharacteristic(rootSourceId);
            if (videoCharacteristic.getCode() == 0) {
                VideoCharacteristicInfo data = videoCharacteristic.getData();
                feature.put("ghId", data.getGhId());
                feature.put("name", data.getName());
                feature.put("videoId", data.getVideoId());
                feature.put("title", data.getTitle());
            }
        } catch (Exception e) {
            log.error("getCreativeInfoFeature rootSourceId: {} \n", rootSourceId, e);
        }


        return feature;
    }
}
