package com.arms.api.analysis.scope.service;


import com.arms.api.analysis.scope.model.dto.ScopeDTO;
import com.arms.api.analysis.scope.model.enums.IssueSort;
import com.arms.api.analysis.scope.model.vo.*;
import com.arms.api.issue.almapi.model.entity.AlmIssueEntity;
import com.arms.api.util.ParseUtil;
import com.arms.api.util.model.dto.PdServiceAndIsReqDTO;
import com.arms.egovframework.javaservice.esframework.esquery.SimpleQuery;
import com.arms.egovframework.javaservice.esframework.esquery.filter.RangeQueryFilter;
import com.arms.egovframework.javaservice.esframework.model.dto.esquery.SortDTO;
import com.arms.egovframework.javaservice.esframework.model.dto.esquery.SubGroupFieldDTO;
import com.arms.egovframework.javaservice.esframework.model.dto.request.AggregationRequestDTO;
import com.arms.egovframework.javaservice.esframework.model.vo.DocumentAggregations;
import com.arms.egovframework.javaservice.esframework.model.vo.DocumentBucket;
import com.arms.egovframework.javaservice.esframework.repository.common.EsCommonRepositoryWrapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.util.*;
import java.util.stream.Collectors;

import static com.arms.egovframework.javaservice.esframework.esquery.SimpleQuery.aggregation;
import static com.arms.egovframework.javaservice.esframework.esquery.SimpleQuery.termQueryMust;
@Slf4j
@Service("analysisScope")
@AllArgsConstructor
public class AnalysisScopeImpl implements AnalysisScope {

    private final EsCommonRepositoryWrapper<AlmIssueEntity> esCommonRepositoryWrapper;

    @Override
    public List<TreeBarIssueVO> treeBarDataV2(ScopeDTO scopeDTO) {

        Long pdServiceId = scopeDTO.getPdServiceAndIsReq().getPdServiceId();

        // 1. 데이터 조회
        List<AlmIssueEntity> reqIssues = retrieveAssignedIssuesByIsReq(scopeDTO, true);
        List<AlmIssueEntity> notReqIssues = retrieveAssignedIssuesByIsReq(scopeDTO, false);

        // 2. 이슈 분류 (한 번의 순회로 처리)
        IssueClassification classification = classifyIssues(reqIssues, notReqIssues, pdServiceId);

        // 3. 이슈 카운트 집계 (최적화된 로직)
        Map<Long, Map<String, AssigneeIssueCountVO>> reqLinkIssueCountMap = aggregateIssueCounts(classification);

        if (reqLinkIssueCountMap.isEmpty()) {
            return Collections.emptyList();
        }

        // 4. 결과 변환 및 정렬
        return convertToTreeBarIssueVOList(reqLinkIssueCountMap, scopeDTO.getTopN());
    }

    /**
     * 이슈를 제품 서비스 ID와 타입에 따라 분류
     */
    private IssueClassification classifyIssues(
            List<AlmIssueEntity> reqIssues,
            List<AlmIssueEntity> notReqIssues,
            Long pdServiceId) {

        Map<String, AlmIssueEntity> rightReqIssues = new HashMap<>();
        Map<String, AlmIssueEntity> rightSubTaskIssues = new HashMap<>();
        Map<String, AlmIssueEntity> linkedIssues = new HashMap<>();

        // 요구사항 이슈 분류
        for (AlmIssueEntity issue : reqIssues) {
            if (pdServiceId.equals(issue.getPdServiceId())) {
                rightReqIssues.put(issue.getRecentId(), issue);
            } else {
                linkedIssues.put(issue.getRecentId(), issue);
            }
        }

        // 하위 이슈 분류
        for (AlmIssueEntity issue : notReqIssues) {
            if (issue.getPdServiceId() == null || issue.getCReqLink() == null) {
                linkedIssues.put(issue.getRecentId(), issue);
            } else if (pdServiceId.equals(issue.getPdServiceId())) {
                rightSubTaskIssues.put(issue.getRecentId(), issue);
            } else {
                linkedIssues.put(issue.getRecentId(), issue);
            }
        }

        return new IssueClassification(rightReqIssues, rightSubTaskIssues, linkedIssues);
    }

    /**
     * 이슈 카운트 집계 - 중복 제거 로직 포함 (assignee 가 있을 경우)
     */
    private Map<Long, Map<String, AssigneeIssueCountVO>> aggregateIssueCounts(
            IssueClassification classification) {

        Map<Long, Map<String, AssigneeIssueCountVO>> result = new HashMap<>();
        Map<Long, Set<String>> processedIssues = new HashMap<>();

        // 1. 요구사항 이슈 처리
        for (AlmIssueEntity issue : classification.rightReqIssues.values()) {
            if (tryAddIssue(processedIssues, issue.getCReqLink(), issue.getRecentId())) {
                incrementCount(result, issue.getCReqLink(), issue.getAssignee(), IssueSort.REQISSUE);
            }
        }

        // 2. 하위 이슈 처리
        for (AlmIssueEntity issue : classification.rightSubTaskIssues.values()) {
            Long cReqLink = issue.getCReqLink();
            String recentId = issue.getRecentId();

            if (tryAddIssue(processedIssues, cReqLink, recentId)) {
                incrementCount(result, cReqLink, issue.getAssignee(), IssueSort.SUBTASK);

                // 연결 이슈 처리
                processLinkedIssuesForSubTask(issue, classification, result, processedIssues);
            }
        }

        // 3. 생성 연결 이슈 처리
        processCreatedLinkedIssues(classification, result, processedIssues);

        return result;
    }

    /**
     * 하위 이슈의 연결 이슈 처리
     */
    private void processLinkedIssuesForSubTask(
            AlmIssueEntity subTaskIssue,
            IssueClassification classification,
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Map<Long, Set<String>> processedIssues) {

        if (ObjectUtils.isEmpty(subTaskIssue.getLinkedIssues())) {
            return;
        }

        for (String linkedIssueId : subTaskIssue.getLinkedIssues()) {
            Long targetReqLink = findReqLinkForLinkedIssue(
                    linkedIssueId,
                    classification.rightReqIssues,
                    classification.rightSubTaskIssues
            );

            if (targetReqLink != null &&
                    tryAddIssue(processedIssues, targetReqLink, subTaskIssue.getRecentId())) {
                incrementCount(result, targetReqLink, subTaskIssue.getAssignee(), IssueSort.LINKEDISSUE);
            }
        }
    }

    /**
     * 생성 연결 이슈 및 그 하위 이슈 처리
     */
    private void processCreatedLinkedIssues(
            IssueClassification classification,
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Map<Long, Set<String>> processedIssues) {

        for (AlmIssueEntity linkedIssue : classification.linkedIssues.values()) {
            boolean isCreatedLink = ObjectUtils.isEmpty(linkedIssue.getCReqLink());

            if (!isCreatedLink) {
                continue; // 다른 제품의 이슈는 스킵
            }

            // 생성 연결 이슈 처리
            if (ObjectUtils.isEmpty(linkedIssue.getParentReqKey())) {
                processCreatedLinkIssue(linkedIssue, classification, result, processedIssues);
            }
            // 생성 연결 이슈의 하위 이슈 처리
            else {
                processCreatedLinkSubIssue(linkedIssue, classification, result, processedIssues);
            }
        }
    }

    /**
     * 생성 연결 이슈의 연결 처리
     */
    private void processCreatedLinkIssue(
            AlmIssueEntity createdLinkIssue,
            IssueClassification classification,
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Map<Long, Set<String>> processedIssues) {

        if (ObjectUtils.isEmpty(createdLinkIssue.getLinkedIssues())) {
            return;
        }

        for (String linkedIssueId : createdLinkIssue.getLinkedIssues()) {
            Long targetReqLink = findReqLinkForLinkedIssue(
                    linkedIssueId,
                    classification.rightReqIssues,
                    classification.rightSubTaskIssues
            );

            if (targetReqLink != null &&
                    tryAddIssue(processedIssues, targetReqLink, createdLinkIssue.getRecentId())) {
                incrementCount(result, targetReqLink, createdLinkIssue.getAssignee(), IssueSort.LINKEDISSUE);
            }
        }
    }

