package com.njcn.gather.icd.mapping.component;

import com.njcn.gather.icd.mapping.pojo.dto.IndexBindingCommand;
import com.njcn.gather.icd.mapping.pojo.dto.IndexSelectionGroupCommand;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexAnalysis;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidate;
import com.njcn.gather.icd.mapping.pojo.bo.analysis.IndexCandidateReportItem;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DataSetNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DoiElementNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.DoiNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.FcdaNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.IcdDocument;
import com.njcn.gather.icd.mapping.pojo.bo.icd.LnNode;
import com.njcn.gather.icd.mapping.pojo.bo.icd.ReportControlNode;
import com.njcn.gather.icd.mapping.pojo.bo.state.DataSetSelectionState;
import com.njcn.gather.icd.mapping.pojo.bo.state.ReportAndDataSetState;
import com.njcn.gather.icd.mapping.pojo.bo.state.ReportBindingState;
import com.njcn.gather.icd.mapping.pojo.bo.state.ReportGroupState;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.DataSetGroupItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.DoiItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.InstItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.MappingDocument;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.ReportMapItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.SdiItem;
import com.njcn.gather.icd.mapping.pojo.bo.mapping.TypeItem;
import com.njcn.gather.icd.mapping.pojo.bo.template.DefaultTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

/**
 * 正式映射生成服务。
 *
 * 说明：
 * 1. 本服务不再使用“单报告 + 单字符串选择”的简化模型；
 * 2. 而是按“业务分组 + 多条绑定关系”的结构生成正式 MappingDocument；
 * 3. 生成时只使用用户明确提交的绑定项，不强制要求模板里的所有标签都必须配置。
 */
@Service
public class MappingGenerationService {

    /**
     * 根据 ICD、默认模板和用户索引绑定生成最终映射文档。
     */
    public MappingDocument generate(IcdDocument icdDocument,
                                    DefaultTemplate template,
                                    IndexAnalysis analysis,
                                    List<IndexSelectionGroupCommand> selections,
                                    String version,
                                    String author) {
        ReportAndDataSetState state = buildState(icdDocument, template, analysis, selections);
        MappingDocument document = new MappingDocument();
        // 原 C# 的 Set_Version() 默认写当天日期，而不是 "1.0"
        document.setVersion(resolveVersion(version));
        document.setAuthor(author == null ? "" : author);
        // 顶层字段严格按原 C# 输出规则生成。
        document.setIed(icdDocument.getIedName());
        // LD：只输出前缀，不带数字尾号。
        document.setLd(icdDocument.getLdPrefix());
        // 固定值，保持和原 C# mainFrom -> IcdCfg 一致
        document.setWaveTimeFlag("BeiJing");
        document.setDataType("1");
        document.setUnit("1");

        // 生成 ReportMap
        // 1. 先按 DefaultCfg.ReportList 分组
        // 2. reportCount 写组内总数
        // 3. 组内每个报告单独生成一条 ReportMapItem
        for (ReportGroupState groupState : state.getReportGroups()) {
            if (groupState.getReportItems() == null || groupState.getReportItems().isEmpty()) {
                continue;
            }

            // 原 C# 的 reportCount：<=1 时写 0，大于 1 才写实际数量
            int reportCount = resolveReportCount(groupState.getReportItems().size());

            for (IndexCandidateReportItem reportItem : groupState.getReportItems()) {
                ReportMapItem item = new ReportMapItem();
                item.setDesc(groupState.getGroupDesc());
                item.setReportCount(reportCount);

                // 单个报告自己的 rptID / name
                item.setRptId(resolveSingleRptId(icdDocument, reportItem.getReportName()));
                item.setName(reportItem.getReportName());

                // buffered 按原 C# 转成 BR / RP
                item.setBuffered(resolveBufferedCode(icdDocument, reportItem.getReportName()));

                // inst / Select / TrgOps 直接来自当前分组匹配到的 DefaultCfg.ReportList 项
                item.setInst(resolveReportInst(groupState.getReportInst()));
                item.setSelect(groupState.getSelect());
                item.setTrgOps(resolveTrgOps(groupState.getTrgOps()));

                // 原 C# 当前固定写 0
                item.setFlickerFlag("0");

                document.getReportMap().add(item);
            }
        }

        // 2. 生成 DataSetList
        // DataSetList 按 desc + lnClass 聚合，保持输出顺序稳定。
        Map<String, DataSetGroupItem> dataSetGroupMap = new LinkedHashMap<String, DataSetGroupItem>();
        for (DataSetSelectionState selectionState : state.getDataSetSelections()) {
            if (selectionState.getLnNodes() == null || selectionState.getLnNodes().isEmpty()) {
                continue;
            }

            // 一个绑定现在可能命中多个 LN，需要逐个展开
            for (LnNode lnNode : selectionState.getLnNodes()) {
                if (lnNode == null) {
                    continue;
                }

                DefaultTemplate.LnClassCfgItem lnClassCfg = findLnClassCfg(template, lnNode.getLnClass());
                if (lnClassCfg == null) {
                    continue;
                }

                String groupKey = lnClassCfg.getDesc() + "__" + lnNode.getLnClass();
                DataSetGroupItem groupItem = dataSetGroupMap.get(groupKey);
                if (groupItem == null) {
                    groupItem = new DataSetGroupItem();
                    groupItem.setDesc(lnClassCfg.getDesc());
                    groupItem.setLnClass(lnNode.getLnClass());
                    dataSetGroupMap.put(groupKey, groupItem);
                }

                InstItem instItem = findOrCreateInst(groupItem, selectionState.getLnInst(), selectionState.getLabel());

                // 关键：把当前正在处理的 lnNode 传进去，而不是只用一个旧的 lnNode
                buildDoiItems(instItem, selectionState, lnNode, icdDocument, template);
            }
        }

        document.getDataSetList().addAll(dataSetGroupMap.values());
        return document;
    }

