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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 把 FunnelContext 序列化成一条 SLS 日志（8 个 step JSON 字段 + filter reasons）。
 */
public class FunnelAggregator {

    public static Map<String, String> toLogItem(FunnelContext ctx) {
        Map<String, String> row = new LinkedHashMap<>();
        if (ctx == null) return row;

        // base
        row.put("traceId", StringUtils.defaultString(ctx.getTraceId()));
        row.put("recommendTraceId", StringUtils.defaultString(ctx.getRecommendTraceId()));
        row.put("sessionId", StringUtils.defaultString(ctx.getSessionId()));
        row.put("subSessionId", StringUtils.defaultString(ctx.getSubSessionId()));
        row.put("rootSessionId", StringUtils.defaultString(ctx.getRootSessionId()));
        row.put("mid", StringUtils.defaultString(ctx.getMid()));
        row.put("uid", StringUtils.defaultString(ctx.getUid()));
        row.put("appType", String.valueOf(ctx.getAppType()));
        row.put("newExpGroup", StringUtils.defaultString(ctx.getNewExpGroup()));
        row.put("abExpCode", JSON.toJSONString(ctx.getAbExpCodes()));

        // 固定字段: 风险/地域上下文
        row.put("scenetype", ctx.getHotSceneType() == null ? "" : String.valueOf(ctx.getHotSceneType()));
        row.put("province", StringUtils.defaultString(ctx.getProvince()));
        row.put("city", StringUtils.defaultString(ctx.getCity()));
        row.put("if_new_user", String.valueOf(ctx.isIfNewUser()));
        row.put("if_risk_mid", String.valueOf(ctx.isIfRiskMid()));
        row.put("if_risk_uid", String.valueOf(ctx.isIfRiskUid()));

        // step 1-3 + filter reasons
        row.put("step_1_recall", JSON.toJSONString(buildStep1(ctx)));
        row.put("step_2_filtered", JSON.toJSONString(buildStep2(ctx)));
        row.put("step_2_filter_reasons", JSON.toJSONString(buildStep2Reasons(ctx)));
        row.put("step_3_truncated", JSON.toJSONString(buildStep3(ctx)));

        // step 4-7 (step_8 已并入 step_7 — 冷启融合后即最终输出)
        row.put("step_4_merged", JSON.toJSONString(buildStep4(ctx)));
        row.put("step_5_ranked", JSON.toJSONString(buildStep5(ctx)));
        row.put("step_6_rank_truncated", JSON.toJSONString(buildStep6(ctx)));
        row.put("step_7_cold_start", JSON.toJSONString(buildStep7(ctx)));

        return row;
    }

    // ===== step 1-3: {pushFrom: [...]} =====
    private static Map<String, JSONArray> buildStep1(FunnelContext ctx) {
        Map<String, JSONArray> out = new LinkedHashMap<>();
        ctx.getStages123RecallByStrategy().forEach((pf, entries) -> {
            JSONArray arr = new JSONArray();
            for (RecallVideoEntry e : entries) {
                JSONObject o = new JSONObject();
                o.put("vid", e.getVideoId());
                o.put("index", displayIndex(e.getIndex()));
                o.put("score", round6(e.getScore()));
                arr.add(o);
            }
            out.put(pf, arr);
        });
        return out;
    }

    private static Map<String, JSONArray> buildStep2(FunnelContext ctx) {
        Map<String, JSONArray> out = new LinkedHashMap<>();
        ctx.getStages123RecallByStrategy().forEach((pf, entries) -> {
            JSONArray arr = new JSONArray();
            for (RecallVideoEntry e : entries) {
                if (!e.isFilteredIn()) continue;
                JSONObject o = new JSONObject();
                o.put("vid", e.getVideoId());
                o.put("index", displayIndex(e.getIndex()));
                o.put("score", round6(e.getScore()));
                o.put("index_new", displayIndex(e.getIndexNewAfterFilter()));
                arr.add(o);
            }
            out.put(pf, arr);
        });
        return out;
    }

    private static Map<String, JSONArray> buildStep2Reasons(FunnelContext ctx) {
        Map<String, JSONArray> out = new LinkedHashMap<>();
        ctx.getStages123RecallByStrategy().forEach((pf, entries) -> {
            JSONArray arr = new JSONArray();
            for (RecallVideoEntry e : entries) {
                if (e.isFilteredIn()) continue;
                JSONObject o = new JSONObject();
                o.put("vid", e.getVideoId());
                o.put("index", displayIndex(e.getIndex()));
                o.put("score", round6(e.getScore()));
                o.put("reasons", e.getFilterReasons());
                arr.add(o);
            }
            if (!arr.isEmpty()) out.put(pf, arr);
        });
        return out;
    }

    private static Map<String, JSONArray> buildStep3(FunnelContext ctx) {
        Map<String, JSONArray> out = new LinkedHashMap<>();
        ctx.getStages123RecallByStrategy().forEach((pf, entries) -> {
            JSONArray arr = new JSONArray();
            for (RecallVideoEntry e : entries) {
                if (!e.isFilteredIn() || e.getSelect() == null) continue;
                JSONObject o = new JSONObject();
                o.put("vid", e.getVideoId());
                o.put("index", displayIndex(e.getIndex()));
                o.put("score", round6(e.getScore()));
                o.put("index_new", displayIndex(e.getIndexNewAfterFilter()));
                o.put("select", e.getSelect().name().toLowerCase());
                o.put("truncate", e.getTruncate());
                arr.add(o);
            }
            out.put(pf, arr);
        });
        return out;
    }