    /**
     * 생성 연결 이슈의 하위 이슈 처리
     */
    private void processCreatedLinkSubIssue(
            AlmIssueEntity subIssue,
            IssueClassification classification,
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Map<Long, Set<String>> processedIssues) {

        String topLevelIssueId = ParseUtil.getPrefixIncludingLastDelimiter(subIssue.getRecentId())
                + subIssue.getParentReqKey();

        AlmIssueEntity topLevelIssue = classification.linkedIssues.get(topLevelIssueId);
        if (topLevelIssue == null) {
            return;
        }

        // Top-level 이슈의 연결 처리
        processIssueLinks(topLevelIssue, subIssue, classification, result, processedIssues);

        // 자신의 연결 처리
        processIssueLinks(subIssue, subIssue, classification, result, processedIssues);
    }

    /**
     * 이슈의 연결 처리 (공통 로직)
     */
    private void processIssueLinks(
            AlmIssueEntity sourceIssue,
            AlmIssueEntity targetIssue,
            IssueClassification classification,
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Map<Long, Set<String>> processedIssues) {

        if (ObjectUtils.isEmpty(sourceIssue.getLinkedIssues())) {
            return;
        }

        for (String linkedIssueId : sourceIssue.getLinkedIssues()) {
            Long targetReqLink = findReqLinkForLinkedIssue(
                    linkedIssueId,
                    classification.rightReqIssues,
                    classification.rightSubTaskIssues
            );

            if (targetReqLink != null &&
                    tryAddIssue(processedIssues, targetReqLink, targetIssue.getRecentId())) {
                incrementCount(result, targetReqLink, targetIssue.getAssignee(), IssueSort.LINKEDISSUE);
            }
        }
    }

    /**
     * 연결 이슈의 cReqLink 찾기
     */
    private Long findReqLinkForLinkedIssue(
            String linkedIssueId,
            Map<String, AlmIssueEntity> reqIssues,
            Map<String, AlmIssueEntity> subTaskIssues) {

        AlmIssueEntity issue = reqIssues.get(linkedIssueId);
        if (issue == null) {
            issue = subTaskIssues.get(linkedIssueId);
        }

        return issue != null ? issue.getCReqLink() : null;
    }

    /**
     * 이슈 추가 시도 (중복 체크)
     * @return true if successfully added, false if already exists
     */
    private boolean tryAddIssue(Map<Long, Set<String>> processedIssues, Long cReqLink, String recentId) {
        Set<String> issueSet = processedIssues.computeIfAbsent(cReqLink, k -> new HashSet<>());
        return issueSet.add(recentId);
    }

    /**
     * 카운트 증가
     */
    private void incrementCount(
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Long cReqLink,
            AlmIssueEntity.Assignee assignee,
            IssueSort sort) {

        result.computeIfAbsent(cReqLink, k -> new HashMap<>())
                .compute(assignee.getAccountId(), (accountId, vo) -> {
                    if (vo == null) {
                        return createNewAssigneeVO(assignee, sort);
                    }
                    updateAssigneeVO(vo, sort);
                    return vo;
                });
    }

    /**
     * 새로운 AssigneeVO 생성
     */
    private AssigneeIssueCountVO createNewAssigneeVO(AlmIssueEntity.Assignee assignee, IssueSort sort) {
        return AssigneeIssueCountVO.builder()
                .name(assignee.getDisplayName())
                .accountId(assignee.getAccountId())
                .emailAddress(assignee.getEmailAddress() != null ? assignee.getEmailAddress() : "")
                .totalCount(1L)
                .reqIssueCount(sort == IssueSort.REQISSUE ? 1L : 0L)
                .subTaskCount(sort == IssueSort.SUBTASK ? 1L : 0L)
                .linkedIssueCount(sort == IssueSort.LINKEDISSUE ? 1L : 0L)
                .build();
    }

    /**
     * AssigneeVO 업데이트
     */
    private void updateAssigneeVO(AssigneeIssueCountVO vo, IssueSort sort) {
        switch (sort) {
            case REQISSUE:
                vo.addReqIssueCount(1L);
                break;
            case SUBTASK:
                vo.addSubTaskCount(1L);
                break;
            case LINKEDISSUE:
                vo.addLinkedIssueCount(1L);
                break;
        }
    }

    /**
     * 이슈 분류 결과를 담는 내부 클래스
     */
    private static class IssueClassification {
        final Map<String, AlmIssueEntity> rightReqIssues;
        final Map<String, AlmIssueEntity> rightSubTaskIssues;
        final Map<String, AlmIssueEntity> linkedIssues;

        IssueClassification(
                Map<String, AlmIssueEntity> rightReqIssues,
                Map<String, AlmIssueEntity> rightSubTaskIssues,
                Map<String, AlmIssueEntity> linkedIssues) {
            this.rightReqIssues = rightReqIssues;
            this.rightSubTaskIssues = rightSubTaskIssues;
            this.linkedIssues = linkedIssues;
        }
    }

