package com.tzld.piaoquan.ad.engine.service.predict.v2;

import com.alibaba.fastjson.JSON;
import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
import com.tzld.piaoquan.ad.engine.commons.enums.RedisPrefixEnum;
import com.tzld.piaoquan.ad.engine.commons.redis.AdRedisHelper;
import com.tzld.piaoquan.ad.engine.service.feature.Feature;
import com.tzld.piaoquan.ad.engine.service.feature.FeatureService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 基于 ROR的广告预测策略
 * <p>
 * 核心逻辑：
 * 1. 根据用户的历史行为特征（启动次数launchs、留存率ror、人群分层ad_level）计算展示广告的概率阈值
 * 2. 通过 mid 的 hash 值生成伪随机分数
 * 3. 如果分数 <= 阈值，则展示广告；否则不展示
 * <p>
 * 用于控制不同用户群体的广告曝光频率，实现精细化运营
 */
@Slf4j
@Service
public class PredictStrategyByRorCopy extends BasicPredict {

    /** 特征服务，用于获取用户行为特征 */
    @Autowired
    private FeatureService featureService;

    /** Redis 客户端，用于获取基于用户特征的概率配置 */
    @Autowired
    private AdRedisHelper adRedisHelper;

    /**
     * Apollo 动态配置：根据 rootSessionId 尾号和 appType 进行流量分桶
     * <p>
     * 配置格式示例：
     * <pre>
     * [
     *   {
     *     "appType": ["0", "3"],
     *     "tail": ["0", "1", "2"],
     *     "config": {"default_probability": 0.5}
     *   }
     * ]
     * </pre>
     */
    @ApolloJsonValue("${experiment.ror.root.session.id.tail.copy.config:[]}")
    private List<RootSessionIdTailConfigItem> configItems;

    @Value("${experiment.ror.show.log.switch:1}")
    private String experimentRorShowLogSwitch;

    private static final String TABLE_NAME = "alg_mid_history_behavior_1month";

    /**
     * 策略名称标识
     */
    @Override
    public String name() {
        return "launch_layer_ror_copy";
    }

    /**
     * 核心预测方法：决定是否向用户展示广告
     *
     * @param ctx 预测上下文，包含 mid、appType、rootSessionId 等信息
     * @return 预测结果 Map，包含：
     *         - ad_predict: 1=不展示广告，2=展示广告
     *         - score: 用户的伪随机分数
     *         - threshold: 广告展示概率阈值
     *         - launchs/ror/ad_level: 用户行为特征
     *         - 返回 null 表示跳过该策略
     */
    @Override
    public Map<String, Object> predict(PredictContext ctx) {

        try {
            String rootSessionId = ctx.getRootSessionId();

            // 前置校验：配置为空或 rootSessionId 为空时，返回 null（跳过该策略）
            if (CollectionUtils.isEmpty(configItems) || StringUtils.isBlank(rootSessionId)) {
                return null;
            }

            String appType = ctx.getAppType();

            // 获取默认概率阈值（基于 rootSessionId 尾号和 appType 匹配配置）
            Double defaultProbability = getDefaultProbability(rootSessionId, appType);

            // 用户不在实验分桶内，跳过该策略
            if (defaultProbability == null) {
                return null;
            }

            Map<String, Object> rtnMap = new HashMap<>();

            // 用户行为特征变量（来自离线特征表 alg_mid_history_behavior_1month）
            String launchs = "-999";   // 启动次数分桶（如 "0-5", "5-10" 等）
            String ror = "-999";       // 留存率分桶
            String adLevel = "无转化";   // 广告等级（用户对广告的敏感度分层）
            String return30day = "r_0_8";   // 用户回流率分桶

            // 根据 mid 获取用户近一个月的历史行为特征
            Feature feature = featureService.getMidBehaviorFeature(TABLE_NAME, ctx.getMid());
            if ("1".equals(experimentRorShowLogSwitch)) {
                log.info("[PredictStrategyByRor] mid:{}, feature:{}, featureStr: {}", ctx.getMid(),feature,JSON.toJSONString(feature));
            }
            // 安全地提取特征值（多层 null 检查）
            if (feature != null && feature.getUserFeature() != null && feature.getUserFeature().get(TABLE_NAME) != null) {
                Map<String, String> algMidHistoryBehavior1month = feature.getUserFeature().get(TABLE_NAME);
                launchs = StringUtils.isBlank(algMidHistoryBehavior1month.get("launchs")) ? launchs : algMidHistoryBehavior1month.get("launchs");
                ror = StringUtils.isBlank(algMidHistoryBehavior1month.get("ror")) ? ror : algMidHistoryBehavior1month.get("ror");
                adLevel = StringUtils.isBlank(algMidHistoryBehavior1month.get("ad_level")) ? adLevel : algMidHistoryBehavior1month.get("ad_level");
                return30day = StringUtils.isBlank(algMidHistoryBehavior1month.get("return_30day")) ? return30day : algMidHistoryBehavior1month.get("return_30day");
            }

            // 计算最终的广告展示概率阈值
            // 优先使用 Redis 中基于用户特征的精细化阈值，否则使用默认阈值
            Double showAdProbability = getShowAdProbability(launchs, ror, adLevel,return30day,defaultProbability);

            if (showAdProbability == null) {
                return null;
            }

            // 基于 mid 的 hash 值生成 [0, 1) 范围内的伪随机分数
            // 同一个 mid 在同一小时内（RandW 每小时更新）会得到相同的分数
            double score = this.calcScoreByMid(ctx.getMid());

            // 核心决策逻辑：分数 <= 阈值 → 展示广告
            if (score < showAdProbability) {
                // 展示广告，ad_predict = 2
                rtnMap.putAll(rtnAdPredict(ctx));
                rtnMap.put("model", this.name());
            } else {
                // 不展示广告，ad_predict = 1
                rtnMap.putAll(rtnNoAdPredict(ctx));
                rtnMap.put("no_ad_strategy", this.name());
            }

            // 记录决策相关的特征和参数，用于日志分析和效果追踪
            rtnMap.put("score", score);
            rtnMap.put("threshold", showAdProbability);
            rtnMap.put("launchs", launchs);
            rtnMap.put("return_30day", return30day);
            rtnMap.put("ror", ror);
            rtnMap.put("ad_level", adLevel);
            return rtnMap;
        } catch (Exception e) {
            log.error("[PredictStrategyByRor] predict error, ctx: {}", ctx, e);
            return null;
        }
    }

