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

import com.arms.api.analysis.resource.model.dto.*;
import com.arms.api.analysis.resource.model.dto.wordcloud.WordCloudExcelDTO;
import com.arms.api.analysis.resource.model.vo.*;
import com.arms.api.analysis.resource.model.vo.horizontalbar.HorizontalBarChartSeriesVO;
import com.arms.api.analysis.resource.model.vo.horizontalbar.HorizontalBarChartYAxisAndSeriesVO;
import com.arms.api.analysis.resource.model.vo.horizontalbar.HorizontalBarChartYAxisVO;
import com.arms.api.analysis.resource.model.vo.horizontalbar.ReqAndNotReqHorizontalBarChartVO;
import com.arms.api.analysis.resource.model.vo.personal.PersonalIssueVO;
import com.arms.api.analysis.resource.model.vo.pie.PieChartVO;
import com.arms.api.analysis.resource.model.vo.pie.ReqAndNotReqPieChartVO;
import com.arms.api.analysis.resource.model.vo.pie.TotalIssueAndPieChartVO;
import com.arms.api.analysis.resource.model.vo.sankey.SankeyChartBaseVO;
import com.arms.api.analysis.resource.model.vo.sankey.VersionAssigneeSummary;
import com.arms.api.analysis.resource.model.vo.sankey.VersionAssigneeSummaryVO;
import com.arms.api.analysis.resource.model.vo.stackedHorizontalBar.StackedHorizontalBarChartVO;
import com.arms.api.analysis.resource.model.vo.stackedHorizontalBar.StackedHorizontalBarChartYAxisVO;
import com.arms.api.analysis.resource.model.vo.stackedHorizontalBar.StackedHorizontalBarSeriesVO;
import com.arms.api.analysis.resource.model.vo.treemap.TreeMapTaskListVO;
import com.arms.api.analysis.resource.model.vo.treemap.TreeMapWorkerVO;
import com.arms.api.analysis.resource.model.vo.wordcloud.WordCloudExcelVO;
import com.arms.api.issue.almapi.model.entity.AlmIssueEntity;
import com.arms.api.util.DateRangeUtil;
import com.arms.api.util.ParseUtil;
import com.arms.api.util.model.dto.PdServiceAndIsReqDTO;
import com.arms.egovframework.javaservice.esframework.esquery.filter.RangeQueryFilter;
import com.arms.egovframework.javaservice.esframework.model.dto.request.AggregationRequestDTO;
import com.arms.egovframework.javaservice.esframework.model.dto.esquery.MainGroupDTO;
import com.arms.egovframework.javaservice.esframework.model.dto.esquery.SubGroupFieldDTO;
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 com.arms.egovframework.javaservice.esframework.esquery.SimpleQuery;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;

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

import static com.arms.egovframework.javaservice.esframework.esquery.SimpleQuery.aggregation;

@Slf4j
@Service("analysisResource")
@AllArgsConstructor
public class AnalysisResourceImpl implements AnalysisResource {

    private final EsCommonRepositoryWrapper<AlmIssueEntity> esCommonRepositoryWrapper;

    // 상수
    private static final String UNKNOWN_STATUS = "Unknown Status";
    private static final String UNKNOWN_PRIORITY = "Unknown Priority";
    private static final String UNKNOWN_ISSUETYPE = "Unknown IssueType";

    /**
     * 이슈 타입 Enum (Sankey, Treemap 사용)
     */
    private enum IssueTypeV3 {
        RIGHT_REQ,              // 적합 요구사항
        RIGHT_SUBTASK,          // 적합 하위이슈
        NOT_REQ_WITH_PD,        // 부적합 요구사항/하위이슈
        LINKED_ONLY,            // 단순 연결이슈
        LINKED_SUBTASK          // 연결이슈의 하위이슈
    }

    @Builder
    @Getter
    private static class SankeyContextV3 {
        private final Map<String, AlmIssueEntity> rightReqIssues;
        private final Map<String, AlmIssueEntity> rightSubtask;
        private final Map<String, AlmIssueEntity> notReqIssuesHasPd;
        private final Map<String, AlmIssueEntity> justLinkedKeyIssues;
        private final Map<Long, VersionAssigneeSummary> summaryMap;
        private final Long pdServiceId;
    }

    @Builder
    @Getter
    private static class TreeMapContextV2 {
        private final Map<String, AlmIssueEntity> rightReqIssues;      // recentId → 적합 요구사항
        private final Map<String, AlmIssueEntity> rightSubtask;        // recentId → 적합 하위이슈
        private final Map<String, AlmIssueEntity> notReqIssuesHasPd;   // recentId → 부적합 이슈
        private final Map<String, AlmIssueEntity> justLinkedKeyIssues; // recentId → 연결이슈

        // TreeMap 전용: Assignee별 요구사항별 count
        private final Map<String, TreeMapWorkerVO> contributionMap;    // accountId → WorkerVO

        // 중복 체크용: accountId + reqKey 조합으로 중복 방지
        private final Map<String, Set<String>> processedIssuesByAssignee; // accountId → Set<processedKey>

        private final Long pdServiceId;
        private final List<VersionIdNameDTO> versionIdNames;
    }

    @Builder
    @Getter
    private static class TreeMapContextV3 {
        // 이슈 분류 Map (V2와 동일)
        private final Map<String, AlmIssueEntity> rightReqIssues;
        private final Map<String, AlmIssueEntity> rightSubtask;
        private final Map<String, AlmIssueEntity> notReqIssuesHasPd;
        private final Map<String, AlmIssueEntity> justLinkedKeyIssues;

        // 이슈 타입 캐싱 (Phase 2에서 재계산 방지)
        private final Map<String, IssueTypeV3> issueTypeCache;

        // 요구사항 정보 캐싱 (versionNames + summary)
        private final Map<String, ReqDisplayInfo> reqInfoCache;

        // TreeMap 결과
        private final Map<String, TreeMapWorkerVO> contributionMap;

        // 중복 체크
        private final Map<String, Set<String>> processedIssuesByAssignee;

        // 설정
        private final Long pdServiceId;
        private final List<VersionIdNameDTO> versionIdNames;

        // ★ V3 신규: Worker별 TaskList를 Map으로 관리 (O(1) 검색)
        private final Map<String, Map<String, TreeMapTaskListVO>> workerTaskMap;
    }

    /**
     * 요구사항 표시 정보 캐싱용 DTO (treemap)
     */
    @Builder
    @Getter
    private static class ReqDisplayInfo {
        private final String reqKey;
        private final String versionNames;
        private final String summary;
        private final String displayName;  // "[ versionNames ] - summary"
    }


    // worker-status datatable
    @Override
    public List<UniqueAssigneeIssueStatusVO> issueStatusDataByAssignee(ResourceRequestDTO resourceDTO) {

        List<String> accounts = resourceDTO.getAccounts();
        if (!ObjectUtils.isEmpty(accounts)) {
            accounts = decodeAccountId(accounts);
            resourceDTO.setAccounts(accounts);
        }

        Map<String, UniqueAssigneeVO> assigneeMap = idAssigneeInfoMap(resourceDTO.getPdServiceAndIsReq());
        List<AlmIssueEntity> retrieveIssues = retrieveRelevantIssues(resourceDTO);

        if (retrieveIssues.isEmpty()) {
            log.info("issueStatusDataByAssignee :: retrieveIssues.size => 0");
            return Collections.emptyList();
        }

        Map<String, Map<String, IssuePropertyVO>> accountIdReqIssueStatusMap = new HashMap<>();
        Map<String, Map<String, IssuePropertyVO>> accountIdNotReqIssueStatusMap = new HashMap<>();

        for (AlmIssueEntity issue : retrieveIssues) {

            if (!isIssueValid(issue, resourceDTO)) {
                continue;
            }

            String accountId = issue.getAssignee().getAccountId();
            String statusName = resolveStatusName(issue);

            if (isRightReqIssue(issue,resourceDTO)) {
                incrementIssueProperty(accountIdReqIssueStatusMap, accountId, statusName);
            } else {
                incrementIssueProperty(accountIdNotReqIssueStatusMap, accountId, statusName);
            }
        }

        Map<String, List<IssuePropertyVO>> accountIdReqIssueStatusList = toListMapForEach(accountIdReqIssueStatusMap);
        Map<String, List<IssuePropertyVO>> accountIdNotReqIssueStatusList = toListMapForEach(accountIdNotReqIssueStatusMap);

        List<UniqueAssigneeIssueStatusVO> issueStatusData = buildUniqueAssigneeIssueStatusList(assigneeMap, accountIdReqIssueStatusList, accountIdNotReqIssueStatusList);

        return sortByTotalReqNotReqCountsDesc(issueStatusData);
    }

    public static Map<String, List<IssuePropertyVO>> toListMapForEach(Map<String, Map<String, IssuePropertyVO>> accountIdReqIssueStatusMap) {
        if (accountIdReqIssueStatusMap == null) return Collections.emptyMap();

        Map<String, List<IssuePropertyVO>> result = new LinkedHashMap<>();
        for (Map.Entry<String, Map<String, IssuePropertyVO>> entry : accountIdReqIssueStatusMap.entrySet()) {
            Map<String, IssuePropertyVO> inner = entry.getValue();
            List<IssuePropertyVO> list = (inner == null)
                    ? new ArrayList<>()
                    : new ArrayList<>(inner.values()); // 방어적 복사
            result.put(entry.getKey(), list);
        }
        return result;
    }

    private List<UniqueAssigneeIssueStatusVO> sortByTotalReqNotReqCountsDesc(List<UniqueAssigneeIssueStatusVO> issueStatusData) {
        issueStatusData.sort(Comparator
            .comparingLong(UniqueAssigneeIssueStatusVO::getTotalIssueCount).reversed()
            .thenComparingLong(UniqueAssigneeIssueStatusVO::getReqIssueCount).reversed()
            .thenComparingLong(UniqueAssigneeIssueStatusVO::getNotReqIssueCount).reversed()
        );
        return issueStatusData;
    }

    private List<UniqueAssigneeIssueStatusVO> buildUniqueAssigneeIssueStatusList (
        Map<String, UniqueAssigneeVO> assigneeVOMap,
        Map<String, List<IssuePropertyVO>> reqIssueUniqueIdStatusData,
        Map<String, List<IssuePropertyVO>> nonReqIssueUniqueIdStatusData
    ) {
        List<UniqueAssigneeIssueStatusVO> returnVO = new ArrayList<>();

        for (Map.Entry<String, UniqueAssigneeVO> entry : assigneeVOMap.entrySet()) {
            String key = entry.getKey();

            UniqueAssigneeVO uniqueAssigneeVO = entry.getValue();

            List<IssuePropertyVO> reqIssueProperties = reqIssueUniqueIdStatusData.getOrDefault(key, Collections.emptyList());
            List<IssuePropertyVO> notReqIssueProperties = nonReqIssueUniqueIdStatusData.getOrDefault(key, Collections.emptyList());

            Long reqIssueCount = reqIssueProperties.stream()
                .mapToLong(IssuePropertyVO::getValue)
                .sum();
            Long notReqIssueCount = notReqIssueProperties.stream()
                .mapToLong(IssuePropertyVO::getValue)
                .sum();
            Long totalIssueCount = reqIssueCount + notReqIssueCount;

            UniqueAssigneeIssueStatusVO uniqueAssigneeIssueStatusVO = UniqueAssigneeIssueStatusVO.builder()
                .uniqueAssigneeVO(uniqueAssigneeVO)
                .totalIssueCount(totalIssueCount)
                .reqIssueCount(reqIssueCount)
                .notReqIssueCount(notReqIssueCount)
                .reqIssueProperties(reqIssueProperties)
                .notReqIssueProperties(notReqIssueProperties)
                .build();

            returnVO.add(uniqueAssigneeIssueStatusVO);
        }

        return returnVO;
    }

    private Map<String, UniqueAssigneeVO> findAssigneeMap(SimpleQuery<MainGroupDTO> simpleQuery) {

        DocumentAggregations documentAggregations = esCommonRepositoryWrapper.aggregateRecent(simpleQuery);
        List<DocumentBucket> deepestList = documentAggregations.deepestList();

        Map<String, UniqueAssigneeVO> accountIdAssigneeMap = new HashMap<>();

        for (DocumentBucket documentBucket : deepestList) {
            String accountId = documentBucket.valueByName("accountId");
            String name = documentBucket.valueByName("assigneeName");
            String email = documentBucket.valueByName("assigneeEmail");

            accountIdAssigneeMap.computeIfAbsent(accountId, k -> UniqueAssigneeVO.builder()
                    .accountId(accountId)
                    .name(name)
                    .emailAddress(Optional.ofNullable(email).orElse(""))
                    .build());
        }

        return accountIdAssigneeMap;
    }

