# PlCommonService.java

    package cloud.wconnect.finance.service.pl;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.finance.cons.pl.PlErrorI18nConstant;
    import cloud.wconnect.finance.cons.pl.RevFormulaTypeEnum;
    import cloud.wconnect.finance.entity.pl.M3PlLayout;
    import cloud.wconnect.finance.entity.pl.M3Row;
    import cloud.wconnect.finance.pojo.dto.pl.M3VariableWithFormulaWrapperDTO;
    import cloud.wconnect.finance.pojo.dto.pl.RevFormulaDTO;
    import cloud.wconnect.finance.pojo.dto.pl.index.IndexCalculationDTO;
    import cloud.wconnect.finance.pojo.dto.pl.index.RowIndexCalculationDTO;
    import com.ezylang.evalex.Expression;
    import com.ezylang.evalex.config.ExpressionConfiguration;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Maps;
    import com.google.common.collect.Sets;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.collections.MapUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.redisson.api.RBucket;
    import org.redisson.api.RedissonClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    import java.math.BigDecimal;
    import java.math.MathContext;
    import java.math.RoundingMode;
    import java.time.Duration;
    import java.time.LocalDate;
    import java.time.temporal.TemporalAdjusters;
    import java.util.*;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import java.util.stream.Collectors;

    /**
     * P&L功能公共方法业务层
     *
     * @author lichen
     * @date 2025/9/25
     */
    @Slf4j
    @Service
    public class PlCommonService {

        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private RedissonClient redissonClient;

        //M3数据处理状态的前缀
        public static final String KEY_PREFIX_M3_DATA_HANDLE_STATUS = "wconnect:property:pl:m3-data-status:";
        /**
         * 功能是匹配花括号{}及其内部内容：\\{匹配左花括号，([^}]+)捕获一个或多个非右花括号字符，}匹配右花括号。常用于提取被花括号包围的文本内容。
         */
        public static final Pattern CURLY_BRACES_PATTERN = Pattern.compile("\\{([^}]+)}");

        /**
         * 判断依赖项是否为 数字:数字 格式
         *
         * @param dependency 依赖项
         * @return boolean
         * @author lichen
         * @date 2025/9/25
         */
        public boolean isValidRowFormat(String dependency) {
            return dependency.matches("\\d+:\\d+");
        }

        /**
         * 判断依赖项是否为 [xxx] 格式
         *
         * @param dependency 依赖项
         * @return boolean
         * @author lichen
         * @date 2025/9/25
         */
        public boolean isValidBracketFormat(String dependency) {
            return dependency.matches("\\[[^]]+]");
        }

        /**
         * 解析Rev指标中用到的RowNumber
         *
         * @param revFormula rev指标公式
         * @return java.util.List<java.lang.String> 集合中的元素，例如：1:11
         * @author lichen
         * @date 2025/9/25
         */
        public List<String> parseRevUsedRowNumber(String revFormula) {
            RevFormulaDTO revFormulaDTO = JsonUtils.convertStringToObject(objectMapper, revFormula, RevFormulaDTO.class);
            ArrayList<Integer> typeList = Lists.newArrayList(RevFormulaTypeEnum.SUBTRACTION.getType(), RevFormulaTypeEnum.DIVISION.getType());
            if (typeList.contains(revFormulaDTO.getType())) {
                //Rev指标公式
                String formula = revFormulaDTO.getValue();
                //解析公式中用到的RowNumber
                return parseFormulaUsedRowNumber(formula);
            } else {
                return Collections.emptyList();
            }
        }

        /**
         * 解析指标公式中用到的RowNumber
         *
         * @param formula 指标公式
         * @return java.util.List<java.lang.String> 集合中的元素，例如：1:11
         * @author lichen
         * @date 2025/9/27
         */
        public List<String> parseFormulaUsedRowNumber(String formula) {
            if (StringUtils.isBlank(formula)) return Collections.emptyList();
            ArrayList<String> result = Lists.newArrayList();

            //匹配花括号 {} 及其内部
            Matcher matcher = PlCommonService.CURLY_BRACES_PATTERN.matcher(formula);
            while (matcher.find()) {
                //花括号内部内容
                String dependency = matcher.group(1);
                // 正则匹配：”数字:数字“ 格式
                if (isValidRowFormat(dependency)) {
                    result.add(dependency);
                }
            }

            return result.stream().distinct().collect(Collectors.toList());
        }

        /**
         * 获取M3数据的处理状态
         *
         * @param m3DataType m3数据类型
         * @param yearMonth  年月，yyyyMM
         * @param propertyId 资产id
         * @return boolean true:已处理 false:未处理
         * @author lichen
         * @date 2025/10/20
         */
        public boolean getM3DataHandleStatus(String m3DataType, String yearMonth, Integer propertyId) {
            ArrayList<String> list = Lists.newArrayList(m3DataType, yearMonth, propertyId.toString());
            String keySuffix = StringUtils.join(list, ":");
            //缓存key
            String key = KEY_PREFIX_M3_DATA_HANDLE_STATUS + keySuffix;

            RBucket<Integer> bucket = redissonClient.getBucket(key);
            Integer status = bucket.get();
            return Objects.nonNull(status);
        }

        /**
         * 设置M3数据已处理
         *
         * @param m3DataType m3数据类型
         * @param yearMonth  年月，yyyyMM
         * @param propertyId 资产id
         * @author lichen
         * @date 2025/10/20
         */
        public void setM3DataAlreadyHandled(String m3DataType, String yearMonth, Integer propertyId) {
            //获取当前日期
            LocalDate now = LocalDate.now();
            //根据now获取最后一天
            LocalDate lastDay = now.with(TemporalAdjusters.lastDayOfMonth());
            //计算日期差
            long days = DateTimeUtils.numberOfDays(now, lastDay);

            ArrayList<String> list = Lists.newArrayList(m3DataType, yearMonth, propertyId.toString());
            String keySuffix = StringUtils.join(list, ":");
            //缓存key
            String key = KEY_PREFIX_M3_DATA_HANDLE_STATUS + keySuffix;

            RBucket<Integer> bucket = redissonClient.getBucket(key);
            //设置为已处理
            bucket.set(1, Duration.ofDays(days));
        }

        /**
         * 使用Kahn算法对布局元素进行拓扑排序
         *
         * @param elements          待排序的布局元素列表
         * @param m3VariableWithFormulaWrapperDTOList 元素依赖项中的变量集合
         * @return java.util.List<cloud.wconnect.finance.entity.pl.M3Row>
         * @author lichen
         * @date 2025/9/27
         */
        public List<M3Row> topologySort(List<M3Row> elements, List<M3VariableWithFormulaWrapperDTO> m3VariableWithFormulaWrapperDTOList) {
            if (elements == null || elements.isEmpty()) {
                return new ArrayList<>();
            }

            //替换公式中的公有变量
            if (CollectionUtils.isNotEmpty(m3VariableWithFormulaWrapperDTOList)) {
                for (M3Row element : elements) {
                    String formula = element.getFormula();
                    if (formula != null && !formula.trim().isEmpty()) {
                        String expandedFormula = replaceVariable(m3VariableWithFormulaWrapperDTOList, formula);
                        element.setFormula(expandedFormula);
                    }
                }
            }

            // 构建元素映射表
            Map<String, M3Row> elementMap = new HashMap<>();
            for (M3Row element : elements) {
                elementMap.put(element.getRowNumber(), element);
            }

            // 构建依赖图和入度统计
            Map<String, Set<String>> dependencies = new HashMap<>(); // 依赖关系：key依赖于value中的元素
            Map<String, Integer> inDegree = new HashMap<>(); // 入度统计

            // 初始化所有元素的入度为0
            for (M3Row element : elements) {
                inDegree.put(element.getRowNumber(), 0);
                dependencies.put(element.getRowNumber(), new HashSet<>());
            }

            // 解析公式中的依赖关系
            Pattern pattern = Pattern.compile("\\{([^}]+)}");
            for (M3Row element : elements) {
                String formula = element.getFormula();
                if (formula != null && !formula.trim().isEmpty()) {
                    Matcher matcher = pattern.matcher(formula);
                    while (matcher.find()) {
                        //获取依赖项
                        String dependency = matcher.group(1).trim();

                        if (elementMap.containsKey(dependency)) {// 检查依赖项是否存在于元素列表中
                            dependencies.get(element.getRowNumber()).add(dependency);
                            inDegree.put(element.getRowNumber(), inDegree.get(element.getRowNumber()) + 1);
                        }
                    }
                }
            }

            // Kahn算法实现
            List<List<M3Row>> result = new ArrayList<>();
            Queue<String> queue = new LinkedList<>();

            // 找到所有入度为0的元素（基础元素或无依赖的元素）
            for (Map.Entry<String, Integer> entry : inDegree.entrySet()) {
                if (entry.getValue() == 0) {
                    queue.offer(entry.getKey());
                }
            }

            while (!queue.isEmpty()) {
                List<M3Row> currentBatch = new ArrayList<>();
                int batchSize = queue.size();

                // 处理当前批次的所有元素
                for (int i = 0; i < batchSize; i++) {
                    String currentId = queue.poll();
                    M3Row currentElement = elementMap.get(currentId);
                    currentBatch.add(currentElement);

                    // 更新依赖于当前元素的其他元素的入度
                    for (M3Row element : elements) {
                        if (dependencies.get(element.getRowNumber()).contains(currentId)) {
                            int newInDegree = inDegree.get(element.getRowNumber()) - 1;
                            inDegree.put(element.getRowNumber(), newInDegree);

                            // 如果入度变为0，加入下一批次的处理队列
                            if (newInDegree == 0) {
                                queue.offer(element.getRowNumber());
                            }
                        }
                    }
                }

                if (!currentBatch.isEmpty()) {
                    result.add(currentBatch);
                }
            }

            // 检查是否存在循环依赖
            int totalProcessed = result.stream().mapToInt(List::size).sum();
            if (totalProcessed != elements.size()) {
                //检测到循环依赖，无法完成拓扑排序
                throw new BadRequestException(PlErrorI18nConstant.LAYOUT_FILE_HAS_CIRCLE_DEPENDENCY,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "Layout.xls file has circle dependency");
            }

            return result.stream().flatMap(List::stream).toList();
        }

        /**
         * 替换公式中的公有变量
         *
         * @param m3VariableWithFormulaWrapperDTOList 公有变量集合
         * @param formula           公式
         * @return java.lang.String
         * @author lichen
         * @date 2025/9/30
         */
        private static String replaceVariable(List<M3VariableWithFormulaWrapperDTO> m3VariableWithFormulaWrapperDTOList, String formula) {
            String expandedFormula = formula;
            // 使用正则表达式安全替换变量引用
            for (M3VariableWithFormulaWrapperDTO dto : m3VariableWithFormulaWrapperDTOList) {
                String variableName = dto.getName();
                String variableValue = dto.getValue();
                // 转义特殊字符并构建精确匹配模式
                String escapedVariableName = Pattern.quote(variableName);
                // 使用正则表达式进行精确替换
                expandedFormula = expandedFormula.replaceAll(escapedVariableName, variableValue);
            }
            return expandedFormula;
        }

        /**
         * 计算指标
         *
         * @param rowNumber                         当前计算的RowNumber
         * @param formula                           公式
         * @param indexType                         待计算的指标类型
         * @param alreadyCalculationRowIndexDTOList 已经计算过的指标结果集合
         * @param indexNameId                       指标名称id,只有计算Forecast的指标时，才用到此字段
         * @return cloud.wconnect.finance.pojo.dto.pl.index.IndexCalculationDTO
         * @author lichen
         * @date 2025/9/27
         */
        public IndexCalculationDTO calculationIndex(String rowNumber, String formula, String indexType, ArrayList<RowIndexCalculationDTO> alreadyCalculationRowIndexDTOList, String indexNameId) {
            if (StringUtils.isBlank(formula)) {
                return IndexCalculationDTO.fail(indexType, indexNameId, Lists.newArrayList(rowNumber));
            }
            //1. 解析公式，获取使用到的RowNumber
            List<String> rowNumberList = parseFormulaUsedRowNumber(formula);
            //2. 公式中使用的RowNumber对应的指标计算结果
            Map<String, List<IndexCalculationDTO>> rowNumber2IndexCalculationDTOS = alreadyCalculationRowIndexDTOList.stream()
                    .filter(item -> rowNumberList.contains(item.getRowNumber()))
                    .collect(Collectors.toMap(RowIndexCalculationDTO::getRowNumber, RowIndexCalculationDTO::getIndexCalculationList));

            //失败的计算因子集合
            ArrayList<String> failFactorList = Lists.newArrayListWithCapacity(rowNumberList.size());
            //未就绪的计算因子
            ArrayList<String> notReadyRowNumberList = Lists.newArrayListWithCapacity(rowNumberList.size());
            //计算因子以及对应的值
            HashMap<String, String> rowNumber2Value = Maps.newHashMapWithExpectedSize(rowNumberList.size());
            //2. 筛选出未就绪的计算因子
            if (MapUtils.isNotEmpty(rowNumber2IndexCalculationDTOS)) {
                for (Map.Entry<String, List<IndexCalculationDTO>> entry : rowNumber2IndexCalculationDTOS.entrySet()) {
                    //rowNumber
                    String key = entry.getKey();
                    //rowNumber对应的指标计算结果
                    List<IndexCalculationDTO> value = entry.getValue();
                    //计算因子指定指标的计算结果
                    IndexCalculationDTO indexCalculationDTO = value.stream()
                            .filter(item -> {
                                if (StringUtils.isNotBlank(indexNameId)) {
                                    return item.getIndexType().equalsIgnoreCase(indexType) && item.getIndexNameId().equalsIgnoreCase(indexNameId);
                                } else {
                                    return item.getIndexType().equalsIgnoreCase(indexType);
                                }
                            })
                            .findFirst().orElse(null);
                    if (Objects.isNull(indexCalculationDTO)) {
                        //计算因子的指标未计算时，加入未就绪的计算因子
                        notReadyRowNumberList.add(key);
                        failFactorList.add(key);
                    } else {
                        if (!indexCalculationDTO.isCalculationResult()) {
                            //指标计算失败时，加入未就绪的计算因子
                            notReadyRowNumberList.add(key);
                            //传递失败原因
                            if (CollectionUtils.isNotEmpty(indexCalculationDTO.getFailFactorList())) {
                                failFactorList.addAll(indexCalculationDTO.getFailFactorList());
                            }
                        } else {
                            //计算成功
                            rowNumber2Value.put(key, indexCalculationDTO.getIndexValue().toString());
                        }
                    }
                }
            } else {
                notReadyRowNumberList.addAll(rowNumberList);
                failFactorList.addAll(rowNumberList);
            }

            //3. 判断计算因子是否已就绪，不就绪时停止计算，记录失败原因
            if (CollectionUtils.isNotEmpty(notReadyRowNumberList)) {
                return IndexCalculationDTO.fail(indexType, indexNameId, failFactorList);
            }

            StringBuilder finalFormula = new StringBuilder();
            //4. try-catch计算公式
            try {
                Matcher matcher = CURLY_BRACES_PATTERN.matcher(formula);
                while (matcher.find()) {
                    String match = matcher.group(1).trim();
                    String replacement = rowNumber2Value.getOrDefault(match, match);
                    matcher.appendReplacement(finalFormula, replacement);
                }
                matcher.appendTail(finalFormula);

                // MathContext.精度(Precision): 指的是有效数字的总位数，而不是小数点后的位数
                MathContext mathContext = new MathContext(BaseConstant.DATA_PRECISION + 4, RoundingMode.HALF_UP);
                ExpressionConfiguration configuration = ExpressionConfiguration.builder()
                        .mathContext(mathContext)
                        .build();
                Expression expression = new Expression(finalFormula.toString(), configuration);
                BigDecimal numberValue = expression.evaluate().getNumberValue();
                return IndexCalculationDTO.success(indexType, numberValue.setScale(BaseConstant.DATA_PRECISION, RoundingMode.HALF_UP), indexNameId);
            } catch (Exception e) {
                if (e.getMessage().equals("Division by zero")) {
                    //分母是0时，认为计算成功，指标结果为0
                    return IndexCalculationDTO.success(indexType, BigDecimal.ZERO, indexNameId);
                } else {
                    log.error("rowNumber:{}, indexType:{}, formula: {}, error: {}", indexType, rowNumber, finalFormula, e.getMessage());
                    log.error(e.getMessage(), e);
                    //根据公式计算失败
                    return IndexCalculationDTO.fail(indexType, indexNameId, Collections.singletonList(rowNumber));
                }
            }
        }

        /**
         * layouts拓扑排序
         *
         * @param layouts Layout集合
         * @return java.util.List<cloud.wconnect.finance.entity.pl.M3PlLayout>
         * @author lichen
         * @date 2025/9/27
         */
        public List<M3PlLayout> topologySortForLayout(List<M3PlLayout> layouts) {
            if (layouts == null || layouts.isEmpty()) {
                return new ArrayList<>();
            }

            // 构建元素映射表
            Map<String, M3PlLayout> elementMap = new HashMap<>();
            for (M3PlLayout element : layouts) {
                elementMap.put(element.getRowNumber(), element);
            }

            // 构建依赖图和入度统计
            Map<String, Set<String>> dependencies = new HashMap<>(); // 依赖关系：key依赖于value中的元素
            Map<String, Integer> inDegree = new HashMap<>(); // 入度统计

            // 初始化所有元素的入度为0
            for (M3PlLayout element : layouts) {
                inDegree.put(element.getRowNumber(), 0);
                dependencies.put(element.getRowNumber(), new HashSet<>());
            }

            for (M3PlLayout element : layouts) {
                //% Rev 减法公式中依赖的RowNumber
                RevFormulaDTO revFormulaDTO = JsonUtils.convertStringToObject(objectMapper, element.getRevFormula(), RevFormulaDTO.class);
                if (revFormulaDTO.getType() == RevFormulaTypeEnum.SUBTRACTION.getType()) {
                    List<String> rowNumberList = parseRevUsedRowNumber(element.getRevFormula());
                    HashSet<String> dependencyList = Sets.newHashSet();
                    dependencyList.addAll(rowNumberList);

                    for (String dependency : dependencyList) {
                        // 检查依赖项是否存在于元素列表中
                        if (elementMap.containsKey(dependency)) {
                            dependencies.get(element.getRowNumber()).add(dependency);
                            inDegree.put(element.getRowNumber(), inDegree.get(element.getRowNumber()) + 1);
                        }
                    }
                }
            }

            // Kahn算法实现
            List<List<M3PlLayout>> result = new ArrayList<>();
            Queue<String> queue = new LinkedList<>();

            // 找到所有入度为0的元素（基础元素或无依赖的元素）
            for (Map.Entry<String, Integer> entry : inDegree.entrySet()) {
                if (entry.getValue() == 0) {
                    queue.offer(entry.getKey());
                }
            }

            while (!queue.isEmpty()) {
                List<M3PlLayout> currentBatch = new ArrayList<>();
                int batchSize = queue.size();

                // 处理当前批次的所有元素
                for (int i = 0; i < batchSize; i++) {
                    String currentId = queue.poll();
                    M3PlLayout currentElement = elementMap.get(currentId);
                    currentBatch.add(currentElement);

                    // 更新依赖于当前元素的其他元素的入度
                    for (M3PlLayout element : layouts) {
                        if (dependencies.get(element.getRowNumber()).contains(currentId)) {
                            int newInDegree = inDegree.get(element.getRowNumber()) - 1;
                            inDegree.put(element.getRowNumber(), newInDegree);

                            // 如果入度变为0，加入下一批次的处理队列
                            if (newInDegree == 0) {
                                queue.offer(element.getRowNumber());
                            }
                        }
                    }
                }

                if (!currentBatch.isEmpty()) {
                    result.add(currentBatch);
                }
            }

            // 检查是否存在循环依赖
            int totalProcessed = result.stream().mapToInt(List::size).sum();
            if (totalProcessed != layouts.size()) {
                //检测到循环依赖，无法完成拓扑排序
                throw new BadRequestException(PlErrorI18nConstant.LAYOUT_CONFIG_HAS_CIRCLE_DEPENDENCY,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "Layout config has circle dependency");
            }

            return result.stream().flatMap(List::stream).toList();
        }

        /**
         * 指标计算
         *
         * @param rowNumber                    Row编号
         * @param formula                      公式
         * @param variable2IndexCalculationDTO 公式中的变量对应的计算结果
         * @param variable2RowNumber           公式中的变量对应的rowNumber
         * @return cloud.wconnect.finance.pojo.dto.pl.index.IndexCalculationDTO
         * @author lichen
         * @date 2025/10/1
         */
        public IndexCalculationDTO calculationIndex(String rowNumber, String indexType, String formula,
                                                    Map<String, IndexCalculationDTO> variable2IndexCalculationDTO,
                                                    Map<String, String> variable2RowNumber) {
            if (StringUtils.isBlank(formula)) {
                return IndexCalculationDTO.fail(indexType, "", Collections.singletonList(rowNumber));
            }

            StringBuilder finalFormula = new StringBuilder();

            HashSet<String> failFactorList = Sets.newHashSetWithExpectedSize(variable2RowNumber.size());

            //正则匹配公式中的变量，并判断变量是否符合要求
            Pattern pattern = Pattern.compile("[a-z]");
            Matcher matcher = pattern.matcher(formula);
            while (matcher.find()) {
                String variable = matcher.group();

                //获取变量的计算结果
                IndexCalculationDTO indexCalculationDTO = variable2IndexCalculationDTO.get(variable);
                if (Objects.isNull(indexCalculationDTO) || !indexCalculationDTO.isCalculationResult()) {
                    failFactorList.add(variable2RowNumber.get(variable));
                    continue;
                }
                BigDecimal indexValue = indexCalculationDTO.getIndexValue();
                matcher.appendReplacement(finalFormula, indexValue.toString());
            }

            if (CollectionUtils.isNotEmpty(failFactorList)) {
                return IndexCalculationDTO.fail(indexType, "", Lists.newArrayList(failFactorList));
            }

            //4. try-catch计算公式
            try {
                // MathContext.精度(Precision): 指的是有效数字的总位数，而不是小数点后的位数
                MathContext mathContext = new MathContext(BaseConstant.DATA_PRECISION + 4, RoundingMode.HALF_UP);
                ExpressionConfiguration configuration = ExpressionConfiguration.builder()
                        .mathContext(mathContext)
                        .build();
                Expression expression = new Expression(finalFormula.toString(), configuration);
                BigDecimal numberValue = expression.evaluate().getNumberValue();
                return IndexCalculationDTO.success(indexType, numberValue.setScale(BaseConstant.DATA_PRECISION, RoundingMode.HALF_UP), "");
            } catch (Exception e) {
                if (e.getMessage().equals("Division by zero")) {
                    //分母是0时，认为计算成功，指标结果为0
                    return IndexCalculationDTO.success(indexType, BigDecimal.ZERO, "");
                } else {
                    log.error("rowNumber:{}, indexType:{}, formula: {}, error: {}", indexType, rowNumber, finalFormula, e.getMessage());
                    log.error(e.getMessage(), e);
                    //根据公式计算失败
                    return IndexCalculationDTO.fail(indexType, "", Collections.singletonList(rowNumber));
                }
            }
        }

    }