package com.ruoyi.ccdi.project.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.dto.CcdiFundGraphEdgeDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiFundGraphManualEdgeSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiFundGraphQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphEdgeVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphNodeVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphStatementVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphVO;
import com.ruoyi.ccdi.project.mapper.CcdiFundGraphMapper;
import com.ruoyi.ccdi.project.service.ICcdiFundGraphService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * 资金流图谱Service实现
 */
@Service
public class CcdiFundGraphServiceImpl implements ICcdiFundGraphService {

    private static final int DEFAULT_LIMIT = 20;
    private static final int MAX_LIMIT = 100;
    private static final BigDecimal DEFAULT_MIN_TOTAL_AMOUNT = new BigDecimal("1000");
    private static final Comparator<CcdiFundGraphEdgeVO> EDGE_COMPARATOR = Comparator
        .comparing(CcdiFundGraphServiceImpl::safeAmount, Comparator.reverseOrder())
        .thenComparing(CcdiFundGraphServiceImpl::safeTransactionCount, Comparator.reverseOrder())
        .thenComparing(CcdiFundGraphServiceImpl::safeDateText, Comparator.reverseOrder())
        .thenComparing(edge -> normalizeSortText(edge == null ? null : edge.getEdgeKey()));

    @Resource
    private CcdiFundGraphMapper fundGraphMapper;

    @Override
    public List<CcdiFundGraphNodeVO> searchSubjects(CcdiFundGraphQueryDTO queryDTO) {
        CcdiFundGraphQueryDTO query = normalizeGraphQuery(queryDTO);
        if (isBlank(query.getKeyword()) && isBlank(query.getObjectKey())) {
            return Collections.emptyList();
        }
        return selectSubjects(query);
    }

    @Override
    public CcdiFundGraphVO getFundGraph(CcdiFundGraphQueryDTO queryDTO) {
        CcdiFundGraphQueryDTO query = normalizeGraphQuery(queryDTO);
        CcdiFundGraphNodeVO centerNode = resolveCenterNode(query);
        if (centerNode == null || isBlank(centerNode.getObjectKey())) {
            return new CcdiFundGraphVO();
        }

        query.setObjectKey(centerNode.getObjectKey());
        List<CcdiFundGraphEdgeVO> edges = new ArrayList<>();
        List<CcdiFundGraphEdgeVO> realEdges = fundGraphMapper.selectFundGraphEdges(query);
        if (realEdges != null) {
            edges.addAll(realEdges);
        }
        List<CcdiFundGraphEdgeVO> manualEdges = fundGraphMapper.selectFundGraphManualEdges(query);
        if (manualEdges != null) {
            edges.addAll(manualEdges);
        }
        edges = sortAndLimitEdges(edges, query.getLimit());
        CcdiFundGraphVO graph = new CcdiFundGraphVO();
        graph.setCenterNode(centerNode);
        graph.setEdges(edges);
        graph.setNodes(buildNodes(centerNode, edges));
        graph.setTransactionCount(edges.stream()
            .map(CcdiFundGraphEdgeVO::getTransactionCount)
            .filter(item -> item != null)
            .reduce(0L, Long::sum));
        graph.setTotalAmount(edges.stream()
            .map(CcdiFundGraphEdgeVO::getTotalAmount)
            .filter(item -> item != null)
            .reduce(BigDecimal.ZERO, BigDecimal::add));
        graph.setMaxDepth(1);
        graph.setTraceReserved(true);
        return graph;
    }

    @Override
    public Page<CcdiFundGraphStatementVO> getEdgeDetails(
        Page<CcdiFundGraphStatementVO> page,
        CcdiFundGraphEdgeDetailQueryDTO queryDTO
    ) {
        CcdiFundGraphEdgeDetailQueryDTO query = normalizeDetailQuery(queryDTO);
        if (isBlank(query.getFromKey()) || isBlank(query.getToKey())) {
            return page;
        }
        return fundGraphMapper.selectFundGraphEdgeDetails(page, query);
    }