    /**
     * 构建正式生成前的中间态。
     *
     * 中间态把报告分组、用户绑定和命中的 LN 节点拆开保存，避免生成 JSON 时重复查找。
     */
    private ReportAndDataSetState buildState(IcdDocument icdDocument,
                                             DefaultTemplate template,
                                             IndexAnalysis analysis,
                                             List<IndexSelectionGroupCommand> selections) {
        ReportAndDataSetState state = new ReportAndDataSetState();
        state.setIedName(icdDocument.getIedName());
        state.setLdInst(icdDocument.getLdInst());

        if (selections == null) {
            return state;
        }

        for (IndexSelectionGroupCommand selectionGroup : selections) {
            IndexCandidate candidate = findCandidate(analysis, selectionGroup.getGroupKey(), selectionGroup.getGroupDesc());
            if (candidate == null) {
                continue;
            }

            ReportGroupState groupState = new ReportGroupState();
            groupState.setGroupKey(candidate.getGroupKey());
            groupState.setGroupDesc(candidate.getGroupDesc());
            groupState.setReportCount(candidate.getReportCount());

            // 关键修正：
            // 不再在这里重新查 DefaultCfg.ReportList，
            // 直接使用 IndexAnalysisService 已经匹配好的配置项
            groupState.setReportInst(candidate.getReportInst());
            groupState.setSelect(candidate.getSelect());
            groupState.setTrgOps(candidate.getTrgOps());

            groupState.getReportItems().addAll(candidate.getReports());

            if (selectionGroup.getBindings() != null) {
                for (IndexBindingCommand command : selectionGroup.getBindings()) {
                    ReportBindingState bindingState = new ReportBindingState();
                    bindingState.setGroupKey(candidate.getGroupKey());
                    bindingState.setGroupDesc(candidate.getGroupDesc());
                    bindingState.setReportName(command.getReportName());
                    bindingState.setDataSetName(command.getDataSetName());
                    bindingState.setLabel(command.getLabel());
                    bindingState.setLnInst(command.getLnInst());
                    groupState.getBindings().add(bindingState);

                    DataSetSelectionState dataSetSelectionState = new DataSetSelectionState();
                    dataSetSelectionState.setGroupKey(candidate.getGroupKey());
                    dataSetSelectionState.setGroupDesc(candidate.getGroupDesc());
                    dataSetSelectionState.setReportName(command.getReportName());
                    dataSetSelectionState.setDataSetName(command.getDataSetName());
                    dataSetSelectionState.setLabel(command.getLabel());
                    dataSetSelectionState.setLnInst(command.getLnInst());
                    // 旧版本只找一个 LN，容易丢掉整组数据。
                    // 新版本改成收集所有匹配 LN。
                    dataSetSelectionState.getLnNodes().addAll(findMatchedLnNodes(icdDocument, command));
                    state.getDataSetSelections().add(dataSetSelectionState);
                }
            }

            state.getReportGroups().add(groupState);
        }

        return state;
    }