    @Override
    public List<TreeBarIssueVO> treeBarData(ScopeDTO scopeDTO){

        Map<Long, Map<String, AssigneeIssueCountVO>> reqLinkIssueCountVOList = new HashMap<>();
        Map<Long, Set<String>> reqLinkRecentIdSetMap = new HashMap<>(); // 넣었는지 카운트 하고 체크하려는 의도

        PdServiceAndIsReqDTO pdServiceAndIsReq = scopeDTO.getPdServiceAndIsReq();
        Long pdServiceId = pdServiceAndIsReq.getPdServiceId();

        List<AlmIssueEntity> reqIssues = retrieveAssignedIssuesByIsReq(scopeDTO, true); // 요구사항 이슈, 연결이슈로 묶인 요구사항 이슈
        List<AlmIssueEntity> notReqIssues = retrieveAssignedIssuesByIsReq(scopeDTO, false); // 하위이슈& 연결이슈와 그 하위이슈

        Map<String, AlmIssueEntity> rightReqIssueMap = new HashMap<>();
        Map<String, AlmIssueEntity> rightSubTaskIssueMap = new HashMap<>();
        Map<String, AlmIssueEntity> linkedIssueMap = new HashMap<>();

        // 1.1 (이슈 분리) 요구사항 이슈 / 다른 제품의 요구사항이슈
        for (AlmIssueEntity reqIssue1 : reqIssues) {
            // 다른 제품서비스의 요구사항 이슈 -> 격리
            if (!reqIssue1.getPdServiceId().equals(pdServiceId)) {
                linkedIssueMap.computeIfAbsent(reqIssue1.getRecentId(), k -> reqIssue1);
                continue;
            }
            rightReqIssueMap.computeIfAbsent(reqIssue1.getRecentId(), k -> reqIssue1);
        }
        // 1.2 (이슈 분리) 하위이슈 / 연결이슈, 다른 제품의 하위이슈
        for (AlmIssueEntity notReqIssue1 : notReqIssues) {
            // 자체생성 이슈, 자체생성 이슈의 하위이슈
            if (notReqIssue1.getPdServiceId() == null
                    || notReqIssue1.getCReqLink() == null) {
                linkedIssueMap.computeIfAbsent(notReqIssue1.getRecentId(), k -> notReqIssue1);
                continue;
            }
            // 연결된 이슈지만, 다른 제품인 하위이슈 -> 격리
            if (!notReqIssue1.getPdServiceId().equals(pdServiceId)) {
                linkedIssueMap.computeIfAbsent(notReqIssue1.getRecentId(), k -> notReqIssue1);
                continue;
            }
            rightSubTaskIssueMap.computeIfAbsent(notReqIssue1.getRecentId(), k -> notReqIssue1);
        }

        // 2.1 요구사항 이슈
        for (AlmIssueEntity reqIssue : rightReqIssueMap.values()) {

            AlmIssueEntity.Assignee assignee = reqIssue.getAssignee();
            Long cReqLink = reqIssue.getCReqLink();
            String recentId = reqIssue.getRecentId();

            //처리 여부 체크
            Set<String> existingSet = reqLinkRecentIdSetMap.getOrDefault(cReqLink, new HashSet<>());
            if (existingSet.contains(recentId)) { // 이미 cReqLink가 갖고있는 이슈임 (처리된 이슈)
                continue;
            }
            if (existingSet.isEmpty()) {
                reqLinkRecentIdSetMap.put(cReqLink, existingSet);
            }
            existingSet.add(recentId);
            //.처리 여부 체크

            processReqLinkAssigneeIssueCount(reqLinkIssueCountVOList, cReqLink, assignee, IssueSort.REQISSUE);

        }

        // 2.2 subtask
        for (AlmIssueEntity notReqIssue : rightSubTaskIssueMap.values()) {
            // 여기서부턴 Just 하위이슈
            AlmIssueEntity.Assignee assignee = notReqIssue.getAssignee();
            Long cReqLink = notReqIssue.getCReqLink();
            String recentId = notReqIssue.getRecentId();

            // 처리 여부 체크
            Set<String> existingSet = reqLinkRecentIdSetMap.getOrDefault(cReqLink, new HashSet<>());
            if (existingSet.contains(recentId)) { // 이미 cReqLink가 갖고있는 이슈임 (처리된 이슈)
                continue;
            }
            if (existingSet.isEmpty()) {
                reqLinkRecentIdSetMap.put(cReqLink, existingSet);
            }
            existingSet.add(recentId);
            //.처리 여부 체크

            processReqLinkAssigneeIssueCount(reqLinkIssueCountVOList, cReqLink, assignee, IssueSort.SUBTASK);

            //
            if(!ObjectUtils.isEmpty(notReqIssue.getLinkedIssues())) {
                notReqIssue.getLinkedIssues().forEach(linkedIssueRecentId -> {
                    // 연결이슈가 요구사항 이슈임.
                    if(rightReqIssueMap.get(linkedIssueRecentId) != null) {
                        AlmIssueEntity fetchedIssue = rightReqIssueMap.get(linkedIssueRecentId);
                        Long reqLinkOfIssue = fetchedIssue.getCReqLink();

                        Set<String> recentIdSet = reqLinkRecentIdSetMap.getOrDefault(reqLinkOfIssue,new HashSet<>());
                        if(!ObjectUtils.isEmpty(recentIdSet) && !recentIdSet.contains(recentId)) {
                            processReqLinkAssigneeIssueCount(reqLinkIssueCountVOList, reqLinkOfIssue, assignee, IssueSort.LINKEDISSUE);
                            recentIdSet.add(recentId); // cReqLink 에 이 이슈가 카운트 됨. OK. (subtask 이든, linkedIssue 든 카운트 됨)
                        }
                    }
                    // 연결이슈가 하위이슈임.
                    if(rightSubTaskIssueMap.get(linkedIssueRecentId) != null) {
                        AlmIssueEntity fetchedIssue = rightSubTaskIssueMap.get(linkedIssueRecentId);
                        Long reqLinkOfIssue = fetchedIssue.getCReqLink();
                        //  작업 후속. cReqLink 당 중복을 제거한 이슈들이 들어가야 하므로 recentIdSet.add(recentId) 는 유의미함.
                        Set<String> recentIdSet = reqLinkRecentIdSetMap.getOrDefault(reqLinkOfIssue, new HashSet<>());
                        if(!ObjectUtils.isEmpty(recentIdSet) && !recentIdSet.contains(recentId)) {
                            processReqLinkAssigneeIssueCount(reqLinkIssueCountVOList, reqLinkOfIssue, assignee, IssueSort.LINKEDISSUE);
                            recentIdSet.add(recentId); // cReqLink 에 이 이슈가 카운트 됨. OK. (subtask 이든, linkedIssue 든 카운트 됨)
                        }
                    }

                    // 연결이슈가 - 생성 연결이슈임 -> 처리하지 않음. 생성 연결이슈는 cReqLink 를 갖지 않으므로.
                });
            }

        } //.subtask

        // 연결이슈들 :: 생성 연결 or 생성 연결의 하위 or (연결)다른 제품 요구사항 or 다른 제품 하위이슈
        for (AlmIssueEntity linkedIssue : linkedIssueMap.values()) {
            AlmIssueEntity.Assignee assignee = linkedIssue.getAssignee();
            String recentId = linkedIssue.getRecentId();
            // 처리 여부 체크 (existingSet) -> 들어가지 않음  ( ∵ cReqLink 있어도, 다른 제품의 cReqLink, 없는건 생성이슈 )

            // 생성 연결 - cReqLink 없음, parentReqKey 없음
            if (ObjectUtils.isEmpty(linkedIssue.getCReqLink()) && ObjectUtils.isEmpty(linkedIssue.getParentReqKey())) {

                // 자신의 연결이슈 찾아서 - count
                if (!ObjectUtils.isEmpty(linkedIssue.getLinkedIssues())) {
                    linkedIssue.getLinkedIssues().forEach(linkedIssueRecentId -> {
                        // 연결이슈가 요구사항 이슈임.
                        if(rightReqIssueMap.get(linkedIssueRecentId) != null) {
                            AlmIssueEntity fetchedIssue = rightReqIssueMap.get(linkedIssueRecentId);
                            Long reqLinkOfIssue = fetchedIssue.getCReqLink();

                            Set<String> recentIdSet = reqLinkRecentIdSetMap.getOrDefault(reqLinkOfIssue, new HashSet<>());
                            if(!recentIdSet.contains(recentId)) {
                                processReqLinkAssigneeIssueCount(reqLinkIssueCountVOList, reqLinkOfIssue, assignee, IssueSort.LINKEDISSUE);
                                recentIdSet.add(recentId); // cReqLink 에 이 이슈가 카운트 됨. OK. (subtask 이든, linkedIssue 든 카운트 됨)
                            }
                        }
                        // 연결이슈가 하위이슈임.
                        if(rightSubTaskIssueMap.get(linkedIssueRecentId) != null) {
                            AlmIssueEntity fetchedIssue = rightSubTaskIssueMap.get(linkedIssueRecentId);
                            Long reqLinkOfIssue = fetchedIssue.getCReqLink();
                            //  작업 후속. cReqLink 당 중복을 제거한 이슈들이 들어가야 하므로 recentIdSet.add(recentId) 는 유의미함.
                            Set<String> recentIdSet = reqLinkRecentIdSetMap.getOrDefault(reqLinkOfIssue, new HashSet<>());
                            if(!recentIdSet.contains(recentId)) {
                                processReqLinkAssigneeIssueCount(reqLinkIssueCountVOList, reqLinkOfIssue, assignee, IssueSort.LINKEDISSUE);
                                recentIdSet.add(recentId); // cReqLink 에 이 이슈가 카운트 됨. OK. (subtask 이든, linkedIssue 든 카운트 됨)
                            }
                        }
                    });
                }
            }

            // 생성의 하위
            if (ObjectUtils.isEmpty(linkedIssue.getCReqLink()) && !ObjectUtils.isEmpty(linkedIssue.getParentReqKey())) {
                String topLevelLinkedIssueRecentId = ParseUtil.getPrefixIncludingLastDelimiter(recentId) + linkedIssue.getParentReqKey();
                // 자신의 top-이슈 찾아서 - count
                if (linkedIssueMap.get(topLevelLinkedIssueRecentId) != null) {
                    AlmIssueEntity topLevelLinkIssue = linkedIssueMap.get(topLevelLinkedIssueRecentId);
                    // topLevel 의 연결이슈들 처리.
                    if (!ObjectUtils.isEmpty(topLevelLinkIssue.getLinkedIssues())) {
                        topLevelLinkIssue.getLinkedIssues().forEach(linkedIssueRecentId -> {
                            // 연결이슈가 요구사항 이슈임.
                            if(rightReqIssueMap.get(linkedIssueRecentId) != null) {
                                AlmIssueEntity fetchedIssue = rightReqIssueMap.get(linkedIssueRecentId);
                                Long reqLinkOfIssue = fetchedIssue.getCReqLink();

                                Set<String> recentIdSet = reqLinkRecentIdSetMap.getOrDefault(reqLinkOfIssue, new HashSet<>());
                                if(!recentIdSet.contains(recentId)) {
                                    processReqLinkAssigneeIssueCount(reqLinkIssueCountVOList, reqLinkOfIssue, assignee, IssueSort.LINKEDISSUE);
                                    recentIdSet.add(recentId); // cReqLink 에 이 이슈가 카운트 됨. OK. (subtask 이든, linkedIssue 든 카운트 됨)
                                }
                            }
                            // 연결이슈가 하위이슈임.
                            if(rightSubTaskIssueMap.get(linkedIssueRecentId) != null) {
                                AlmIssueEntity fetchedIssue = rightSubTaskIssueMap.get(linkedIssueRecentId);
                                Long reqLinkOfIssue = fetchedIssue.getCReqLink();
                                //  작업 후속. cReqLink 당 중복을 제거한 이슈들이 들어가야 하므로 recentIdSet.add(recentId) 는 유의미함.
                                Set<String> recentIdSet = reqLinkRecentIdSetMap.getOrDefault(reqLinkOfIssue, new HashSet<>());
                                if(!recentIdSet.contains(recentId)) {
                                    processReqLinkAssigneeIssueCount(reqLinkIssueCountVOList, reqLinkOfIssue, assignee, IssueSort.LINKEDISSUE);
                                    recentIdSet.add(recentId); // cReqLink 에 이 이슈가 카운트 됨. OK. (subtask 이든, linkedIssue 든 카운트 됨)
                                }
                            }
                        });
                    }
                }
                // 자신의 연결이슈 찾아서 - count
                if (!ObjectUtils.isEmpty(linkedIssue.getLinkedIssues())) {
                    linkedIssue.getLinkedIssues().forEach(linkedIssueRecentId -> {
                        // 연결이슈가 요구사항 이슈임.
                        if(rightReqIssueMap.get(linkedIssueRecentId) != null) {
                            AlmIssueEntity fetchedIssue = rightReqIssueMap.get(linkedIssueRecentId);
                            Long reqLinkOfIssue = fetchedIssue.getCReqLink();

                            Set<String> recentIdSet = reqLinkRecentIdSetMap.getOrDefault(reqLinkOfIssue, new HashSet<>());
                            if(!recentIdSet.contains(recentId)) {
                                processReqLinkAssigneeIssueCount(reqLinkIssueCountVOList, reqLinkOfIssue, assignee, IssueSort.LINKEDISSUE);
                                recentIdSet.add(recentId); // cReqLink 에 이 이슈가 카운트 됨. OK. (subtask 이든, linkedIssue 든 카운트 됨)
                            }
                        }
                        // 연결이슈가 하위이슈임.
                        if(rightSubTaskIssueMap.get(linkedIssueRecentId) != null) {
                            AlmIssueEntity fetchedIssue = rightSubTaskIssueMap.get(linkedIssueRecentId);
                            Long reqLinkOfIssue = fetchedIssue.getCReqLink();
                            //  작업 후속. cReqLink 당 중복을 제거한 이슈들이 들어가야 하므로 recentIdSet.add(recentId) 는 유의미함.
                            Set<String> recentIdSet = reqLinkRecentIdSetMap.getOrDefault(reqLinkOfIssue, new HashSet<>());
                            if(!recentIdSet.contains(recentId)) {
                                processReqLinkAssigneeIssueCount(reqLinkIssueCountVOList, reqLinkOfIssue, assignee, IssueSort.LINKEDISSUE);
                                recentIdSet.add(recentId); // cReqLink 에 이 이슈가 카운트 됨. OK. (subtask 이든, linkedIssue 든 카운트 됨)
                            }
                        }
                    });
                }
            }
        } //.linkedIssues

        if(reqLinkIssueCountVOList.isEmpty()) {
            return Collections.emptyList();
        }

        return convertToTreeBarIssueVOList(reqLinkIssueCountVOList, scopeDTO.getTopN());
    }

