package com.agent.management.rag.graph;

import com.agent.management.rag.model.RagQuery;
import com.agent.management.service.GraphSourceService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;

@Component
@RequiredArgsConstructor
public class GraphQueryIntentPlanner {
    private static final Pattern IDENTIFIER = Pattern.compile("(?:[A-Z][A-Z0-9]*_)+(?:[A-Z0-9]+)");
    private static final int MAX_ROWS = 50;
    private final GraphSourceService graphs;
    private final GraphSchemaValidator validator;

    public Optional<PlannedCypher> plan(RagQuery query, Long graphSourceId, GraphSchemaSnapshot schema,
                                        GraphEntityGroundingService.GroundingResult grounding, int maxDepth) {
        if (!enabled(query)) return Optional.empty();
        String question = query.getQuery() == null ? "" : query.getQuery();
        String text = question.toLowerCase(Locale.ROOT);
        Optional<PlannedCypher> planned = modeEnabled(query, "RELATION_RANKING") ? relationRanking(question, text, schema, grounding) : Optional.empty();
        if (planned.isEmpty() && modeEnabled(query, "COUNT_RELATED")) planned = countRelated(question, text, schema);
        if (planned.isEmpty() && modeEnabled(query, "CAPABILITY_MATCHING")) planned = capabilityMatching(question, text, graphSourceId, schema, genericTerms(query));
        if (planned.isEmpty() && modeEnabled(query, "ENTITY_NEIGHBORHOOD")) planned = entityNeighborhood(question, text, schema, grounding);
        if (planned.isEmpty() && modeEnabled(query, "PROPERTY_FILTERED_EVIDENCE")) planned = propertyFilteredEvidence(question, text, graphSourceId, schema);
        planned.ifPresent(item -> validator.validate(item.cypher(), schema, maxDepth));
        return planned;
    }

    private Optional<PlannedCypher> relationRanking(String question, String text, GraphSchemaSnapshot schema,
                                                    GraphEntityGroundingService.GroundingResult grounding) {
        if (grounding.matches().isEmpty() || !containsAny(text, "预计算", "支援关系", "协防关系",
                "support", "ranking", "score", "评分", "排序", "排名", "最高", "top")) {
            return Optional.empty();
        }
        GraphEntityGroundingService.EntityMatch entity = grounding.matches().get(0);
        List<RankingBranch> branches = new ArrayList<>();
        for (GraphSchemaSnapshot.RelationshipSchema relationship : schema.relationships()) {
            Optional<String> scoreProperty = scoreProperty(relationship);
            if (scoreProperty.isEmpty()) continue;
            for (GraphSchemaSnapshot.RelationshipEndpoint endpoint : relationship.endpoints()) {
                for (String label : entity.labels()) {
                    if (label.equals(endpoint.endLabel())) {
                        branches.add(new RankingBranch(endpoint.startLabel(), relationship.type(), endpoint.endLabel(), true,
                                scoreProperty.get()));
                    } else if (label.equals(endpoint.startLabel())) {
                        branches.add(new RankingBranch(endpoint.endLabel(), relationship.type(), endpoint.startLabel(), false,
                                scoreProperty.get()));
                    }
                }
            }
        }
        if (branches.isEmpty()) return Optional.empty();
        String targetLabel = entity.labels().stream()
                .filter(label -> schema.node(label).map(node -> node.properties().containsKey(entity.property())).orElse(false))
                .findFirst().orElse(entity.labels().get(0));
        StringBuilder cypher = new StringBuilder();
        LinkedHashSet<String> seen = new LinkedHashSet<>();
        for (RankingBranch branch : branches) {
            String key = branch.otherLabel() + "|" + branch.relationship() + "|" + branch.incomingToEntity();
            if (!seen.add(key)) continue;
            if (!cypher.isEmpty()) cypher.append("\nUNION ALL\n");
            if (branch.incomingToEntity()) {
                cypher.append("MATCH (x:").append(id(branch.otherLabel())).append(")-[r:")
                        .append(id(branch.relationship())).append("]->(target:")
                        .append(id(targetLabel)).append(" {").append(id(entity.property())).append(": ")
                        .append(lit(entity.mention())).append("})\n");
            } else {
                cypher.append("MATCH (target:").append(id(targetLabel)).append(" {")
                        .append(id(entity.property())).append(": ").append(lit(entity.mention()))
                        .append("})-[r:").append(id(branch.relationship())).append("]->(x:")
                        .append(id(branch.otherLabel())).append(")\n");
            }
            String idExpression = property(branch.otherLabel(), schema, "id")
                    .map(prop -> "x." + id(prop)).orElse("null");
            String nameExpression = property(branch.otherLabel(), schema, "name")
                    .map(prop -> "x." + id(prop)).orElse(idExpression);
            cypher.append("RETURN x, r, target, labels(x) AS node_labels, ")
                    .append(idExpression).append(" AS id, ").append(nameExpression).append(" AS name, ")
                    .append("r.").append(id(branch.scoreProperty())).append(" AS score, ")
                    .append("r.label AS label, r.algorithm AS algorithm");
        }
        cypher.append("\nORDER BY score DESC\nLIMIT ").append(MAX_ROWS);
        return Optional.of(new PlannedCypher(cypher.toString(), "RELATION_RANKING",
                Map.of("entity", entity.toMetadata(), "question", question), 0.90));
    }