    // 전체 작업자 정보
    // uniqueId = "{ALM_server_id}-{assignee.assignee_accountId}" 형태 (To-do 추후 필요시 작업. 25.09.03)
    private Map<String, UniqueAssigneeVO> idAssigneeInfoMap(ResourceRequestDTO dto) {
        ResourceRequestDTO resourceDTO = new ResourceRequestDTO();
        resourceDTO.setPdServiceAndIsReq(dto.getPdServiceAndIsReq());
        // 전체 작업자 정보 조회를 위해, 기간과 id는 넣지 않음.
        PdServiceAndIsReqDTO pdServiceAndIsReq = resourceDTO.getPdServiceAndIsReq();
        Long pdServiceId = pdServiceAndIsReq.getPdServiceId();
        List<Long> pdServiceVersions = pdServiceAndIsReq.getPdServiceVersions();

        Map<String, UniqueAssigneeVO> assigneeMapByPdAndVersionFields = findAssigneeMap(
                aggregation(
                    AggregationRequestDTO.builder()
                            .mainField("assignee.assignee_accountId.keyword")
                            .mainFieldAlias("accountId")
                            .addGroup(
                                    SubGroupFieldDTO.builder()
                                            .subFieldAlias("assigneeName")
                                            .subField("assignee.assignee_displayName.keyword")
                                            .build(),
                                    SubGroupFieldDTO.builder()
                                            .subFieldAlias("assigneeEmail")
                                            .subField("assignee.assignee_emailAddress.keyword")
                                            .build()
                            )
                            .build()
                )
                .andTermQueryMust("pdServiceId", pdServiceId)
                .andTermsQueryFilter("pdServiceVersions", pdServiceVersions)
                .andRangeQueryFilter(RangeQueryFilter.of("updated")
                        .betweenDate(resourceDTO.getStartDate(), resourceDTO.getEndDate()))
                .andExistsQueryFilter("assignee")
                .andTermsQueryFilter("assignee.assignee_accountId.keyword", resourceDTO.getAccounts()));

        Map<String, UniqueAssigneeVO> assigneeMapByLinkedFields = findAssigneeMap(
                aggregation(
                        AggregationRequestDTO.builder()
                                .mainField("assignee.assignee_accountId.keyword")
                                .mainFieldAlias("accountId")
                                .addGroup(
                                        SubGroupFieldDTO.builder()
                                                .subFieldAlias("assigneeName")
                                                .subField("assignee.assignee_displayName.keyword")
                                                .build(),
                                        SubGroupFieldDTO.builder()
                                                .subFieldAlias("assigneeEmail")
                                                .subField("assignee.assignee_emailAddress.keyword")
                                                .build()
                                )
                                .build()
                )
                .andTermsQueryFilter("linkedIssuePdServiceIds", List.of(pdServiceId))
                .andTermsQueryFilter("linkedIssuePdServiceVersions", pdServiceVersions)
                .andRangeQueryFilter(RangeQueryFilter.of("updated")
                        .betweenDate(resourceDTO.getStartDate(), resourceDTO.getEndDate()))
                .andExistsQueryFilter("assignee")
                .andTermsQueryFilter("assignee.assignee_accountId.keyword", resourceDTO.getAccounts()));

        return mergeAssigneeMaps(assigneeMapByLinkedFields, assigneeMapByPdAndVersionFields);
    }

    private Map<String, UniqueAssigneeVO> mergeAssigneeMaps(Map<String, UniqueAssigneeVO> map1, Map<String, UniqueAssigneeVO> map2) {
        Map<String, UniqueAssigneeVO> mergedMap = new HashMap<>(map2);  // map2를 기준으로 시작

        // map1의 각 엔트리에 대해 검사
        map1.forEach((accountId, assigneeVO1) -> {
            UniqueAssigneeVO assigneeVO2 = mergedMap.get(accountId);

            if (assigneeVO2 != null) {
                // accountId가 존재하면 emailAddress 확인
                String email1 = assigneeVO1.getEmailAddress();
                String email2 = assigneeVO2.getEmailAddress();

                // email2가 없고 email1이 있는 경우에만 email1로 업데이트
                if (ObjectUtils.isEmpty(email2) && !ObjectUtils.isEmpty(email1)) {
                    assigneeVO2.setEmailAddress(email1);
                }
            } else {
                // map2에 없는 accountId인 경우 map1의 VO를 추가
                mergedMap.put(accountId, assigneeVO1);
            }
        });

        return mergedMap;
    }


    private Map<String, UniqueAssigneeVO> idAssigneeInfoMap(PdServiceAndIsReqDTO pdServiceAndIsReq) {
        ResourceRequestDTO resourceDTO = new ResourceRequestDTO();
        resourceDTO.setPdServiceAndIsReq(pdServiceAndIsReq);
        return idAssigneeInfoMap(resourceDTO);
    }


    @Override
    public List<WordCloudExcelVO> wordCloudData(ResourceRequestDTO resourceDTO) {

        List<String> accounts = resourceDTO.getAccounts();
        if (!ObjectUtils.isEmpty(accounts)) {
            accounts = decodeAccountId(accounts);
            resourceDTO.setAccounts(accounts);
        }

        Map<String, UniqueAssigneeVO> assigneeMap = idAssigneeInfoMap(resourceDTO.getPdServiceAndIsReq());
        List<AlmIssueEntity> retrieveIssues = retrieveRelevantIssues(resourceDTO);

        if (retrieveIssues.isEmpty()) {
            log.info("wordCloudData :: retrieveIssues.size => 0");
            return Collections.emptyList();
        }

        Map<String, WordCloudExcelDTO> accountIdWordCloudDTOMap = new HashMap<>();

        for (AlmIssueEntity issue : retrieveIssues) {

            if (!isIssueValid(issue, resourceDTO)) {
                continue;
            }

            String accountId = issue.getAssignee().getAccountId();

            if (accountIdWordCloudDTOMap.containsKey(accountId)) {
                WordCloudExcelDTO wordCloudExcelDTO = accountIdWordCloudDTOMap.get(accountId);

                if (isRightReqIssue(issue, resourceDTO)) {
                    wordCloudExcelDTO.setReqIssueTotal(wordCloudExcelDTO.getReqIssueTotal() + 1L);
                } else {
                    wordCloudExcelDTO.setNotReqIssueTotal(wordCloudExcelDTO.getNotReqIssueTotal() + 1L);
                }
                wordCloudExcelDTO.setIssueTotal(wordCloudExcelDTO.getIssueTotal() + 1L);
            }
            else {
                WordCloudExcelDTO wordCloudExcelDTO = new WordCloudExcelDTO();
                wordCloudExcelDTO.setAccountId(accountId);
                wordCloudExcelDTO.setName(assigneeMap.get(accountId).getName());
                wordCloudExcelDTO.setIssueTotal(1L);

                if (isRightReqIssue(issue, resourceDTO)) {
                    wordCloudExcelDTO.setReqIssueTotal(1L);
                    wordCloudExcelDTO.setNotReqIssueTotal(0L);
                } else {
                    wordCloudExcelDTO.setReqIssueTotal(0L);
                    wordCloudExcelDTO.setNotReqIssueTotal(1L);
                }

                accountIdWordCloudDTOMap.put(accountId, wordCloudExcelDTO);
            }
        }

        return accountIdWordCloudDTOMap.values().stream()
                .map(WordCloudExcelVO::fromDTO)
                .collect(Collectors.toList());
    }

    @Override
    public StackedHorizontalBarChartVO issueStatusStackedBarChartData(ResourceRequestDTO resourceDTO) {

        List<String> accounts = resourceDTO.getAccounts();

        if (!ObjectUtils.isEmpty(accounts)) {
            accounts = decodeAccountId(accounts);
            resourceDTO.setAccounts(accounts);
        }

        Map<String, UniqueAssigneeVO> assigneeMap = idAssigneeInfoMap(resourceDTO);
        List<AlmIssueEntity> retrieveIssues = retrieveRelevantIssues(resourceDTO);

        if (retrieveIssues.isEmpty()) {
            log.info("issueStatusStackedBarChartData :: retrieveIssues.size => 0");
            return StackedHorizontalBarChartVO.builder()
                    .yAxisVO(null)
                    .series(null)
                    .build();
        }

        Map<String, List<IssuePropertyVO>> accountIdIssueStatusListMap = new HashMap<>();
        Set<String> statusNameSet = new HashSet<>();

        for (AlmIssueEntity issue : retrieveIssues) {

            if (!isIssueValid(issue, resourceDTO)) {
                continue;
            }

            String accountId = issue.getAssignee().getAccountId();
            String statusName = issue.getStatus().getName();

            statusNameSet.add(statusName);

            if (accountIdIssueStatusListMap.containsKey(accountId)) {
                List<IssuePropertyVO> issueStatusList = accountIdIssueStatusListMap.get(accountId);

                boolean isStatusFound = false;
                 for (IssuePropertyVO issueStatus : issueStatusList) {
                     if (issueStatus.getName().equals(statusName)) {
                         // 2. statusName이 같으면 value를 1 증가
                         issueStatus.setValue(issueStatus.getValue() + 1);
                         isStatusFound = true;
                         break; // 찾았으니 반복문 종료
                     }
                 }
                 // 3. statusName이 없으면 새로운 요소 추가
                 if (!isStatusFound) {
                     IssuePropertyVO issueStatus = IssuePropertyVO.builder().name(statusName).value(1L).build();
                     issueStatusList.add(issueStatus);
                 }

            } else {
                IssuePropertyVO issueStatus = IssuePropertyVO.builder().name(statusName).value(1L).build();
                accountIdIssueStatusListMap.put(accountId, new ArrayList<>(List.of(issueStatus)));
            }
        }

        List<String> statusNames = new ArrayList<>(statusNameSet);

        // uniqueIdIssueProperties 에서 yAxis에 넣을 이름순서 설정
        List<UniqueAssigneeVO> assigneeList = assigneesByAccountIdIssueProperties(assigneeMap, accountIdIssueStatusListMap);
        List<String> assigneeNames = assigneeList.stream().map(UniqueAssigneeVO::getName).collect(Collectors.toList());

        // 3. yAxisVO 설정
        StackedHorizontalBarChartYAxisVO chartYAxisVO = StackedHorizontalBarChartYAxisVO.builder()
                .type("category")
                .data(assigneeNames)
                .build();

        // 4. Series 만들기 (by issueStatus, assignees, assignee-properties)
        List<StackedHorizontalBarSeriesVO> stackedHorizontalBarSeriesVOS = stackedHorizontalBarSeriesVO(statusNames, assigneeList, accountIdIssueStatusListMap);

        return StackedHorizontalBarChartVO.builder()
                .yAxisVO(chartYAxisVO)
                .series(stackedHorizontalBarSeriesVOS)
                .build();

    }


    private List<UniqueAssigneeVO> assigneesByAccountIdIssueProperties(Map<String, UniqueAssigneeVO> allAssigneeMap, Map<String, List<IssuePropertyVO>> accountIdIssueProperties) {
        List<UniqueAssigneeVO> assignees = new ArrayList<>();

        // accountId에 대한, 이름 (이메일아이디) 에 대한 세팅으로 HorizontalStackedBarChartYAxisVO
        for (Map.Entry<String, List<IssuePropertyVO>> entry : accountIdIssueProperties.entrySet()) {

            UniqueAssigneeVO uniqueAssigneeVO = allAssigneeMap.get(entry.getKey());

            String nameWithEmailId = uniqueAssigneeVO.getName();
            if (!uniqueAssigneeVO.getEmailAddress().isEmpty()) {
                String emailId = ParseUtil.extractUsernameFromEmail(uniqueAssigneeVO.getEmailAddress());
                nameWithEmailId += " (" + emailId +")";
            }

            UniqueAssigneeVO vo = UniqueAssigneeVO.builder()
                    .accountId(uniqueAssigneeVO.getAccountId())
                    .name(nameWithEmailId)
                    .serverId(uniqueAssigneeVO.getServerId())
                    .emailAddress(uniqueAssigneeVO.getEmailAddress())
                    .build();

            assignees.add(vo);
        }
        return assignees;
    }


    private List<StackedHorizontalBarSeriesVO> stackedHorizontalBarSeriesVO(List<String> statusNames, List<UniqueAssigneeVO> assignees, Map<String, List<IssuePropertyVO>> accountIdIssueProperties) {
        List<StackedHorizontalBarSeriesVO> series = new ArrayList<>();

        for (String statusName : statusNames) {
            List<Long> data = new ArrayList<>();

            for (UniqueAssigneeVO assignee : assignees) {
                List<IssuePropertyVO> issueProperties = accountIdIssueProperties.get(assignee.getAccountId());
                Optional<IssuePropertyVO> issueStatusProperty = issueProperties.stream()
                        .filter(proterty -> proterty.getName().equals(statusName))
                        .findFirst();

                if (issueStatusProperty.isPresent()) {
                    data.add(issueStatusProperty.get().getValue());
                } else {
                    data.add(0L);
                }
            }
            series.add(
                    StackedHorizontalBarSeriesVO.builder()
                            .type("bar")
                            .name(statusName)
                            .data(data).build()
            );
        }
        return series;
    }


