package com.tzld.piaoquan.ad.engine.commons.helper;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.tzld.piaoquan.ad.engine.commons.redis.AlgorithmRedisHelper;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.Map;

@Slf4j
@Component
public class ModelUserLayerDataHelper {
    @Autowired
    protected AlgorithmRedisHelper algRedisHelper;

    private static final String redisKey = "ad:engine:strategy:model_ctcvr_calibration_v1";
    private static final String keyFormat = "%s:%s:%s:%s:%s:%s:%s";
    private static final String SUM = "sum";
    /** 曝光大于该阈值才使用当前层校准，否则回退下一层 */
    private static final double MIN_CALIBRATION_EXPOSURE = 5000d;

    private volatile static Map<String, String> dataMap = Collections.emptyMap();

    // 服务启动时初始化数据
    @PostConstruct
    public void init() {
        Map<String, String> map = updateDataMap();
        if (map != null) {
            dataMap = Collections.unmodifiableMap(map);
        }
    }

    // 每5分钟更新一次数据
    @Scheduled(fixedRate = 5 * 60 * 1000)
    public void scheduledUpdate() {
        Map<String, String> map = updateDataMap();
        if (map != null) {
            dataMap = Collections.unmodifiableMap(map);
        }
    }

    /**
     * 按粒度回退取校准数据，并返回命中的校准层：
     * l1=agent+customer 粒度，l2=profession 粒度，l3=layer 粒度。
     * 当前层曝光必须大于 {@link #MIN_CALIBRATION_EXPOSURE}，否则回退下一层。
     * key/value 均以 ':' 分隔；
     * value: exposure_cnt:conversion_cnt:pred_conversion_cnt:real_ctcvr:pred_ctcvr:copc
     */
    public static CalibrationData getCopcWithLayer(String model, String landingPageType, String targetingConversion,
                                                   String layer, String profession, String agentId, String customerId) {
        if (null != dataMap) {
            model = nullToSum(model);
            landingPageType = nullToSum(landingPageType);
            targetingConversion = nullToSum(targetingConversion);
            layer = nullToSum(layer);
            profession = nullToSum(profession);
            agentId = nullToSum(agentId);
            customerId = nullToSum(customerId);

            String key = String.format(keyFormat, model, landingPageType, targetingConversion, layer, profession, agentId, customerId);
            CalibrationData data = parseValue(dataMap.get(key));
            if (isExposureEnough(data)) {
                data.setLayer("l1");
                return data;
            }

            key = String.format(keyFormat, model, landingPageType, targetingConversion, layer, profession, SUM, SUM);
            data = parseValue(dataMap.get(key));
            if (isExposureEnough(data)) {
                data.setLayer("l2");
                return data;
            }

            key = String.format(keyFormat, model, landingPageType, targetingConversion, layer, SUM, SUM, SUM);
            data = parseValue(dataMap.get(key));
            if (isExposureEnough(data)) {
                data.setLayer("l3");
                return data;
            }
        }
        return null;
    }

    private static boolean isExposureEnough(CalibrationData data) {
        return data != null && data.getExposureCnt() != null && data.getExposureCnt() > MIN_CALIBRATION_EXPOSURE;
    }

    private static CalibrationData parseValue(String value) {
        if (value == null || value.trim().isEmpty()) {
            return null;
        }
        try {
            String[] parts = value.split(":");
            if (parts.length != 6) {
                log.warn("parse model layer calib value failed, expect 6 parts, value={}", value);
                return null;
            }

            CalibrationData data = new CalibrationData();
            data.setExposureCnt(Double.parseDouble(parts[0].trim()));
            data.setConversionCnt(Double.parseDouble(parts[1].trim()));
            data.setPredConversionCnt(Double.parseDouble(parts[2].trim()));
            data.setRealCtcvr(Double.parseDouble(parts[3].trim()));
            data.setPredCtcvr(Double.parseDouble(parts[4].trim()));
            data.setCopc(Double.parseDouble(parts[5].trim()));
            if (data.getCopc().isNaN() || data.getCopc().isInfinite()) {
                return null;
            }
            return data;
        } catch (Exception e) {
            log.warn("parse model layer calib value error, value={}", value, e);
            return null;
        }
    }

    private static String nullToSum(String value) {
        if (value == null || value.isEmpty() || "null".equalsIgnoreCase(value)) {
            return SUM;
        }
        return value;
    }

    /**
     * @return 新数据；失败返回 null，调用方保留旧 dataMap
     */
    private Map<String, String> updateDataMap() {
        try {
            String value = algRedisHelper.get(redisKey);

            if (value == null || value.trim().isEmpty()) {
                log.warn("update model layer copc skipped: redis value is empty, keep old dataMap size={}", dataMap.size());
                return null;
            }

            Map<String, String> newDataMap = JSON.parseObject(
                    value,
                    new TypeReference<Map<String, String>>() {}
            );

            if (newDataMap == null) {
                log.warn("update model layer copc skipped: parsed map is null, keep old dataMap size={}", dataMap.size());
                return null;
            }

            log.info("update model layer copc success size={}", newDataMap.size());
            return newDataMap;
        } catch (Exception e) {
            log.error("update model layer copc error, keep old dataMap size={}", dataMap.size(), e);
            return null;
        }
    }

    @Data
    public static class CalibrationData {
        private Double exposureCnt;
        private Double conversionCnt;
        private Double predConversionCnt;
        private Double realCtcvr;
        private Double predCtcvr;
        private Double copc;
        /** 命中校准层：l1 / l2 / l3 */
        private String layer;
    }
}