    /**
     * 查找当前绑定命中的所有 LN。
     *
     * 旧逻辑只返回一个 LN，会导致：
     * - MSQI 整组缺失
     * - MHAI 只生成一半
     *
     * 新逻辑：
     * 1. 先从当前 DataSet 的 FCDA 中，找出与 lnInst 相同的所有 lnClass；
     * 2. 再回到 ICD 逻辑节点列表中，收集这些 lnClass + lnInst 对应的 LN；
     * 3. 如果仍然找不到，再退化为按 lnInst 收集全部 LN。
     */
    private List<LnNode> findMatchedLnNodes(IcdDocument icdDocument, IndexBindingCommand command) {
        List<LnNode> result = new ArrayList<LnNode>();
        if (icdDocument == null || command == null) {
            return result;
        }

        Set<String> lnClasses = new LinkedHashSet<String>();

        // 1. 优先通过当前 DataSet 的 FCDA 找关联 lnClass
        DataSetNode dataSetNode = icdDocument.getDataSets().get(command.getDataSetName());
        if (dataSetNode != null && dataSetNode.getFcdas() != null) {
            for (FcdaNode fcda : dataSetNode.getFcdas()) {
                if (fcda == null) {
                    continue;
                }
                if (equalsTrim(fcda.getLnInst(), command.getLnInst()) && fcda.getLnClass() != null) {
                    lnClasses.add(fcda.getLnClass());
                }
            }
        }

        // 2. 用 lnClass + lnInst 收集所有 LN
        if (!lnClasses.isEmpty()) {
            for (LnNode node : icdDocument.getLogicalNodes()) {
                if (node == null) {
                    continue;
                }
                if (equalsTrim(node.getLnInst(), command.getLnInst()) && lnClasses.contains(node.getLnClass())) {
                    result.add(node);
                }
            }
        }

        // 3. 如果还没找到，则退化成按 lnInst 收集全部 LN
        if (result.isEmpty()) {
            for (LnNode node : icdDocument.getLogicalNodes()) {
                if (node == null) {
                    continue;
                }
                if (equalsTrim(node.getLnInst(), command.getLnInst())) {
                    result.add(node);
                }
            }
        }

        return result;
    }

    /**
     * 在 lnClass 分组下查找或创建指定 lnInst 的输出节点。
     */
    private InstItem findOrCreateInst(DataSetGroupItem groupItem, String inst, String desc) {
        for (InstItem item : groupItem.getInstList()) {
            if (equalsTrim(item.getInst(), inst)) {
                if (item.getDesc() == null || item.getDesc().trim().isEmpty()) {
                    item.setDesc(desc);
                }
                return item;
            }
        }
        InstItem item = new InstItem();
        item.setInst(inst);
        item.setDesc(desc);
        groupItem.getInstList().add(item);
        return item;
    }

    /**
     * 构建当前 inst 下的 doiList。
     *
     * 关键修正：
     * 1. 不再分“先跑一遍 nameList，再跑一遍 queueList”；
     * 2. 改成按单个 DOI 逐个判断：if(nameList) else if(queueList)；
     * 3. 这和 C# 原版一致，避免 queueList 误生成多余项。
     */
    private void buildDoiItems(InstItem instItem,
                               DataSetSelectionState selectionState,
                               LnNode lnNode,
                               IcdDocument icdDocument,
                               DefaultTemplate template) {
        if (lnNode == null) {
            return;
        }

        List<DefaultTemplate.DataObjectCfgItem> dataObjectCfgItems =
                findDataObjectCfgItems(template, selectionState.getLabel());
        if (dataObjectCfgItems.isEmpty()) {
            return;
        }

        for (DefaultTemplate.DataObjectCfgItem dataObjectCfg : dataObjectCfgItems) {
            if (dataObjectCfg.getObjectList() == null) {
                continue;
            }

            for (DefaultTemplate.ObjectCfgItem objectCfg : dataObjectCfg.getObjectList()) {
                List<DoiMatch> matches = collectMatchedDoiNodes(lnNode, objectCfg);
                for (DoiMatch match : matches) {
                    addDoiItem(instItem, selectionState, lnNode, icdDocument, template, objectCfg, match.getDoiNode(), match.isQueueMode());
                }
            }
        }
    }

