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

import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
import com.tzld.piaoquan.recommend.server.common.ThreadPoolFactory;
import com.tzld.piaoquan.recommend.server.common.enums.AppTypeEnum;
import com.tzld.piaoquan.recommend.server.common.enums.TabType;
import com.tzld.piaoquan.recommend.server.model.Video;
import com.tzld.piaoquan.recommend.server.service.ExperimentService;
import com.tzld.piaoquan.recommend.server.service.funnel.FunnelContext;
import com.tzld.piaoquan.recommend.server.service.funnel.RecallVideoEntry;
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.StringUtils;
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.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
 * @author dyp
 */
@Service
@Slf4j
public class RecallService implements ApplicationContextAware {

    @Resource
    private ExperimentService experimentService;

    private final Map<String, RecallStrategy> strategyMap = new HashMap<>();
    private ApplicationContext applicationContext;
    private final ExecutorService pool = ThreadPoolFactory.recallPool();
    @ApolloJsonValue("${alg.recall.special.app&vid:{}}")
    protected Map<String, List<Long>> specialAppVid;

    @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 RecallResult recall(RecallParam param) {
        List<RecallStrategy> strategies = getRecallStrategy(param);
//        log.info("recall strategies {}", JSONUtils.toJson(CommonCollectionUtils.toList(strategies,
//                o -> o.getClass().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 = Collections.emptyList();
                try {
                    result = strategy.recall(param);
                    //log.info("recall result {} {}", strategy.getClass().getSimpleName(), JSONUtils.toJson(result));
                } catch (Throwable e) {
                    log.error("recall error {}", strategy.getClass().getSimpleName(), e);
                }
                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);
            }
        }

        syncScoresToFunnel(param.getFunnelContext(), results);
        return new RecallResult(results);
    }

    /**
     * 漏斗：把 filter 后 Video.rovScore 同步回 funnel entry。
     * 部分 strategy 在 filter 前没传 scoresMap (entry.score 默认 0)，
     * filter 后 strategy 才把 rovScore set 到 Video 上。这里统一回写。
     */
    private static void syncScoresToFunnel(FunnelContext ctx, List<RecallResult.RecallData> results) {
        if (ctx == null || results == null) return;
        for (RecallResult.RecallData data : results) {
            if (data == null || data.getVideos() == null) continue;
            List<RecallVideoEntry> entries = ctx.getStages123RecallByStrategy().get(data.getPushFrom());
            if (entries == null) continue;
            java.util.Map<Long, Double> videoScores = new java.util.HashMap<>(data.getVideos().size());
            for (Video v : data.getVideos()) {
                videoScores.put(v.getVideoId(), v.getRovScore());
            }
            for (RecallVideoEntry e : entries) {
                Double s = videoScores.get(e.getVideoId());
                if (s != null) e.setScore(s);   // 覆盖：filter 通过的 entry 用 Video.rovScore（最终 score）
            }
        }
    }

    private List<RecallStrategy> getRecallStrategy(RecallParam param) {
        List<RecallStrategy> strategies = new ArrayList<>();

        if (2 == param.getRecommendType()) {
            strategies.add(strategyMap.get(HotReturnUvRecallStrategy.class.getSimpleName()));
            return strategies;
        }
        if (param.isSpecialRecommend()) {
            strategies.add(strategyMap.get(SpecialRecallStrategy.class.getSimpleName()));
            return strategies;
        }

        // 标签页的召回
        if (Objects.nonNull(param.getTab())) {
            TabType tabType = TabType.getByType(param.getTab().getType());
            if (TabType.territory.equals(tabType)) {
                strategies.add(strategyMap.get(CityRovnAllRovRecallStrategy.class.getSimpleName()));
                strategies.add(strategyMap.get(CityRovnRecallStrategy.class.getSimpleName()));
                strategies.add(strategyMap.get(RegionHRecallStrategy.class.getSimpleName()));
                strategies.add(strategyMap.get(RegionRealtimeRecallStrategyV1.class.getSimpleName()));
                return strategies;
            } else if (TabType.personalization.equals(tabType)) {
                strategies.add(strategyMap.get(PersonalizationTabRovnRecallStrategy.class.getSimpleName()));
                return strategies;
            }
        }

        //1：通过“产品”控制“召回子策略”. 票圈美好祝福与内部tab只走祝福召回。APP只走固定列表。特殊配置的app只有固定召回列表。
        if (this.matchSpecialApp(param.getAppType())) {
            strategies.add(strategyMap.get(RegionRealtimeRecallStrategyV5Hand.class.getSimpleName()));
            return strategies;
        }
        if (param.getAppType() == AppTypeEnum.PIAO_QUAN_MEIHAO_ZHUFU.getCode() && Objects.equals(param.getCategoryId(), 100)) {
            strategies.add(strategyMap.get(BlessRecallStrategy.class.getSimpleName()));
            return strategies;
        }
        if (param.getAppType() == AppTypeEnum.APP_SPEED.getCode()) {
            strategies.add(strategyMap.get(AppFallbackRecallStrategy.class.getSimpleName()));
            return strategies;
        }

        strategies.add(strategyMap.get(RegionRealtimeRecallStrategyV1.class.getSimpleName()));
        strategies.addAll(getRegionRecallStrategy(param));
        strategies.add(strategyMap.get(ReturnVideoRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(SceneCFRovnRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(SceneCFRosnRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(UserCate1RecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(UserCate2RecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(HeadProvinceCate1RecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(HeadProvinceCate2RecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(HeadCate2RovRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(CityRovnRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(PrioriProvinceRovnRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(PrioriProvinceStrRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(PrioriProvinceRosRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(Return1Cate2RosRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(Return1Cate2StrRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(YearShareCate1RecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(YearShareCate2RecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(YearReturnCate2RecallStrategy.class.getSimpleName()));

        Set<String> abExpCodes = param.getAbExpCodes();
        if (CollectionUtils.isNotEmpty(abExpCodes)) {
            // if (abExpCodes.contains("568")) {
            //     strategies.add(strategyMap.get(PrioriPremiumRovnRecallStrategy.class.getSimpleName()));
            // }
            // if (abExpCodes.contains("566")){
            //     strategies.add(strategyMap.get(SocialI2IDirectRecallStrategy.class.getSimpleName()));
            //     strategies.add(strategyMap.get(SocialI2IHistoryShareRecallStrategy.class.getSimpleName()));
            //     strategies.add(strategyMap.get(SocialI2IHistoryClickRecallStrategy.class.getSimpleName()));
            // }
        }

        boolean isHit568Exp = experimentService.judgeHitAlgoExp(param.getAppType(), param.getRootSessionId(), abExpCodes, "568");
        if (isHit568Exp) {
            strategies.add(strategyMap.get(UserCate1RecallStrategyV568.class.getSimpleName()));
            strategies.add(strategyMap.get(UserCate2RecallStrategyV568.class.getSimpleName()));
        }

        boolean isHit842Exp = experimentService.judgeHitAlgoExp(param.getAppType(), param.getRootSessionId(), abExpCodes, "842");
        if (isHit842Exp) {
            strategies.add(strategyMap.get(UserDeconstructionKeywordsRecallStrategy.class.getSimpleName()));
        }

        // V562/V565/V567 同步成 V566 模式 (rank 类粗排分统一截断 + 老 region/city 白名单),
        // 召回侧不再做"追加 all_rov + removeIf 老 region/city"的对调 —— 与 V564/V566 一致行为, 让公共池
        // 22 路召回都跑, rank 类 extractAllAndTruncateByCoarseRank 按白名单 + 粗排分挑选.
        //
        // V536/V562/V565/V569 各承载一个 dk_elements 实验, 互相隔离便于独立归因:
        //   V562 → YearShareDkElements:    用户近期 share 行为 join dk_elements -> elements_rovn_recall 倒排
        //   V536 → YearShareDkElements:    同 V562, 但在 V536 rank 类中归非个性化白名单 (配额归属差异)
        //   V839 → YearValidPlayDkElements: V839 用 valid_play 行为源替换 V562 的 share 行为源,
        //                                    形成"分享 vs 有效播放"双行为 dk_elements 召回 AB 对照.
        //   V565 → UserProfileDkElements:  用户元素画像 (s_z_y_s/zt_gyf)         -> elements_rovn_recall 倒排
        //   V569 → YearReturnDkElements:   用户近期 click 回流行为 join dk_elements -> elements_rovn_recall 倒排
        boolean isHit536Exp = experimentService.judgeHitAlgoExp(param.getAppType(), param.getRootSessionId(), abExpCodes, "536");
        boolean isHit839Exp = experimentService.judgeHitAlgoExp(param.getAppType(), param.getRootSessionId(), abExpCodes, "839");
        if (isHit536Exp) {
            strategies.add(strategyMap.get(YearShareDkElementsRecallStrategy.class.getSimpleName()));
        }
        if (isHit839Exp) {
            strategies.add(strategyMap.get(YearValidPlayDkElementsRecallStrategy.class.getSimpleName()));
        }
        boolean isHit565Exp = experimentService.judgeHitAlgoExp(param.getAppType(), param.getRootSessionId(), abExpCodes, "565");
        if (isHit565Exp) {
            strategies.add(strategyMap.get(UserProfileDkElementsRecallStrategy.class.getSimpleName()));
        }
        boolean isHit562Exp = experimentService.judgeHitAlgoExp(param.getAppType(), param.getRootSessionId(), abExpCodes, "562");
        if (isHit562Exp) {
            strategies.add(strategyMap.get(UserProfileDkElementsFXRecallStrategy.class.getSimpleName()));
        }
        boolean isHit569Exp = experimentService.judgeHitAlgoExp(param.getAppType(), param.getRootSessionId(), abExpCodes, "569");
        if (isHit569Exp) {
            strategies.add(strategyMap.get(UserProfileDkElementsXFRecallStrategy.class.getSimpleName()));
        }

        // V564 实验：召回侧不做任何剔除/新增——让所有公共池召回都跑，
        // 由 V564 rank 类 (mergeAndRankRovRecall) 在 extractAllAndTruncateByCoarseRank
        // 里按全局粗排分统一截断。

        // null uid 的 guest user 不走流量池: 风控修复全量后这批 guest 不再 setRiskUser(true),
        // 若不在此处隔离会涌进流量池稀释真实有效 uid 的曝光。
        boolean isNullUidGuest = "null".equals(param.getUid());

        // 命中用户黑名单不走流量池
        // 命中安全测试风险地域不走流量池
        if (!param.isRiskUser() && !param.isTestingRiskRegion() && !isNullUidGuest) {
            strategies.add(strategyMap.get(QuickFlowPoolWithLevelRecallStrategy.class.getSimpleName()));
            strategies.add(strategyMap.get(FlowPoolWithLevelRecallStrategyTomson.class.getSimpleName()));
            strategies.add(strategyMap.get(DouHotFlowPoolRecallStrategy.class.getSimpleName()));
        }
        return strategies;
    }

    private List<RecallStrategy> getRegionRecallStrategy(RecallParam param) {

        List<RecallStrategy> strategies = new ArrayList<>();
        if (StringUtils.isNotBlank(param.getRegionCode())) {
            strategies.add(strategyMap.get(RegionHRecallStrategy.class.getSimpleName()));
            strategies.add(strategyMap.get(Region24HRecallStrategy.class.getSimpleName()));
        }
        strategies.add(strategyMap.get(RegionHDupRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(RegionRelative24HRecallStrategy.class.getSimpleName()));
        strategies.add(strategyMap.get(RegionRelative24HDupRecallStrategy.class.getSimpleName()));

        return strategies;
    }

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

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