    private List<TreeBarIssueVO> convertToTreeBarIssueVOList(Map<Long, Map<String, AssigneeIssueCountVO>> reqLinkIssueCountVOList, Integer topN) {
        List<TreeBarIssueVO> result = new ArrayList<>();

        // 1. Map.Entry를 리스트로 변환하고 value의 size로 정렬
        List<Map.Entry<Long, Map<String, AssigneeIssueCountVO>>> sortedEntries =
                reqLinkIssueCountVOList.entrySet()
                        .stream()
                        .sorted((e1, e2) -> Integer.compare(
                                e2.getValue().size(), // 내림차순 정렬
                                e1.getValue().size()
                        ))
                        .collect(Collectors.toList());

        // 2. topN 처리
        if (topN != null && topN > 0) {
            sortedEntries = sortedEntries.stream()
                    .limit(topN)
                    .collect(Collectors.toList());
        }

        // 3. TreeBarIssueVO 생성
        for (Map.Entry<Long, Map<String, AssigneeIssueCountVO>> entry : sortedEntries) {
            Long cReqLink = entry.getKey();
            Map<String, AssigneeIssueCountVO> assigneeMap = entry.getValue();

            for (AssigneeIssueCountVO assigneeVO : assigneeMap.values()) {
                String displayName = assigneeVO.getName();

                // emailAddress가 존재하면 username 추출하여 괄호에 추가
                if (assigneeVO.getEmailAddress() != null && !assigneeVO.getEmailAddress().isEmpty()) {
                    String username = ParseUtil.extractUsernameFromEmail(assigneeVO.getEmailAddress());
                    displayName = displayName + "(" + username + ")";
                }

                TreeBarIssueVO treeBarIssueVO = TreeBarIssueVO.builder()
                        .cReqLink(cReqLink)
                        .assigneeDisplayName(displayName)
                        .assigneeCount(assigneeVO.getTotalCount())
                        .build();

                result.add(treeBarIssueVO);
            }
        }


        return result;
    }


    private void processReqLinkAssigneeIssueCount(
            Map<Long, Map<String, AssigneeIssueCountVO>> reqLinkIssueCountVOList,
            Long cReqLink,
            AlmIssueEntity.Assignee assignee,
            IssueSort sort) {

        String accountId = assignee.getAccountId();
        String name = assignee.getDisplayName();
        String emailAddress = assignee.getEmailAddress() == null ? "" : assignee.getEmailAddress();

        reqLinkIssueCountVOList.compute(cReqLink, (key, idAssigneeIssueCountVOMap) -> {
            // 1. cReqLink에 대한 Map이 없는 경우
            if (idAssigneeIssueCountVOMap == null) {
                Map<String, AssigneeIssueCountVO> newMap = new HashMap<>();
                newMap.put(accountId, AssigneeIssueCountVO.builder()
                        .name(name)
                        .accountId(accountId)
                        .emailAddress(emailAddress)
                        .totalCount(1L)
                        .reqIssueCount(sort.equals(IssueSort.REQISSUE) ? 1L : 0L)
                        .subTaskCount(sort.equals(IssueSort.SUBTASK) ? 1L : 0L)
                        .linkedIssueCount(sort.equals(IssueSort.LINKEDISSUE) ? 1L : 0L)
                        .build());
                return newMap;
            }
            // 2. cReqLink는 있지만 해당 accountId가 없는 경우
            idAssigneeIssueCountVOMap.compute(accountId, (id, assigneeVO) -> {
                if (assigneeVO == null) {
                    return AssigneeIssueCountVO.builder()
                            .name(name)
                            .accountId(accountId)
                            .emailAddress(emailAddress)
                            .totalCount(1L)
                            .reqIssueCount(sort.equals(IssueSort.REQISSUE) ? 1L : 0L)
                            .subTaskCount(sort.equals(IssueSort.SUBTASK) ? 1L : 0L)
                            .linkedIssueCount(sort.equals(IssueSort.LINKEDISSUE) ? 1L : 0L)
                            .build();
                }

                // 3. 이미 있는 경우 카운트만 증가
                if(sort.equals(IssueSort.REQISSUE)) {
                    assigneeVO.addReqIssueCount(1L);
                }
                if (sort.equals(IssueSort.SUBTASK)) {
                    assigneeVO.addSubTaskCount(1L);
                }
                if (sort.equals(IssueSort.LINKEDISSUE)) {
                    assigneeVO.addLinkedIssueCount(1L);
                }

                return assigneeVO;
            });
            return idAssigneeIssueCountVOMap;
        });
    }