    /**
     * 新增一个 DOI 映射项。
     *
     * queueMode = true 表示当前 DOI 是按 queueList 命中的，
     * 对应 C# 里的“基波指标/队列指标”那条分支。
     */
    private void addDoiItem(InstItem instItem,
                            DataSetSelectionState selectionState,
                            LnNode lnNode,
                            IcdDocument icdDocument,
                            DefaultTemplate template,
                            DefaultTemplate.ObjectCfgItem objectCfg,
                            DoiNode doiNode,
                            boolean queueMode) {
        if (doiNode == null) {
            return;
        }

        int icdCount = resolveIcdCount(icdDocument, selectionState, doiNode);

        // C# 原版 queue 分支有这个关键判断：
        // if (doi.tNUM == objectCfgItem.queuecount) break;
        // 这会让 “基波有功/无功/视在功率” 这类项不生成。
        if (shouldSkipQueueItem(queueMode, icdCount, objectCfg)) {
            return;
        }

        if (containsDoi(instItem, lnNode.getLnClass(), selectionState.getLnInst(), doiNode.getName(), objectCfg.getDesc())) {
            return;
        }

        DoiItem doiItem = new DoiItem();
        doiItem.setName(doiNode.getName());
        doiItem.setDesc(objectCfg.getDesc());

        // 原样保留 baseflag
        doiItem.setBaseflag(objectCfg.getBaseflag());

        // 只有 queueMode 时，basecount 才按 queuecount 输出
        doiItem.setBasecount(queueMode && objectCfg.getQueuecount() > 0
                ? objectCfg.getQueuecount()
                : objectCfg.getBasecount());

        doiItem.setIcdcout(icdCount);

        int[] range = resolveRange(icdCount, objectCfg, queueMode);
        doiItem.setStart(range[0]);
        doiItem.setEnd(range[1]);

        doiItem.setUnit(resolveUnit(doiNode, template));
        doiItem.setCoefficient(resolveCoefficient(doiNode, template));
        doiItem.getSdiList().addAll(buildSdiItems(doiNode, template));

        instItem.getDoiList().add(doiItem);
    }

    /**
     * 判断 queueList 命中的对象是否应按原 C# 规则跳过。
     */
    private boolean shouldSkipQueueItem(boolean queueMode, int icdCount, DefaultTemplate.ObjectCfgItem objectCfg) {
        if (!queueMode || objectCfg == null) {
            return false;
        }

        // queueList 本来就是给 baseflag=2 这类对象准备的
        if (objectCfg.getBaseflag() != 2) {
            return false;
        }

        // C#：if (doi.tNUM == objectCfgItem.queuecount) break;
        return objectCfg.getQueuecount() > 0 && icdCount == objectCfg.getQueuecount();
    }

    /**
     * 判断 DOI 是否已经存在。
     *
     * 旧逻辑只按 name + desc 去重，过于激进，
     * 容易把本应保留的项压掉。
     *
     * 新逻辑至少把：
     * - lnClass
     * - lnInst
     * - doiName
     * - desc
     * 一起考虑。
     *
     * 注意：
     * 当前 InstItem 已经按 group(lnClass) + inst 组织，
     * 所以这里仍然是保守去重，不会过度复杂。
     */
    private boolean containsDoi(InstItem instItem,
                                String lnClass,
                                String lnInst,
                                String doiName,
                                String desc) {
        for (DoiItem item : instItem.getDoiList()) {
            boolean sameName = equalsTrim(item.getName(), doiName);
            boolean sameDesc = equalsTrim(item.getDesc(), desc);
            if (sameName && sameDesc) {
                return true;
            }
        }
        return false;
    }

    /**
     * 根据用户绑定标签查找对应的 DataObjectsList 配置项。
     */
    private List<DefaultTemplate.DataObjectCfgItem> findDataObjectCfgItems(DefaultTemplate template, String label) {
        List<DefaultTemplate.DataObjectCfgItem> result = new ArrayList<DefaultTemplate.DataObjectCfgItem>();
        if (template == null || template.getDataObjectsList() == null) {
            return result;
        }
        for (DefaultTemplate.DataObjectCfgItem item : template.getDataObjectsList()) {
            if (item.getLnInstList() != null && item.getLnInstList().contains(label)) {
                result.add(item);
            }
        }
        return result;
    }

    /**
     * 单个 DOI 的匹配结果。
     *
     * queueMode=false：命中 nameList
     * queueMode=true ：命中 queueList
     */
    private static final class DoiMatch {
        private final DoiNode doiNode;
        private final boolean queueMode;

        private DoiMatch(DoiNode doiNode, boolean queueMode) {
            this.doiNode = doiNode;
            this.queueMode = queueMode;
        }

        public DoiNode getDoiNode() {
            return doiNode;
        }

        public boolean isQueueMode() {
            return queueMode;
        }
    }