    private Optional<PlannedCypher> countRelated(String question, String text, GraphSchemaSnapshot schema) {
        if (!containsAny(text, "最多", "最少", "数量最多", "数量最少", "highest", "lowest", "most", "least")) {
            return Optional.empty();
        }
        String order = containsAny(text, "最少", "lowest", "least") ? "ASC" : "DESC";
        LabelScore target = bestLabel(question, schema, "target");
        if (target == null) return Optional.empty();
        List<CountCandidate> candidates = new ArrayList<>();
        for (GraphSchemaSnapshot.RelationshipSchema relationship : schema.relationships()) {
            for (GraphSchemaSnapshot.RelationshipEndpoint endpoint : relationship.endpoints()) {
                if (endpoint.endLabel().equals(target.label())) {
                    candidates.add(new CountCandidate(endpoint.startLabel(), target.label(), relationship.type(), true,
                            target.score() + labelScore(question, endpoint.startLabel(), "source")));
                } else if (endpoint.startLabel().equals(target.label())) {
                    candidates.add(new CountCandidate(endpoint.endLabel(), target.label(), relationship.type(), false,
                            target.score() + labelScore(question, endpoint.endLabel(), "source")));
                }
            }
        }
        CountCandidate best = candidates.stream().max(Comparator.comparingDouble(CountCandidate::score)).orElse(null);
        if (best == null || best.score() < 0.55) return Optional.empty();
        String relatedId = propertyOrFirst(best.targetLabel(), schema, "id").orElseThrow();
        String relatedName = property(best.targetLabel(), schema, "name").orElse(relatedId);
        String sourceId = propertyOrFirst(best.sourceLabel(), schema, "id").orElseThrow();
        String sourceName = property(best.sourceLabel(), schema, "name").orElse(sourceId);
        String pattern = best.outgoing()
                ? "(source:" + id(best.sourceLabel()) + ")-[rel:" + id(best.relationship()) + "]->(related:" + id(best.targetLabel()) + ")"
                : "(related:" + id(best.targetLabel()) + ")-[rel:" + id(best.relationship()) + "]->(source:" + id(best.sourceLabel()) + ")";
        String cypher = "MATCH " + pattern + "\n"
                + "WITH source, collect(DISTINCT related) AS relatedNodes, count(DISTINCT related) AS relatedCount\n"
                + "ORDER BY relatedCount " + order + ", source." + id(sourceId) + "\n"
                + "LIMIT 1\n"
                + "RETURN source, relatedNodes, [node IN relatedNodes | node." + id(relatedId) + "] AS relatedIds, "
                + "[node IN relatedNodes | node." + id(relatedName) + "] AS relatedNames, "
                + "source." + id(sourceId) + " AS sourceId, source." + id(sourceName) + " AS sourceName, relatedCount";
        return Optional.of(new PlannedCypher(cypher, "COUNT_RELATED",
                Map.of("sourceLabel", best.sourceLabel(), "targetLabel", best.targetLabel(),
                        "relationship", best.relationship(), "order", order), best.score()));
    }