    /**
     * 获取广告展示概率阈值
     * <p>
     * 策略：根据用户的 (ad_level, launchs, ror) 组合从 Redis 查询对应的概率值
     * 如果查询失败或无数据，则使用默认概率
     *
     * @param launchs            启动次数分桶
     * @param ror                留存率分桶
     * @param ad_level           人群分层
     *
     * @return 广告展示概率阈值 [0, 1]
     */
    private Double getShowAdProbability(String launchs, String ror, String ad_level,String return30day, Double defaultProbability) {
        // 任一特征为空，使用默认概率
        if (StringUtils.isAnyBlank(launchs, ror, ad_level,return30day)) {
            return defaultProbability;
        }
        try {
            // 构建 Redis key：格式为 "ad_level:launchs:ror"，例如 "有转化:10:000"
            String keyId = ad_level + ":" + launchs + ":" + ror + ":" + return30day;
            String key = String.format(RedisPrefixEnum.AD_USER_ROR_BEHAVIOR_COPY.getPrefix(), keyId);

            // 从 Redis 获取概率值
            String probability = adRedisHelper.get(key);

            // 解析概率值，如果为 null 则使用默认值
            return StringUtils.isBlank(probability) ? defaultProbability : Double.parseDouble(probability);
        } catch (Exception e) {
            // 解析失败（如非数字字符串）或 Redis 异常，记录错误并使用默认值
            log.error("getShowAdProbability error, launchs: {}, ror: {}, ad_level: {}, e = ", launchs, ror, ad_level, e);
            return defaultProbability;
        }
    }

    /**
     * 获取默认概率阈值
     * <p>
     * 根据 rootSessionId 的最后一位字符（尾号）和 appType 匹配配置，用于流量分桶实验
     *
     * @param rootSessionId 根会话 ID
     * @param appType       应用类型
     * @return 默认概率，如果不匹配任何配置则返回 null
     */
    private Double getDefaultProbability(String rootSessionId, String appType) {
        // 前置校验
        if (CollectionUtils.isEmpty(configItems) || StringUtils.isAnyBlank(rootSessionId) || appType == null) {
            return null;
        }

        // 提取 rootSessionId 的最后一个字符作为尾号，用于流量分桶
        String tail = rootSessionId.substring(rootSessionId.length() - 1);

        // 遍历配置项，查找同时匹配 appType 和尾号的配置
        for (RootSessionIdTailConfigItem item : configItems) {
            if (item.getAppType() != null && item.getTail() != null) {
                if (item.getAppType().contains(appType) && item.getTail().contains(tail)) {
                    return item.getConfig().get("default_probability");
                }
            }

        }

        // 未匹配到任何配置
        return null;
    }

}