    /**
     * 按 objectCfg 级别实现 “nameList 优先，找不到才退回 queueList”。
     *
     * 关键点：
     * 1. 先扫描整个 lnNode，收集所有命中 nameList 的 DOI；
     * 2. 只要 nameList 命中了任意一个，就直接返回这些 nameList 命中项；
     * 3. 只有当 nameList 一个都没命中时，才允许 queueList 命中。
     *
     * 这样像：
     * - nameList = [HFundPhV, FundPhV]
     * - queueList = [HPhV]
     *
     * 当 ICD 里已经存在 HFundPhV / FundPhV 时，
     * 就不会再让 HPhV 派生出“相电压基波有效值”。
     */
    private List<DoiMatch> collectMatchedDoiNodes(LnNode lnNode, DefaultTemplate.ObjectCfgItem objectCfg) {
        List<DoiMatch> nameMatches = new ArrayList<DoiMatch>();
        List<DoiMatch> queueMatches = new ArrayList<DoiMatch>();

        if (lnNode == null || lnNode.getDoiList() == null || objectCfg == null) {
            return nameMatches;
        }

        for (DoiNode doiNode : lnNode.getDoiList()) {
            if (doiNode == null || doiNode.getName() == null) {
                continue;
            }

            String doiName = doiNode.getName();

            if (objectCfg.getNameList() != null && objectCfg.getNameList().contains(doiName)) {
                nameMatches.add(new DoiMatch(doiNode, false));
                continue;
            }

            if (objectCfg.getQueueList() != null && objectCfg.getQueueList().contains(doiName)) {
                queueMatches.add(new DoiMatch(doiNode, true));
            }
        }

        // 只要 nameList 命中了任何 DOI，就彻底禁止 queueList 再参与
        if (!nameMatches.isEmpty()) {
            return nameMatches;
        }

        return queueMatches;
    }

    /**
     * 解析当前 DOI 在 ICD 中真实存在的序列数量。
     *
     * 规则贴近 C#：
     * 1. 优先使用 SclGeneratedModelReader 已经按模板 count 回填好的 doi.sequenceCount；
     * 2. 再用当前 DataSet 的 FCDA / ix 做兜底；
     * 3. 这样 icdcout 才能和原版保持一致。
     */
    private int resolveIcdCount(IcdDocument icdDocument, DataSetSelectionState selectionState, DoiNode doiNode) {
        int max = doiNode == null ? 0 : doiNode.getSequenceCount();

        if (doiNode != null && doiNode.getIx() != null && doiNode.getIx() > max) {
            max = doiNode.getIx().intValue();
        }

        if (icdDocument == null || icdDocument.getDataSets() == null || selectionState == null || doiNode == null) {
            return max;
        }

        DataSetNode dataSetNode = icdDocument.getDataSets().get(selectionState.getDataSetName());
        if (dataSetNode == null || dataSetNode.getFcdas() == null) {
            return max;
        }

        for (FcdaNode fcda : dataSetNode.getFcdas()) {
            if (fcda == null) {
                continue;
            }

            boolean sameLnClass = equalsTrim(fcda.getLnClass(), doiNode.getLnClass());
            boolean sameLnInst = equalsTrim(fcda.getLnInst(), selectionState.getLnInst());
            boolean sameDoName = equalsTrim(fcda.getDoName(), doiNode.getName());

            if (sameLnClass && sameLnInst && sameDoName) {
                if (fcda.getSequenceCount() > max) {
                    max = fcda.getSequenceCount();
                }
                if (fcda.getIx() != null && fcda.getIx() > max) {
                    max = fcda.getIx().intValue();
                }
            }
        }

        return max;
    }

    /**
     * 计算 DOI 的 start / end。
     *
     * 严格按 C# 规则：
     *
     * 普通序列项：
     * - start 默认 0
     * - 如果 basecount < icdCount 且 basecount != 0，则 start = 1
     * - 如果 icdCount > 0，则 end = icdCount - 1
     *
     * queueList 命中的基波项：
     * - start = 0
     * - end = 0
     *
     * 注意：
     * 这里不是 1 基区间，也不是 end = icdCount。
     */
    private int[] resolveRange(int icdCount, DefaultTemplate.ObjectCfgItem objectCfg, boolean queueMode) {
        // queueList 命中的基波项，C# 原版就是固定 0~0
        if (queueMode) {
            return new int[]{0, 0};
        }

        int start = 0;
        int end = 0;

        if (objectCfg != null) {
            if (objectCfg.getBasecount() != 0 && objectCfg.getBasecount() < icdCount) {
                start = 1;
            }
        }

        if (icdCount > 0) {
            end = icdCount - 1;
        }

        return new int[]{start, end};
    }

    /**
     * 解析 DOI 的单位。
     *
     * 修正说明：
     * 1. 不再扫描整棵 DOI 子树的所有叶子值；
     * 2. 只读取 DAI -> Val 的配置值；
     * 3. 找不到匹配单位时，默认返回 "other"；
     * 4. 这样更接近原 C# 的 ReadtUnNaming 逻辑。
     */
    private String resolveUnit(DoiNode doiNode, DefaultTemplate template) {
        List<String> daiValues = new ArrayList<String>();
        collectDaiValues(doiNode.getChildren(), daiValues);

        for (String value : daiValues) {
            DefaultTemplate.UnitCfgItem unitCfg = findUnitCfg(template, value);
            if (unitCfg != null && unitCfg.getDesc() != null && !unitCfg.getDesc().trim().isEmpty()) {
                return unitCfg.getDesc().trim();
            }
        }

        // 原映射里找不到单位时，统一归为 other
        return "other";
    }