    @Override
    public TotalIssueAndPieChartVO findPieChartDataExpand(ResourceRequestDTO resourceDTO) {
        PdServiceAndIsReqDTO pdServiceAndIsReq = resourceDTO.getPdServiceAndIsReq();

        Map<String, UniqueAssigneeVO> assigneeMap = idAssigneeInfoMap(resourceDTO.getPdServiceAndIsReq());

        List<String> accounts = resourceDTO.getAccounts();
        if (!ObjectUtils.isEmpty(accounts)) {
            accounts = decodeAccountId(accounts);
            resourceDTO.setAccounts(accounts);
        }
        // 요구사항 이슈만 가져올 때.
        if (!ObjectUtils.isEmpty(pdServiceAndIsReq.getIsReq()) && Boolean.TRUE.equals(pdServiceAndIsReq.getIsReq())) {
            DocumentAggregations documentAggregations = esCommonRepositoryWrapper.aggregateRecent(
                    SimpleQuery.aggregation(
                                    AggregationRequestDTO.builder()
                                            .mainField("assignee.assignee_accountId.keyword")
                                            .mainFieldAlias("accountId")
                                            .size(resourceDTO.getSize())
                                            .build()
                            )
                            .andTermQueryMust("pdServiceId", pdServiceAndIsReq.getPdServiceId())
                            .andTermsQueryFilter("pdServiceVersions", pdServiceAndIsReq.getPdServiceVersions())
                            .andTermQueryMust("isReq", pdServiceAndIsReq.getIsReq())
                            .andTermsQueryFilter("assignee.assignee_accountId.keyword", resourceDTO.getAccounts())
                            .andRangeQueryFilter(
                                    RangeQueryFilter.of("updated")
                                            .betweenDate(resourceDTO.getStartDate(), resourceDTO.getEndDate())
                            )
                            .andExistsQueryFilter("assignee")
            );

            return toTotalIssueAndPieChartVO(documentAggregations, assigneeMap);
        }
        // 하위 및 연결이슈만 가져올 떄.
        else {
            // 전체 이슈
            List<AlmIssueEntity> almIssueEntities = retrieveRelevantIssues(resourceDTO);
            // 요구사항 이슈면서, pdServiceId 일치 이슈 제외
            List<AlmIssueEntity> retrievedIssues = excludeRightReqIssue(almIssueEntities, resourceDTO);

            Long totalDocCounts = (long) retrievedIssues.size();

            if (ObjectUtils.isEmpty(retrievedIssues)) {
                return TotalIssueAndPieChartVO.builder()
                        .totalIssueCount(0L)
                        .assigneeAndIssueCounts(Collections.emptyList())
                        .build();
            }

            Map<String, Long> accountIdIssueCountMap = new HashMap<>();

            for (AlmIssueEntity issue : retrievedIssues) {

                if (!isIssueValid(issue, resourceDTO)) {
                    continue;
                }
                String accountId = issue.getAssignee().getAccountId();

                accountIdIssueCountMap.compute(accountId, (k, v) -> v == null ? 1L : v + 1L);
            }

            List<PieChartVO> assigneeAndIssueCounts = accountIdIssueCountMap.entrySet().stream()
                    .map(entry -> {
                    String accountId = entry.getKey();
                    Long issueCount = entry.getValue();
                    UniqueAssigneeVO assignee = assigneeMap.get(accountId);

                    return PieChartVO.builder()
                            .name(assignee.getName())
                            .value(issueCount)
                            .serverId(assignee.getServerId())
                            .accountId(accountId)
                            .emailAddress(assignee.getEmailAddress())
                            .build();
            })
             .sorted(Comparator.comparing(PieChartVO::getValue).reversed())
            .collect(Collectors.toList());

            return TotalIssueAndPieChartVO.builder()
                    .totalIssueCount(totalDocCounts)
                    .assigneeAndIssueCounts(assigneeAndIssueCounts)
                    .build();
        }
    }


    @Override
    public List<HorizontalBarChartYAxisAndSeriesVO> findHorizontalBarChartData(ResourceRequestDTO resourceDTO) {

        PdServiceAndIsReqDTO pdServiceAndIsReq = resourceDTO.getPdServiceAndIsReq();

        List<String> accounts = resourceDTO.getAccounts();
        if (!ObjectUtils.isEmpty(accounts)) {
            accounts = decodeAccountId(accounts);
            resourceDTO.setAccounts(accounts);
        }

        Map<String, UniqueAssigneeVO> assigneeMap = idAssigneeInfoMap(resourceDTO);

        List<AlmIssueEntity> retrievedIssues;

        if (!ObjectUtils.isEmpty(pdServiceAndIsReq.getIsReq()) && Boolean.TRUE.equals(pdServiceAndIsReq.getIsReq())) {
            // true 만
            retrievedIssues = retrieveReqIssues(resourceDTO);
        } else {
            // 전체 이슈
            List<AlmIssueEntity> almIssueEntities = retrieveRelevantIssues(resourceDTO);
            // 요구사항 이슈면서, pdServiceId 일치 이슈 제외
            retrievedIssues = excludeRightReqIssue(almIssueEntities, resourceDTO);
        }

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

        return getHorizontalBarChartYAxisAndSeriesVOS(resourceDTO, retrievedIssues, assigneeMap);
    }

    @NotNull
    private List<HorizontalBarChartYAxisAndSeriesVO> getHorizontalBarChartYAxisAndSeriesVOS(ResourceRequestDTO resourceDTO, List<AlmIssueEntity> retrievedIssues, Map<String, UniqueAssigneeVO> assigneeMap) {
        // issueProperties 는 pie 로 부터 담당자 정보를 받아서 전달.
        Map<String, Map<String, IssuePropertyVO>> accountIdIssueStatusMap = new HashMap<>();
        Map<String, Map<String, IssuePropertyVO>> accountIdIssuePriorityMap = new HashMap<>();
        Map<String, Map<String, IssuePropertyVO>> accountIdIssueTypeMap = new HashMap<>();

        for (AlmIssueEntity issue : retrievedIssues) {

            if (!isIssueValid(issue, resourceDTO)) {
                continue;
            }

            String accountId = issue.getAssignee().getAccountId();

            String statusName = resolveStatusName(issue);
            String priorityName = resolvePriorityName(issue);
            String issueTypeName = resolveIssueTypeName(issue);

            incrementIssueProperty(accountIdIssueStatusMap, accountId, statusName);
            incrementIssueProperty(accountIdIssuePriorityMap, accountId, priorityName);
            incrementIssueProperty(accountIdIssueTypeMap, accountId, issueTypeName);

        }

        List<AssigneeIssuePropertiesVO> issueStatusList =
                buildAssigneeIssuePropertiesList(accountIdIssueStatusMap, assigneeMap);
        List<AssigneeIssuePropertiesVO> issuePriorityList =
                buildAssigneeIssuePropertiesList(accountIdIssuePriorityMap, assigneeMap);
        List<AssigneeIssuePropertiesVO> issueTypeList =
                buildAssigneeIssuePropertiesList(accountIdIssueTypeMap, assigneeMap);

        HorizontalBarChartYAxisVO issueStatusHorizontalBarChartYAxisVO = toHorizontalBarChartYAxisVO(issueStatusList);
        HorizontalBarChartYAxisVO issuePriorityHorizontalBarChartYAxisVO = toHorizontalBarChartYAxisVO(issuePriorityList);
        HorizontalBarChartYAxisVO issueTypeHorizontalBarChartYAxisVO = toHorizontalBarChartYAxisVO(issueTypeList);

        List<HorizontalBarChartSeriesVO> issueStatusHorizontalBarChartSeries =
                toHorizontalBarChartSeries(issueStatusList, issueStatusHorizontalBarChartYAxisVO);
        List<HorizontalBarChartSeriesVO> issuePriorityHorizontalBarChartSeries =
                toHorizontalBarChartSeries(issuePriorityList, issuePriorityHorizontalBarChartYAxisVO);
        List<HorizontalBarChartSeriesVO> issueTypeHorizontalBarChartSeries =
                toHorizontalBarChartSeries(issueTypeList, issueTypeHorizontalBarChartYAxisVO);

        List<HorizontalBarChartYAxisAndSeriesVO> horizontalBarChartYAxisAndSeriesVOList = new ArrayList<>();
        horizontalBarChartYAxisAndSeriesVOList.add(
                HorizontalBarChartYAxisAndSeriesVO.builder()
                        .name("status")
                        .yAxisVO(issueStatusHorizontalBarChartYAxisVO)
                        .series(issueStatusHorizontalBarChartSeries)
                        .build());
        horizontalBarChartYAxisAndSeriesVOList.add(
                HorizontalBarChartYAxisAndSeriesVO.builder()
                        .name("priority")
                        .yAxisVO(issuePriorityHorizontalBarChartYAxisVO)
                        .series(issuePriorityHorizontalBarChartSeries)
                        .build());
        horizontalBarChartYAxisAndSeriesVOList.add(
                HorizontalBarChartYAxisAndSeriesVO.builder()
                        .name("issuetype")
                        .yAxisVO(issueTypeHorizontalBarChartYAxisVO)
                        .series(issueTypeHorizontalBarChartSeries)
                        .build());
        return horizontalBarChartYAxisAndSeriesVOList;
    }

    @Override
    public ReqAndNotReqHorizontalBarChartVO findHorizontalBarChartDataBySameAccounts(ResourceRequestDTO resourceDTO) {
        // size = 5 정도만 필요할 수 있다. -> 이 경우, assignee Id에 제한을 걸어서 찾는 방안!!
        // 선 집계 -> 집계를 바탕으로 검색 조건 설정!!
        List<String> accounts = resourceDTO.getAccounts();

        if (!ObjectUtils.isEmpty(accounts)) {
            accounts = decodeAccountId(accounts);
            resourceDTO.setAccounts(accounts);
        }

        Map<String, UniqueAssigneeVO> assigneeMap = idAssigneeInfoMap(resourceDTO);
        List<AlmIssueEntity> retrieveIssues = retrieveRelevantIssues(resourceDTO);

        if (retrieveIssues.isEmpty()) {
            log.info("findHorizontalBarCartData :: retrieveIssues.size => 0");
            return ReqAndNotReqHorizontalBarChartVO.builder().build();
        }

        Map<String, Map<String, IssuePropertyVO>> reqAccountIdIssueStatusMap = new HashMap<>();
        Map<String, Map<String, IssuePropertyVO>> reqAccountIdIssuePriorityMap = new HashMap<>();
        Map<String, Map<String, IssuePropertyVO>> reqAccountIdIssueTypeMap = new HashMap<>();
        Map<String, Map<String, IssuePropertyVO>> notReqAccountIdIssueStatusMap = new HashMap<>();
        Map<String, Map<String, IssuePropertyVO>> notReqAccountIdIssuePriorityMap = new HashMap<>();
        Map<String, Map<String, IssuePropertyVO>> notReqAccountIdIssueTypeMap = new HashMap<>();

        for (AlmIssueEntity issue : retrieveIssues) {

            if (!isIssueValid(issue, resourceDTO)) {
                continue;
            }

            String accountId = issue.getAssignee().getAccountId();

            String statusName = resolveStatusName(issue);
            String priorityName = resolvePriorityName(issue);
            String issueTypeName = resolveIssueTypeName(issue);

            if (isRightReqIssue(issue, resourceDTO)) {
                // Req 맵들에 카운트 반영
                incrementIssueProperty(reqAccountIdIssueStatusMap, accountId, statusName);
                incrementIssueProperty(reqAccountIdIssuePriorityMap, accountId, priorityName);
                incrementIssueProperty(reqAccountIdIssueTypeMap, accountId, issueTypeName);
            } else {
                // Not-Req 맵들에 카운트 반영
                incrementIssueProperty(notReqAccountIdIssueStatusMap, accountId, statusName);
                incrementIssueProperty(notReqAccountIdIssuePriorityMap, accountId, priorityName);
                incrementIssueProperty(notReqAccountIdIssueTypeMap, accountId, issueTypeName);
            }
        }

        // Req
        List<AssigneeIssuePropertiesVO> reqIssueStatusList   =
                buildAssigneeIssuePropertiesList(reqAccountIdIssueStatusMap, assigneeMap);
        List<AssigneeIssuePropertiesVO> reqIssuePriorityList =
                buildAssigneeIssuePropertiesList(reqAccountIdIssuePriorityMap, assigneeMap);
        List<AssigneeIssuePropertiesVO> reqIssueTypeList     =
                buildAssigneeIssuePropertiesList(reqAccountIdIssueTypeMap, assigneeMap);

        // Not-Req
        List<AssigneeIssuePropertiesVO> notReqIssueStatusList   =
                buildAssigneeIssuePropertiesList(notReqAccountIdIssueStatusMap, assigneeMap);
        List<AssigneeIssuePropertiesVO> notReqIssuePriorityList =
                buildAssigneeIssuePropertiesList(notReqAccountIdIssuePriorityMap, assigneeMap);
        List<AssigneeIssuePropertiesVO> notReqIssueTypeList     =
                buildAssigneeIssuePropertiesList(notReqAccountIdIssueTypeMap, assigneeMap);

        HorizontalBarChartYAxisVO reqIssueStatusHorizontalBarChartYAxisVO = toHorizontalBarChartYAxisVO(reqIssueStatusList);
        HorizontalBarChartYAxisVO reqIssuePriorityHorizontalBarChartYAxisVO = toHorizontalBarChartYAxisVO(reqIssuePriorityList);
        HorizontalBarChartYAxisVO reqIssueTypeHorizontalBarChartYAxisVO = toHorizontalBarChartYAxisVO(reqIssueTypeList);
        HorizontalBarChartYAxisVO notReqIssueStatusHorizontalBarChartYAxisVO = toHorizontalBarChartYAxisVO(notReqIssueStatusList);
        HorizontalBarChartYAxisVO notReqIssuePriorityHorizontalBarChartYAxisVO = toHorizontalBarChartYAxisVO(notReqIssuePriorityList);
        HorizontalBarChartYAxisVO notReqIssueTypeHorizontalBarChartYAxisVO = toHorizontalBarChartYAxisVO(notReqIssueTypeList);

        List<HorizontalBarChartSeriesVO> reqIssueStatusHorizontalBarChartSeries =
                toHorizontalBarChartSeries(reqIssueStatusList, reqIssueStatusHorizontalBarChartYAxisVO);
        List<HorizontalBarChartSeriesVO> reqIssuePriorityHorizontalBarChartSeries =
                toHorizontalBarChartSeries(reqIssuePriorityList, reqIssuePriorityHorizontalBarChartYAxisVO);
        List<HorizontalBarChartSeriesVO> reqIssueTypeHorizontalBarChartSeries =
                toHorizontalBarChartSeries(reqIssueTypeList, reqIssueTypeHorizontalBarChartYAxisVO);

        List<HorizontalBarChartSeriesVO> notReqIssueStatusHorizontalBarChartSeries =
                toHorizontalBarChartSeries(notReqIssueStatusList, notReqIssueStatusHorizontalBarChartYAxisVO);
        List<HorizontalBarChartSeriesVO> notReqIssuePriorityHorizontalBarChartSeries =
                toHorizontalBarChartSeries(notReqIssuePriorityList, notReqIssuePriorityHorizontalBarChartYAxisVO);
        List<HorizontalBarChartSeriesVO> notReqIssueTypeHorizontalBarChartSeries =
                toHorizontalBarChartSeries(notReqIssueTypeList, notReqIssueTypeHorizontalBarChartYAxisVO);

        List<HorizontalBarChartYAxisAndSeriesVO> reqChartYAxisAndSeriesVOList = new ArrayList<>();
        List<HorizontalBarChartYAxisAndSeriesVO> notReqChartYAxisAndSeriesVOList = new ArrayList<>();

        reqChartYAxisAndSeriesVOList.add(
                HorizontalBarChartYAxisAndSeriesVO.builder()
                        .name("status")
                        .yAxisVO(reqIssueStatusHorizontalBarChartYAxisVO)
                        .series(reqIssueStatusHorizontalBarChartSeries)
                        .build()
        );
        reqChartYAxisAndSeriesVOList.add(
                HorizontalBarChartYAxisAndSeriesVO.builder()
                        .name("priority")
                        .yAxisVO(reqIssuePriorityHorizontalBarChartYAxisVO)
                        .series(reqIssuePriorityHorizontalBarChartSeries)
                        .build()
        );
        reqChartYAxisAndSeriesVOList.add(
                HorizontalBarChartYAxisAndSeriesVO.builder()
                        .name("issuetype")
                        .yAxisVO(reqIssueTypeHorizontalBarChartYAxisVO)
                        .series(reqIssueTypeHorizontalBarChartSeries)
                        .build()
        );
        notReqChartYAxisAndSeriesVOList.add(
                HorizontalBarChartYAxisAndSeriesVO.builder()
                        .name("status")
                        .yAxisVO(notReqIssueStatusHorizontalBarChartYAxisVO)
                        .series(notReqIssueStatusHorizontalBarChartSeries)
                        .build()
        );
        notReqChartYAxisAndSeriesVOList.add(
            HorizontalBarChartYAxisAndSeriesVO.builder()
                    .name("priority")
                    .yAxisVO(notReqIssuePriorityHorizontalBarChartYAxisVO)
                    .series(notReqIssuePriorityHorizontalBarChartSeries)
                    .build()
        );
        notReqChartYAxisAndSeriesVOList.add(
            HorizontalBarChartYAxisAndSeriesVO.builder()
                    .name("issuetype")
                    .yAxisVO(notReqIssueTypeHorizontalBarChartYAxisVO)
                    .series(notReqIssueTypeHorizontalBarChartSeries)
                    .build()
        );

        return ReqAndNotReqHorizontalBarChartVO.builder()
                .reqChartYAxisAndSeriesVOList(reqChartYAxisAndSeriesVOList)
                .notReqChartYAxisAndSeriesVOList(notReqChartYAxisAndSeriesVOList)
                .build();
    }

