package com.agent.management.service;

import com.agent.management.common.exception.BusinessException;
import com.agent.management.config.Neo4jProperties;
import com.agent.management.model.entity.GraphSource;
import com.agent.management.rag.graph.GraphSchemaSnapshot;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.neo4j.driver.AuthTokens;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.neo4j.driver.Record;
import org.neo4j.driver.Session;
import org.neo4j.driver.TransactionConfig;
import org.neo4j.driver.Value;
import org.neo4j.driver.exceptions.ServiceUnavailableException;
import org.neo4j.driver.exceptions.Neo4jException;
import org.neo4j.driver.SessionConfig;
import org.springframework.stereotype.Service;
import jakarta.annotation.PreDestroy;
import org.neo4j.driver.types.Node;
import org.neo4j.driver.types.Path;
import org.neo4j.driver.types.Relationship;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Neo4j 动态 Driver 管理 + Cypher 执行
 *
 * <p>使用 LRU 策略缓存 Driver（accessOrder=true），最大 5 个。
 * 调用方按 graphId 维度获取 driver；更新/删除 GraphSource 时由 Service 层调用 {@link #invalidate}。</p>
 *
 * <p>降级策略：Driver 创建时不预连接，调用 {@link #executeQuery} 时才真正建连，
 * ServiceUnavailableException 转换为业务异常返回前端。</p>
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class Neo4jExecutorService {

    private static final int MAX_DRIVERS = 5;
    /** collectValue 递归深度上限，防止恶意构造的深度嵌套 LIST 导致 StackOverflow */
    private static final int MAX_RECURSION_DEPTH = 10;

    private final Neo4jProperties props;

    /**
     * LRU 缓存：graphId -> Driver
     */
    private final Map<Long, Driver> driverCache =
            Collections.synchronizedMap(new LinkedHashMap<Long, Driver>(16, 0.75f, true) {
                @Override
                protected boolean removeEldestEntry(Map.Entry<Long, Driver> eldest) {
                    if (size() > MAX_DRIVERS) {
                        log.info("Driver 缓存超过 {} 上限，关闭 eldest: graphId={}", MAX_DRIVERS, eldest.getKey());
                        try { eldest.getValue().close(); } catch (Exception e) {
                            log.warn("关闭旧 Driver 失败: {}", e.getMessage());
                        }
                        return true;
                    }
                    return false;
                }
            });

    /**
     * 测试连接（创建临时 driver，验证后关闭）
     *
     * @param uri           Neo4j bolt uri
     * @param user          用户名
     * @param plainPassword 明文密码
     * @param database      数据库名（可空）
     */
    public void testConnection(String uri, String user, String plainPassword, String database) {
        if (uri == null || uri.isBlank()) throw new BusinessException("Neo4j URI 不能为空");
        Driver driver = GraphDatabase.driver(uri, AuthTokens.basic(user, plainPassword));
        try {
            driver.verifyConnectivity();
            log.info("Neo4j 连接测试成功: uri={}", uri);
        } catch (ServiceUnavailableException e) {
            throw new BusinessException("Neo4j 连接失败：" + e.getMessage());
        } catch (Neo4jException e) {
            throw new BusinessException("Neo4j 认证或授权失败：" + e.getMessage());
        } finally {
            try { driver.close(); } catch (Exception e) { /* 静默 */ }
        }
    }

    /**
     * 执行只读 Cypher 查询，返回 cytoscape elements 格式
     *
     * @param gs     已通过 CredentialManager.decryptToTransient 解密好 decryptedPassword 的 GraphSource
     * @param cypher 已通过 CypherGuardService 校验
     * @return { nodes: [...], edges: [...], truncated: bool, durationMs: long }
     */
    public Map<String, Object> executeQuery(GraphSource gs, String cypher) {
        Driver driver = getOrCreateDriver(gs);
        long t0 = System.currentTimeMillis();

        // 用 LinkedHashMap 去重（elementId 唯一）
        Map<String, Map<String, Object>> nodes = new LinkedHashMap<>();
        Map<String, Map<String, Object>> edges = new LinkedHashMap<>();
        List<Map<String, Object>> records = new ArrayList<>();

        TransactionConfig.Builder txConfigBuilder = TransactionConfig.builder();
        if (props.getQueryTimeoutSeconds() > 0) {
            txConfigBuilder.withTimeout(Duration.ofSeconds(props.getQueryTimeoutSeconds()));
        }
        TransactionConfig txConfig = txConfigBuilder.build();

        try (var session = newSession(driver, gs.getDatabase())) {
            var result = session.run(cypher, txConfig);
            for (Record record : result.list()) {
                Map<String, Object> row = new LinkedHashMap<>();
                for (var pair : record.fields()) {
                    collectValue(pair.value(), nodes, edges, 0);
                    row.put(pair.key(), toSerializableValue(pair.value(), 0));
                }
                records.add(row);
                // 提前截断检查
                if (nodes.size() > props.getMaxNodes() || edges.size() > props.getMaxEdges()) {
                    break;
                }
            }
        } catch (ServiceUnavailableException e) {
            throw new BusinessException("Neo4j 服务不可用：" + e.getMessage());
        } catch (Neo4jException e) {
            throw new BusinessException("Cypher 执行失败：" + e.getMessage());
        }

        boolean truncated = nodes.size() > props.getMaxNodes() || edges.size() > props.getMaxEdges();
        // 截断到上限
        List<Map<String, Object>> nodeList = truncate(new ArrayList<>(nodes.values()), props.getMaxNodes());
        List<Map<String, Object>> edgeList = truncate(new ArrayList<>(edges.values()), props.getMaxEdges());

        long durationMs = System.currentTimeMillis() - t0;

        Map<String, Object> response = new LinkedHashMap<>();
        response.put("nodes", nodeList);
        response.put("edges", edgeList);
        response.put("records", records);
        response.put("truncated", truncated);
        response.put("durationMs", durationMs);
        log.info("GraphSource id={} 查询完成：{} nodes / {} edges in {} ms",
                gs.getId(), nodeList.size(), edgeList.size(), durationMs);
        return response;
    }

    /**
     * 列出图谱的所有 node labels（用于表单提示）
     */
    public List<String> listLabels(GraphSource gs) {
        Driver driver = getOrCreateDriver(gs);
        try (var session = newSession(driver, gs.getDatabase())) {
            var result = session.run("CALL db.labels() YIELD label RETURN label ORDER BY label");
            List<String> labels = new ArrayList<>();
            while (result.hasNext()) {
                labels.add(result.next().get("label").asString());
            }
            return labels;
        } catch (Neo4jException e) {
            throw new BusinessException("获取 labels 失败：" + e.getMessage());
        }
    }

    public GraphSchemaSnapshot discoverSchema(GraphSource gs) {
        Driver driver = getOrCreateDriver(gs);
        Map<String, Map<String, List<String>>> nodeProperties = new TreeMap<>();
        Map<String, Map<String, List<String>>> relationshipProperties = new TreeMap<>();
        Map<String, Set<GraphSchemaSnapshot.RelationshipEndpoint>> endpoints = new TreeMap<>();
        try (var session = newSession(driver, gs.getDatabase())) {
            var nodes = session.run("CALL db.schema.nodeTypeProperties() " +
                    "YIELD nodeLabels, propertyName, propertyTypes " +
                    "RETURN nodeLabels, propertyName, propertyTypes");
            while (nodes.hasNext()) {
                Record record = nodes.next();
                for (String label : record.get("nodeLabels").asList(Value::asString)) {
                    Map<String, List<String>> properties = nodeProperties.computeIfAbsent(label, ignored -> new TreeMap<>());
                    if (!record.get("propertyName").isNull()) {
                        properties.put(record.get("propertyName").asString(),
                                record.get("propertyTypes").asList(Value::asString));
                    }
                }
            }

            var relationshipProps = session.run("CALL db.schema.relTypeProperties() " +
                    "YIELD relType, propertyName, propertyTypes " +
                    "RETURN relType, propertyName, propertyTypes");
            while (relationshipProps.hasNext()) {
                Record record = relationshipProps.next();
                String type = normalizeSchemaToken(record.get("relType").asString());
                relationshipProperties.computeIfAbsent(type, ignored -> new TreeMap<>());
                if (!record.get("propertyName").isNull()) {
                    relationshipProperties.get(type).put(record.get("propertyName").asString(),
                            record.get("propertyTypes").asList(Value::asString));
                }
            }

            var directions = session.run("MATCH (start)-[relationship]->(end) " +
                    "UNWIND labels(start) AS startLabel UNWIND labels(end) AS endLabel " +
                    "RETURN DISTINCT type(relationship) AS relationshipType, startLabel, endLabel");
            while (directions.hasNext()) {
                Record record = directions.next();
                String type = record.get("relationshipType").asString();
                endpoints.computeIfAbsent(type, ignored -> new LinkedHashSet<>())
                        .add(new GraphSchemaSnapshot.RelationshipEndpoint(
                                record.get("startLabel").asString(), record.get("endLabel").asString()));
                relationshipProperties.computeIfAbsent(type, ignored -> new TreeMap<>());
            }
        } catch (Neo4jException e) {
            throw new BusinessException("获取图 Schema 失败：" + e.getMessage());
        }

        List<GraphSchemaSnapshot.NodeSchema> nodes = nodeProperties.entrySet().stream()
                .map(entry -> new GraphSchemaSnapshot.NodeSchema(entry.getKey(), entry.getValue()))
                .toList();
        List<GraphSchemaSnapshot.RelationshipSchema> relationships = relationshipProperties.entrySet().stream()
                .map(entry -> {
                    List<GraphSchemaSnapshot.RelationshipEndpoint> pairs =
                            new ArrayList<>(endpoints.getOrDefault(entry.getKey(), Set.of()));
                    return new GraphSchemaSnapshot.RelationshipSchema(entry.getKey(),
                            pairs.stream().map(GraphSchemaSnapshot.RelationshipEndpoint::startLabel).distinct().toList(),
                            pairs.stream().map(GraphSchemaSnapshot.RelationshipEndpoint::endLabel).distinct().toList(),
                            entry.getValue(), pairs);
                })
                .toList();
        return new GraphSchemaSnapshot(nodes, relationships);
    }

    public Map<String,Object> discoverMetadata(GraphSource gs) {
        Driver driver = getOrCreateDriver(gs);
        try (var session = newSession(driver, gs.getDatabase())) {
            List<Map<String,Object>> constraints = session.run("SHOW CONSTRAINTS YIELD name, type, labelsOrTypes, properties RETURN name, type, labelsOrTypes, properties")
                    .list(record -> record.asMap(Value::asObject));
            List<Map<String,Object>> indexes = session.run("SHOW INDEXES YIELD name, type, entityType, labelsOrTypes, properties, state RETURN name, type, entityType, labelsOrTypes, properties, state")
                    .list(record -> record.asMap(Value::asObject));
            List<Map<String,Object>> counts = session.run("MATCH (n) UNWIND labels(n) AS label RETURN label, count(*) AS count ORDER BY count DESC")
                    .list(record -> record.asMap(Value::asObject));
            Record totals = session.run("MATCH (n) WITH count(n) AS nodeCount MATCH ()-[r]->() RETURN nodeCount, count(r) AS relationshipCount").single();
            return Map.of(
                    "constraints", constraints,
                    "indexes", indexes,
                    "labelCounts", counts,
                    "nodeCount", totals.get("nodeCount").asLong(),
                    "relationshipCount", totals.get("relationshipCount").asLong());
        } catch (Neo4jException e) { throw new BusinessException("获取图索引/约束失败：" + e.getMessage()); }
    }

    private static String normalizeSchemaToken(String value) {
        return value == null ? "" : value.replace("`", "").replace(":", "").trim();
    }

    /**
     * 关闭并清除指定图谱的 driver
     */
    public void invalidate(Long graphId) {
        Driver driver = driverCache.remove(graphId);
        if (driver != null) {
            try {
                driver.close();
                log.info("已关闭 graphId={} 的 driver", graphId);
            } catch (Exception e) {
                log.warn("关闭 driver 失败: {}", e.getMessage());
            }
        }
    }

    @PreDestroy
    public void destroy() {
        closeAll();
    }

    public void closeAll() {
        synchronized (driverCache) {
            for (Map.Entry<Long, Driver> e : driverCache.entrySet()) {
                try { e.getValue().close(); } catch (Exception ex) { /* 静默 */ }
            }
            driverCache.clear();
        }
    }

    // =========================== 内部 ===========================

    private Session newSession(Driver driver, String database) {
        if (database == null || database.isBlank()) {
            return driver.session();
        }
        return driver.session(SessionConfig.forDatabase(database));
    }

    private Driver getOrCreateDriver(GraphSource gs) {
        return driverCache.computeIfAbsent(gs.getId(), id -> {
            // decryptedPassword 已由 GraphSourceService.decryptToTransient 写入
            String plain = gs.getDecryptedPassword();
            if (plain == null || plain.isEmpty()) {
                throw new BusinessException("图谱源密码未解密，无法创建 Driver");
            }
            log.info("为 graphId={} 创建 Neo4j Driver: {}", id, gs.getUri());
            return GraphDatabase.driver(gs.getUri(), AuthTokens.basic(gs.getUsername(), plain));
        });
    }

    private void collectValue(Value value,
                               Map<String, Map<String, Object>> nodes,
                               Map<String, Map<String, Object>> edges,
                               int depth) {
        if (depth > MAX_RECURSION_DEPTH) {
            log.warn("collectValue 超过最大递归深度 {}，跳过嵌套值", MAX_RECURSION_DEPTH);
            return;
        }
        // 通过 type name 区分，避免依赖内部 *Value 类
        String typeName;
        try {
            typeName = value.type().name();
        } catch (Exception e) {
            return;
        }
        switch (typeName) {
            case "NODE" -> putNode(value.asNode(), nodes);
            case "RELATIONSHIP" -> putRelationship(value.asRelationship(), edges);
            case "PATH" -> {
                Path path = value.asPath();
                for (Node n : path.nodes()) putNode(n, nodes);
                for (Relationship r : path.relationships()) putRelationship(r, edges);
            }
            case "LIST" -> {
                for (Value inner : value.asList(v -> v)) {
                    collectValue(inner, nodes, edges, depth + 1);
                }
            }
            default -> { /* 其他类型忽略 */ }
        }
    }

    private Object toSerializableValue(Value value, int depth) {
        if (depth > MAX_RECURSION_DEPTH || value == null || value.isNull()) return null;
        String typeName;
        try {
            typeName = value.type().name();
        } catch (Exception e) {
            return null;
        }
        return switch (typeName) {
            case "NODE" -> nodeData(value.asNode());
            case "RELATIONSHIP" -> relationshipData(value.asRelationship());
            case "PATH" -> {
                Path path = value.asPath();
                List<Map<String, Object>> pathNodes = new ArrayList<>();
                List<Map<String, Object>> pathEdges = new ArrayList<>();
                path.nodes().forEach(node -> pathNodes.add(nodeData(node)));
                path.relationships().forEach(rel -> pathEdges.add(relationshipData(rel)));
                yield Map.of("nodes", pathNodes, "edges", pathEdges);
            }
            case "LIST" -> value.asList(item -> toSerializableValue(item, depth + 1));
            case "MAP" -> value.asMap(item -> toSerializableValue(item, depth + 1));
            default -> value.asObject();
        };
    }

    private void putNode(Node node, Map<String, Map<String, Object>> nodes) {
        String id = node.elementId();
        if (nodes.containsKey(id)) return;
        nodes.put(id, nodeData(node));
    }

    private Map<String, Object> nodeData(Node node) {
        Map<String, Object> data = new LinkedHashMap<>();
        String id = node.elementId();
        data.put("id", id);
        // label：取第一个 label 或回退到 id
        List<String> labels = new ArrayList<>();
        for (String l : node.labels()) labels.add(l);
        data.put("labels", labels);
        data.put("label", !labels.isEmpty() ? labels.get(0) : ("node-" + node.id()));
        // properties 中寻找合适的显示文本
        Map<String, Object> props = node.asMap();
        Object display = pickDisplay(props);
        if (display != null) data.put("label", String.valueOf(display));
        data.put("properties", props);
        return data;
    }

    private void putRelationship(Relationship rel, Map<String, Map<String, Object>> edges) {
        String id = rel.elementId();
        if (edges.containsKey(id)) return;
        edges.put(id, relationshipData(rel));
    }

    private Map<String, Object> relationshipData(Relationship rel) {
        Map<String, Object> data = new LinkedHashMap<>();
        String id = rel.elementId();
        data.put("id", id);
        data.put("source", rel.startNodeElementId());
        data.put("target", rel.endNodeElementId());
        data.put("label", rel.type());
        data.put("type", rel.type());
        data.put("properties", rel.asMap());
        return data;
    }

    /** 优先选 name / title / label 作为显示 */
    private Object pickDisplay(Map<String, Object> props) {
        for (String k : new String[]{"name", "title", "label", "displayName"}) {
            if (props.containsKey(k) && props.get(k) != null) return props.get(k);
        }
        return null;
    }

    private <T> List<T> truncate(List<T> list, int max) {
        if (list.size() <= max) return list;
        return new ArrayList<>(list.subList(0, max));
    }
}