    @Override
    public CcdiFundGraphEdgeVO saveManualEdge(CcdiFundGraphManualEdgeSaveDTO saveDTO, String operator) {
        CcdiFundGraphManualEdgeSaveDTO dto = normalizeManualEdge(saveDTO);
        String fromObjectKey = dto.getFromObjectKey();
        String toObjectKey = resolveManualToObjectKey(dto);
        dto.setToObjectKey(toObjectKey);

        String edgeObjectKey = md5("MANUAL_EDGE|" + fromObjectKey + "|" + toObjectKey + "|" + dto.getDirection()
            + "|" + UUID.randomUUID());
        fundGraphMapper.insertManualEdge(edgeObjectKey, dto, normalizeText(operator));

        CcdiFundGraphEdgeVO edge = new CcdiFundGraphEdgeVO();
        edge.setEdgeKey(edgeObjectKey);
        edge.setFromKey(toSubjectKey(fromObjectKey));
        edge.setToKey(toSubjectKey(toObjectKey));
        edge.setFromObjectKey(fromObjectKey);
        edge.setToObjectKey(toObjectKey);
        edge.setFromName(dto.getFromName());
        edge.setToName(dto.getToName());
        edge.setTotalAmount(dto.getAmount());
        edge.setTransactionCount(dto.getTransactionCount() == null ? 1L : dto.getTransactionCount().longValue());
        edge.setDirection(dto.getDirection());
        edge.setRelationDesc(dto.getRelationDesc());
        edge.setSourceDesc(dto.getSourceDesc());
        edge.setRemark(dto.getRemark());
        edge.setSourceType("MANUAL");
        edge.setDepth(1);
        edge.setCanTrace(false);
        return edge;
    }

    private CcdiFundGraphNodeVO resolveCenterNode(CcdiFundGraphQueryDTO query) {
        if (isBlank(query.getObjectKey()) && isBlank(query.getKeyword())) {
            return null;
        }
        List<CcdiFundGraphNodeVO> subjects = selectSubjects(query);
        if (subjects == null || subjects.isEmpty()) {
            return null;
        }
        return subjects.get(0);
    }

    private List<CcdiFundGraphNodeVO> selectSubjects(CcdiFundGraphQueryDTO query) {
        if (query == null) {
            return Collections.emptyList();
        }
        if (!isBlank(query.getObjectKey())) {
            return fundGraphMapper.selectFundGraphSubjects(query);
        }
        if (isBlank(query.getKeyword())) {
            return Collections.emptyList();
        }
        List<CcdiFundGraphNodeVO> exactSubjects = fundGraphMapper.selectFundGraphSubjectsByExactKeyword(query);
        if (exactSubjects != null && !exactSubjects.isEmpty()) {
            return exactSubjects;
        }
        return fundGraphMapper.selectFundGraphSubjectsByName(query);
    }

    private List<CcdiFundGraphNodeVO> buildNodes(CcdiFundGraphNodeVO centerNode, List<CcdiFundGraphEdgeVO> edges) {
        Map<String, CcdiFundGraphNodeVO> nodeMap = new LinkedHashMap<>();
        Map<String, CcdiFundGraphNodeVO> subjectCache = new LinkedHashMap<>();
        subjectCache.put(centerNode.getObjectKey(), centerNode);
        addNode(nodeMap, centerNode, centerNode.getNodeKey(), centerNode.getObjectKey(), centerNode.getNodeName(),
            centerNode.getRelationType(), centerNode.getCanExpand(), BigDecimal.ZERO, 0L);

        for (CcdiFundGraphEdgeVO edge : edges) {
            String centerObjectKey = centerNode.getObjectKey();
            String fromRelationType = centerObjectKey != null && centerObjectKey.equals(edge.getFromObjectKey())
                ? null
                : edge.getFamilyRelationType();
            String toRelationType = centerObjectKey != null && centerObjectKey.equals(edge.getToObjectKey())
                ? null
                : edge.getFamilyRelationType();
            addNode(nodeMap, lookupSubject(edge.getFromObjectKey(), subjectCache), edge.getFromKey(),
                edge.getFromObjectKey(), edge.getFromName(), fromRelationType, true, edge.getTotalAmount(),
                edge.getTransactionCount());
            addNode(nodeMap, lookupSubject(edge.getToObjectKey(), subjectCache), edge.getToKey(),
                edge.getToObjectKey(), edge.getToName(), toRelationType, edge.getCanTrace(),
                edge.getTotalAmount(), edge.getTransactionCount());
        }
        return List.copyOf(nodeMap.values());
    }