    private List<AlmIssueEntity> retrieveAssignedIssuesByIsReq(ScopeDTO scopeDTO, Boolean isReq) {
        PdServiceAndIsReqDTO pdServiceAndIsReq = scopeDTO.getPdServiceAndIsReq();

        Long pdServiceId = pdServiceAndIsReq.getPdServiceId();
        List<Long> pdServiceVersions = pdServiceAndIsReq.getPdServiceVersions();

        return esCommonRepositoryWrapper.findRecentHits(
                SimpleQuery.termsQueryFilter("linkedIssuePdServiceIds", List.of(pdServiceId))
                        .andTermsQueryFilter("linkedIssuePdServiceVersions", pdServiceVersions)
                        .andTermQueryMust("isReq", isReq)
                        .andRangeQueryFilter(
                                RangeQueryFilter.of("updated")
                                .betweenDate(scopeDTO.getStartDate(), scopeDTO.getEndDate())
                        )
                        .andExistsQueryFilter("assignee")
        ).toDocs();

    }

    private List<AlmIssueEntity> retrieveIssuesByIsReq(ScopeDTO scopeDTO, Boolean isReq) {
        PdServiceAndIsReqDTO pdServiceAndIsReq = scopeDTO.getPdServiceAndIsReq();

        Long pdServiceId = pdServiceAndIsReq.getPdServiceId();
        List<Long> pdServiceVersions = pdServiceAndIsReq.getPdServiceVersions();

        return esCommonRepositoryWrapper.findRecentHits(
                SimpleQuery.termsQueryFilter("linkedIssuePdServiceIds", List.of(pdServiceId))
                        .andTermsQueryFilter("linkedIssuePdServiceVersions", pdServiceVersions)
                        .andTermQueryMust("isReq", isReq)
                        .andRangeQueryFilter(
                                RangeQueryFilter.of("updated")
                                        .betweenDate(scopeDTO.getStartDate(), scopeDTO.getEndDate())
                        )
        ).toDocs();

    }


    @Override
    public List<PdServiceVersionsAndReqVO> getPdServiceVersionsAndReq(ScopeDTO scopeDto) {

        List<AlmIssueEntity> reqIssueList = getReqIssueList(scopeDto);
        Map<String, Integer> subTaskIssueCount = getSubTaskIssueCount(scopeDto);

        return reqIssueList.stream()
                .flatMap(reqIssue -> {
                    Long pdServiceId = reqIssue.getPdServiceId();
                    String recentId = reqIssue.getRecentId();
                    String key = reqIssue.getKey();
                    Long cReqLink = reqIssue.getCReqLink();

                    long subTaskCount = subTaskIssueCount.getOrDefault(recentId, 0);
                    int linkedIssueCount  = Optional.ofNullable(reqIssue.getLinkedIssues())
                            .map(List::size).orElse(0);

                    return reqIssue.getPdServiceVersions().stream()
                            .map(version -> PdServiceVersionsAndReqVO.builder()
                                    .pdServiceId(pdServiceId)
                                    .pdServiceVersionId(version)
                                    .reqId(key)
                                    .recentId(recentId)
                                    .cReqLink(cReqLink)
                                    .relatedIssueCount(subTaskCount+linkedIssueCount)
                                    .issueType("REQ")
                                    .build());
                })
                .collect(Collectors.toList());
    }



    @Override
    public List<IssueVO> getVersionsAndIssuList(ScopeDTO scopeDTO) {
        // 1. 제품 버전의 isReq가 ture인 데이터 조회 -> 제품 및 버전에 생성된 요구사항 (타 요구사항의 연결 이슈 가능성 있음)
        List<AlmIssueEntity>  requirements = getReqIssueList(scopeDTO);
        List<String> reqRecentIds  = requirements.stream().map(AlmIssueEntity::getRecentId).toList();
        // 2. 제품 버전의 isReq가 false인 데이터 조회 -> 제품 및 버전에 생성된 모든 하위이슈 (타 요구사항의 연결 이슈 가능성 있음)
        List<AlmIssueEntity> subtasks = getVersionsAndSubTaskIssue(scopeDTO);

        List<AlmIssueEntity> allAlmIssuesCreatedInternally = getAllAlmIssuesCreatedInternally();
        List<AlmIssueEntity> relatedAlmIssue = findAllRelatedAlmIssues(allAlmIssuesCreatedInternally, reqRecentIds);

        List<AlmIssueEntity> merged = new ArrayList<>();

        if (requirements != null) merged.addAll(requirements);
        if (subtasks != null) merged.addAll(subtasks);
        if (relatedAlmIssue != null) merged.addAll(relatedAlmIssue);

        return setIssueData(merged);

    }

    @Override
    public List<IssueVO> getIssueList(ScopeDTO scopeDTO) {

        List<AlmIssueEntity> reqIssueList = getReqIssue(scopeDTO);
        List<String> reqRecentIds  = reqIssueList.stream().map(AlmIssueEntity::getRecentId).toList();
        List<AlmIssueEntity> subtasks = getSubTaskAndLinkedIssue(scopeDTO);

        List<AlmIssueEntity> allAlmIssuesCreatedInternally = getAllAlmIssuesCreatedInternally();
        List<AlmIssueEntity> relatedAlmIssue = findAllRelatedAlmIssues(allAlmIssuesCreatedInternally, reqRecentIds);

        List<AlmIssueEntity> merged = new ArrayList<>();
        if (reqIssueList != null) merged.addAll(reqIssueList);
        if (subtasks != null) merged.addAll(subtasks);
        if (relatedAlmIssue != null) merged.addAll(relatedAlmIssue);


        return setIssueData(merged);
    }
    private List<IssueVO> setIssueData(List<AlmIssueEntity> issueList) {
        
        Map<String, AlmIssueEntity> byRecentId = issueList.stream()
                .filter(Objects::nonNull)
                .filter(i -> i.getRecentId() != null && !i.getRecentId().isEmpty())
                .collect(Collectors.toMap(AlmIssueEntity::getRecentId, i -> i, (a, b) -> a, LinkedHashMap::new));


        List<IssueVO> out = new ArrayList<>(byRecentId.size());
        for (AlmIssueEntity e : byRecentId.values()) {
            out.add(IssueVO.builder()
                    .recentId(e.getRecentId())
                    .issueKey(e.getKey())
                    .parentReqKey(e.getParentReqKey())
                    .upperKey(e.getUpperKey())
                    .isReq(e.getIsReq())
                    .issueType(classifyLinkType(e))         // "REQ" | "SUB" | "ALM"
                    .serverId(e.getJira_server_id())
                    .cReqLink(e.getCReqLink())
                    .projectKey(e.getProject().getKey())
                    .versions(e.getPdServiceVersions())        
                    .linkedIssueKeys(e.getLinkedIssues())
                    .build());
        }
        return out;
    }

    private String classifyLinkType(AlmIssueEntity e) {
        if (Boolean.TRUE.equals(e.getIsReq())) return "REQ";
        
        if (e.getCReqLink() != null) return "SUB";
        
        if (e.getParentReqKey() != null && !e.getParentReqKey().isEmpty()) return "SUB"; // 자체 생성의 하위
        return "ALM";
    }
    
    private List<AlmIssueEntity> getReqIssueList(ScopeDTO scopeDTO) {

        Long serviceId = scopeDTO.getPdServiceAndIsReq().getPdServiceId();
        List<Long> versions = scopeDTO.getPdServiceAndIsReq().getPdServiceVersions();

        return esCommonRepositoryWrapper.findRecentHits(
                SimpleQuery.termQueryMust("pdServiceId", serviceId)
                        .andTermsQueryFilter("pdServiceVersions", versions)
                        .andTermQueryMust("isReq",true)
        ).toDocs();
    }
    