    private Optional<PlannedCypher> capabilityMatching(String question, String text, Long graphSourceId,
                                                       GraphSchemaSnapshot schema, Set<String> genericTerms) {
        if (!containsAny(text, "能力", "匹配", "需要", "保障", "capability", "require", "support")) {
            return Optional.empty();
        }
        String capabilityLabel = schema.labelNames().stream()
                .filter(label -> splitCamel(label).toLowerCase(Locale.ROOT).contains("capability"))
                .findFirst().orElse(null);
        if (capabilityLabel == null) return Optional.empty();
        List<String> capabilityProperties = List.of("id", "name", "category").stream()
                .map(preferred -> property(capabilityLabel, schema, preferred))
                .flatMap(Optional::stream)
                .distinct().toList();
        if (capabilityProperties.isEmpty()) return Optional.empty();
        LinkedHashSet<String> terms = capabilityTerms(question, text, graphSourceId, capabilityLabel, capabilityProperties, genericTerms);
        if (terms.isEmpty()) return Optional.empty();
        List<CapabilityBranch> branches = new ArrayList<>();
        for (GraphSchemaSnapshot.RelationshipSchema relationship : schema.relationships()) {
            if (!splitCamel(relationship.type()).toLowerCase(Locale.ROOT).contains("capability")) continue;
            for (GraphSchemaSnapshot.RelationshipEndpoint endpoint : relationship.endpoints()) {
                if (endpoint.endLabel().equals(capabilityLabel)) {
                    branches.add(new CapabilityBranch(endpoint.startLabel(), relationship.type(), true));
                } else if (endpoint.startLabel().equals(capabilityLabel)) {
                    branches.add(new CapabilityBranch(endpoint.endLabel(), relationship.type(), false));
                }
            }
        }
        if (branches.isEmpty()) return Optional.empty();
        String condition = capabilityCondition("cap", capabilityProperties, terms);
        String capabilityId = property(capabilityLabel, schema, "id").orElse(capabilityProperties.get(0));
        String capabilityName = property(capabilityLabel, schema, "name").orElse(capabilityId);
        StringBuilder cypher = new StringBuilder();
        LinkedHashSet<String> seen = new LinkedHashSet<>();
        for (CapabilityBranch branch : branches) {
            String key = branch.entityLabel() + "|" + branch.relationship() + "|" + branch.outgoingToCapability();
            if (!seen.add(key)) continue;
            if (!cypher.isEmpty()) cypher.append("\nUNION ALL\n");
            if (branch.outgoingToCapability()) {
                cypher.append("MATCH (entity:").append(id(branch.entityLabel())).append(")-[rel:")
                        .append(id(branch.relationship())).append("]->(cap:")
                        .append(id(capabilityLabel)).append(")\n");
            } else {
                cypher.append("MATCH (cap:").append(id(capabilityLabel)).append(")-[rel:")
                        .append(id(branch.relationship())).append("]->(entity:")
                        .append(id(branch.entityLabel())).append(")\n");
            }
            String entityId = property(branch.entityLabel(), schema, "id")
                    .map(prop -> "entity." + id(prop)).orElse("null");
            String entityName = property(branch.entityLabel(), schema, "name")
                    .map(prop -> "entity." + id(prop)).orElse(entityId);
            cypher.append("WHERE ").append(condition).append("\n")
                    .append("WITH entity, collect(DISTINCT cap) AS matchedCapabilities, ")
                    .append("count(DISTINCT cap) AS matchedCount, ")
                    .append("avg(toFloat(coalesce(rel.level, rel.confidence, 0))) AS avgLevel\n")
                    .append("RETURN entity, matchedCapabilities, labels(entity) AS nodeLabels, ")
                    .append(entityId).append(" AS entityId, ").append(entityName).append(" AS entityName, ")
                    .append("[cap IN matchedCapabilities | cap.").append(id(capabilityId)).append("] AS capabilityIds, ")
                    .append("[cap IN matchedCapabilities | cap.").append(id(capabilityName)).append("] AS capabilityNames, ")
                    .append("matchedCount, avgLevel");
        }
        cypher.append("\nORDER BY matchedCount DESC, avgLevel DESC, entityId\nLIMIT ").append(MAX_ROWS);
        return Optional.of(new PlannedCypher(cypher.toString(), "CAPABILITY_MATCHING",
                Map.of("capabilityLabel", capabilityLabel, "terms", new ArrayList<>(terms),
                        "relationships", branches.stream().map(CapabilityBranch::relationship).distinct().toList()),
                Math.min(0.9, 0.55 + terms.size() * 0.08)));
    }