    /**
     * 递归收集 DOI 子树中所有 DAI 节点上的 Val。
     *
     * 这里只读取 DAI 的 values，不把 SDI 名称、其他叶子值混进来。
     */
    private void collectDaiValues(List<DoiElementNode> nodes, List<String> result) {
        if (nodes == null) {
            return;
        }

        for (DoiElementNode node : nodes) {
            if (node == null) {
                continue;
            }

            if ("DAI".equalsIgnoreCase(node.getKind()) && node.getValues() != null) {
                for (String value : node.getValues()) {
                    if (value != null && !value.trim().isEmpty()) {
                        result.add(value.trim());
                    }
                }
            }

            collectDaiValues(node.getChildren(), result);
        }
    }

    /**
     * 解析 DOI 的倍率系数。
     *
     * 找不到模板倍率配置时默认返回 1.0。
     */
    private float resolveCoefficient(DoiNode doiNode, DefaultTemplate template) {
        Set<String> values = collectAllLeafValues(doiNode.getChildren());
        for (String value : values) {
            DefaultTemplate.MultiplierCfgItem multiplierCfg = findMultiplierCfg(template, value);
            if (multiplierCfg != null) {
                return multiplierCfg.getMultiplier();
            }
        }
        return 1.0f;
    }

    /**
     * 构建当前 DOI 下的 sdiList。
     *
     * 修正说明：
     * 1. 原 C# 中，如果命中了类型节点（如 mag/ang），但前面没有命中相别节点，
     *    会自动创建一个“无相别”的 sdiItem：
     *    - name = null
     *    - desc = 无相别
     * 2. 当前 Java 版之前没有这层兜底，导致无相别内容直接变成空数组。
     * 3. 这里改成按“相别节点 / 类型节点”双层递归处理，尽量贴近原 C# 的 ReadtUnNaming 逻辑。
     */
    private List<SdiItem> buildSdiItems(DoiNode doiNode, DefaultTemplate template) {
        List<SdiItem> result = new ArrayList<SdiItem>();
        if (doiNode == null || doiNode.getChildren() == null) {
            return result;
        }

        for (DoiElementNode child : doiNode.getChildren()) {
            // currentSdi 表示当前递归上下文中已命中的相别节点
            collectSdiItems(result, child, template, null);
        }

        return result;
    }

    /**
     * 递归收集 DOI 下的相别 / 类型信息。
     *
     * @param result     当前 DOI 的 sdiList 结果集
     * @param node       当前递归节点
     * @param template   DefaultCfg 模板
     * @param currentSdi 当前已经命中的相别节点；如果为 null，说明当前路径还没有相别
     *
     * 逻辑说明：
     * 1. 如果当前 SDI 命中 PhaseList，则创建一个新的相别 sdiItem；
     * 2. 如果当前 SDI 命中 TypeList：
     *    - 如果已经有 currentSdi，则把类型挂到这个相别下；
     *    - 如果没有 currentSdi，则自动创建“无相别” sdiItem，再挂类型；
     * 3. 只有 type 节点才会创建 typeItem，且只保留 mag/ang。
     */
    private void collectSdiItems(List<SdiItem> result,
                                 DoiElementNode node,
                                 DefaultTemplate template,
                                 SdiItem currentSdi) {
        if (node == null) {
            return;
        }

        if ("SDI".equalsIgnoreCase(node.getKind())) {
            // 1. 命中相别节点：例如 phsA / phsB / phsC / phsAB ...
            if (isPhaseNode(template, node.getName())) {
                SdiItem phaseItem = new SdiItem();
                phaseItem.setName(node.getName());
                phaseItem.setDesc(resolvePhaseDesc(template, node.getName()));
                result.add(phaseItem);

                if (node.getChildren() != null) {
                    for (DoiElementNode child : node.getChildren()) {
                        collectSdiItems(result, child, template, phaseItem);
                    }
                }
                return;
            }

            // 2. 命中类型节点：例如 mag / ang
            if (isTypeNode(template, node.getName())) {
                // 如果当前没有相别上下文，就按原 C# 规则自动创建“无相别”
                SdiItem target = currentSdi;
                if (target == null) {
                    target = findOrCreateNoPhaseSdiItem(result);
                }

                addTypeIfAbsent(target, node.getName(), template);

                if (node.getChildren() != null) {
                    for (DoiElementNode child : node.getChildren()) {
                        collectSdiItems(result, child, template, target);
                    }
                }
                return;
            }
        }

        // 3. 其他节点继续往下递归，继承当前上下文
        if (node.getChildren() != null) {
            for (DoiElementNode child : node.getChildren()) {
                collectSdiItems(result, child, template, currentSdi);
            }
        }
    }