    private List<AlmIssueEntity> getReqIssue(ScopeDTO scopeDTO) {

        Long serviceId = scopeDTO.getPdServiceAndIsReq().getPdServiceId();
        List<Long> versions = scopeDTO.getPdServiceAndIsReq().getPdServiceVersions();

        List<AlmIssueEntity> issues = esCommonRepositoryWrapper.findRecentHits(
                termQueryMust("pdServiceId", serviceId)
                .andTermQueryMust("isReq",true)
                .andTermQueryMust("cReqLink",scopeDTO.getReqId())
                .andTermsQueryFilter("pdServiceVersions",versions)
        ).toDocs();

        // recentId 기준 중복 제거
        Map<String, AlmIssueEntity> uniqueIssuesMap = issues.stream()
                .collect(Collectors.toMap(
                        AlmIssueEntity::getRecentId,
                        issue -> issue,
                        (existing, replacement) -> existing
                ));

        return new ArrayList<>(uniqueIssuesMap.values());
    }

    private List<AlmIssueEntity> getAllAlmIssuesCreatedInternally() {
        return esCommonRepositoryWrapper.findAllRecentDocsBySearchAfter(
                termQueryMust("isReq", false)
                        .andExistQueryMustNot("cReqLink")
                        .orderBy(SortDTO.builder().field("@timestamp").sortType("desc").build())
        );
    }
   // alm에서 직접 생성한 모든 데이터를 긁어온다음 인메모리에서 연관관계를 파악하여 필터
    private List<AlmIssueEntity> findAllRelatedAlmIssues(List<AlmIssueEntity> allAlmIssues, List<String> reqRecentIds) {

        // 연결이슈 찾기 용 맵
        Map<String, AlmIssueEntity> byRecentId = allAlmIssues.stream()
                .filter(Objects::nonNull)
                .filter(i -> i.getRecentId() != null && !i.getRecentId().isEmpty())
                .collect(Collectors.toMap(AlmIssueEntity::getRecentId, i -> i, (a, b) -> a, LinkedHashMap::new));

        // 부모키 -> 하위 이슈 맵
        Map<String, List<AlmIssueEntity>> childrenByParent = buildChildrenIndex(allAlmIssues);

        Set<String> reqRecentIdsSet = new HashSet<>(reqRecentIds);

        // 요구사항에 연결된 ALM에서 직접 생성한 연결이슈 목록
        List<AlmIssueEntity> seed = allAlmIssues.stream()
                .filter(Objects::nonNull)
                .filter(issue -> {
                    List<String> linked = issue.getLinkedIssues();
                    if (linked == null || linked.isEmpty()) return false;
                    for (String rid : linked) {
                        if (rid != null && reqRecentIdsSet.contains(rid)) return true;
                    }
                    return false;
                })
                .toList();

        if (seed.isEmpty()) {
            return Collections.emptyList();
        }

        // 순회 준비
        Set<String> outVisited   = new HashSet<>(); // 수집/중복 방지
        Set<String> linkExpanded = new HashSet<>(); // 연결 확장 여부

        List<AlmIssueEntity> out = new ArrayList<>();   // 최종 결과

        Deque<AlmIssueEntity> wave = new ArrayDeque<>(seed); // 최초 시드 세팅

        // 차피 연결이슈 기준으로 깊게 들어가는거고 풀데이터 이상으로 방문할 이유가 없음
        int MAX_EXPANSIONS = Math.max(1, allAlmIssues.size());

        while (!wave.isEmpty() && linkExpanded.size() <= MAX_EXPANSIONS) {
            AlmIssueEntity cur = wave.pollFirst();
            if (cur == null) continue;

            String curRid = cur.getRecentId();
            if (curRid == null) continue;

            // 연결 확장은 "연결 경로"로 큐에 나온 노드에서만 1회 수행
            if (!linkExpanded.add(curRid)) continue;

            // 결과 수집(중복 방지)
            if (outVisited.add(curRid)) out.add(cur);

            // 하위 트리: out에만 넣고, 큐에는 넣지 않음 (하위에서 출발한 링크는 확장하지 않음)
            String curKey = cur.getKey();
            if (curKey != null && !curKey.isEmpty()) {
                collectDescendants(curKey, childrenByParent, outVisited, out);
            }

            // 연결의 연결: 큐에 추가 (연결 사슬만 타기)
            List<String> linked = cur.getLinkedIssues();
            if (linked != null && !linked.isEmpty()) {
                for (String nextRid : linked) {
                    if (nextRid == null || nextRid.isEmpty()) continue;
                    AlmIssueEntity next = byRecentId.get(nextRid);
                    if (next == null) continue;
                    // 이미 그 노드의 "연결 확장"을 끝냈다면 큐에 안 넣어도 됨
                    if (!linkExpanded.contains(nextRid)) {
                        wave.addLast(next);
                    }
                }
            }
        }

        return out;
    }

    private Map<String, List<AlmIssueEntity>> buildChildrenIndex(List<AlmIssueEntity> issues) {
        Map<String, List<AlmIssueEntity>> map = new HashMap<>();
        for (AlmIssueEntity issue : issues) {
            if (issue == null) continue;
            String parent = issue.getParentReqKey(); // 하위이슈가 가리키는 상위 key
            if (parent == null || parent.isEmpty()) continue;
            map.computeIfAbsent(parent, k -> new ArrayList<>()).add(issue);
        }
        for (Map.Entry<String, List<AlmIssueEntity>> e : map.entrySet()) {
            e.setValue(Collections.unmodifiableList(e.getValue()));
        }
        return map;
    }

    // parentKey 하위 하위 out에 추가
    private void collectDescendants(String parentKey,
                                    Map<String, List<AlmIssueEntity>> childrenByParent,
                                    Set<String> outVisited,
                                    List<AlmIssueEntity> out) {
        Deque<String> stack = new ArrayDeque<>();
        stack.push(parentKey);

        while (!stack.isEmpty()) {
            String curKey = stack.pop();
            if (curKey == null || curKey.isEmpty()) continue;

            List<AlmIssueEntity> children = childrenByParent.get(curKey);
            if (children == null || children.isEmpty()) continue;

            for (AlmIssueEntity ch : children) {
                if (ch == null) continue;
                String rid = ch.getRecentId();
                if (rid == null) continue;

                if (outVisited.add(rid)) {
                    out.add(ch);
                    String nextKey = ch.getKey();
                    if (nextKey != null && !nextKey.isEmpty()) {
                        stack.push(nextKey); // 하위의 하위 계속 수집
                    }
                }
            }
        }
    }

    private List<AlmIssueEntity> getVersionsAndSubTaskIssue(ScopeDTO scopeDTO) {

        return esCommonRepositoryWrapper.findRecentHits(
                termQueryMust("pdServiceId", scopeDTO.getPdServiceAndIsReq().getPdServiceId())
                        .andTermQueryMust("isReq",false)
                        .andTermsQueryFilter("pdServiceVersions", scopeDTO.getPdServiceAndIsReq().getPdServiceVersions())
        ).toDocs();
    }

    private List<AlmIssueEntity> getSubTaskAndLinkedIssue(ScopeDTO scopeDTO) {


        List<AlmIssueEntity> issues = esCommonRepositoryWrapper.findRecentHits(
                termQueryMust("pdServiceId", scopeDTO.getPdServiceAndIsReq().getPdServiceId())
                        .andTermQueryMust("isReq",false)
                        .andTermQueryMust("cReqLink",scopeDTO.getReqId())
                        .andTermsQueryFilter("pdServiceVersions",scopeDTO.getPdServiceAndIsReq().getPdServiceVersions())
        ).toDocs();

        // recentId 기준 중복 제거
        Map<String, AlmIssueEntity> uniqueIssuesMap = issues.stream()
                .collect(Collectors.toMap(
                        AlmIssueEntity::getRecentId,
                        issue -> issue,
                        (existing, replacement) -> existing
                ));

        return new ArrayList<>(uniqueIssuesMap.values());
    }