    private Optional<PlannedCypher> propertyFilteredEvidence(String question, String text, Long graphSourceId,
                                                             GraphSchemaSnapshot schema) {
        if (!containsAny(text, "状态", "公开", "证据", "未知", "维护", "available", "unknown", "public", "evidence", "status")) {
            return Optional.empty();
        }
        List<PropertyFilter> filters = filtersFromSampledValues(question, text, graphSourceId, schema);
        if (filters.isEmpty()) return Optional.empty();
        PropertyFilter primary = filters.get(0);
        Optional<DocumentEvidence> evidence = documentEvidence(schema, primary.label());
        String where = filters.stream()
                .filter(filter -> filter.label().equals(primary.label()))
                .map(filter -> "entity." + id(filter.property()) + " = " + lit(filter.value()))
                .reduce((left, right) -> left + " AND " + right).orElse("");
        if (where.isBlank()) return Optional.empty();
        StringBuilder cypher = new StringBuilder();
        if (evidence.isPresent()) {
            DocumentEvidence doc = evidence.get();
            if (doc.outgoingFromDocument()) {
                cypher.append("MATCH p=(doc:").append(id(doc.documentLabel())).append(")-[ev:")
                        .append(id(doc.relationship())).append("]->(entity:").append(id(primary.label())).append(")\n");
            } else {
                cypher.append("MATCH p=(entity:").append(id(primary.label())).append(")-[ev:")
                        .append(id(doc.relationship())).append("]->(doc:").append(id(doc.documentLabel())).append(")\n");
            }
        } else {
            cypher.append("MATCH (entity:").append(id(primary.label())).append(")\n");
        }
        cypher.append("WHERE ").append(where).append("\n")
                .append("RETURN ").append(evidence.isPresent() ? "p, doc, ev, " : "")
                .append("entity, entity.id AS entityId, entity.name AS name LIMIT ").append(MAX_ROWS);
        return Optional.of(new PlannedCypher(cypher.toString(), "PROPERTY_FILTERED_EVIDENCE",
                Map.of("filters", filters.stream().map(PropertyFilter::toMetadata).toList()), 0.82));
    }

    private Optional<PlannedCypher> entityNeighborhood(String question, String text, GraphSchemaSnapshot schema,
                                                       GraphEntityGroundingService.GroundingResult grounding) {
        if (grounding.matches().isEmpty()) return Optional.empty();
        if (containsAny(text, "最多", "最少", "排名", "排序", "评分", "score", "top")) return Optional.empty();
        if (!containsAny(text, "关联", "相关", "周边", "有哪些", "是什么", "状态", "部署", "链", "证据", "能力")) {
            return Optional.empty();
        }
        GraphEntityGroundingService.EntityMatch entity = grounding.matches().get(0);
        String entityLabel = entity.labels().stream()
                .filter(label -> schema.node(label).map(node -> node.properties().containsKey(entity.property())).orElse(false))
                .findFirst().orElse(null);
        if (entityLabel == null) return Optional.empty();
        List<Neighbor> neighbors = neighbors(schema, new LinkedHashSet<>(entity.labels()));
        if (neighbors.isEmpty()) return Optional.empty();
        StringBuilder cypher = new StringBuilder("MATCH (n:").append(id(entityLabel)).append(" {")
                .append(id(entity.property())).append(": ").append(lit(entity.mention())).append("})\n");
        List<String> returns = new ArrayList<>(List.of("n"));
        for (int i = 0; i < neighbors.size(); i++) {
            Neighbor neighbor = neighbors.get(i);
            String path = "p" + (i + 1);
            String node = "m" + (i + 1);
            cypher.append("OPTIONAL MATCH ").append(path).append("=");
            if (neighbor.outgoing()) {
                cypher.append("(n)-[:").append(id(neighbor.relationship())).append("]->(")
                        .append(node).append(":").append(id(neighbor.otherLabel())).append(")\n");
            } else {
                cypher.append("(").append(node).append(":").append(id(neighbor.otherLabel()))
                        .append(")-[:").append(id(neighbor.relationship())).append("]->(n)\n");
            }
            returns.add(path);
        }
        cypher.append("RETURN ").append(String.join(", ", returns)).append(" LIMIT ").append(MAX_ROWS);
        return Optional.of(new PlannedCypher(cypher.toString(), "ENTITY_NEIGHBORHOOD",
                Map.of("entity", entity.toMetadata(), "expandedRelationships",
                        neighbors.stream().map(Neighbor::relationship).distinct().toList()), 0.88));
    }

