package tech.easyflow.ai.easyagentsflow.service;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.flow.core.parser.ChainParser;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckIssue;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.node.filegeneration.FileGenerationRules;
import tech.easyflow.ai.node.filegeneration.SourceFormat;
import tech.easyflow.ai.node.filegeneration.TargetFormat;
import tech.easyflow.ai.plugin.workflow.dependency.WorkflowPluginDependencyService;
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.datacenter.execution.model.DatacenterSchemaResponse;

import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class WorkflowCheckService {
    private static final String LEVEL_ERROR = "ERROR";
    private static final String LEVEL_WARNING = "WARNING";
    private static final String TYPE_START = "startNode";
    private static final String TYPE_END = "endNode";
    private static final String TYPE_LOOP = "loopNode";
    private static final String TYPE_WORKFLOW = "workflow-node";
    private static final String TYPE_PLUGIN = "plugin-node";
    private static final String TYPE_MAKE_FILE = "make-file";
    private static final String SYSTEM_START_PARAM_NAME = "user_input";

    @Resource
    private WorkflowService workflowService;
    @Resource
    private ChainParser chainParser;
    @Resource
    private WorkflowDatacenterContentService workflowDatacenterContentService;
    @Resource
    private WorkflowPluginDependencyService workflowPluginDependencyService;
    @Resource
    private PluginItemService pluginItemService;
    @Resource
    private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;

    public WorkflowCheckResult checkWorkflow(BigInteger workflowId, WorkflowCheckStage stage) {
        if (workflowId == null) {
            throw new BusinessException("工作流ID不能为空");
        }
        Workflow workflow = workflowService.getById(workflowId);
        if (workflow == null) {
            throw new BusinessException("工作流不存在: " + workflowId);
        }
        return checkContent(workflow.getContent(), stage, workflowId);
    }

    public WorkflowCheckResult checkContent(String content, WorkflowCheckStage stage, BigInteger currentWorkflowId) {
        if (stage == null) {
            throw new BusinessException("校验阶段不能为空");
        }
        List<WorkflowCheckIssue> issues = new ArrayList<>();
        Set<String> issueKeys = new LinkedHashSet<>();
        ParsedWorkflow parsedWorkflow = parseAndCheckBase(content, issues, issueKeys);
        if (parsedWorkflow != null) {
            List<NodeView> startNodes = parsedWorkflow.nodes.stream()
                    .filter(node -> TYPE_START.equals(node.type))
                    .collect(Collectors.toList());
            checkStartFormSchema(startNodes, issues, issueKeys);
            checkPluginSchemaHashes(parsedWorkflow, issues, issueKeys);
        }

        if (stage == WorkflowCheckStage.PRE_EXECUTE && parsedWorkflow != null) {
            runStrictChecks(content, parsedWorkflow, currentWorkflowId, issues, issueKeys);
        }
        return buildResult(stage, issues);
    }

    public void checkOrThrow(String content, WorkflowCheckStage stage, BigInteger currentWorkflowId) {
        WorkflowCheckResult result = checkContent(content, stage, currentWorkflowId);
        throwIfFailed(result);
    }

    public void checkOrThrow(BigInteger workflowId, WorkflowCheckStage stage) {
        WorkflowCheckResult result = checkWorkflow(workflowId, stage);
        throwIfFailed(result);
    }

    private ParsedWorkflow parseAndCheckBase(String content, List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
        if (!StringUtils.hasText(content)) {
            addIssue(issues, issueKeys, "INVALID_JSON", "工作流内容不能为空", null, null, null);
            return null;
        }

        Object parsed;
        try {
            parsed = JSON.parse(content);
        } catch (Exception e) {
            addIssue(issues, issueKeys, "INVALID_JSON", "工作流内容不是合法JSON: " + shortError(e), null, null, null);
            return null;
        }

        if (!(parsed instanceof JSONObject)) {
            addIssue(issues, issueKeys, "INVALID_JSON_OBJECT", "工作流内容必须是JSON对象", null, null, null);
            return null;
        }

        JSONObject root = (JSONObject) parsed;
        JSONArray nodesArray = getArrayField(root, "nodes", "NODES_NOT_ARRAY", "nodes 必须是数组", issues, issueKeys);
        JSONArray edgesArray = getArrayField(root, "edges", "EDGES_NOT_ARRAY", "edges 必须是数组", issues, issueKeys);

        List<NodeView> nodes = new ArrayList<>();
        Map<String, NodeView> nodeMap = new LinkedHashMap<>();
        Set<String> nodeIds = new HashSet<>();
        Map<String, ?> parserMap = chainParser.getNodeParserMap() == null ? Collections.emptyMap() : chainParser.getNodeParserMap();

        for (int i = 0; i < nodesArray.size(); i++) {
            Object nodeObject = nodesArray.get(i);
            if (!(nodeObject instanceof JSONObject)) {
                addIssue(issues, issueKeys, "NODE_INVALID", "第 " + (i + 1) + " 个节点不是对象", null, null, null);
                continue;
            }
            JSONObject nodeJson = (JSONObject) nodeObject;
            NodeView node = new NodeView();
            node.id = trimToNull(nodeJson.getString("id"));
            node.type = trimToNull(nodeJson.getString("type"));
            node.parentId = trimToNull(nodeJson.getString("parentId"));
            node.data = nodeJson.getJSONObject("data");
            node.name = extractNodeName(nodeJson, node.data, node.id);

            if (!StringUtils.hasText(node.id)) {
                addIssue(issues, issueKeys, "NODE_ID_EMPTY", "存在节点缺少 id", null, null, node.name);
                continue;
            }
            if (!nodeIds.add(node.id)) {
                addIssue(issues, issueKeys, "NODE_ID_DUPLICATE", "节点ID重复: " + node.id, node.id, null, node.name);
            }
            if (!StringUtils.hasText(node.type) || !parserMap.containsKey(node.type)) {
                addIssue(issues, issueKeys, "NODE_TYPE_UNKNOWN", "节点类型无法识别: " + safe(node.type), node.id, null, node.name);
            }
            if (StringUtils.hasText(node.parentId) && node.parentId.equals(node.id)) {
                addIssue(issues, issueKeys, "NODE_PARENT_SELF", "节点不能引用自己作为父节点", node.id, null, node.name);
            }
            nodes.add(node);
            nodeMap.put(node.id, node);
        }

        for (NodeView node : nodes) {
            if (StringUtils.hasText(node.parentId) && !nodeMap.containsKey(node.parentId)) {
                addIssue(issues, issueKeys, "NODE_PARENT_NOT_FOUND",
                        "父节点不存在: " + node.parentId, node.id, null, node.name);
            }
        }

        List<EdgeView> edges = new ArrayList<>();
        Set<String> edgeIds = new HashSet<>();
        for (int i = 0; i < edgesArray.size(); i++) {
            Object edgeObject = edgesArray.get(i);
            if (!(edgeObject instanceof JSONObject)) {
                addIssue(issues, issueKeys, "EDGE_INVALID", "第 " + (i + 1) + " 条连线不是对象", null, null, null);
                continue;
            }
            JSONObject edgeJson = (JSONObject) edgeObject;
            EdgeView edge = new EdgeView();
            edge.id = trimToNull(edgeJson.getString("id"));
            edge.source = trimToNull(edgeJson.getString("source"));
            edge.target = trimToNull(edgeJson.getString("target"));

            if (!StringUtils.hasText(edge.id)) {
                addIssue(issues, issueKeys, "EDGE_ID_EMPTY", "存在连线缺少 id", null, null, null);
                continue;
            }
            if (!edgeIds.add(edge.id)) {
                addIssue(issues, issueKeys, "EDGE_ID_DUPLICATE", "连线ID重复: " + edge.id, null, edge.id, null);
            }
            if (!StringUtils.hasText(edge.source)) {
                addIssue(issues, issueKeys, "EDGE_SOURCE_EMPTY", "连线 source 不能为空", null, edge.id, null);
            }
            if (!StringUtils.hasText(edge.target)) {
                addIssue(issues, issueKeys, "EDGE_TARGET_EMPTY", "连线 target 不能为空", null, edge.id, null);
            }
            edges.add(edge);
        }

        for (EdgeView edge : edges) {
            if (StringUtils.hasText(edge.source) && !nodeMap.containsKey(edge.source)) {
                addIssue(issues, issueKeys, "EDGE_SOURCE_NOT_FOUND",
                        "连线 source 不存在: " + edge.source, edge.source, edge.id, null);
            }
            if (StringUtils.hasText(edge.target) && !nodeMap.containsKey(edge.target)) {
                addIssue(issues, issueKeys, "EDGE_TARGET_NOT_FOUND",
                        "连线 target 不存在: " + edge.target, edge.target, edge.id, null);
            }
        }

        ParsedWorkflow parsedWorkflow = new ParsedWorkflow();
        parsedWorkflow.nodes = nodes;
        parsedWorkflow.edges = edges;
        parsedWorkflow.nodeMap = nodeMap;
        checkDatacenterNodes(parsedWorkflow, issues, issueKeys);
        return parsedWorkflow;
    }

    private void checkDatacenterNodes(ParsedWorkflow parsed, List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
        for (NodeView node : parsed.nodes) {
            if (node == null) {
                continue;
            }
            if (TYPE_MAKE_FILE.equals(node.type)) {
                checkMakeFileNode(node, issues, issueKeys);
                continue;
            }
            if (workflowDatacenterContentService.isSearchDatasetNode(node.type)) {
                checkSearchDatasetNode(node, issues, issueKeys);
                continue;
            }
            if (workflowDatacenterContentService.isSaveDatasetNode(node.type)) {
                checkSaveDatasetNode(node, issues, issueKeys);
                continue;
            }
            if (workflowDatacenterContentService.isLlmNode(node.type)) {
                checkLlmQueryContext(node, parsed, issues, issueKeys);
            }
        }
    }

    private void checkMakeFileNode(NodeView node,
                                   List<WorkflowCheckIssue> issues,
                                   Set<String> issueKeys) {
        try {
            String targetFormatValue = node.data == null ? null : trimToNull(node.data.getString("targetFormat"));
            if (!StringUtils.hasText(targetFormatValue)) {
                targetFormatValue = TargetFormat.DOCX.getValue();
            }
            String sourceFormatValue = node.data == null ? null : trimToNull(node.data.getString("sourceFormat"));
            if (!StringUtils.hasText(sourceFormatValue)) {
                sourceFormatValue = SourceFormat.MARKDOWN.getValue();
            }
            TargetFormat targetFormat = TargetFormat.fromValue(targetFormatValue);
            SourceFormat sourceFormat = SourceFormat.fromValue(sourceFormatValue);
            FileGenerationRules.requireSupportedCombination(sourceFormat, targetFormat);
        } catch (BusinessException e) {
            addIssue(issues, issueKeys, "MAKE_FILE_INVALID", e.getMessage(), node.id, null, node.name);
        }
    }

    private void checkSearchDatasetNode(NodeView node,
                                        List<WorkflowCheckIssue> issues,
                                        Set<String> issueKeys) {
        try {
            workflowDatacenterContentService.requireSearchDatasetRef(node.data);
        } catch (BusinessException e) {
            addIssue(issues, issueKeys, "SEARCH_DATASET_INVALID", e.getMessage(), node.id, null, node.name);
        } catch (Exception e) {
            addIssue(issues, issueKeys, "SEARCH_DATASET_INVALID",
                    "查询数据节点校验失败: " + shortError(e), node.id, null, node.name);
        }
    }

    private void checkSaveDatasetNode(NodeView node,
                                      List<WorkflowCheckIssue> issues,
                                      Set<String> issueKeys) {
        try {
            workflowDatacenterContentService.requireSaveDatasetRef(node.data);
        } catch (BusinessException e) {
            addIssue(issues, issueKeys, "SAVE_DATASET_INVALID", e.getMessage(), node.id, null, node.name);
        } catch (Exception e) {
            addIssue(issues, issueKeys, "SAVE_DATASET_INVALID",
                    "写入数据节点校验失败: " + shortError(e), node.id, null, node.name);
        }
    }

    private void checkLlmQueryContext(NodeView node,
                                      ParsedWorkflow parsed,
                                      List<WorkflowCheckIssue> issues,
                                      Set<String> issueKeys) {
        if (node.data == null) {
            return;
        }
        Object rawNodeIds = node.data.get("queryContextNodeIds");
        if (rawNodeIds == null) {
            return;
        }
        if (!(rawNodeIds instanceof JSONArray nodeIds)) {
            addIssue(issues, issueKeys, "LLM_QUERY_CONTEXT_INVALID",
                    "查询上下文配置无效，请重新选择查询数据节点", node.id, null, node.name);
            return;
        }
        for (int i = 0; i < nodeIds.size(); i++) {
            String refNodeId = trimToNull(nodeIds.getString(i));
            if (!StringUtils.hasText(refNodeId)) {
                addIssue(issues, issueKeys, "LLM_QUERY_CONTEXT_EMPTY",
                        "查询上下文配置无效，请重新选择查询数据节点", node.id, null, node.name);
                continue;
            }
            NodeView refNode = parsed.nodeMap.get(refNodeId);
            if (refNode == null) {
                addIssue(issues, issueKeys, "LLM_QUERY_CONTEXT_NOT_FOUND",
                        "查询上下文节点不存在: " + refNodeId, node.id, null, node.name);
                continue;
            }
            if (!workflowDatacenterContentService.isSearchDatasetNode(refNode.type)) {
                addIssue(issues, issueKeys, "LLM_QUERY_CONTEXT_TYPE_INVALID",
                        "查询上下文只能选择查询数据节点", node.id, null, node.name);
            }
        }
    }

    private void runStrictChecks(String content, ParsedWorkflow parsed, BigInteger currentWorkflowId,
                                 List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
        if (parsed.nodes.isEmpty()) {
            addIssue(issues, issueKeys, "NODES_EMPTY", "预执行校验失败：nodes 不能为空", null, null, null);
        }
        if (parsed.edges.isEmpty()) {
            addIssue(issues, issueKeys, "EDGES_EMPTY", "预执行校验失败：edges 不能为空", null, null, null);
        }

        try {
            String preparedContent = workflowDatacenterContentService.prepareContent(content);
            Object definition = chainParser.parse(preparedContent);
            if (definition == null) {
                addIssue(issues, issueKeys, "PARSE_NULL", "预执行校验失败：节点配置错误，请检查", null, null, null);
            }
        } catch (Exception e) {
            addIssue(issues, issueKeys, "PARSE_FAILED", "预执行校验失败：解析流程失败 - " + shortError(e), null, null, null);
        }

        List<NodeView> startNodes = parsed.nodes.stream()
                .filter(node -> TYPE_START.equals(node.type))
                .collect(Collectors.toList());
        List<NodeView> endNodes = parsed.nodes.stream()
                .filter(node -> TYPE_END.equals(node.type))
                .collect(Collectors.toList());
        if (startNodes.isEmpty()) {
            addIssue(issues, issueKeys, "START_NODE_MISSING", "预执行校验失败：至少需要一个 startNode", null, null, null);
        }
        if (endNodes.isEmpty()) {
            addIssue(issues, issueKeys, "END_NODE_MISSING", "预执行校验失败：至少需要一个 endNode", null, null, null);
        }

        List<NodeView> rootNodes = parsed.nodes.stream()
                .filter(NodeView::isRootLevel)
                .collect(Collectors.toList());
        Map<String, Integer> rootInDegree = new HashMap<>();
        for (NodeView rootNode : rootNodes) {
            rootInDegree.put(rootNode.id, 0);
        }
        for (EdgeView edge : parsed.edges) {
            NodeView source = parsed.nodeMap.get(edge.source);
            NodeView target = parsed.nodeMap.get(edge.target);
            if (source == null || target == null) {
                continue;
            }
            if (source.isRootLevel() && target.isRootLevel() && isSameParent(source, target)) {
                rootInDegree.put(target.id, rootInDegree.getOrDefault(target.id, 0) + 1);
            }
        }

        List<NodeView> rootEntries = new ArrayList<>();
        for (NodeView rootNode : rootNodes) {
            if (rootInDegree.getOrDefault(rootNode.id, 0) == 0) {
                rootEntries.add(rootNode);
            }
        }
        for (NodeView entry : rootEntries) {
            if (!TYPE_START.equals(entry.type)) {
                addIssue(issues, issueKeys, "ROOT_ENTRY_NOT_START",
                        "根级入度为0的节点必须是 startNode", entry.id, null, entry.name);
            }
        }

        detectExplicitCycle(parsed, issues, issueKeys);

        Map<String, Set<String>> runtimeGraph = buildRuntimeGraph(parsed);
        Set<String> entryNodeIds = rootEntries.stream()
                .map(node -> node.id)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (entryNodeIds.isEmpty()) {
            for (NodeView startNode : startNodes) {
                entryNodeIds.add(startNode.id);
            }
        }
        Set<String> reachable = bfsReachable(runtimeGraph, entryNodeIds);

        for (String nodeId : reachable) {
            NodeView node = parsed.nodeMap.get(nodeId);
            if (node == null || TYPE_END.equals(node.type)) {
                continue;
            }
            if (runtimeGraph.getOrDefault(nodeId, Collections.emptySet()).isEmpty()) {
                addIssue(issues, issueKeys, "DEAD_END_NODE",
                        "存在无法继续执行的死路节点（非 end 且无后继）", node.id, null, node.name);
            }
        }

        if (!endNodes.isEmpty()) {
            Set<String> canReachEnd = reverseReachable(runtimeGraph, endNodes.stream()
                    .map(node -> node.id)
                    .collect(Collectors.toSet()));
            for (String nodeId : reachable) {
                if (!canReachEnd.contains(nodeId)) {
                    NodeView node = parsed.nodeMap.get(nodeId);
                    if (node != null) {
                        addIssue(issues, issueKeys, "END_UNREACHABLE",
                                "该节点无法到达任一 endNode", node.id, null, node.name);
                    }
                }
            }
        }

        checkWorkflowReferences(parsed, currentWorkflowId, content, issues, issueKeys);
    }

    private void checkWorkflowReferences(ParsedWorkflow parsed, BigInteger currentWorkflowId, String currentContent,
                                         List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
        Map<String, String> contentCache = new HashMap<>();
        String currentWorkflowIdString = currentWorkflowId == null ? null : currentWorkflowId.toString();
        if (StringUtils.hasText(currentWorkflowIdString)) {
            contentCache.put(currentWorkflowIdString, currentContent);
        }

        for (NodeView node : parsed.nodes) {
            if (!TYPE_WORKFLOW.equals(node.type)) {
                if (!TYPE_PLUGIN.equals(node.type)) {
                    continue;
                }
                checkPluginWorkflowReference(node, currentWorkflowIdString, currentContent, contentCache, issues, issueKeys);
                continue;
            }
            String workflowId = getWorkflowIdInNode(node);
            if (!StringUtils.hasText(workflowId)) {
                addIssue(issues, issueKeys, "WORKFLOW_REF_EMPTY", "子流程节点缺少 workflowId", node.id, null, node.name);
                continue;
            }
            if (StringUtils.hasText(currentWorkflowIdString) && currentWorkflowIdString.equals(workflowId)) {
                addIssue(issues, issueKeys, "WORKFLOW_REF_CYCLE", "子流程递归引用：工作流不能引用自身", node.id, null, node.name);
                continue;
            }
            String workflowContent = loadWorkflowContent(workflowId, currentWorkflowIdString, currentContent, contentCache);
            if (!StringUtils.hasText(workflowContent)) {
                addIssue(issues, issueKeys, "WORKFLOW_REF_NOT_FOUND",
                        "子流程引用不存在: " + workflowId, node.id, null, node.name);
            }
        }

        if (!StringUtils.hasText(currentWorkflowIdString)) {
            return;
        }
        detectWorkflowReferenceCycle(currentWorkflowIdString, currentWorkflowIdString, currentContent, contentCache, issues, issueKeys);
    }

    /**
     * 校验开始节点开场表单 Schema 的最小约束。
     *
     * @param startNodes 开始节点列表
     * @param issues 问题收集
     * @param issueKeys 去重键集合
     */
    private void checkStartFormSchema(List<NodeView> startNodes,
                                      List<WorkflowCheckIssue> issues,
                                      Set<String> issueKeys) {
        for (NodeView startNode : startNodes) {
            if (startNode == null || startNode.data == null) {
                continue;
            }
            JSONArray schema = startNode.data.getJSONArray("startFormSchema");
            if (schema == null || schema.isEmpty()) {
                continue;
            }

            JSONObject userInputField = null;
            for (int i = 0; i < schema.size(); i++) {
                JSONObject field = schema.getJSONObject(i);
                if (field == null) {
                    continue;
                }
                String key = trimToNull(field.getString("key"));
                if (!StringUtils.hasText(key)) {
                    addIssue(issues, issueKeys, "START_FORM_FIELD_KEY_EMPTY",
                            "开始表单字段缺少 key", startNode.id, null, startNode.name);
                    continue;
                }
                String type = trimToNull(field.getString("type"));
                if (Arrays.asList("radio", "checkbox", "select").contains(type)) {
                    JSONArray options = field.getJSONArray("options");
                    if (options == null || options.isEmpty()) {
                        addIssue(issues, issueKeys, "START_FORM_OPTIONS_EMPTY",
                                "选择类开始表单字段必须配置至少一个选项", startNode.id, null, startNode.name);
                    }
                }
                if (SYSTEM_START_PARAM_NAME.equals(key)) {
                    userInputField = field;
                }
            }

            if (userInputField == null) {
                addIssue(issues, issueKeys, "START_FORM_USER_INPUT_MISSING",
                        "开始表单必须包含 user_input 主问题字段", startNode.id, null, startNode.name);
                continue;
            }

            if (!Boolean.TRUE.equals(userInputField.getBoolean("required"))) {
                addIssue(issues, issueKeys, "START_FORM_USER_INPUT_REQUIRED",
                        "开始表单的 user_input 字段必须为必填", startNode.id, null, startNode.name);
            }

            String userInputType = trimToNull(userInputField.getString("type"));
            if (!Arrays.asList("text", "textarea").contains(userInputType)) {
                addIssue(issues, issueKeys, "START_FORM_USER_INPUT_TYPE_INVALID",
                        "开始表单的 user_input 字段只能使用 text 或 textarea", startNode.id, null, startNode.name);
            }
        }
    }

    private void detectWorkflowReferenceCycle(String rootWorkflowId, String currentWorkflowId, String currentContent,
                                              Map<String, String> contentCache,
                                              List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
        Set<String> visited = new HashSet<>();
        LinkedHashSet<String> visiting = new LinkedHashSet<>();
        dfsWorkflowGraph(rootWorkflowId, currentWorkflowId, currentContent, contentCache, visited, visiting, issues, issueKeys);
    }

    private void dfsWorkflowGraph(String workflowId, String currentWorkflowId, String currentContent,
                                  Map<String, String> contentCache,
                                  Set<String> visited, LinkedHashSet<String> visiting,
                                  List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
        if (visited.contains(workflowId)) {
            return;
        }
        if (visiting.contains(workflowId)) {
            addIssue(issues, issueKeys, "WORKFLOW_REF_CYCLE",
                    "检测到子流程递归引用: " + formatCyclePath(visiting, workflowId), null, null, null);
            return;
        }

        String content = loadWorkflowContent(workflowId, currentWorkflowId, currentContent, contentCache);
        if (!StringUtils.hasText(content)) {
            visited.add(workflowId);
            return;
        }

        visiting.add(workflowId);
        Set<String> referencedWorkflowIds = extractWorkflowRefIds(content);
        for (String refWorkflowId : referencedWorkflowIds) {
            if (!StringUtils.hasText(refWorkflowId)) {
                continue;
            }
            if (visiting.contains(refWorkflowId)) {
                addIssue(issues, issueKeys, "WORKFLOW_REF_CYCLE",
                        "检测到子流程递归引用: " + formatCyclePath(visiting, refWorkflowId), null, null, null);
                continue;
            }
            String refContent = loadWorkflowContent(refWorkflowId, currentWorkflowId, currentContent, contentCache);
            if (!StringUtils.hasText(refContent)) {
                continue;
            }
            dfsWorkflowGraph(refWorkflowId, currentWorkflowId, currentContent, contentCache, visited, visiting, issues, issueKeys);
        }
        visiting.remove(workflowId);
        visited.add(workflowId);
    }

    private String loadWorkflowContent(String workflowId, String currentWorkflowId, String currentContent, Map<String, String> contentCache) {
        if (!StringUtils.hasText(workflowId)) {
            return null;
        }
        if (StringUtils.hasText(currentWorkflowId) && currentWorkflowId.equals(workflowId)) {
            return currentContent;
        }
        if (contentCache.containsKey(workflowId)) {
            return contentCache.get(workflowId);
        }
        Workflow workflow = workflowService.getById(workflowId);
        String content = workflow == null ? null : workflow.getContent();
        contentCache.put(workflowId, content);
        return content;
    }

    private Set<String> extractWorkflowRefIds(String content) {
        Set<String> refs = new LinkedHashSet<>();
        if (!StringUtils.hasText(content)) {
            return refs;
        }
        try {
            Object parsed = JSON.parse(content);
            if (!(parsed instanceof JSONObject)) {
                return refs;
            }
            JSONArray nodes = ((JSONObject) parsed).getJSONArray("nodes");
            if (nodes == null) {
                return refs;
            }
            for (int i = 0; i < nodes.size(); i++) {
                JSONObject node = nodes.getJSONObject(i);
                if (node == null) {
                    continue;
                }
                if (!TYPE_WORKFLOW.equals(trimToNull(node.getString("type")))) {
                    continue;
                }
                JSONObject data = node.getJSONObject("data");
                String workflowId = trimToNull(data == null ? null : data.getString("workflowId"));
                if (StringUtils.hasText(workflowId)) {
                    refs.add(workflowId);
                }
            }
            refs.addAll(workflowPluginDependencyService.extractWorkflowIdsFromPluginNodes(content));
        } catch (Exception ignored) {
            // ignore
        }
        return refs;
    }

    private void checkPluginWorkflowReference(NodeView node,
                                              String currentWorkflowIdString,
                                              String currentContent,
                                              Map<String, String> contentCache,
                                              List<WorkflowCheckIssue> issues,
                                              Set<String> issueKeys) {
        String pluginWorkflowId = getWorkflowIdInPluginNode(node);
        if (!StringUtils.hasText(pluginWorkflowId)) {
            addIssue(issues, issueKeys, "PLUGIN_WORKFLOW_REF_NOT_FOUND",
                    "插件节点未绑定有效工作流插件", node.id, null, node.name);
            return;
        }
        if (StringUtils.hasText(currentWorkflowIdString) && currentWorkflowIdString.equals(pluginWorkflowId)) {
            addIssue(issues, issueKeys, "PLUGIN_WORKFLOW_REF_CYCLE",
                    "插件递归引用：工作流不能通过插件引用自身", node.id, null, node.name);
            return;
        }
        String workflowContent = loadWorkflowContent(pluginWorkflowId, currentWorkflowIdString, currentContent, contentCache);
        if (!StringUtils.hasText(workflowContent)) {
            addIssue(issues, issueKeys, "PLUGIN_WORKFLOW_REF_NOT_FOUND",
                    "插件绑定工作流不存在: " + pluginWorkflowId, node.id, null, node.name);
        }
    }

    private void checkPluginSchemaHashes(ParsedWorkflow parsed,
                                         List<WorkflowCheckIssue> issues,
                                         Set<String> issueKeys) {
        for (NodeView node : parsed.nodes) {
            if (!TYPE_PLUGIN.equals(node.type) || node.data == null) {
                continue;
            }
            String pluginItemId = trimToNull(node.data.getString("pluginId"));
            if (!StringUtils.hasText(pluginItemId)) {
                continue;
            }
            String workflowId = workflowPluginDependencyService.resolveWorkflowIdByPluginItemId(pluginItemId);
            if (!StringUtils.hasText(workflowId)) {
                continue;
            }
            String latestSchemaHash = resolveLatestPluginSchemaHash(pluginItemId, workflowId);
            if (!StringUtils.hasText(latestSchemaHash)) {
                continue;
            }
            String currentSchemaHash = trimToNull(node.data.getString("schemaHash"));
            if (!latestSchemaHash.equals(currentSchemaHash)) {
                addIssue(issues, issueKeys, "PLUGIN_SCHEMA_OUTDATED",
                        "当前插件节点绑定工作流的已发布参数契约已更新，请重新选择插件同步节点定义",
                        node.id, null, node.name);
            }
        }
    }

    private String getWorkflowIdInPluginNode(NodeView node) {
        if (node == null || node.data == null) {
            return null;
        }
        return workflowPluginDependencyService.resolveWorkflowIdByPluginItemId(
                trimToNull(node.data.getString("pluginId"))
        );
    }

    private String resolveLatestPluginSchemaHash(String pluginItemId, String workflowId) {
        PluginItem pluginItem = pluginItemService.getById(pluginItemId);
        if (pluginItem != null && StringUtils.hasText(pluginItem.getSchemaHash())) {
            return pluginItem.getSchemaHash();
        }
        Workflow workflow = workflowService.getPublishedById(new BigInteger(workflowId));
        if (workflow == null) {
            return null;
        }
        return workflowPluginSnapshotResolver.resolveSchemaHash(workflow);
    }

    private String formatCyclePath(LinkedHashSet<String> visiting, String cycleStart) {
        List<String> chain = new ArrayList<>();
        boolean started = false;
        for (String id : visiting) {
            if (id.equals(cycleStart)) {
                started = true;
            }
            if (started) {
                chain.add(id);
            }
        }
        chain.add(cycleStart);
        return String.join(" -> ", chain);
    }

    private String getWorkflowIdInNode(NodeView node) {
        if (node == null || node.data == null) {
            return null;
        }
        return trimToNull(node.data.getString("workflowId"));
    }

    private void detectExplicitCycle(ParsedWorkflow parsed, List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
        Map<String, List<String>> graph = new HashMap<>();
        for (NodeView node : parsed.nodes) {
            graph.put(node.id, new ArrayList<>());
        }
        for (EdgeView edge : parsed.edges) {
            if (!StringUtils.hasText(edge.source) || !StringUtils.hasText(edge.target)) {
                continue;
            }
            if (!parsed.nodeMap.containsKey(edge.source) || !parsed.nodeMap.containsKey(edge.target)) {
                continue;
            }
            if (edge.source.equals(edge.target)) {
                addIssue(issues, issueKeys, "GRAPH_CYCLE", "检测到自环连线", edge.source, edge.id, parsed.nodeMap.get(edge.source).name);
            }
            graph.get(edge.source).add(edge.target);
        }

        Map<String, Integer> state = new HashMap<>();
        Deque<String> path = new ArrayDeque<>();
        for (NodeView node : parsed.nodes) {
            if (state.getOrDefault(node.id, 0) != 0) {
                continue;
            }
            if (dfsCycle(node.id, graph, state, path, parsed, issues, issueKeys)) {
                return;
            }
        }
    }

    private boolean dfsCycle(String current, Map<String, List<String>> graph, Map<String, Integer> state,
                             Deque<String> path, ParsedWorkflow parsed, List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
        state.put(current, 1);
        path.addLast(current);
        for (String next : graph.getOrDefault(current, Collections.emptyList())) {
            int nextState = state.getOrDefault(next, 0);
            if (nextState == 0) {
                if (dfsCycle(next, graph, state, path, parsed, issues, issueKeys)) {
                    return true;
                }
            } else if (nextState == 1) {
                String cyclePath = formatNodeCyclePath(path, next);
                NodeView node = parsed.nodeMap.get(next);
                addIssue(issues, issueKeys, "GRAPH_CYCLE", "检测到环路: " + cyclePath, next, null, node == null ? null : node.name);
                return true;
            }
        }
        path.removeLast();
        state.put(current, 2);
        return false;
    }

    private String formatNodeCyclePath(Deque<String> path, String cycleStart) {
        List<String> list = new ArrayList<>(path);
        int index = list.indexOf(cycleStart);
        if (index < 0) {
            return String.join(" -> ", list);
        }
        List<String> cycle = new ArrayList<>(list.subList(index, list.size()));
        cycle.add(cycleStart);
        return String.join(" -> ", cycle);
    }

    private Map<String, Set<String>> buildRuntimeGraph(ParsedWorkflow parsed) {
        Map<String, Set<String>> graph = new HashMap<>();
        for (NodeView node : parsed.nodes) {
            graph.put(node.id, new LinkedHashSet<>());
        }
        for (EdgeView edge : parsed.edges) {
            if (!StringUtils.hasText(edge.source) || !StringUtils.hasText(edge.target)) {
                continue;
            }
            NodeView source = parsed.nodeMap.get(edge.source);
            NodeView target = parsed.nodeMap.get(edge.target);
            if (source == null || target == null) {
                continue;
            }
            if (TYPE_END.equals(source.type)) {
                continue;
            }
            if (isSameParent(source, target)
                    || (TYPE_LOOP.equals(source.type) && source.id.equals(target.parentId))) {
                graph.get(source.id).add(target.id);
            }
        }

        for (NodeView node : parsed.nodes) {
            if (TYPE_END.equals(node.type)) {
                continue;
            }
            Set<String> nextNodes = graph.getOrDefault(node.id, new LinkedHashSet<>());
            if (nextNodes.isEmpty() && StringUtils.hasText(node.parentId) && parsed.nodeMap.containsKey(node.parentId)) {
                nextNodes.add(node.parentId);
            }
            graph.put(node.id, nextNodes);
        }
        return graph;
    }

    private Set<String> bfsReachable(Map<String, Set<String>> graph, Set<String> starts) {
        Set<String> visited = new LinkedHashSet<>();
        Deque<String> queue = new ArrayDeque<>();
        for (String start : starts) {
            if (StringUtils.hasText(start) && graph.containsKey(start)) {
                queue.add(start);
                visited.add(start);
            }
        }
        while (!queue.isEmpty()) {
            String current = queue.pollFirst();
            for (String next : graph.getOrDefault(current, Collections.emptySet())) {
                if (visited.add(next)) {
                    queue.addLast(next);
                }
            }
        }
        return visited;
    }

    private Set<String> reverseReachable(Map<String, Set<String>> graph, Set<String> endNodes) {
        Map<String, Set<String>> reverseGraph = new HashMap<>();
        for (String source : graph.keySet()) {
            reverseGraph.computeIfAbsent(source, key -> new LinkedHashSet<>());
            for (String target : graph.getOrDefault(source, Collections.emptySet())) {
                reverseGraph.computeIfAbsent(target, key -> new LinkedHashSet<>()).add(source);
            }
        }
        Set<String> visited = new LinkedHashSet<>();
        Deque<String> queue = new ArrayDeque<>();
        for (String endNode : endNodes) {
            if (StringUtils.hasText(endNode) && reverseGraph.containsKey(endNode)) {
                visited.add(endNode);
                queue.add(endNode);
            }
        }
        while (!queue.isEmpty()) {
            String current = queue.pollFirst();
            for (String prev : reverseGraph.getOrDefault(current, Collections.emptySet())) {
                if (visited.add(prev)) {
                    queue.addLast(prev);
                }
            }
        }
        return visited;
    }

    private boolean isSameParent(NodeView source, NodeView target) {
        return Objects.equals(trimToNull(source.parentId), trimToNull(target.parentId));
    }

    private JSONArray getArrayField(JSONObject root, String fieldName, String issueCode, String issueMessage,
                                    List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
        Object value = root.get(fieldName);
        if (value == null) {
            return new JSONArray();
        }
        if (!(value instanceof JSONArray)) {
            addIssue(issues, issueKeys, issueCode, issueMessage, null, null, null);
            return new JSONArray();
        }
        return (JSONArray) value;
    }

    private WorkflowCheckResult buildResult(WorkflowCheckStage stage, List<WorkflowCheckIssue> issues) {
        WorkflowCheckResult result = new WorkflowCheckResult();
        result.setStage(stage);
        result.setIssues(issues);
        result.setIssueCount(issues.size());
        result.setPassed(issues.stream().noneMatch(issue -> LEVEL_ERROR.equalsIgnoreCase(issue.getLevel())));
        return result;
    }

    private void throwIfFailed(WorkflowCheckResult result) {
        if (result == null) {
            return;
        }
        List<WorkflowCheckIssue> errorIssues = result.getIssues().stream()
                .filter(issue -> LEVEL_ERROR.equalsIgnoreCase(issue.getLevel()))
                .collect(Collectors.toList());
        if (errorIssues.isEmpty()) {
            return;
        }
        String summary = errorIssues.stream()
                .limit(5)
                .map(WorkflowCheckIssue::getMessage)
                .collect(Collectors.joining("；"));
        if (errorIssues.size() > 5) {
            summary = summary + "；等";
        }
        throw new BusinessException("工作流校验未通过(" + result.getStage() + ")，共 " + errorIssues.size() + " 项：" + summary);
    }

    private void addIssue(List<WorkflowCheckIssue> issues, Set<String> issueKeys, String code,
                          String message, String nodeId, String edgeId, String nodeName) {
        addIssue(issues, issueKeys, code, LEVEL_ERROR, message, nodeId, edgeId, nodeName);
    }

    private void addWarning(List<WorkflowCheckIssue> issues, Set<String> issueKeys, String code,
                            String message, String nodeId, String edgeId, String nodeName) {
        addIssue(issues, issueKeys, code, LEVEL_WARNING, message, nodeId, edgeId, nodeName);
    }

    private void addIssue(List<WorkflowCheckIssue> issues, Set<String> issueKeys, String code, String level,
                          String message, String nodeId, String edgeId, String nodeName) {
        String key = code + "|" + safe(nodeId) + "|" + safe(edgeId) + "|" + message;
        if (!issueKeys.add(key)) {
            return;
        }
        issues.add(new WorkflowCheckIssue(code, level, message, nodeId, edgeId, nodeName));
    }

    private String extractNodeName(JSONObject nodeJson, JSONObject data, String fallback) {
        String name = null;
        if (data != null) {
            name = trimToNull(data.getString("title"));
        }
        if (!StringUtils.hasText(name) && nodeJson != null) {
            name = trimToNull(nodeJson.getString("label"));
        }
        return StringUtils.hasText(name) ? name : fallback;
    }

    private String trimToNull(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        return value.trim();
    }

    private String safe(String value) {
        return value == null ? "" : value;
    }

    private String shortError(Throwable throwable) {
        if (throwable == null) {
            return "unknown";
        }
        String message = throwable.getMessage();
        if (!StringUtils.hasText(message)) {
            return throwable.getClass().getSimpleName();
        }
        return message.length() > 120 ? message.substring(0, 120) + "..." : message;
    }

    private static class ParsedWorkflow {
        private List<NodeView> nodes = new ArrayList<>();
        private List<EdgeView> edges = new ArrayList<>();
        private Map<String, NodeView> nodeMap = new HashMap<>();
    }

    private static class NodeView {
        private String id;
        private String type;
        private String parentId;
        private String name;
        private JSONObject data;

        private boolean isRootLevel() {
            return !StringUtils.hasText(parentId);
        }
    }

    private static class EdgeView {
        private String id;
        private String source;
        private String target;
    }
}