    private CcdiFundGraphNodeVO lookupSubject(String objectKey, Map<String, CcdiFundGraphNodeVO> subjectCache) {
        if (isBlank(objectKey)) {
            return null;
        }
        if (subjectCache.containsKey(objectKey)) {
            return subjectCache.get(objectKey);
        }
        CcdiFundGraphQueryDTO query = new CcdiFundGraphQueryDTO();
        query.setObjectKey(objectKey);
        query.setLimit(DEFAULT_LIMIT);
        List<CcdiFundGraphNodeVO> subjects = fundGraphMapper.selectFundGraphSubjects(query);
        CcdiFundGraphNodeVO subject = subjects == null || subjects.isEmpty() ? null : subjects.get(0);
        subjectCache.put(objectKey, subject);
        return subject;
    }

    private String resolveManualToObjectKey(CcdiFundGraphManualEdgeSaveDTO dto) {
        if (!isBlank(dto.getToObjectKey())) {
            ensureManualSubject(dto.getToObjectKey(), dto.getToIdNo(), dto.getToName());
            return dto.getToObjectKey();
        }
        String objectKey = !isBlank(dto.getToIdNo())
            ? md5(dto.getToIdNo())
            : md5("MANUAL_NODE|" + dto.getToName() + "|" + UUID.randomUUID());
        dto.setToObjectKey(objectKey);
        ensureManualSubject(objectKey, dto.getToIdNo(), dto.getToName());
        return objectKey;
    }

    private void ensureManualSubject(String objectKey, String idNo, String name) {
        if (fundGraphMapper.countSubjectByObjectKey(objectKey) > 0) {
            return;
        }
        fundGraphMapper.insertManualSubject(objectKey, normalizeText(idNo), normalizeText(name));
    }

    private CcdiFundGraphManualEdgeSaveDTO normalizeManualEdge(CcdiFundGraphManualEdgeSaveDTO saveDTO) {
        CcdiFundGraphManualEdgeSaveDTO dto = saveDTO == null ? new CcdiFundGraphManualEdgeSaveDTO() : saveDTO;
        dto.setFromObjectKey(normalizeText(dto.getFromObjectKey()));
        dto.setFromName(normalizeText(dto.getFromName()));
        dto.setToObjectKey(normalizeText(dto.getToObjectKey()));
        dto.setToName(normalizeText(dto.getToName()));
        dto.setToIdNo(normalizeText(dto.getToIdNo()));
        dto.setDirection(normalizeText(dto.getDirection()));
        dto.setRelationDesc(normalizeText(dto.getRelationDesc()));
        dto.setSourceDesc(normalizeText(dto.getSourceDesc()));
        dto.setRemark(normalizeText(dto.getRemark()));
        if (isBlank(dto.getFromObjectKey())) {
            throw new IllegalArgumentException("起点主体不能为空");
        }
        if (isBlank(dto.getToObjectKey()) && isBlank(dto.getToName())) {
            throw new IllegalArgumentException("终点主体不能为空");
        }
        if (isBlank(dto.getDirection())) {
            dto.setDirection("1");
        }
        if (dto.getAmount() == null) {
            dto.setAmount(BigDecimal.ZERO);
        }
        if (dto.getTransactionCount() == null || dto.getTransactionCount() <= 0) {
            dto.setTransactionCount(1);
        }
        return dto;
    }

    private void addNode(
        Map<String, CcdiFundGraphNodeVO> nodeMap,
        CcdiFundGraphNodeVO subject,
        String nodeKey,
        String objectKey,
        String nodeName,
        String relationType,
        Boolean canExpand,
        BigDecimal edgeAmount,
        Long edgeCount
    ) {
        if (isBlank(nodeKey)) {
            return;
        }
        CcdiFundGraphNodeVO node = nodeMap.computeIfAbsent(nodeKey, key -> {
            CcdiFundGraphNodeVO item = new CcdiFundGraphNodeVO();
            item.setNodeKey(key);
            item.setObjectKey(objectKey);
            item.setNodeName(subject != null && !isBlank(subject.getNodeName())
                ? subject.getNodeName()
                : (isBlank(nodeName) ? "未知主体" : nodeName));
            item.setIdNo(subject == null ? null : subject.getIdNo());
            item.setCinocsno(subject == null ? null : subject.getCinocsno());
            item.setIdnoType(subject == null ? null : subject.getIdnoType());
            item.setStaffId(subject == null ? null : subject.getStaffId());
            item.setSourceType(subject == null ? null : subject.getSourceType());
            item.setNodeType(subject != null && !isBlank(subject.getNodeType()) ? subject.getNodeType() : "PERSON");
            item.setIdentityType(subject != null && !isBlank(subject.getIdentityType()) ? subject.getIdentityType() : "IDNO");
            item.setRelationType(relationType);
            item.setAccountCount(subject == null ? 0L : subject.getAccountCount());
            item.setCreatedTime(subject == null ? null : subject.getCreatedTime());
            item.setUpdatedTime(subject == null ? null : subject.getUpdatedTime());
            item.setCanExpand(Boolean.TRUE.equals(canExpand));
            item.setDepth(1);
            item.setTotalAmount(BigDecimal.ZERO);
            item.setTransactionCount(0L);
            return item;
        });
        if (isBlank(node.getObjectKey())) {
            node.setObjectKey(objectKey);
        }
        if (isBlank(node.getRelationType())) {
            node.setRelationType(relationType);
        }
        if (Boolean.TRUE.equals(canExpand)) {
            node.setCanExpand(true);
        }
        node.setTotalAmount(node.getTotalAmount().add(edgeAmount == null ? BigDecimal.ZERO : edgeAmount));
        node.setTransactionCount(node.getTransactionCount() + (edgeCount == null ? 0L : edgeCount));
    }