    private List<PropertyFilter> filtersFromSampledValues(String question, String text, Long graphSourceId,
                                                          GraphSchemaSnapshot schema) {
        List<PropertyFilter> result = new ArrayList<>();
        for (GraphSchemaSnapshot.NodeSchema node : schema.nodes()) {
            for (String property : node.properties().keySet()) {
                if (!isFilterProperty(property, text)) continue;
                for (String value : sampleValues(graphSourceId, node.label(), property)) {
                    if (valueMatchesQuestion(value, question, text)) {
                        result.add(new PropertyFilter(node.label(), property, value));
                    }
                }
            }
        }
        return result.stream().distinct().limit(4).toList();
    }

    @SuppressWarnings("unchecked")
    private List<String> sampleValues(Long graphSourceId, String label, String property) {
        try {
            String cypher = "MATCH (n:" + id(label) + ") WHERE n." + id(property)
                    + " IS NOT NULL RETURN DISTINCT n." + id(property) + " AS value LIMIT 100";
            Object records = graphs.executeQuery(graphSourceId, cypher).get("records");
            if (!(records instanceof Collection<?> rows)) return List.of();
            List<String> values = new ArrayList<>();
            for (Object row : rows) {
                if (row instanceof Map<?, ?> map && map.get("value") != null) {
                    values.add(String.valueOf(map.get("value")));
                }
            }
            return values;
        } catch (Exception ignored) {
            return List.of();
        }
    }

    private static Optional<DocumentEvidence> documentEvidence(GraphSchemaSnapshot schema, String entityLabel) {
        List<String> documentLabels = schema.labelNames().stream()
                .filter(label -> label.toLowerCase(Locale.ROOT).contains("document")
                        || label.toLowerCase(Locale.ROOT).contains("evidence")
                        || label.equalsIgnoreCase("Doc")).toList();
        for (String documentLabel : documentLabels) {
            for (GraphSchemaSnapshot.RelationshipSchema relationship : schema.relationships()) {
                for (GraphSchemaSnapshot.RelationshipEndpoint endpoint : relationship.endpoints()) {
                    if (endpoint.startLabel().equals(documentLabel) && endpoint.endLabel().equals(entityLabel)) {
                        return Optional.of(new DocumentEvidence(documentLabel, relationship.type(), true));
                    }
                    if (endpoint.endLabel().equals(documentLabel) && endpoint.startLabel().equals(entityLabel)) {
                        return Optional.of(new DocumentEvidence(documentLabel, relationship.type(), false));
                    }
                }
            }
        }
        return Optional.empty();
    }

    private static List<Neighbor> neighbors(GraphSchemaSnapshot schema, Set<String> entityLabels) {
        LinkedHashSet<Neighbor> result = new LinkedHashSet<>();
        for (GraphSchemaSnapshot.RelationshipSchema relationship : schema.relationships()) {
            for (GraphSchemaSnapshot.RelationshipEndpoint endpoint : relationship.endpoints()) {
                if (entityLabels.contains(endpoint.startLabel())) {
                    result.add(new Neighbor(relationship.type(), endpoint.endLabel(), true));
                } else if (entityLabels.contains(endpoint.endLabel())) {
                    result.add(new Neighbor(relationship.type(), endpoint.startLabel(), false));
                }
                if (result.size() >= 8) return new ArrayList<>(result);
            }
        }
        return new ArrayList<>(result);
    }

    private static LabelScore bestLabel(String question, GraphSchemaSnapshot schema, String role) {
        return schema.labelNames().stream()
                .map(label -> new LabelScore(label, labelScore(question, label, role)))
                .max(Comparator.comparingDouble(LabelScore::score))
                .filter(score -> score.score() >= 0.25).orElse(null);
    }