    // 1) 공통 메서드: accountId 기준으로 propertyName(예: status/priority/issueType 이름)의 카운트 +1
    private void incrementIssueProperty(
            Map<String, Map<String, IssuePropertyVO>> targetMap, String accountId, String propertyName) {
        String key = (propertyName == null || propertyName.isBlank()) ? "UNKNOWN" : propertyName;

        Map<String, IssuePropertyVO> innerMap = targetMap.computeIfAbsent(accountId, k -> new HashMap<>());

        innerMap.compute(propertyName, (k, vo) -> {
           if (vo == null) {
               return IssuePropertyVO.builder().name(key).value(1L).build();
           }
           vo.setValue(vo.getValue() + 1);
           return vo;
        });

    }

    // 공통 변환 메서드: accountId → (propertyName → IssuePropertyVO) 구조를
    // List<AssigneeIssuePropertiesVO>로 변환
    private List<AssigneeIssuePropertiesVO> buildAssigneeIssuePropertiesList(
            Map<String, Map<String, IssuePropertyVO>> accountIdToProperties,
            Map<String, UniqueAssigneeVO> assigneeMap
    ) {
        List<AssigneeIssuePropertiesVO> result = new ArrayList<>();

        for (Map.Entry<String, Map<String, IssuePropertyVO>> entry : accountIdToProperties.entrySet()) {
            String accountId = entry.getKey();
            Map<String, IssuePropertyVO> properties = entry.getValue();

            UniqueAssigneeVO assignee = assigneeMap.get(accountId);

            String name = assignee != null ? assignee.getName() : accountId;
            String serverId = assignee != null ? assignee.getServerId() : null;
            String emailAddress = assignee != null ? assignee.getEmailAddress() : null;

            AssigneeIssuePropertiesVO vo = AssigneeIssuePropertiesVO.builder()
                    .name(name)
                    .serverId(serverId)
                    .accountId(accountId)
                    .emailAddress(emailAddress)
                    .issueProperties(new ArrayList<>(properties != null ? properties.values() : List.of()))
                    .build();

            result.add(vo);
        }

        return result;
    }



    @Override
    public ReqAndNotReqPieChartVO findPieChartData(ResourceRequestDTO resourceDTO) {
        // size = 5 정도만 필요할 수 있다. -> 이 경우, assignee Id에 제한을 걸어서 찾는 방안!!
        // 선 집계 -> 집계를 바탕으로 검색 조건 설정!!
        List<String> accounts = resourceDTO.getAccounts();

        if (!ObjectUtils.isEmpty(accounts)) {
            accounts = decodeAccountId(accounts);
            resourceDTO.setAccounts(accounts);
        }

        Map<String, UniqueAssigneeVO> assigneeMap = idAssigneeInfoMap(resourceDTO);
        List<AlmIssueEntity> retrieveIssues = retrieveRelevantIssues(resourceDTO);

        if (retrieveIssues.isEmpty()) {
            log.info("findPieChartData :: retrieveIssues.size => 0");
            return ReqAndNotReqPieChartVO.builder().build();
        }
        List<PieChartVO> reqIssuePieChartVOList;
        List<PieChartVO> notReqIssuePieChartVOList;
        Map<String, PieChartDTO> accountIdReqIssuePieChartDTOMap = new HashMap<>();
        Map<String, PieChartDTO> accountIdNotReqIssuePieChartDTOMap = new HashMap<>();

        for (AlmIssueEntity issue : retrieveIssues) {

            if (!isIssueValid(issue, resourceDTO)) {
                continue;
            }

            String accountId = issue.getAssignee().getAccountId();

            // req and also same pdService.
            if (isRightReqIssue(issue, resourceDTO)) {

                if (accountIdReqIssuePieChartDTOMap.containsKey(accountId)) {
                    PieChartDTO pieChartDTO = accountIdReqIssuePieChartDTOMap.get(accountId);
                    pieChartDTO.setValue(pieChartDTO.getValue() + 1);
                } else {
                    accountIdReqIssuePieChartDTOMap.put(accountId,
                            PieChartDTO.builder().name(assigneeMap.get(accountId).getName())
                                    .accountId(accountId)
                                    .emailAddress(Optional.ofNullable(assigneeMap.get(accountId).getEmailAddress()).orElse(""))
                                    .value(1L).build());
                }
            } else {
                // subtask, relatedIssues (including req-issue has different pdServiceId)
                if (accountIdNotReqIssuePieChartDTOMap.containsKey(accountId)) {
                    PieChartDTO pieChartDTO = accountIdNotReqIssuePieChartDTOMap.get(accountId);
                    pieChartDTO.setValue(pieChartDTO.getValue() + 1);
                } else {
                    accountIdNotReqIssuePieChartDTOMap.put(accountId,
                            PieChartDTO.builder().name(assigneeMap.get(accountId).getName())
                                    .accountId(accountId)
                                    .emailAddress(Optional.ofNullable(assigneeMap.get(accountId).getEmailAddress()).orElse(""))
                                    .value(1L).build());
                }
            }
        }

        reqIssuePieChartVOList = accountIdReqIssuePieChartDTOMap.values().stream().map(PieChartVO::fromDTO).collect(Collectors.toList());
        notReqIssuePieChartVOList = accountIdNotReqIssuePieChartDTOMap.values().stream().map(PieChartVO::fromDTO).collect(Collectors.toList());

        return ReqAndNotReqPieChartVO.builder()
                .reqPieChartVO(totalIssueAndPieChartVOFromPieChartList(reqIssuePieChartVOList))
                .notReqPieChartVO(totalIssueAndPieChartVOFromPieChartList(notReqIssuePieChartVOList))
                .build();
    }

    private TotalIssueAndPieChartVO totalIssueAndPieChartVOFromPieChartList(List<PieChartVO> pieChartVOList) {
        if (ObjectUtils.isEmpty(pieChartVOList)) {
            return TotalIssueAndPieChartVO.builder()
                    .totalIssueCount(0L).assigneeAndIssueCounts(Collections.emptyList()).build();
        }
        pieChartVOList.sort(Comparator.comparing(PieChartVO::getValue).reversed());

        Long totalIssueCount = pieChartVOList.stream()
                .mapToLong(PieChartVO::getValue)
                .sum();

        return TotalIssueAndPieChartVO.builder()
                .totalIssueCount(totalIssueCount)
                .assigneeAndIssueCounts(pieChartVOList)
                .build();
    }

    private TotalIssueAndPieChartVO toTotalIssueAndPieChartVO(DocumentAggregations documentAggregations, Map<String, UniqueAssigneeVO> assigneeMap) {

        List<PieChartVO> assigneeAndIssueCounts = new ArrayList<>();

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

        for (DocumentBucket doc : documentBuckets) {
            PieChartVO.PieChartVOBuilder pieChartVOBuilder = PieChartVO.builder();
            String accountId = doc.valueByName("accountId");

            UniqueAssigneeVO assigneeInfo = assigneeMap.get(accountId);

            pieChartVOBuilder.accountId(assigneeInfo.getAccountId())
                            .name(assigneeInfo.getName())
                            .value(doc.countByName("accountId"));

            String assigneeEmail = assigneeInfo.getEmailAddress();

            if (assigneeEmail != null && !assigneeEmail.isEmpty()) {
                pieChartVOBuilder.emailAddress(assigneeEmail);
            }

            assigneeAndIssueCounts.add(pieChartVOBuilder.build());
        }

        TotalIssueAndPieChartVO.TotalIssueAndPieChartVOBuilder totalIssueAndPieChartVOBuilder = TotalIssueAndPieChartVO.builder();

        totalIssueAndPieChartVOBuilder.totalIssueCount(documentAggregations.getTotalHits()); // 전체 hit 수
        totalIssueAndPieChartVOBuilder.assigneeAndIssueCounts(assigneeAndIssueCounts);

        return totalIssueAndPieChartVOBuilder.build();
    }


    private List<HorizontalBarChartSeriesVO> toHorizontalBarChartSeries(
        List<AssigneeIssuePropertiesVO> assigneeIssuePropertyVOsList, HorizontalBarChartYAxisVO horizontalBarChartYAxisVO) {

        List<HorizontalBarChartSeriesVO> horizontalBarChartSeries = new ArrayList<>();

        List<String> propertyNames = horizontalBarChartYAxisVO.getData();

        for (AssigneeIssuePropertiesVO assigneeIssuePropertiesVO : assigneeIssuePropertyVOsList) {
            HorizontalBarChartSeriesVO.HorizontalBarChartSeriesVOBuilder horizontalBarChartSeriesVOBuilder =
                HorizontalBarChartSeriesVO.builder()
                    .name(assigneeIssuePropertiesVO.getName())
                    .accountId(assigneeIssuePropertiesVO.getAccountId())
                    .emailAddress(Optional.ofNullable(assigneeIssuePropertiesVO.getEmailAddress()).orElse(""))
                    .type("bar");

            List<IssuePropertyVO> issueProperties = assigneeIssuePropertiesVO.getIssueProperties();


            Map<String, Long> issuePropertyMap = issueProperties.stream()
                .collect(Collectors.toMap(IssuePropertyVO::getName, IssuePropertyVO::getValue, (a, b) -> b, LinkedHashMap::new));

            horizontalBarChartSeriesVOBuilder.data(
                propertyNames.stream()
                    .map(propertyName -> issuePropertyMap.getOrDefault(propertyName, 0L))
                    .collect(Collectors.toList()));

            horizontalBarChartSeries.add(horizontalBarChartSeriesVOBuilder.build());
        }

        return horizontalBarChartSeries;
    }