    /**
     * 查找或创建“无相别” sdiItem。
     *
     * 原 C# 的行为：
     * 当命中了类型节点，但前面没有任何相别节点时，
     * 会自动创建：
     * - name = null
     * - desc = 无相别
     *
     * 这样像 Hz / TotW / TotPF 这类无相别 DOI，仍然能挂 mag$f / ang$f。
     */
    private SdiItem findOrCreateNoPhaseSdiItem(List<SdiItem> result) {
        for (SdiItem item : result) {
            if (item.getName() == null && "无相别".equals(item.getDesc())) {
                return item;
            }
        }

        SdiItem noPhaseItem = new SdiItem();
        noPhaseItem.setName(null);
        noPhaseItem.setDesc("无相别");
        result.add(noPhaseItem);
        return noPhaseItem;
    }

    /**
     * 向当前 sdiItem 下添加类型项。
     *
     * 规则：
     * 1. 只保留 mag / ang；
     * 2. 输出格式固定为 mag$f / ang$f；
     * 3. 避免重复加入同名类型。
     */
    private void addTypeIfAbsent(SdiItem sdiItem, String typeName, DefaultTemplate template) {
        if (sdiItem == null || typeName == null) {
            return;
        }

        // 只保留原映射里真正需要的类型
        if (!"mag".equalsIgnoreCase(typeName) && !"ang".equalsIgnoreCase(typeName)) {
            return;
        }

        String finalName = typeName + "$f";

        for (TypeItem item : sdiItem.getTypeList()) {
            if (finalName.equals(item.getName())) {
                return;
            }
        }

        TypeItem typeItem = new TypeItem();
        typeItem.setName(finalName);
        typeItem.setDesc(resolveTypeDesc(template, typeName));
        sdiItem.getTypeList().add(typeItem);
    }