    @Override
    public List<CircularPackingChartVO> getCircularPackingChartDataV2(ScopeDTO scopeDTO) {

        Long pdServiceId = scopeDTO.getPdServiceAndIsReq().getPdServiceId();
        // 1. 데이터 조회
        List<AlmIssueEntity> reqIssues = retrieveIssuesByIsReq(scopeDTO, true);
        List<AlmIssueEntity> notReqIssues = retrieveIssuesByIsReq(scopeDTO, false);

        // 2. 이슈 분류 (한 번의 순회로 처리)
        IssueClassification classification = classifyIssues(reqIssues, notReqIssues, pdServiceId);
        // 여기까진 괜찮음 -> 이슈들을 분류했으므로.

        // 3. 이슈 카운트 집계 (최적화된 로직)
        Map<Long, Map<String, AssigneeIssueCountVO>> reqLinkIssueCountMap = aggregateIssueCountsV2(classification); // recentId 및 사람 count.

        // 4. CircularPackingChartVO 생성
        return buildCircularPackingChartVOList(classification.rightReqIssues, reqLinkIssueCountMap);
    }

    /**
     * CircularPackingChartVO 리스트 생성
     */
    private List<CircularPackingChartVO> buildCircularPackingChartVOList(
            Map<String, AlmIssueEntity> rightReqIssues,
            Map<Long, Map<String, AssigneeIssueCountVO>> reqLinkIssueCountMap) {

        List<CircularPackingChartVO> result = new ArrayList<>(rightReqIssues.size());

        for (AlmIssueEntity reqIssue : rightReqIssues.values()) {
            Long cReqLink = reqIssue.getCReqLink();

            // 해당 cReqLink의 집계 데이터 가져오기
            Map<String, AssigneeIssueCountVO> assigneeMap = reqLinkIssueCountMap.getOrDefault(
                    cReqLink,
                    Collections.emptyMap()
            );

            // 집계 데이터 계산
            CircularPackingAggregation aggregation = calculateAggregation(assigneeMap);

            // ALM 상태
            String almState = (reqIssue.getStatus() != null)
                    ? reqIssue.getStatus().getName()
                    : null;

            CircularPackingChartVO chartVO = CircularPackingChartVO.builder()
                    .almState(almState)
                    .linkedIssueCount(aggregation.totalLinkedIssueCount)
                    .subTaskCount(aggregation.totalSubTaskCount)
                    .numOfWorkers(aggregation.numOfWorkers)
                    .workersList(aggregation.workersList)
                    .recentId(reqIssue.getRecentId())
                    .issueKey(reqIssue.getKey())
                    .cReqLink(cReqLink)
                    .versions(reqIssue.getPdServiceVersions())
                    .serviceId(reqIssue.getPdServiceId())
                    .type("ALM")
                    .build();

            result.add(chartVO);
        }

        return result;
    }

    /**
     * AssigneeIssueCountVO Map으로부터 집계 데이터 계산
     */
    private CircularPackingAggregation calculateAggregation(
            Map<String, AssigneeIssueCountVO> assigneeMap) {

        if (assigneeMap.isEmpty()) {
            return new CircularPackingAggregation(0, 0, Collections.emptyList());
        }

        int totalSubTaskCount = 0;
        int totalLinkedIssueCount = 0;
        List<String> workersList = new ArrayList<>();

        for (AssigneeIssueCountVO assigneeVO : assigneeMap.values()) {
            // SubTask 개수 합산
            totalSubTaskCount += assigneeVO.getSubTaskCount();

            // LinkedIssue 개수 합산
            totalLinkedIssueCount += assigneeVO.getLinkedIssueCount();

            // 작업자 이름 추가
            String displayName = assigneeVO.getName();
            if (displayName != null && !displayName.isEmpty()) {
                workersList.add(displayName);
            }
        }

        return new CircularPackingAggregation(
                totalSubTaskCount,
                totalLinkedIssueCount,
                workersList
        );
    }

    /**
     * 집계 데이터를 담는 내부 클래스
     */
    private static class CircularPackingAggregation {
        final int totalSubTaskCount;
        final int totalLinkedIssueCount;
        final int numOfWorkers;
        final List<String> workersList;

        CircularPackingAggregation(
                int totalSubTaskCount,
                int totalLinkedIssueCount,
                List<String> workersList) {
            this.totalSubTaskCount = totalSubTaskCount;
            this.totalLinkedIssueCount = totalLinkedIssueCount;
            this.workersList = workersList;
            this.numOfWorkers = workersList.size();
        }
    }


    @Override
    public List<NetworkChartExcelDataVO> getNetworkChartExcelData(ScopeDTO scopeDTO){

        List<AlmIssueEntity> reqIssueList = getReqIssueInDataRange(scopeDTO);

        Map<String,Integer> subTaskAndLinkedIssueCount = getSubTaskIssueCount(scopeDTO);

        return reqIssueList.stream()
                .map(reqIssue -> NetworkChartExcelDataVO.builder()
                        .createDate(reqIssue.stringValueOfCreatedDate())
                        .pdServiceVersions(reqIssue.getPdServiceVersions())
                        .key(reqIssue.getKey())
                        .summary(reqIssue.getSummary())
                        .issueType("requirement")
                        .subTaskCount(subTaskAndLinkedIssueCount.getOrDefault(reqIssue.getRecentId(), 0) - reqIssue.getLinkedIssues().size())
                        .linkedIssueCount(reqIssue.getLinkedIssues().size())
                        .build())
                .collect(Collectors.toList());
    }

    private List<AlmIssueEntity> getReqIssueInDataRange(ScopeDTO scopeDTO) {

        return esCommonRepositoryWrapper.findRecentHits(
            SimpleQuery.termQueryFilter("isReq", true)
                    .andTermQueryFilter("pdServiceId", scopeDTO.getPdServiceAndIsReq().getPdServiceId())
                    .andTermsQueryFilter("pdServiceVersions", scopeDTO.getPdServiceAndIsReq().getPdServiceVersions())
                    .andRangeQueryFilter(
                        RangeQueryFilter.of("created").betweenDate(scopeDTO.getStartDate(), scopeDTO.getEndDate())
                    )
        ).toDocs();

    }

    private Map<String,Integer> getSubTaskIssueCount(ScopeDTO scopeDTO){

        Long serviceId = scopeDTO.getPdServiceAndIsReq().getPdServiceId();
        List<Long> versions = scopeDTO.getPdServiceAndIsReq().getPdServiceVersions();
        // 추가 검토 (server_id -> project_key -> parentReqKey 순)
        DocumentAggregations aggregations = esCommonRepositoryWrapper.aggregateRecent(
                aggregation(
                        AggregationRequestDTO.builder()
                                .mainField("jira_server_id")
                                .mainFieldAlias("jira_server_id")
                                .subGroupFieldDTOS(
                                        List.of(
                                                SubGroupFieldDTO.builder()
                                                        .subFieldAlias("parentReqKey")
                                                        .subField("parentReqKey")
                                                        .build(),
                                                SubGroupFieldDTO.builder()
                                                        .subFieldAlias("projectKey")
                                                        .subField("project.project_key.keyword")
                                                        .build()
                                        )
                                )
                                .build()
                )
                        .andTermQueryMust("pdServiceId",serviceId)
                        .andTermQueryMust("isReq", false)
                        .andTermsQueryFilter("pdServiceVersions", versions)
        );



        List<DocumentBucket> documentBuckets = aggregations.deepestList();

        Map<String,Integer> subTaskCount = new HashMap<>();

        for(DocumentBucket documentBucket : documentBuckets){
            String jira_server_id = documentBucket.valueByName("jira_server_id");
            String key = documentBucket.valueByName("parentReqKey");
            String projectKey = documentBucket.valueByName("projectKey");
            Long count = documentBucket.countByName("parentReqKey");
            String id = jira_server_id+"_"+projectKey+"_"+key;
            subTaskCount.put(id ,count.intValue());
        }
        return subTaskCount;
    }