    // ===== step 4-8: 按 vid 组织，每条带完整 recalls 历史 =====
    private static JSONArray buildStep4(FunnelContext ctx) {
        JSONArray arr = new JSONArray();
        for (Long vid : ctx.getStep4MergedVideoIds()) {
            JSONObject o = new JSONObject();
            o.put("vid", vid);
            o.put("attributedPushFrom", ctx.getStep4MergedAttribution().get(vid));
            o.put("recalls", buildRecallsForVid(ctx, vid));
            arr.add(o);
        }
        return arr;
    }

    private static JSONArray buildStep5(FunnelContext ctx) {
        JSONArray arr = new JSONArray();
        int rankIdx = 0;
        for (Long vid : ctx.getStep4MergedVideoIds()) {
            RankVideoEntry r = ctx.getStep5RankedData().get(vid);
            if (r == null) continue;
            rankIdx++;
            JSONObject o = new JSONObject();
            o.put("vid", vid);
            o.put("attributedPushFrom", ctx.getStep4MergedAttribution().get(vid));
            o.put("recalls", buildRecallsForVid(ctx, vid));
            o.put("rank", buildRankBlock(r));
            o.put("rank_index", rankIdx);
            arr.add(o);
        }
        return arr;
    }

    /** 排序截断: rovRecallRank 的前 size 条（不含冷启）— 给 BI 做"不冷启会是谁"的参考 */
    private static JSONArray buildStep6(FunnelContext ctx) {
        JSONArray arr = new JSONArray();
        for (int i = 0; i < ctx.getStep6RankTruncatedVideoIds().size(); i++) {
            long vid = ctx.getStep6RankTruncatedVideoIds().get(i);
            JSONObject o = new JSONObject();
            o.put("vid", vid);
            o.put("attributedPushFrom", ctx.getStep4MergedAttribution().get(vid));
            o.put("recalls", buildRecallsForVid(ctx, vid));
            RankVideoEntry r = ctx.getStep5RankedData().get(vid);
            if (r != null) o.put("rank", buildRankBlock(r));
            o.put("rank_index", i + 1);
            arr.add(o);
        }
        return arr;
    }

    /** 冷启融合后的最终输出（实际下发列表）— 用 step8OutputVideoIds */
    private static JSONArray buildStep7(FunnelContext ctx) {
        JSONArray arr = new JSONArray();
        for (int i = 0; i < ctx.getStep8OutputVideoIds().size(); i++) {
            long vid = ctx.getStep8OutputVideoIds().get(i);
            JSONObject o = new JSONObject();
            o.put("vid", vid);
            o.put("attributedPushFrom", ctx.getStep4MergedAttribution().get(vid));
            o.put("recalls", buildRecallsForVid(ctx, vid));
            RankVideoEntry r = ctx.getStep5RankedData().get(vid);
            if (r != null) o.put("rank", buildRankBlock(r));
            ColdStartAction action = ctx.getStep7ColdStartActions().getOrDefault(vid, ColdStartAction.NONE);
            o.put("if_cold_start", action != ColdStartAction.NONE);
            o.put("cold_start_action", action.name());
            o.put("rank_index", i + 1);
            arr.add(o);
        }
        return arr;
    }

    // ===== helpers =====
    private static JSONArray buildRecallsForVid(FunnelContext ctx, long vid) {
        JSONArray recalls = new JSONArray();
        ctx.getStages123RecallByStrategy().forEach((pf, entries) -> {
            for (RecallVideoEntry e : entries) {
                if (e.getVideoId() != vid) continue;
                if (e.getSelect() == null) continue;   // 列出该 vid 在该路 top-recallNum 名次内的命中（self + other）
                JSONObject r = new JSONObject();
                r.put("strategy", pf);
                r.put("index", displayIndex(e.getIndex()));
                r.put("score", round6(e.getScore()));
                r.put("index_new", displayIndex(e.getIndexNewAfterFilter()));
                r.put("select", e.getSelect().name().toLowerCase());
                r.put("truncate", e.getTruncate());
                recalls.add(r);
                break;
            }
        });
        return recalls;
    }

    private static JSONObject buildRankBlock(RankVideoEntry r) {
        JSONObject o = new JSONObject();
        o.put("score", round6(r.getRankScore()));
        if (MapUtils.isNotEmpty(r.getSubScores())) {
            r.getSubScores().forEach((k, v) -> o.put(k, round6(v)));
        }
        return o;
    }

    /** double → 保留 6 位小数 (NaN / Infinity 兜底为 0，避免 BigDecimal.valueOf 抛 NumberFormatException) */
    private static BigDecimal round6(double v) {
        if (Double.isNaN(v) || Double.isInfinite(v)) {
            return BigDecimal.ZERO.setScale(6);
        }
        return BigDecimal.valueOf(v).setScale(6, RoundingMode.HALF_UP);
    }

    /** 内部 0-based 位置 → 对外展示 1-based。负值（如 -1 表"未通过"）原样返回 */
    private static int displayIndex(int zeroBased) {
        return zeroBased < 0 ? zeroBased : zeroBased + 1;
    }
}