    /**
     * 判断当前 SDI 是否属于“相别类”节点。
     *
     * 例如：
     * phsA / phsB / phsC / phsAB / phsBC / phsCA
     */
    private boolean isPhaseNode(DefaultTemplate template, String name) {
        if (template == null || template.getPhaseList() == null || name == null) {
            return false;
        }

        for (DefaultTemplate.PhaseCfgItem item : template.getPhaseList()) {
            if (item.getNameList() != null && item.getNameList().contains(name)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 判断当前 SDI 是否属于“类型类”节点。
     *
     * 当前只关心 mag / ang。
     */
    private boolean isTypeNode(DefaultTemplate template, String name) {
        if (template == null || template.getTypeList() == null || name == null) {
            return false;
        }

        for (DefaultTemplate.TypeCfgItem item : template.getTypeList()) {
            if (item.getNameList() != null && item.getNameList().contains(name)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 从模板 LnClassList 中查找当前 LN 的业务分组配置。
     */
    private DefaultTemplate.LnClassCfgItem findLnClassCfg(DefaultTemplate template, String lnClass) {
        if (template == null || template.getLnClassList() == null) {
            return null;
        }
        for (DefaultTemplate.LnClassCfgItem item : template.getLnClassList()) {
            if (item.getNameList() != null && item.getNameList().contains(lnClass)) {
                return item;
            }
        }
        return null;
    }


    /**
     * 在候选分析结果中定位当前前端回传的业务分组。
     */
    private IndexCandidate findCandidate(IndexAnalysis analysis, String groupKey, String groupDesc) {
        if (analysis == null || analysis.getCandidates() == null) {
            return null;
        }
        for (IndexCandidate candidate : analysis.getCandidates()) {
            if (equalsTrim(candidate.getGroupKey(), groupKey) || equalsTrim(candidate.getGroupDesc(), groupDesc)) {
                return candidate;
            }
        }
        return null;
    }

    /**
     * 递归收集 DOI 子树下所有 Val 文本，用于匹配倍率等模板配置。
     */
    private Set<String> collectAllLeafValues(List<DoiElementNode> nodes) {
        Set<String> result = new LinkedHashSet<String>();
        if (nodes == null) {
            return result;
        }
        for (DoiElementNode node : nodes) {
            if (node.getValues() != null) {
                for (String value : node.getValues()) {
                    if (value != null && !value.trim().isEmpty()) {
                        result.add(value.trim());
                    }
                }
            }
            result.addAll(collectAllLeafValues(node.getChildren()));
        }
        return result;
    }

    /**
     * 从模板 UnitList 中查找指定 DAI 值对应的单位配置。
     */
    private DefaultTemplate.UnitCfgItem findUnitCfg(DefaultTemplate template, String value) {
        if (template == null || template.getUnitList() == null) {
            return null;
        }
        for (DefaultTemplate.UnitCfgItem item : template.getUnitList()) {
            if (item.getNameList() != null && item.getNameList().contains(value)) {
                return item;
            }
        }
        return null;
    }

    /**
     * 从模板 MultiplierList 中查找指定 DAI 值对应的倍率配置。
     */
    private DefaultTemplate.MultiplierCfgItem findMultiplierCfg(DefaultTemplate template, String value) {
        if (template == null || template.getMultiplierList() == null) {
            return null;
        }
        for (DefaultTemplate.MultiplierCfgItem item : template.getMultiplierList()) {
            if (item.getNameList() != null && item.getNameList().contains(value)) {
                return item;
            }
        }
        return null;
    }

    /**
     * 将相别节点名称转换为模板中的中文描述。
     */
    private String resolvePhaseDesc(DefaultTemplate template, String value) {
        if (template == null || template.getPhaseList() == null) {
            return value;
        }
        for (DefaultTemplate.PhaseCfgItem item : template.getPhaseList()) {
            if (item.getNameList() != null && item.getNameList().contains(value)) {
                return item.getDesc();
            }
        }
        return value;
    }

    /**
     * 将类型节点名称转换为模板中的中文描述。
     */
    private String resolveTypeDesc(DefaultTemplate template, String value) {
        if (template == null || template.getTypeList() == null) {
            return value;
        }
        for (DefaultTemplate.TypeCfgItem item : template.getTypeList()) {
            if (item.getNameList() != null && item.getNameList().contains(value)) {
                return item.getDesc();
            }
        }
        return value;
    }


    /**
     * 空安全、去首尾空白的字符串比较。
     */
    private boolean equalsTrim(String left, String right) {
        if (left == null && right == null) {
            return true;
        }
        if (left == null || right == null) {
            return false;
        }
        return left.trim().equals(right.trim());
    }
    /**
     * 解析单个报告的 rptID。
     *
     * 原 C# 是直接取 tReportControl.rptID，
     * 所以这里优先从 ICD 解析后的报告控制块对象里读。
     */
    private String resolveSingleRptId(IcdDocument icdDocument, String reportName) {
        if (icdDocument == null || icdDocument.getReportControls() == null || reportName == null) {
            return null;
        }

        for (ReportControlNode reportControl : icdDocument.getReportControls()) {
            if (reportName.equals(reportControl.getName())) {
                return reportControl.getRptId();
            }
        }
        return null;
    }

    /**
     * 将布尔 buffered 转成原 C# 风格的 BR / RP。
     *
     * 原 C#：
     * - true -> BR
     * - false -> RP
     * - 默认 -> RP
     */
    private String resolveBufferedCode(IcdDocument icdDocument, String reportName) {
        if (icdDocument == null || icdDocument.getReportControls() == null || reportName == null) {
            return "RP";
        }

        for (ReportControlNode reportControl : icdDocument.getReportControls()) {
            if (reportName.equals(reportControl.getName())) {
                Boolean buffered = reportControl.getBuffered();
                if (Boolean.TRUE.equals(buffered)) {
                    return "BR";
                }
                return "RP";
            }
        }
        return "RP";
    }

    /**
     * inst 按原 C# 规则，只允许 01~04，其余默认 01。
     */
    private String resolveReportInst(String inst) {
        if ("01".equals(inst) || "02".equals(inst) || "03".equals(inst) || "04".equals(inst)) {
            return inst;
        }
        return "01";
    }

    /**
     * TrgOps 按原 C# 规则，只接受 40 / 96，其余默认 96。
     */
    private String resolveTrgOps(String trgOps) {
        if ("40".equals(trgOps) || "96".equals(trgOps)) {
            return trgOps;
        }
        return "96";
    }

    /**
     * 解析输出版本号；请求未传时使用当天日期。
     */
    private String resolveVersion(String version) {
        if (version != null && !version.trim().isEmpty()) {
            return version.trim();
        }
        return LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
    }

    /**
     * 按原 C# 规则转换 reportCount：单报告写 0，多报告写实际数量。
     */
    private int resolveReportCount(int size) {
        return size <= 1 ? 0 : size;
    }
}