    private CcdiFundGraphQueryDTO normalizeGraphQuery(CcdiFundGraphQueryDTO queryDTO) {
        CcdiFundGraphQueryDTO query = queryDTO == null ? new CcdiFundGraphQueryDTO() : queryDTO;
        query.setKeyword(normalizeText(query.getKeyword()));
        query.setObjectKey(normalizeText(query.getObjectKey()));
        query.setTransactionStartTime(normalizeText(query.getTransactionStartTime()));
        query.setTransactionEndTime(normalizeText(query.getTransactionEndTime()));
        query.setDirection(normalizeText(query.getDirection()));
        if (query.getMinTotalAmount() == null) {
            query.setMinTotalAmount(DEFAULT_MIN_TOTAL_AMOUNT);
        }
        query.setLimit(normalizeLimit(query.getLimit()));
        query.setDepth(1);
        return query;
    }

    private CcdiFundGraphEdgeDetailQueryDTO normalizeDetailQuery(CcdiFundGraphEdgeDetailQueryDTO queryDTO) {
        CcdiFundGraphEdgeDetailQueryDTO query = queryDTO == null ? new CcdiFundGraphEdgeDetailQueryDTO() : queryDTO;
        query.setKeyword(normalizeText(query.getKeyword()));
        query.setFromKey(normalizeText(query.getFromKey()));
        query.setToKey(normalizeText(query.getToKey()));
        query.setDirection(normalizeText(query.getDirection()));
        query.setTransactionStartTime(normalizeText(query.getTransactionStartTime()));
        query.setTransactionEndTime(normalizeText(query.getTransactionEndTime()));
        return query;
    }

    private Integer normalizeLimit(Integer limit) {
        if (limit == null || limit <= 0) {
            return DEFAULT_LIMIT;
        }
        return Math.min(limit, MAX_LIMIT);
    }

    private List<CcdiFundGraphEdgeVO> sortAndLimitEdges(List<CcdiFundGraphEdgeVO> edges, Integer limit) {
        if (edges == null || edges.isEmpty()) {
            return Collections.emptyList();
        }
        List<CcdiFundGraphEdgeVO> sorted = new ArrayList<>(edges);
        sorted.sort(EDGE_COMPARATOR);
        int finalLimit = normalizeLimit(limit);
        if (sorted.size() > finalLimit) {
            return List.copyOf(sorted.subList(0, finalLimit));
        }
        return List.copyOf(sorted);
    }

    private String normalizeText(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }

    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }

    private String toSubjectKey(String objectKey) {
        return "idno_node/" + objectKey;
    }

    private String md5(String value) {
        return DigestUtils.md5DigestAsHex(value.trim().getBytes(StandardCharsets.UTF_8));
    }

    private static BigDecimal safeAmount(CcdiFundGraphEdgeVO edge) {
        return edge == null || edge.getTotalAmount() == null ? BigDecimal.ZERO : edge.getTotalAmount();
    }

    private static Long safeTransactionCount(CcdiFundGraphEdgeVO edge) {
        return edge == null || edge.getTransactionCount() == null ? 0L : edge.getTransactionCount();
    }

    private static String safeDateText(CcdiFundGraphEdgeVO edge) {
        return normalizeSortText(edge == null ? null : edge.getLastTrxDate());
    }

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