    @Override
    public List<ReqDataTableIssuesVO> getDataTableIssues(ScopeDTO scopeDTO) {
        List<AlmIssueEntity> reqIssueList = getReqIssueInDataRange(scopeDTO);
        Map<String, Integer> subTaskAndLinkedIssueCount = getSubTaskIssueCount(scopeDTO);

        Set<Long> uniqueReqLinks = new HashSet<>();

        return reqIssueList.stream()
                .filter(reqIssue -> uniqueReqLinks.add(reqIssue.getCReqLink())) // 중복 제거
                .map(reqIssue -> {
                    String rawDate = reqIssue.stringValueOfCreatedDate();
                    String formattedDate = rawDate != null && rawDate.length() >= 10
                            ? rawDate.substring(0, 10)
                            : rawDate;

                    return ReqDataTableIssuesVO.builder()
                            .reqLink(reqIssue.getCReqLink())
                            .pdServiceVersionLinks(reqIssue.getPdServiceVersions())
                            .priorityName(reqIssue.getCReqProperty().getCReqPriorityName())
                            .difficultyName(reqIssue.getCReqProperty().getCReqDifficultyName())
                            .stateName(reqIssue.getCReqProperty().getCReqStateName())
                            .summary(reqIssue.getSummary())
                            .createDate(formattedDate)
                            .subTaskCount(subTaskAndLinkedIssueCount.getOrDefault(reqIssue.getRecentId(), 0))
                            .linkedIssueCount(reqIssue.getLinkedIssues().size())
                            .build();
                })
                .collect(Collectors.toList());
    }


    /**
     * circularPacking V2 를 위한 method
     */

    /**
     * V2용 이슈 카운트 집계 - Assignee가 없는 이슈도 처리
     */
    private Map<Long, Map<String, AssigneeIssueCountVO>> aggregateIssueCountsV2(
            IssueClassification classification) {

        Map<Long, Map<String, AssigneeIssueCountVO>> result = new HashMap<>();
        Map<Long, Set<String>> processedIssues = new HashMap<>();

        // 1. 요구사항 이슈 처리
        for (AlmIssueEntity issue : classification.rightReqIssues.values()) {
            if (tryAddIssue(processedIssues, issue.getCReqLink(), issue.getRecentId())) {
                incrementCountV2(result, issue.getCReqLink(), issue.getAssignee(), IssueSort.REQISSUE);
            }
        }

        // 2. 하위 이슈 처리
        for (AlmIssueEntity issue : classification.rightSubTaskIssues.values()) {
            Long cReqLink = issue.getCReqLink();
            String recentId = issue.getRecentId();

            if (tryAddIssue(processedIssues, cReqLink, recentId)) {
                incrementCountV2(result, cReqLink, issue.getAssignee(), IssueSort.SUBTASK);
                processLinkedIssuesForSubTaskV2(issue, classification, result, processedIssues);
            }
        }

        // 3. 생성 연결 이슈 처리
        processCreatedLinkedIssuesV2(classification, result, processedIssues);

        return result;
    }

    /**
     * V2용 카운트 증가 - Assignee가 null인 경우 "UNASSIGNED"로 처리
     */
    private void incrementCountV2(
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Long cReqLink,
            AlmIssueEntity.Assignee assignee,
            IssueSort sort) {

        // Assignee가 null인 경우 기본값 사용
        String accountId = (assignee != null) ? assignee.getAccountId() : "UNASSIGNED";
        String name = (assignee != null) ? assignee.getDisplayName() : "Unassigned";
        String emailAddress = (assignee != null && assignee.getEmailAddress() != null)
                ? assignee.getEmailAddress()
                : "";

        result.computeIfAbsent(cReqLink, k -> new HashMap<>())
                .compute(accountId, (id, vo) -> {
                    if (vo == null) {
                        return AssigneeIssueCountVO.builder()
                                .name(name)
                                .accountId(accountId)
                                .emailAddress(emailAddress)
                                .totalCount(1L)
                                .reqIssueCount(sort == IssueSort.REQISSUE ? 1L : 0L)
                                .subTaskCount(sort == IssueSort.SUBTASK ? 1L : 0L)
                                .linkedIssueCount(sort == IssueSort.LINKEDISSUE ? 1L : 0L)
                                .build();
                    }
                    updateAssigneeVO(vo, sort);
                    return vo;
                });
    }

    private void processLinkedIssuesForSubTaskV2(
            AlmIssueEntity subTaskIssue,
            IssueClassification classification,
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Map<Long, Set<String>> processedIssues) {

        if (ObjectUtils.isEmpty(subTaskIssue.getLinkedIssues())) {
            return;
        }

        for (String linkedIssueId : subTaskIssue.getLinkedIssues()) {
            Long targetReqLink = findReqLinkForLinkedIssue(
                    linkedIssueId,
                    classification.rightReqIssues,
                    classification.rightSubTaskIssues
            );

            if (targetReqLink != null &&
                    tryAddIssue(processedIssues, targetReqLink, subTaskIssue.getRecentId())) {
                incrementCountV2(result, targetReqLink, subTaskIssue.getAssignee(), IssueSort.LINKEDISSUE);
            }
        }
    }

    private void processCreatedLinkedIssuesV2(
            IssueClassification classification,
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Map<Long, Set<String>> processedIssues) {

        for (AlmIssueEntity linkedIssue : classification.linkedIssues.values()) {
            boolean isCreatedLink = ObjectUtils.isEmpty(linkedIssue.getCReqLink());

            if (!isCreatedLink) {
                continue;
            }

            if (ObjectUtils.isEmpty(linkedIssue.getParentReqKey())) {
                processCreatedLinkIssueV2(linkedIssue, classification, result, processedIssues);
            } else {
                processCreatedLinkSubIssueV2(linkedIssue, classification, result, processedIssues);
            }
        }
    }

    private void processCreatedLinkIssueV2(
            AlmIssueEntity createdLinkIssue,
            IssueClassification classification,
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Map<Long, Set<String>> processedIssues) {

        if (ObjectUtils.isEmpty(createdLinkIssue.getLinkedIssues())) {
            return;
        }

        for (String linkedIssueId : createdLinkIssue.getLinkedIssues()) {
            Long targetReqLink = findReqLinkForLinkedIssue(
                    linkedIssueId,
                    classification.rightReqIssues,
                    classification.rightSubTaskIssues
            );

            if (targetReqLink != null &&
                    tryAddIssue(processedIssues, targetReqLink, createdLinkIssue.getRecentId())) {
                incrementCountV2(result, targetReqLink, createdLinkIssue.getAssignee(), IssueSort.LINKEDISSUE);
            }
        }
    }

    private void processCreatedLinkSubIssueV2(
            AlmIssueEntity subIssue,
            IssueClassification classification,
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Map<Long, Set<String>> processedIssues) {

        String topLevelIssueId = ParseUtil.getPrefixIncludingLastDelimiter(subIssue.getRecentId())
                + subIssue.getParentReqKey();

        AlmIssueEntity topLevelIssue = classification.linkedIssues.get(topLevelIssueId);
        if (topLevelIssue == null) {
            return;
        }

        processIssueLinksV2(topLevelIssue, subIssue, classification, result, processedIssues);
        processIssueLinksV2(subIssue, subIssue, classification, result, processedIssues);
    }

    private void processIssueLinksV2(
            AlmIssueEntity sourceIssue,
            AlmIssueEntity targetIssue,
            IssueClassification classification,
            Map<Long, Map<String, AssigneeIssueCountVO>> result,
            Map<Long, Set<String>> processedIssues) {

        if (ObjectUtils.isEmpty(sourceIssue.getLinkedIssues())) {
            return;
        }

        for (String linkedIssueId : sourceIssue.getLinkedIssues()) {
            Long targetReqLink = findReqLinkForLinkedIssue(
                    linkedIssueId,
                    classification.rightReqIssues,
                    classification.rightSubTaskIssues
            );

            if (targetReqLink != null &&
                    tryAddIssue(processedIssues, targetReqLink, targetIssue.getRecentId())) {
                incrementCountV2(result, targetReqLink, targetIssue.getAssignee(), IssueSort.LINKEDISSUE);
            }
        }
    }
}
