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


import com.alibaba.fastjson.JSONObject;
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.ThreadPoolFactory;
import com.tzld.piaoquan.recommend.server.common.base.RankItem;
import com.tzld.piaoquan.recommend.server.gen.recommend.RecommendRequest;
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.service.RecommendService;
import com.tzld.piaoquan.recommend.server.service.flowpool.FlowPoolConstants;
import com.tzld.piaoquan.recommend.server.service.rank.RankParam;
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.RecallStrategy;
import com.tzld.piaoquan.recommend.server.service.recall.strategy.FlowPoolWithLevelRecallStrategyTomson;
import com.tzld.piaoquan.recommend.server.service.recall.strategy.FlowPoolWithLevelScoreRecallStrategy;
import com.tzld.piaoquan.recommend.server.service.recall.strategy.FlowPoolWithScoreRecallStrategy;
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 org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

@Service
public class FlowPoolRecommendPipeline implements ApplicationContextAware {

    private final Logger log = LoggerFactory.getLogger(FlowPoolRecommendPipeline.class);

    private final static Map<String, RecallStrategy> strategyMap = new HashMap<>();

    @Resource
    private RecommendService recommendService;

    private ApplicationContext applicationContext;

    private final ExecutorService pool = ThreadPoolFactory.recallPool();

    @PostConstruct
    public void init() {
        Map<String, RecallStrategy> type = applicationContext.getBeansOfType(RecallStrategy.class);
        for (Map.Entry<String, RecallStrategy> entry : type.entrySet()) {
            RecallStrategy value = entry.getValue();
            strategyMap.put(value.getClass().getSimpleName(), value);
        }
    }


    public List<Video> feedByRec(final RecommendRequest requestData,
                                 final int recommendType, Boolean logPrint) {
        List<RecallStrategy> strategies = new ArrayList<>();

        RecommendParam param = recommendService.genRecommendParam(requestData, recommendType);
        RecallParam recallParam = recommendService.convertToRecallParam(param);
        RecallResult recallResult = getRecallResult(strategies, recallParam);
        if (logPrint) {
            log.info("traceId = {}, recallResult [{}]", requestData.getRequestId(),
                    JSONObject.toJSONString(recallResult));
        }

        if (recallResult == null || CollectionUtils.isEmpty(recallResult.getData())) {
            return Collections.emptyList();
        }

        RankParam rankParam = recommendService.convertToRankParam(param, recallResult);
        List<Video> videoList = mergeAndRankFlowPoolRecall(rankParam);
        if (logPrint) {
            log.info("traceId = {}, videoList [{}]", requestData.getRequestId(),
                    JSONObject.toJSONString(videoList));
        }
        return videoList;
    }

    public List<Video> mergeAndRankFlowPoolRecall(RankParam param) {
        return sortFlowPoolByThompson(param, FlowPoolConstants.PUSH_FORM);
    }

    public List<Video> sortFlowPoolByThompson(RankParam param, String pushFrom) {

        //初始化 userid
        UserFeature userFeature = new UserFeature();
        userFeature.setMid(param.getMid());

        // 初始化RankItem
        Optional<RecallResult.RecallData> data = param.getRecallResult().getData().stream()
                .filter(d -> d.getPushFrom().equals(pushFrom))
                .findFirst();
        if (!data.isPresent()) {
            return Collections.emptyList();
        }
        List<Video> videoList = data.get().getVideos();
        if (videoList == null) {
            return Collections.emptyList();
        }
        List<RankItem> rankItems = new ArrayList<>();
        for (int i = 0; i < videoList.size(); i++) {
            RankItem rankItem = new RankItem(videoList.get(i));
            rankItems.add(rankItem);
        }

        // 初始化上下文参数
        ScoreParam scoreParam = convert(param);
        List<RankItem> rovRecallScore = ScorerUtils.getScorerPipeline(ScorerUtils.FLOWPOOL_CONF)
                .scoring(scoreParam, userFeature, rankItems);

        if (rovRecallScore == null) {
            return Collections.emptyList();
        }

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

    private RecallResult getRecallResult(List<RecallStrategy> strategies, RecallParam param) {
        if (param.getFlowPoolAbtestGroup().equals(FlowPoolConstants.EXPERIMENTAL_FLOW_SET_LEVEL)) {
            strategies.add(strategyMap.get(FlowPoolWithLevelRecallStrategyTomson.class.getSimpleName()));
        } else if (param.getFlowPoolAbtestGroup().equals(FlowPoolConstants.EXPERIMENTAL_FLOW_SET_LEVEL_SCORE)) {
            strategies.add(strategyMap.get(FlowPoolWithLevelScoreRecallStrategy.class.getSimpleName()));
        } else {
            strategies.add(strategyMap.get(FlowPoolWithScoreRecallStrategy.class.getSimpleName()));
        }

        CountDownLatch cdl = new CountDownLatch(strategies.size());
        List<Future<RecallResult.RecallData>> recallResultFutures = new ArrayList<>();
        for (final RecallStrategy strategy : strategies) {
            Future<RecallResult.RecallData> future = pool.submit(() -> {
                List<Video> result = strategy.recall(param);
                cdl.countDown();
                return new RecallResult.RecallData(strategy.pushFrom(), result);
            });
            recallResultFutures.add(future);
        }
        try {
            cdl.await(3000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            log.error("recall error", e);
            return null;
        }

        List<RecallResult.RecallData> results = new ArrayList<>();
        for (Future<RecallResult.RecallData> f : recallResultFutures) {
            try {
                RecallResult.RecallData data = f.get();
                results.add(data);
            } catch (Exception e) {
                log.error("future get error ", e);
            }
        }

        return new RecallResult(results);
    }


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