    private static double labelScore(String question, String label, String role) {
        String text = question == null ? "" : question.toLowerCase(Locale.ROOT);
        String normalized = splitCamel(label).toLowerCase(Locale.ROOT);
        double score = 0;
        for (String token : normalized.split("[^a-z0-9]+")) {
            if (!token.isBlank() && text.contains(token)) score += 0.35;
        }
        if (normalized.contains("sensor") && containsAny(text, "传感器", "雷达", "sensor")) score += role.equals("target") ? 1.0 : 0.4;
        if (normalized.contains("document") && containsAny(text, "文档", "证据", "document", "evidence")) score += role.equals("target") ? 0.8 : 0.5;
        if ((normalized.contains("site") || normalized.contains("region") || normalized.contains("area") || normalized.contains("location"))
                && containsAny(text, "部署", "区域", "地点", "位置", "site", "region")) score += role.equals("target") ? 0.8 : 0.5;
        if (normalized.contains("capability") && containsAny(text, "能力", "capability")) score += role.equals("target") ? 0.8 : 0.5;
        if ((normalized.contains("system") || normalized.contains("equipment") || normalized.contains("platform"))
                && containsAny(text, "系统", "装备", "平台", "候选", "system", "equipment", "platform")) score += role.equals("source") ? 1.0 : 0.3;
        if ((normalized.contains("ship") || normalized.contains("carrier") || normalized.contains("platform"))
                && containsAny(text, "舰", "船", "航母", "ship", "carrier")) score += 0.7;
        return score;
    }

    private static Optional<String> property(String label, GraphSchemaSnapshot schema, String preferred) {
        return schema.node(label).flatMap(node -> node.properties().keySet().stream()
                .filter(name -> name.equalsIgnoreCase(preferred) || name.toLowerCase(Locale.ROOT).endsWith("_" + preferred))
                .findFirst());
    }

    private static Optional<String> propertyOrFirst(String label, GraphSchemaSnapshot schema, String preferred) {
        Optional<String> matched = property(label, schema, preferred);
        if (matched.isPresent()) return matched;
        return schema.node(label).flatMap(node -> node.properties().keySet().stream().findFirst());
    }

    private static boolean isFilterProperty(String property, String text) {
        String name = property.toLowerCase(Locale.ROOT);
        return (name.contains("status") && containsAny(text, "状态", "unknown", "ready", "维护", "status"))
                || ((name.contains("nature") || name.contains("type") || name.contains("category"))
                && containsAny(text, "公开", "public", "模拟", "类型", "类别"));
    }

    private static boolean valueMatchesQuestion(String value, String question, String text) {
        String normalized = value.toLowerCase(Locale.ROOT);
        if (text.contains(normalized)) return true;
        if (normalized.contains("unknown") && containsAny(text, "未知", "unknown")) return true;
        if (normalized.contains("public") && containsAny(text, "公开", "public")) return true;
        if (normalized.contains("maintenance") && containsAny(text, "维护", "maintenance")) return true;
        if (normalized.contains("ready") && containsAny(text, "可用", "就绪", "ready")) return true;
        if (normalized.contains("simulated") && containsAny(text, "模拟", "simulated")) return true;
        return question != null && question.contains(value);
    }

    private static Optional<String> scoreProperty(GraphSchemaSnapshot.RelationshipSchema relationship) {
        return relationship.properties().keySet().stream().filter(property -> {
            String name = property.toLowerCase(Locale.ROOT);
            return name.equals("score") || name.endsWith("_score") || name.contains("rank")
                    || name.contains("priority") || name.equals("weight") || name.equals("rating");
        }).findFirst();
    }

    private LinkedHashSet<String> capabilityTerms(String question, String text, Long graphSourceId,
                                                  String capabilityLabel, List<String> properties,
                                                  Set<String> genericTerms) {
        LinkedHashSet<String> result = new LinkedHashSet<>();
        for (String property : properties) {
            for (String value : sampleValues(graphSourceId, capabilityLabel, property)) {
                result.addAll(overlappingTerms(value, question, text, genericTerms));
            }
        }
        return result;
    }