    private HorizontalBarChartYAxisVO toHorizontalBarChartYAxisVO(List<AssigneeIssuePropertiesVO> assigneeIssuePropertyVOS) {
        Set<String> propertyNameSet = assigneeIssuePropertyVOS.stream()
            .flatMap(issueProperties -> issueProperties.getIssueProperties().stream())
            .map(IssuePropertyVO::getName)
            .collect(Collectors.toSet());

        List<String> propertyNames = new ArrayList<>(propertyNameSet);

        return HorizontalBarChartYAxisVO.builder()
            .type("category")
            .data(propertyNames)
            .build();
    }

    @Override
    public List<UniqueAssigneeVO> findAssigneesInfo(ResourceRequestDTO resourceDTO) {

        Map<String, UniqueAssigneeVO> uniquedAssigneeMap = idAssigneeInfoMap(resourceDTO);

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

    private List<String> decodeAccountId(List<String> accountIdList) {
        List<String> decodedList = new ArrayList<>();
        for (String accountId : accountIdList) {
            String decoded = new String(Base64.getDecoder().decode(accountId));
            decodedList.add(decoded);
        }
        return decodedList;
    }

    @Override
    public List<TreeMapWorkerVO> findTreeMapChartDataV3(ResourceWithVersionIdNamesDTO dto) {

        ResourceRequestDTO resourceDTO = new ResourceRequestDTO();
        resourceDTO.setSize(dto.getSize());
        resourceDTO.setPdServiceAndIsReq(dto.getPdServiceAndIsReq());
        resourceDTO.setAccounts(dto.getAccounts());

        List<AlmIssueEntity> retrieveIssues = retrieveRelevantIssuesRegardsLessOfAssignee(resourceDTO);

        if (retrieveIssues.isEmpty()) {
            log.info("findTreeMapChartDataV3 :: retrieveIssues.size => 0");
            return Collections.emptyList();
        }

        // 2. Context 생성
        TreeMapContextV3 context = TreeMapContextV3.builder()
                .rightReqIssues(new HashMap<>())
                .rightSubtask(new HashMap<>())
                .notReqIssuesHasPd(new HashMap<>())
                .justLinkedKeyIssues(new HashMap<>())
                .issueTypeCache(new HashMap<>())           // V3 신규
                .reqInfoCache(new HashMap<>())             // V3 신규
                .contributionMap(new HashMap<>())
                .processedIssuesByAssignee(new HashMap<>())
                .pdServiceId(resourceDTO.getPdServiceAndIsReq().getPdServiceId())
                .versionIdNames(dto.getVersionIdNames())
                .workerTaskMap(new HashMap<>())            // V3 신규
                .build();


        // 3. Phase 1: 이슈 분류 및 Map 저장 + 타입 캐싱
        for (AlmIssueEntity issue : retrieveIssues) {
            IssueTypeV3 type = categorizeIssueV3(issue, resourceDTO, context.getPdServiceId());

            // 타입 캐싱 (Phase 2에서 재계산 방지)
            context.getIssueTypeCache().put(issue.getRecentId(), type);

            storeIssueByTypeForTreeMapV3(issue, type, context);
        }

        // 4. 요구사항 정보 캐싱 (1회만 계산)
        cacheReqDisplayInfo(context);

        // 5. Phase 2: 이슈별 처리 (캐싱된 타입 사용)
        for (AlmIssueEntity issue : retrieveIssues) {
            if (!isIssueValid(issue, resourceDTO)) {
                continue;
            }

            // 캐싱된 타입 사용 (재계산 없음)
            IssueTypeV3 type = context.getIssueTypeCache().get(issue.getRecentId());
            processIssueByType(issue, type, context);
        }

        // 6. 결과 변환 및 정렬
        return buildSortedResultForTreeMap(context, dto.getSize());
    }

    // ============================================================================
    // Phase 1: 이슈 분류 및 저장
    // ============================================================================

    private void storeIssueByTypeForTreeMapV3(
            AlmIssueEntity issue,
            IssueTypeV3 type,
            TreeMapContextV3 context) {

        String recentId = issue.getRecentId();

        switch (type) {
            case RIGHT_REQ:
                context.getRightReqIssues().put(recentId, issue);
                break;
            case RIGHT_SUBTASK:
                context.getRightSubtask().put(recentId, issue);
                break;
            case NOT_REQ_WITH_PD:
                context.getNotReqIssuesHasPd().put(recentId, issue);
                break;
            case LINKED_ONLY:
            case LINKED_SUBTASK:
                context.getJustLinkedKeyIssues().put(recentId, issue);
                break;
        }
    }

    // ============================================================================
    // 요구사항 정보 캐싱 (V3 신규)
    // ============================================================================

    /**
     * 모든 RIGHT_REQ 이슈의 표시 정보를 미리 캐싱
     */
    private void cacheReqDisplayInfo(TreeMapContextV3 context) {
        for (AlmIssueEntity reqIssue : context.getRightReqIssues().values()) {
            String reqKey = reqIssue.getKey();

            if (!context.getReqInfoCache().containsKey(reqKey)) {
                String versionNames = getVersionName(
                        reqIssue.getPdServiceVersions(),
                        context.getVersionIdNames()
                );
                String summary = reqIssue.getSummary() != null
                        ? reqIssue.getSummary()
                        : "Requirement-Issue not found.";

                ReqDisplayInfo info = ReqDisplayInfo.builder()
                        .reqKey(reqKey)
                        .versionNames(versionNames)
                        .summary(summary)
                        .displayName("[ " + versionNames + " ] - " + summary)
                        .build();

                context.getReqInfoCache().put(reqKey, info);
            }
        }
    }

    // ============================================================================
    // Phase 2: 이슈 타입별 처리 (간소화됨)
    // ============================================================================

    private void processIssueByType(
            AlmIssueEntity issue,
            IssueTypeV3 type,
            TreeMapContextV3 context) {

        String accountId = issue.getAssignee().getAccountId();
        String displayName = issue.getAssignee().getDisplayName();

        switch (type) {
            case RIGHT_REQ:
                // 자신을 기준으로 count
                addContribution(accountId, displayName, issue.getKey(), issue.getRecentId(), context);
                processLinkedIssuesForTreeMapV3(issue, accountId, displayName, context);
                break;

            case RIGHT_SUBTASK:
                // 최상위 요구사항 기준 count
                AlmIssueEntity parentReq = findParentReqIssue(issue, context);
                if (parentReq != null) {
                    addContribution(accountId, displayName, parentReq.getKey(), issue.getRecentId(), context);
                }
                processLinkedIssuesForTreeMapV3(issue, accountId, displayName, context);
                break;

            case NOT_REQ_WITH_PD:
            case LINKED_ONLY:
                processLinkedIssuesForTreeMapV3(issue, accountId, displayName, context);
                break;

            case LINKED_SUBTASK:
                processLinkedIssuesForTreeMapV3(issue, accountId, displayName, context);
                processParentLinkedIssues(issue, accountId, displayName, context);
                break;
        }
    }

    // ============================================================================
    // 요구사항 찾기 헬퍼 (V3 신규 - 로직 분리)
    // ============================================================================

    /**
     * 하위이슈로부터 최상위 요구사항 찾기
     */
    private AlmIssueEntity findParentReqIssue(
            AlmIssueEntity subtask,
            TreeMapContextV3 context) {

        String parentReqRecentId = getRecentIdFromSubLinkIssue(subtask);
        return context.getRightReqIssues().get(parentReqRecentId);
    }

    /**
     * 연결이슈로부터 요구사항 찾기 (RIGHT_REQ 또는 RIGHT_SUBTASK의 상위)
     */
    private AlmIssueEntity findReqFromLinkedIssue(
            String linkedRecentId,
            TreeMapContextV3 context) {

        // Case 1: 연결이슈가 RIGHT_REQ
        AlmIssueEntity reqIssue = context.getRightReqIssues().get(linkedRecentId);
        if (reqIssue != null) {
            return reqIssue;
        }

        // Case 2: 연결이슈가 RIGHT_SUBTASK → 상위 요구사항 찾기
        AlmIssueEntity subtask = context.getRightSubtask().get(linkedRecentId);
        if (subtask != null) {
            return findParentReqIssue(subtask, context);
        }

        return null;
    }

    // ============================================================================
    // 연결이슈 처리 (간소화됨)
    // ============================================================================

    private void processLinkedIssuesForTreeMapV3(
            AlmIssueEntity currentIssue,
            String accountId,
            String displayName,
            TreeMapContextV3 context) {

        List<String> linkedIssues = currentIssue.getLinkedIssues();
        if (linkedIssues == null || linkedIssues.isEmpty()) {
            return;
        }

        for (String linkedRecentId : linkedIssues) {
            AlmIssueEntity reqIssue = findReqFromLinkedIssue(linkedRecentId, context);
            if (reqIssue != null) {
                addContribution(accountId, displayName, reqIssue.getKey(), currentIssue.getRecentId(), context);
            }
        }
    }

    private void processParentLinkedIssues(
            AlmIssueEntity issue,
            String accountId,
            String displayName,
            TreeMapContextV3 context) {

        if (ObjectUtils.isEmpty(issue.getParentReqKey())) {
            return;
        }

        String parentRecentId = getRecentIdFromSubLinkIssue(issue);
        AlmIssueEntity parent = context.getJustLinkedKeyIssues().get(parentRecentId);

        if (parent != null && parent.getLinkedIssues() != null) {
            for (String linkedRecentId : parent.getLinkedIssues()) {
                AlmIssueEntity reqIssue = findReqFromLinkedIssue(linkedRecentId, context);
                if (reqIssue != null) {
                    addContribution(accountId, displayName, reqIssue.getKey(), issue.getRecentId(), context);
                }
            }
        }
    }

    // ============================================================================
    // Count 증가 (V3: O(1) 최적화)
    // ============================================================================

    private void addContribution(
            String accountId,
            String displayName,
            String reqKey,
            String currentIssueRecentId,
            TreeMapContextV3 context) {

        // 1. 중복 체크
        String duplicateKey = reqKey + "::" + currentIssueRecentId;
        Set<String> processedKeys = context.getProcessedIssuesByAssignee()
                .computeIfAbsent(accountId, k -> new HashSet<>());

        if (processedKeys.contains(duplicateKey)) {
            return;
        }
        processedKeys.add(duplicateKey);

        // 2. Worker 가져오기/생성
        TreeMapWorkerVO worker = context.getContributionMap().computeIfAbsent(accountId, id -> {
            Map<String, Integer> dataMap = new HashMap<>();
            dataMap.put("totalInvolvedCount", 0);
            return TreeMapWorkerVO.builder()
                    .id(accountId)
                    .name(displayName)
                    .data(dataMap)
                    .children(new ArrayList<>())  // 최종 결과용
                    .build();
        });

        // 3. TaskList Map에서 O(1) 조회
        Map<String, TreeMapTaskListVO> taskMap = context.getWorkerTaskMap()
                .computeIfAbsent(accountId, k -> new HashMap<>());

        TreeMapTaskListVO taskList = taskMap.computeIfAbsent(reqKey, k -> {
            // 캐싱된 요구사항 정보 사용
            ReqDisplayInfo info = context.getReqInfoCache().get(reqKey);
            String taskName = (info != null) ? info.getDisplayName() : "[ Unknown ] - " + reqKey;

            Map<String, Integer> dataList = new HashMap<>();
            dataList.put("involvedCount", 0);

            return TreeMapTaskListVO.builder()
                    .id(reqKey)
                    .name(taskName)
                    .data(dataList)
                    .build();
        });

        // 4. Count 증가
        taskList.getData().put("involvedCount", taskList.getData().get("involvedCount") + 1);
        worker.getData().put("totalInvolvedCount", worker.getData().get("totalInvolvedCount") + 1);
    }

    // ============================================================================
    // 결과 변환 및 정렬 (V3 신규)
    // ============================================================================
    private List<TreeMapWorkerVO> buildSortedResultForTreeMap(TreeMapContextV3 context, int size) {

        // 1. Worker별 TaskList Map → List 변환
        for (Map.Entry<String, TreeMapWorkerVO> entry : context.getContributionMap().entrySet()) {
            String accountId = entry.getKey();
            TreeMapWorkerVO worker = entry.getValue();

            Map<String, TreeMapTaskListVO> taskMap = context.getWorkerTaskMap().get(accountId);
            if (taskMap != null) {
                worker.getChildren().clear();
                worker.getChildren().addAll(taskMap.values());
            }
        }

        // 2. 정렬 및 제한
        Stream<TreeMapWorkerVO> sortedStream = context.getContributionMap().values().stream()
                .sorted((w1, w2) ->
                        w2.getData().get("totalInvolvedCount").compareTo(w1.getData().get("totalInvolvedCount")));

        if (size > 0) {
            sortedStream = sortedStream.limit(size);
        }

        return sortedStream.toList();
    }

    @Override
    public List<TreeMapWorkerVO> findTreeMapChartDataV2(ResourceWithVersionIdNamesDTO dto) {

        ResourceRequestDTO resourceDTO = new ResourceRequestDTO();
        resourceDTO.setSize(dto.getSize());
        resourceDTO.setPdServiceAndIsReq(dto.getPdServiceAndIsReq());
        resourceDTO.setAccounts(dto.getAccounts());

        // 1. assignee 무관하게 모든 관련 이슈 조회
        List<AlmIssueEntity> retrieveIssues = retrieveRelevantIssuesRegardsLessOfAssignee(resourceDTO);

        if (retrieveIssues.isEmpty()) {
            log.info("findTreeMapChartDataV2 :: retrieveIssues.size => 0");
            return Collections.emptyList();
        }

        // 2. Context 생성
        TreeMapContextV2 context = TreeMapContextV2.builder()
                .rightReqIssues(new HashMap<>())
                .rightSubtask(new HashMap<>())
                .notReqIssuesHasPd(new HashMap<>())
                .justLinkedKeyIssues(new HashMap<>())
                .contributionMap(new HashMap<>())
                .processedIssuesByAssignee(new HashMap<>())
                .pdServiceId(resourceDTO.getPdServiceAndIsReq().getPdServiceId())
                .versionIdNames(dto.getVersionIdNames())
                .build();

        // 3. Phase 1: 모든 이슈 분류 및 Map 저장 (assignee 무관)
        for (AlmIssueEntity issue : retrieveIssues) {
            IssueTypeV3 type = categorizeIssueV3(issue, resourceDTO, context.getPdServiceId());
            storeIssueByTypeForTreeMap(issue, type, context);
        }

        // 4. Phase 2: 이슈별 처리 (assignee 필터링 적용)
        for (AlmIssueEntity issue : retrieveIssues) {
            // isIssueValid에서 assignee 존재 및 accounts 파라미터 체크
            if (!isIssueValid(issue, resourceDTO)) {
                continue;
            }
            processIssueForTreeMap(issue, resourceDTO, context);
        }

        // 5. 결과 정렬 및 반환
        Stream<TreeMapWorkerVO> sortedVOList = context.getContributionMap().values().stream()
                .sorted((w1, w2) ->
                        w2.getData().get("totalInvolvedCount").compareTo(w1.getData().get("totalInvolvedCount")));

        if (dto.getSize() > 0) {
            sortedVOList = sortedVOList.limit(dto.getSize());
        }

        return sortedVOList.toList();
    }

    // ============================================================================
    // Phase 1: 이슈 분류 및 저장
    // ============================================================================
    /**
     * 타입별로 Map에 저장 (TreeMap용)
     */
    private void storeIssueByTypeForTreeMap(
            AlmIssueEntity issue,
            IssueTypeV3 type,
            TreeMapContextV2 context) {

        switch (type) {
            case RIGHT_REQ:
                context.getRightReqIssues().put(issue.getRecentId(), issue);
                break;
            case RIGHT_SUBTASK:
                context.getRightSubtask().put(issue.getRecentId(), issue);
                break;
            case NOT_REQ_WITH_PD:
                context.getNotReqIssuesHasPd().put(issue.getRecentId(), issue);
                break;
            case LINKED_ONLY:
            case LINKED_SUBTASK:
                context.getJustLinkedKeyIssues().put(issue.getRecentId(), issue);
                break;
        }
    }

    // ============================================================================
    // Phase 2: 이슈 타입별 처리
    // ============================================================================
    /**
     * 이슈 타입에 따라 적절한 처리 수행
     */
    private void processIssueForTreeMap(
            AlmIssueEntity issue,
            ResourceRequestDTO resourceDTO,
            TreeMapContextV2 context) {

        IssueTypeV3 type = categorizeIssueV3(issue, resourceDTO, context.getPdServiceId());

        String accountId = issue.getAssignee().getAccountId();
        String displayName = issue.getAssignee().getDisplayName();

        switch (type) {
            case RIGHT_REQ:
                // (a) 자신을 기준으로 count + 연결이슈 처리
                addContributionWithDuplicateCheck(accountId, displayName, issue, issue.getRecentId(), context);
                processLinkedIssuesForTreeMap(issue, context);
                break;

            case RIGHT_SUBTASK:
                // (b) 최상위 요구사항 기준 count + 연결이슈 처리
                String parentReqRecentId = getRecentIdFromSubLinkIssue(issue);
                AlmIssueEntity parentReqIssue = context.getRightReqIssues().get(parentReqRecentId);
                if (parentReqIssue != null) {
                    addContributionWithDuplicateCheck(accountId, displayName, parentReqIssue, issue.getRecentId(), context);
                }
                processLinkedIssuesForTreeMap(issue, context);
                break;

            case NOT_REQ_WITH_PD:
                // (c) 연결이슈 처리만
                processLinkedIssuesForTreeMap(issue, context);
                break;

            case LINKED_ONLY:
                // (d) 연결이슈 처리만
                processLinkedIssuesForTreeMap(issue, context);
                break;

            case LINKED_SUBTASK:
                // (e) 연결이슈 처리 + 부모의 연결이슈 처리
                processLinkedIssuesForTreeMap(issue, context);
                processParentLinkedIssuesForTreeMap(issue, context);
                break;
        }
    }

    // ============================================================================
    // 연결이슈 처리 메서드들
    // ============================================================================
    /**
     * 현재 이슈의 연결이슈 처리 (현재 이슈의 assignee 사용)
     */
    private void processLinkedIssuesForTreeMap(
            AlmIssueEntity currentIssue,
            TreeMapContextV2 context) {

        if (currentIssue.getLinkedIssues() == null || currentIssue.getLinkedIssues().isEmpty()) {
            return;
        }

        String accountId = currentIssue.getAssignee().getAccountId();
        String displayName = currentIssue.getAssignee().getDisplayName();

        processLinkedIssuesForTreeMapWithAssignee(
                currentIssue.getLinkedIssues(),
                accountId,
                displayName,
                currentIssue.getRecentId(),
                context
        );
    }

    /**
     * 연결이슈 처리 (지정된 assignee 정보 사용)
     * - 부모 이슈의 연결이슈를 처리할 때, 현재 이슈의 assignee로 count해야 하므로 분리
     */
    private void processLinkedIssuesForTreeMapWithAssignee(
            List<String> linkedIssueRecentIds,
            String accountId,
            String displayName,
            String currentIssueRecentId,
            TreeMapContextV2 context) {

        if (linkedIssueRecentIds == null || linkedIssueRecentIds.isEmpty()) {
            return;
        }

        for (String linkedRecentId : linkedIssueRecentIds) {

            // Case 1: 연결이슈가 RIGHT_REQ인 경우
            AlmIssueEntity linkedReqIssue = context.getRightReqIssues().get(linkedRecentId);
            if (linkedReqIssue != null) {
                addContributionWithDuplicateCheck(
                        accountId, displayName, linkedReqIssue, currentIssueRecentId, context);
                continue;
            }

            // Case 2: 연결이슈가 RIGHT_SUBTASK인 경우
            AlmIssueEntity linkedSubtask = context.getRightSubtask().get(linkedRecentId);
            if (linkedSubtask != null) {
                // 하위이슈의 최상위 요구사항 찾기
                String parentReqRecentId = getRecentIdFromSubLinkIssue(linkedSubtask);
                AlmIssueEntity parentReqIssue = context.getRightReqIssues().get(parentReqRecentId);

                if (parentReqIssue != null) {
                    addContributionWithDuplicateCheck(
                            accountId, displayName, parentReqIssue, currentIssueRecentId, context);
                }
            }
        }
    }

    /**
     * LINKED_SUBTASK의 부모 이슈 연결이슈 처리
     * - 부모 이슈의 연결이슈들을 현재 이슈의 assignee에게 귀속
     */
    private void processParentLinkedIssuesForTreeMap(
            AlmIssueEntity issue,
            TreeMapContextV2 context) {

        if (ObjectUtils.isEmpty(issue.getParentReqKey())) {
            return;
        }

        // 부모 이슈의 recentId 도출
        String parentLinkedIssueRecentId = getRecentIdFromSubLinkIssue(issue);

        // justLinkedKeyIssues에서 부모 찾기
        AlmIssueEntity parent = context.getJustLinkedKeyIssues().get(parentLinkedIssueRecentId);

        if (parent != null) {
            // ★ 부모의 assignee가 아닌 현재 이슈의 assignee로 처리
            // 부모에 assignee가 없어도, 현재 이슈의 assignee로 count됨
            processLinkedIssuesForTreeMapWithAssignee(
                    parent.getLinkedIssues(),
                    issue.getAssignee().getAccountId(),
                    issue.getAssignee().getDisplayName(),
                    issue.getRecentId(),
                    context
            );
        }
    }

    // ============================================================================
    // Count 증가 및 중복 체크
    // ============================================================================

    /**
     * 중복 체크 후 contribution count 증가
     */
    private void addContributionWithDuplicateCheck(
            String accountId,
            String displayName,
            AlmIssueEntity reqIssue,           // 기준이 되는 RIGHT_REQ 이슈
            String currentIssueRecentId,       // 현재 처리 중인 이슈의 recentId
            TreeMapContextV2 context) {

        // 중복 체크 키: reqKey + currentIssueRecentId
        String duplicateKey = reqIssue.getKey() + "::" + currentIssueRecentId;

        Set<String> processedKeys = context.getProcessedIssuesByAssignee()
                .computeIfAbsent(accountId, k -> new HashSet<>());

        if (processedKeys.contains(duplicateKey)) {
            return; // 이미 처리됨
        }
        processedKeys.add(duplicateKey);

        // TreeMapWorkerVO 가져오기 또는 생성
        TreeMapWorkerVO worker = context.getContributionMap().computeIfAbsent(accountId, id -> {
            Map<String, Integer> dataMap = new HashMap<>();
            dataMap.put("totalInvolvedCount", 0);
            return TreeMapWorkerVO.builder()
                    .id(accountId)
                    .name(displayName)
                    .data(dataMap)
                    .children(new ArrayList<>())
                    .build();
        });

        // 버전명과 summary 조합
        String versionNames = getVersionName(reqIssue.getPdServiceVersions(), context.getVersionIdNames());
        String summary = reqIssue.getSummary() != null ? reqIssue.getSummary() : "Requirement-Issue not found.";

        // TaskList 가져오기 또는 생성
        TreeMapTaskListVO taskList = worker.getChildren().stream()
                .filter(task -> task.getId().equals(reqIssue.getKey()))
                .findFirst()
                .orElseGet(() -> {
                    Map<String, Integer> dataList = new HashMap<>();
                    dataList.put("involvedCount", 0);
                    TreeMapTaskListVO newTask = TreeMapTaskListVO.builder()
                            .id(reqIssue.getKey())
                            .name("[ " + versionNames + " ] - " + summary)
                            .data(dataList)
                            .build();
                    worker.getChildren().add(newTask);
                    return newTask;
                });

        // Count 증가
        taskList.getData().put("involvedCount", taskList.getData().get("involvedCount") + 1);
        worker.getData().put("totalInvolvedCount", worker.getData().get("totalInvolvedCount") + 1);
    }
    ///  .treemap

    private static String getVersionName(List<Long> pdServiceVersions, List<VersionIdNameDTO> versionIdNames) {
        return pdServiceVersions.stream()
                .map(versionId ->
                        versionIdNames.stream()
                                .filter(p -> p.getC_id().equals(versionId.toString()))
                                .findFirst()
                                .map(VersionIdNameDTO::getC_title).orElse(null))
                .filter(Objects::nonNull)
                .collect(Collectors.joining(","));
    }

    // 적합 요구사항만
    private List<AlmIssueEntity> retrieveReqIssues(ResourceRequestDTO resourceDTO) {
        PdServiceAndIsReqDTO pdServiceAndIsReq = resourceDTO.getPdServiceAndIsReq();
        Long pdServiceId = pdServiceAndIsReq.getPdServiceId();
        List<Long> pdServiceVersions = pdServiceAndIsReq.getPdServiceVersions();
        return esCommonRepositoryWrapper.findRecentHits(
                    SimpleQuery.termQueryMust("pdServiceId", pdServiceId)
                            .andTermsQueryFilter("pdServiceVersions", pdServiceVersions)
                            .andTermQueryFilter("isReq", Boolean.TRUE)
                            .andTermsQueryFilter("assignee.assignee_accountId.keyword", resourceDTO.getAccounts())
                            .andRangeQueryFilter(RangeQueryFilter.of("updated")
                                    .betweenDate(resourceDTO.getStartDate(), resourceDTO.getEndDate()))
                            .andExistsQueryFilter("assignee")
                ).toDocs();
    }

    // 관련 이슈 전체 -> 적합 요구사항을 빼야, 하위 및 연결이슈만 가져옴.
    private List<AlmIssueEntity> retrieveRelevantIssues(ResourceRequestDTO resourceDTO) {
        PdServiceAndIsReqDTO pdServiceAndIsReq = resourceDTO.getPdServiceAndIsReq();
        Long pdServiceId = pdServiceAndIsReq.getPdServiceId();
        List<Long> pdServiceVersions = pdServiceAndIsReq.getPdServiceVersions();
        return esCommonRepositoryWrapper.findRecentHits(
                SimpleQuery.termsQueryFilter("linkedIssuePdServiceIds", List.of(pdServiceId))
                        .andTermsQueryFilter("linkedIssuePdServiceVersions", pdServiceVersions)
                        .andTermsQueryFilter("assignee.assignee_accountId.keyword", resourceDTO.getAccounts())
                        .andRangeQueryFilter(RangeQueryFilter.of("updated")
                                .betweenDate(resourceDTO.getStartDate(), resourceDTO.getEndDate()))
                        .andExistsQueryFilter("assignee")
        ).toDocs();
    }

    private List<AlmIssueEntity> retrieveRelevantIssuesRegardsLessOfAssignee(ResourceRequestDTO resourceDTO) {
        PdServiceAndIsReqDTO pdServiceAndIsReq = resourceDTO.getPdServiceAndIsReq();
        Long pdServiceId = pdServiceAndIsReq.getPdServiceId();
        List<Long> pdServiceVersions = pdServiceAndIsReq.getPdServiceVersions();
        return esCommonRepositoryWrapper.findRecentHits(
                SimpleQuery.termsQueryFilter("linkedIssuePdServiceIds", List.of(pdServiceId))
                        .andTermsQueryFilter("linkedIssuePdServiceVersions", pdServiceVersions)
                        .andRangeQueryFilter(RangeQueryFilter.of("updated")
                                .betweenDate(resourceDTO.getStartDate(), resourceDTO.getEndDate()))
        ).toDocs();
    }

    private List<AlmIssueEntity> excludeRightReqIssue(List<AlmIssueEntity> targetList, ResourceRequestDTO resourceDTO) {
        if (ObjectUtils.isEmpty(targetList)) {
            return Collections.emptyList();
        }
        return targetList.stream()
                .filter(issue -> !isRightReqIssue(issue, resourceDTO))
                .collect(Collectors.toList());
    }

    private boolean isRightReqIssue(AlmIssueEntity issue, ResourceRequestDTO resourceDTO) {

        if (ObjectUtils.isEmpty(issue.getPdServiceId())) {
            return false;
        }

        return Boolean.TRUE.equals(issue.getIsReq())
                && Objects.equals(issue.getPdServiceId(), resourceDTO.getPdServiceAndIsReq().getPdServiceId());
    }

    private String resolveStatusName(AlmIssueEntity issue) {
        String name = (issue.getStatus() == null ? null : issue.getStatus().getName());
        if (name == null || name.isBlank()) {
            return UNKNOWN_STATUS;
        }
        return name;
    }

    private String resolvePriorityName(AlmIssueEntity issue) {
        String name = (issue.getPriority() == null ? null : issue.getPriority().getName());
        if (name == null || name.isBlank()) {
            return UNKNOWN_PRIORITY;
        }
        return name;
    }

    private String resolveIssueTypeName(AlmIssueEntity issue) {
        String name = (issue.getIssuetype() == null ? null : issue.getIssuetype().getName());
        if (name == null || name.isBlank()) {
            return UNKNOWN_ISSUETYPE;
        }
        return name;
    }

    // ========================================
    // 유틸리티 메서드
    // ========================================

    /**
    * (생성) 연결이슈의 하위이슈로부터 최상위 이슈의 RecentId 를 도출
    * ∵ 하위 이슈는, 상위 이슈와 ALM Project 가 일치하므로, ServerId 및 ProjectKey 까지 동일함
    */
    private String getRecentIdFromSubLinkIssue(AlmIssueEntity issue) {
        String serverProjectPrefix = ParseUtil.getPrefixIncludingLastDelimiter(issue.recentId());
        if (ObjectUtils.isEmpty(serverProjectPrefix)) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        sb.append(serverProjectPrefix);
        sb.append(issue.getParentReqKey());

        return sb.toString();
    }

    /**
     * 안전한 버전 목록 조회
     */
    private List<Long> getVersionsSafely(AlmIssueEntity issue) {
        return Optional.ofNullable(issue.getPdServiceVersions())
                .filter(list -> !list.isEmpty())
                .orElse(Collections.emptyList());
    }

    /**
     * Account ID 디코딩
     */
    private void decodeAccountsIfNeeded(ResourceRequestDTO resourceDTO) {
        List<String> accounts = resourceDTO.getAccounts();
        if (!ObjectUtils.isEmpty(accounts)) {
            accounts = decodeAccountId(accounts);
            resourceDTO.setAccounts(accounts);
        }
    }

    /**
     * 공통 유효성 검사: ResourceDTO 기반
     * - Assignee 존재 여부
     * - accounts 필터 (있을 때만)
     * - 날짜 범위 필터 (start/end 중 하나라도 있을 때만)
     */
    private boolean isIssueValid(AlmIssueEntity issue, ResourceRequestDTO dto) {
        if (issue == null || issue.getAssignee() == null) {
            return false;
        }

        // accounts 필터
        var accounts = dto.getAccounts();
        if (accounts != null && !accounts.isEmpty()) {
            var accountId = issue.getAssignee().getAccountId();
            if (accountId == null || !accounts.contains(accountId)) {
                return false;
            }
        }

        // 날짜 범위 필터
        var startDate = dto.getStartDate();
        var endDate = dto.getEndDate();
        if ((startDate != null && !startDate.isBlank()) || (endDate != null && !endDate.isBlank())) {
            if (DateRangeUtil.isNotWithinDateRange(issue.getUpdated(), startDate, endDate)) {
                return false;
            }
        }

        return true;
    }

    @Override
    public List<SankeyChartBaseVO> sankeyChartBaseDataV3(ResourceRequestDTO resourceDTO) {
        // 1. 초기화
        decodeAccountsIfNeeded(resourceDTO);
        List<AlmIssueEntity> retrieveIssues = retrieveRelevantIssues(resourceDTO);

        if (retrieveIssues.isEmpty()) {
            log.info("sankeyChartBaseDataV3Refactor :: retrieveIssues.size => 0");
            return Collections.emptyList();
        }

        // 2. Context 생성
        SankeyContextV3 context = SankeyContextV3.builder()
                .rightReqIssues(new HashMap<>())
                .rightSubtask(new HashMap<>())
                .notReqIssuesHasPd(new HashMap<>())
                .justLinkedKeyIssues(new HashMap<>())
                .summaryMap(new HashMap<>())
                .pdServiceId(resourceDTO.getPdServiceAndIsReq().getPdServiceId())
                .build();

        // 3. Phase 1: 모든 이슈 분류 (먼저 Map에 저장)
        for (AlmIssueEntity issue : retrieveIssues) {
            IssueTypeV3 type = categorizeIssueV3(issue, resourceDTO, context.getPdServiceId());
            storeIssueByTypeForSankey(issue, type, context);
        }

        // 4. Phase 2: 모든 이슈 처리 (이제 모든 이슈가 Map에 있음)
        for (AlmIssueEntity issue : retrieveIssues) {
            if (!isIssueValid(issue, resourceDTO)) {
                continue;
            }
            processIssueForSankeyV3(issue, resourceDTO, context);
        }

        // 5. 결과 변환
        return buildSankeyChartBaseListV3(context.getSummaryMap());
    }

    /**
     * Phase 2: 이슈 처리 (분류는 이미 완료됨)
     */
    private void processIssueForSankeyV3(
            AlmIssueEntity issue,
            ResourceRequestDTO resourceDTO,
            SankeyContextV3 context) {

        // 이슈 타입 다시 확인
        IssueTypeV3 type = categorizeIssueV3(issue, resourceDTO, context.getPdServiceId());

        // 타입별 처리
        switch (type) {
            case RIGHT_REQ:
                processRightReqIssueForSankeyV3(issue, context, true);
                break;
            case RIGHT_SUBTASK:
                processRightReqIssueForSankeyV3(issue, context, false);
                break;
            case NOT_REQ_WITH_PD:
                processNotReqWithPdIssueV3(issue, context);
                break;
            case LINKED_ONLY:
                processLinkedOnlyIssueV3(issue, context, false);
                break;
            case LINKED_SUBTASK:
                processLinkedOnlyIssueV3(issue, context, true);
                break;
        }
    }

    /**
     * 이슈 타입 분류 (sankey, treemap)
     */
    private IssueTypeV3 categorizeIssueV3(
            AlmIssueEntity issue,
            ResourceRequestDTO resourceDTO,
            Long pdServiceId) {

        // 1. 적합 요구사항
        if (isRightReqIssue(issue, resourceDTO)) {
            return IssueTypeV3.RIGHT_REQ;
        }

        // 2. 적합 하위이슈
        if (!ObjectUtils.isEmpty(issue.getPdServiceId())
                && Objects.equals(issue.getPdServiceId(), pdServiceId)
                && Boolean.FALSE.equals(issue.getIsReq())) {
            return IssueTypeV3.RIGHT_SUBTASK;
        }

        // 3. 부적합 요구사항/하위이슈
        if (!ObjectUtils.isEmpty(issue.getPdServiceId())) {
            return IssueTypeV3.NOT_REQ_WITH_PD;
        }

        // 4. 연결이슈의 하위이슈
        if (!ObjectUtils.isEmpty(issue.getParentReqKey())) {
            return IssueTypeV3.LINKED_SUBTASK;
        }

        // 5. 단순 연결이슈
        return IssueTypeV3.LINKED_ONLY;
    }

    /**
     * 타입별로 Map에 저장
     */
    private void storeIssueByTypeForSankey(
            AlmIssueEntity issue,
            IssueTypeV3 type,
            SankeyContextV3 context) {

        switch (type) {
            case RIGHT_REQ:
                context.getRightReqIssues().put(issue.getRecentId(), issue);
                break;
            case RIGHT_SUBTASK:
                context.getRightSubtask().put(issue.getRecentId(), issue);
                break;
            case NOT_REQ_WITH_PD:
                context.getNotReqIssuesHasPd().put(issue.getRecentId(), issue);
                break;
            case LINKED_ONLY:
            case LINKED_SUBTASK:
                context.getJustLinkedKeyIssues().put(issue.getRecentId(), issue);
                break;
        }
    }

    /**
     * 적합 요구사항/하위이슈 처리
     */
    private void processRightReqIssueForSankeyV3(
            AlmIssueEntity issue,
            SankeyContextV3 context,
            boolean isReqIssue) {

        // 1. 자신의 버전에 대해 count
        getVersionsSafely(issue).forEach(versionId ->
                updateVersionSummary(versionId, issue, context.getSummaryMap(), isReqIssue)
        );

        // 2. 연결이슈 처리
        processLinkedIssuesV3(issue, context);
    }

    /**
     * 부적합 요구사항/하위이슈 처리
     */
    private void processNotReqWithPdIssueV3(
            AlmIssueEntity issue,
            SankeyContextV3 context) {

        // 연결이슈만 처리
        processLinkedIssuesV3(issue, context);
    }

    /**
     * 단순 연결이슈 및 연결이슈의 하위이슈 처리
     */
    private void processLinkedOnlyIssueV3(
            AlmIssueEntity issue,
            SankeyContextV3 context,
            boolean hasParent) {

        // 1. 자신의 연결이슈 처리
        processLinkedIssuesV3(issue, context);

        // 2. 부모의 연결이슈 처리 (하위이슈인 경우)
        if (hasParent) {
            findParentAndProcessLinked(issue, context);
        }
    }

    /**
     * 연결이슈 처리 (핵심 로직)
     */
    private void processLinkedIssuesV3(
            AlmIssueEntity currentIssue,
            SankeyContextV3 context) {

        if (currentIssue.getLinkedIssues() == null || currentIssue.getLinkedIssues().isEmpty()) {
            return;
        }

        for (String linkedRecentId : currentIssue.getLinkedIssues()) {
            processLinkedIssue(currentIssue, linkedRecentId, context);
        }
    }

    /**
     * 개별 연결이슈 처리
     */
    private void processLinkedIssue(
            AlmIssueEntity currentIssue,
            String linkedRecentId,
            SankeyContextV3 context) {

        // 1. 적합 요구사항인지 확인
        AlmIssueEntity linkedIssue = context.getRightReqIssues().get(linkedRecentId);
        boolean isReqIssue;

        // 2. 적합 하위이슈인지 확인
        if (linkedIssue == null) {
            linkedIssue = context.getRightSubtask().get(linkedRecentId);
            isReqIssue = false;
        } else {
            isReqIssue = true;
        }

        // 3. 적합 이슈가 아니면 종료
        if (linkedIssue == null) {
            return;
        }

        // 4. 연결된 이슈의 각 버전에 대해 처리
        getVersionsSafely(linkedIssue).forEach(versionId ->
                updateVersionSummaryWithDuplicateCheck(
                        versionId, currentIssue, context.getSummaryMap(), isReqIssue)
        );
    }

    /**
     * 버전 Summary 업데이트 (중복 체크 포함)
     */
    private void updateVersionSummaryWithDuplicateCheck(
            Long versionId,
            AlmIssueEntity issue,
            Map<Long, VersionAssigneeSummary> summaryMap,
            boolean isReqIssue) {

        // 1. Summary 가져오기 또는 생성
        VersionAssigneeSummary summary = summaryMap.computeIfAbsent(versionId,
                k -> VersionAssigneeSummary.builder()
                        .versionId(versionId)
                        .treatedIssue(new HashSet<>())
                        .accountIdSummaryMap(new HashMap<>())
                        .build()
        );

        // 2. 중복 체크
        if (summary.getTreatedIssue().contains(issue.getRecentId())) {
            return;
        }

        // 3. Assignee 확인
        if (issue.getAssignee() == null || issue.getAssignee().getAccountId() == null) {
            return;
        }

        // 4. Count 업데이트
        updateCount(summary, issue.getAssignee(), isReqIssue);

        // 5. 처리 완료 마킹
        summary.getTreatedIssue().add(issue.getRecentId());
    }

    /**
     * 버전 Summary 업데이트 (단순)
     */
    private void updateVersionSummary(
            Long versionId,
            AlmIssueEntity issue,
            Map<Long, VersionAssigneeSummary> summaryMap,
            boolean isReqIssue) {

        // treatedIssue 체크 없이 바로 업데이트
        VersionAssigneeSummary summary = summaryMap.computeIfAbsent(versionId,
                k -> VersionAssigneeSummary.builder()
                        .versionId(versionId)
                        .treatedIssue(new HashSet<>())
                        .accountIdSummaryMap(new HashMap<>())
                        .build()
        );

        // 중복 체크
        if (summary.getTreatedIssue().contains(issue.getRecentId())) {
            return;
        }

        updateCount(summary, issue.getAssignee(), isReqIssue);
        summary.getTreatedIssue().add(issue.getRecentId());
    }

    /**
     * Count 업데이트 (공통 로직)
     */
    private void updateCount(
            VersionAssigneeSummary summary,
            AlmIssueEntity.Assignee assignee,
            boolean isReqIssue) {

        String accountId = assignee.getAccountId();

        VersionAssigneeSummaryVO vo = summary.getAccountIdSummaryMap().computeIfAbsent(
                accountId,
                k -> {
                    VersionAssigneeSummaryVO newVo = VersionAssigneeSummaryVO.builder()
                            .versionId(summary.getVersionId())
                            .accountId(accountId)
                            .name(assignee.getDisplayName())
                            .emailAddress(assignee.getEmailAddress())
                            .totalIssue(0L)
                            .reqIssueCount(0L)
                            .notReqIssueCount(0L)
                            .build();
                    newVo.setVersionAccountId(summary.getVersionId(), accountId);
                    return newVo;
                }
        );

        if (isReqIssue) {
            vo.addReqIssueCount();
            vo.sumReqIssueCountToTotal(1L);
        } else {
            vo.addNotReqIssueCount();
            vo.sumNotReqIssueCountToTotal(1L);
        }
    }

    /**
     * 부모 찾아서 연결이슈 처리
     */
    private void findParentAndProcessLinked(
            AlmIssueEntity issue,
            SankeyContextV3 context) {

        if (ObjectUtils.isEmpty(issue.getParentReqKey())) {
            return;
        }

        String parentLinkedIssueRecentId = getRecentIdFromSubLinkIssue(issue);

        AlmIssueEntity parent = context.getJustLinkedKeyIssues().values().stream()
                .filter(i -> parentLinkedIssueRecentId.equals(i.getKey()))
                .findFirst()
                .orElse(null);

        if (parent != null) {
            processLinkedIssuesV3(parent, context);
        }
    }

    /**
     * 연결이슈 처리
     * - 현재 이슈의 linkedIssues를 순회
     * - 연결된 이슈가 적합 요구사항/하위이슈인 경우 해당 버전에 count
     */
    private void processLinkedIssuesOnly(
            AlmIssueEntity currentIssue,
            Map<String, AlmIssueEntity> rightReqIssues,
            Map<String, AlmIssueEntity> rightSubtask,
            Map<Long, VersionAssigneeSummary> summaryMap) {

        // linkedIssues가 없으면 종료
        if (currentIssue.getLinkedIssues() == null || currentIssue.getLinkedIssues().isEmpty()) {
            return;
        }

        // 각 연결된 이슈 처리
        for (String linkedRecentId : currentIssue.getLinkedIssues()) {
            AlmIssueEntity linkedIssue = null;
            boolean isReqIssue = false;

            // 연결된 이슈가 적합 요구사항인지 확인
            if (rightReqIssues.containsKey(linkedRecentId)) {
                linkedIssue = rightReqIssues.get(linkedRecentId);
                isReqIssue = true;
            }
            // 연결된 이슈가 적합 하위이슈인지 확인
            else if (rightSubtask.containsKey(linkedRecentId)) {
                linkedIssue = rightSubtask.get(linkedRecentId);
                isReqIssue = false;
            }

            // 적합 이슈가 아니면 skip
            if (linkedIssue == null) {
                continue;
            }

            // 연결된 이슈의 각 버전에 대해 처리
            if (linkedIssue.getPdServiceVersions() != null
                    && !linkedIssue.getPdServiceVersions().isEmpty()) {

                for (Long versionId : linkedIssue.getPdServiceVersions()) {
                    countLinkedIssueForVersion(versionId, currentIssue, summaryMap, isReqIssue);
                }
            }
        }
    }

    /**
     * 연결이슈를 특정 버전에 count
     * - treatedIssue 체크하여 중복 방지
     */
    private void countLinkedIssueForVersion(
            Long versionId,
            AlmIssueEntity currentIssue,
            Map<Long, VersionAssigneeSummary> summaryMap,
            boolean isReqIssue) {

        // VersionAssigneeSummary 가져오기 (없으면 생성하지 않음)
        VersionAssigneeSummary summary = summaryMap.get(versionId);
        if (summary == null) {
            // 버전이 아직 초기화되지 않은 경우 생성
            summary = VersionAssigneeSummary.builder()
                    .versionId(versionId)
                    .treatedIssue(new HashSet<>())
                    .accountIdSummaryMap(new HashMap<>())
                    .build();
            summaryMap.put(versionId, summary);
        }

        // 이미 처리된 이슈인지 확인
        if (summary.getTreatedIssue().contains(currentIssue.getRecentId())) {
            return; // 이미 처리됨
        }

        // Assignee 정보 확인
        if (currentIssue.getAssignee() == null
                || currentIssue.getAssignee().getAccountId() == null) {
            return;
        }

        String accountId = currentIssue.getAssignee().getAccountId();

        // VersionAssigneeSummaryVO 가져오기 또는 생성
        VersionAssigneeSummaryVO vo = summary.getAccountIdSummaryMap().computeIfAbsent(accountId,
                k -> {
                    VersionAssigneeSummaryVO newVo = VersionAssigneeSummaryVO.builder()
                            .versionId(versionId)
                            .accountId(accountId)
                            .name(currentIssue.getAssignee().getDisplayName())
                            .emailAddress(currentIssue.getAssignee().getEmailAddress())
                            .totalIssue(0L)
                            .reqIssueCount(0L)
                            .notReqIssueCount(0L)
                            .build();
                    newVo.setVersionAccountId(versionId, accountId);
                    return newVo;
                }
        );

        // Count 증가
        if (isReqIssue) {
            vo.addReqIssueCount();
            vo.sumReqIssueCountToTotal(1L);
        } else {
            vo.addNotReqIssueCount();
            vo.sumNotReqIssueCountToTotal(1L);
        }

        // 처리된 이슈로 마킹
        summary.getTreatedIssue().add(currentIssue.getRecentId());
    }

    /**
     * 단순 연결이슈 및 연결이슈의 하위이슈 처리
     * - 자신의 연결이슈 처리
     * - parentReqKey가 있고 부모가 justLinkedKeyIssues에 있으면 부모의 연결이슈도 처리
     */
    private void processJustLinkedIssue(
            AlmIssueEntity issue,
            Map<String, AlmIssueEntity> rightReqIssues,
            Map<String, AlmIssueEntity> rightSubtask,
            Map<String, AlmIssueEntity> justLinkedKeyIssues,
            Map<Long, VersionAssigneeSummary> summaryMap) {

        // 1. 자신의 연결이슈 처리
        processLinkedIssuesOnly(issue, rightReqIssues, rightSubtask, summaryMap);

        // 2. 연결이슈의 하위이슈인 경우 (parentReqKey 존재)
        if (!ObjectUtils.isEmpty(issue.getParentReqKey())) {
            // 부모 이슈 찾기 (justLinkedKeyIssues에서)
            AlmIssueEntity parentIssue = findParentIssueInJustLinked(
                    issue.getParentReqKey(),
                    justLinkedKeyIssues);

            if (parentIssue != null) {
                // 부모의 연결이슈 처리
                processLinkedIssuesOnly(parentIssue, rightReqIssues, rightSubtask, summaryMap);
            }
        }
    }

    /**
     * justLinkedKeyIssues에서 parentReqKey로 부모 이슈 찾기
     */
    private AlmIssueEntity findParentIssueInJustLinked(
            String parentReqKey,
            Map<String, AlmIssueEntity> justLinkedKeyIssues) {

        // parentReqKey는 key 값이므로, recentId로 된 맵에서 찾으려면 순회 필요
        for (AlmIssueEntity issue : justLinkedKeyIssues.values()) {
            if (parentReqKey.equals(issue.getKey())) {
                return issue;
            }
        }
        return null;
    }

    /**
     * VersionAssigneeSummary 맵을 SankeyChartBaseVO 리스트로 변환
     */
    private List<SankeyChartBaseVO> buildSankeyChartBaseListV3(
            Map<Long, VersionAssigneeSummary> summaryMap) {

        if (summaryMap == null || summaryMap.isEmpty()) {
            return Collections.emptyList();
        }

        return summaryMap.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .map(entry -> {
                    Long versionId = entry.getKey();
                    VersionAssigneeSummary summary = entry.getValue();

                    List<VersionAssigneeSummaryVO> sortedList =
                            summary.getAccountIdSummaryMap().values().stream()
                                    .filter(Objects::nonNull)
                                    .sorted(Comparator
                                            .comparingLong((VersionAssigneeSummaryVO vo) ->
                                                    Optional.ofNullable(vo.getTotalIssue()).orElse(0L))
                                            .reversed()
                                            .thenComparing(vo -> Optional.ofNullable(vo.getAccountId())
                                                    .orElse("")))
                                    .collect(Collectors.toList());

                    return SankeyChartBaseVO.builder()
                            .versionId(versionId)
                            .versionAssigneeSummaryVOList(sortedList)
                            .build();
                })
                .collect(Collectors.toList());
    }

    @Override
    public PersonalIssueVO getPersonalIssueData(ResourceRequestDTO dto) {
        ResourceRequestDTO resourceDTO = new ResourceRequestDTO();
        resourceDTO.setPdServiceAndIsReq(dto.getPdServiceAndIsReq());

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

        SimpleQuery<MainGroupDTO> mainGroupDTOSimpleQuery = aggregation(
                AggregationRequestDTO.builder()
                        .mainField("assignee.assignee_accountId.keyword")
                        .mainFieldAlias("accountId")
                        .addGroup(
                                SubGroupFieldDTO.builder()
                                        .subFieldAlias("isReq")
                                        .subField("isReq")
                                        .build()
                        )
                        .addGroup(
                                SubGroupFieldDTO.builder()
                                        .subFieldAlias("status")
                                        .subField("status.status_name.keyword")
                                        .build()
                        )
                        .build()
        )
                .andTermsQueryFilter("linkedIssuePdServiceIds", List.of(pdServiceId))
                .andTermsQueryFilter("linkedIssuePdServiceVersions", pdServiceVersions)
                .andRangeQueryFilter(RangeQueryFilter.of("updated")
                        .betweenDate(resourceDTO.getStartDate(), resourceDTO.getEndDate()))
                .andExistsQueryFilter("assignee")
                .andTermsQueryFilter("assignee.assignee_accountId.keyword", resourceDTO.getAccounts());

        DocumentAggregations documentAggregations = esCommonRepositoryWrapper.aggregateRecent(mainGroupDTOSimpleQuery);
        List<DocumentBucket> deepestList = documentAggregations.deepestList();

        long reqIssueCount = 0L;
        long notReqIssueCount = 0L;
        long completedReqIssueCount = 0L;
        long inProgressReqIssueCount = 0L;
        long openReqIssueCount = 0L;
        long etcReqIssueCount = 0L;

        for (DocumentBucket documentBucket : deepestList) {
            String isReqName = documentBucket.valueByName("isReq");
            String statusName = documentBucket.valueByName("status");
            Long statusCount = documentBucket.countByName("status");

            if (statusCount == null) {
                statusCount = 0L;
            }

            boolean isReq = "true".equalsIgnoreCase(isReqName);

            if (isReq) {
                reqIssueCount += statusCount;

                if ("완료".equals(statusName) || "Resolved".equals(statusName) || "닫힘".equals(statusName)) {
                    completedReqIssueCount += statusCount;
                } else if ("진행 중".equals(statusName) || "In Progress".equals(statusName)) {
                    inProgressReqIssueCount += statusCount;
                } else if ("열림".equals(statusName) || "Open".equals(statusName)) {
                    openReqIssueCount += statusCount;
                } else {
                    etcReqIssueCount += statusCount;
                }
            } else {
                notReqIssueCount += statusCount;
            }
        }

        return PersonalIssueVO.builder()
                .reqIssueCount(reqIssueCount)
                .notReqIssueCount(notReqIssueCount)
                .totalReqIssueCount(reqIssueCount + notReqIssueCount)
                .completedReqIssueCount(completedReqIssueCount)
                .inProgressReqIssueCount(inProgressReqIssueCount)
                .openReqIssueCount(openReqIssueCount)
                .etcReqIssueCount(etcReqIssueCount)
                .build();
    }
}