    private static List<String> overlappingTerms(String value, String question, String text, Set<String> genericTerms) {
        if (value == null || value.isBlank()) return List.of();
        LinkedHashSet<String> result = new LinkedHashSet<>();
        String normalized = value.toLowerCase(Locale.ROOT);
        if (text.contains(normalized) && normalized.length() >= 2) result.add(value);
        java.util.regex.Matcher han = Pattern.compile("\\p{IsHan}+").matcher(value);
        while (han.find()) {
            String run = han.group();
            if (question != null && question.contains(run) && !genericTerms.contains(run)) result.add(run);
            for (int i = 0; i < run.length() - 1; i++) {
                String term = run.substring(i, i + 2);
                if (question != null && question.contains(term) && !genericTerms.contains(term)) result.add(term);
            }
        }
        for (String token : normalized.split("[^a-z0-9]+")) {
            if (token.length() >= 3 && text.contains(token)) result.add(token);
        }
        return new ArrayList<>(result);
    }

    private static String capabilityCondition(String variable, List<String> properties, Set<String> terms) {
        List<String> parts = new ArrayList<>();
        for (String property : properties) {
            for (String term : terms) {
                if (containsHan(term)) {
                    parts.add(variable + "." + id(property) + " CONTAINS " + lit(term));
                } else {
                    parts.add("toLower(toString(" + variable + "." + id(property) + ")) CONTAINS "
                            + lit(term.toLowerCase(Locale.ROOT)));
                }
            }
        }
        return "(" + String.join(" OR ", parts) + ")";
    }

    private static boolean containsHan(String value) {
        return value != null && Pattern.compile("\\p{IsHan}").matcher(value).find();
    }

    private static boolean enabled(RagQuery query) {
        Object value = query.getFilters() == null ? null : query.getFilters().get("enableGenericGraphPlanner");
        return !Boolean.FALSE.equals(value);
    }

    private static boolean modeEnabled(RagQuery query, String mode) {
        Object value = query.getFilters() == null ? null : query.getFilters().get("enabledGenericQueryModes");
        if (!(value instanceof List<?> list) || list.isEmpty()) return true;
        return list.stream().map(String::valueOf).anyMatch(mode::equalsIgnoreCase);
    }

    private static Set<String> genericTerms(RagQuery query) {
        Object value = query.getFilters() == null ? null : query.getFilters().get("genericTerms");
        if (value instanceof List<?> list && !list.isEmpty()) {
            return list.stream().map(String::valueOf)
                    .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
        }
        return Set.of("保障", "补给", "支援", "能力", "任务", "平台", "模拟", "直接", "匹配", "组合",
                "淇濋殰", "琛ョ粰", "鏀彺", "鑳藉姏", "浠诲姟", "骞冲彴", "妯℃嫙", "鐩存帴", "鍖归厤", "缁勫悎");
    }
    private static String id(String value) {
        return "`" + value.replace("`", "``") + "`";
    }

    private static String lit(String value) {
        return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'";
    }

    private static String splitCamel(String value) {
        return value == null ? "" : value.replaceAll("([a-z])([A-Z])", "$1 $2").replace('_', ' ');
    }

    private static boolean containsAny(String text, String... values) {
        for (String value : values) {
            if (text.contains(value)) return true;
        }
        return false;
    }

    public record PlannedCypher(String cypher, String generationMode, Map<String, Object> intentPlan,
                                double groundingConfidence) {
        public PlannedCypher {
            groundingConfidence = Math.max(0, Math.min(1, groundingConfidence));
        }
    }
    private record RankingBranch(String otherLabel, String relationship, String entityLabel, boolean incomingToEntity,
                                 String scoreProperty) {}
    private record CountCandidate(String sourceLabel, String targetLabel, String relationship, boolean outgoing, double score) {}
    private record CapabilityBranch(String entityLabel, String relationship, boolean outgoingToCapability) {}
    private record LabelScore(String label, double score) {}
    private record PropertyFilter(String label, String property, String value) {
        Map<String, Object> toMetadata() {
            return Map.of("label", label, "property", property, "value", value);
        }
    }
    private record DocumentEvidence(String documentLabel, String relationship, boolean outgoingFromDocument) {}
    private record Neighbor(String relationship, String otherLabel, boolean outgoing) {}
}
