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

import com.arms.api.analysis.time.model.dto.TimeDTO;
import com.arms.api.analysis.time.model.vo.*;
import com.arms.api.issue.almapi.model.entity.AlmIssueEntity;
import com.arms.api.requirement.service.RequirementService;
import com.arms.api.util.ParseUtil;
import com.arms.api.util.model.dto.PdServiceAndIsReqDTO;
import com.arms.egovframework.javaservice.esframework.esquery.SimpleQuery;
import com.arms.egovframework.javaservice.esframework.esquery.filter.RangeQueryFilter;
import com.arms.egovframework.javaservice.esframework.model.dto.esquery.CompositeSourceDTO;
import com.arms.egovframework.javaservice.esframework.model.dto.esquery.SortDTO;
import com.arms.egovframework.javaservice.esframework.model.dto.esquery.SubGroupFieldDTO;
import com.arms.egovframework.javaservice.esframework.model.dto.request.AggregationCardinalityRequestDTO;
import com.arms.egovframework.javaservice.esframework.model.vo.DocumentAggregations;
import com.arms.egovframework.javaservice.esframework.model.vo.DocumentBucket;
import com.arms.egovframework.javaservice.esframework.repository.common.EsCommonRepositoryWrapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@Service("analysisTime")
@AllArgsConstructor
public class AnalysisTimeImpl implements AnalysisTime {

    private final EsCommonRepositoryWrapper<AlmIssueEntity> esCommonRepositoryWrapper;

    private final RequirementService requirementService;

    // 스캐터, 히트맵에 사용.
    @Override
    public DateIssueCountVO getDateHistogramCardinalityData(TimeDTO timeDTO) {

        Long pdServiceId = timeDTO.getPdServiceAndIsReq().getPdServiceId();
        List<Long> versions = timeDTO.getPdServiceAndIsReq().getPdServiceVersions();

        // pdServiceId, Versions, isReq, 날짜가 모두 해당하는 요구사항 이슈 집계
        DocumentAggregations reqIssueAggregations = esCommonRepositoryWrapper.aggregateCardinalityByDay(
                SimpleQuery.aggregation(
                                AggregationCardinalityRequestDTO.builder()
                                        .mainFieldAlias("updated")
                                        .mainField("updated")
                                        .cardinality(
                                                SubGroupFieldDTO.builder()
                                                        .subFieldAlias("key")
                                                        .subField("key")
                                                        .build()
                                        )
                                        .build())
                        .andTermQueryMust("pdServiceId", pdServiceId)
                        .andTermsQueryFilter("pdServiceVersions", versions)
                        .andTermQueryMust("isReq", Boolean.TRUE)
                        .andRangeQueryFilter(
                                RangeQueryFilter.of("updated")
                                        .betweenDate(String.valueOf(timeDTO.getStartDate()), String.valueOf(timeDTO.getEndDate()))
                        )
        );

        Map<String, Integer> reqIssueCountsOfDate = new HashMap<>();
        for (DocumentBucket docBucket : reqIssueAggregations.deepestList()) {
            String date = docBucket.valueByName("updated");
            Float key = docBucket.floatCountByName("key");
            int integerOfKey = Math.round(key);
            reqIssueCountsOfDate.put(transformDate(date), integerOfKey);
        }

        // 전체 이슈 집계 (pdServiceId, Versions, 날짜 해당)
        DocumentAggregations allIssueAggregations = esCommonRepositoryWrapper.aggregateCardinalityByDay(
                SimpleQuery.aggregation(
                                AggregationCardinalityRequestDTO.builder()
                                        .mainFieldAlias("updated")
                                        .mainField("updated")
                                        .cardinality(
                                                SubGroupFieldDTO.builder()
                                                        .subFieldAlias("key")
                                                        .subField("key")
                                                        .build()
                                        )
                                        .build())
                        .andTermsQueryFilter("linkedIssuePdServiceIds", List.of(pdServiceId))
                        .andTermsQueryFilter("linkedIssuePdServiceVersions", versions)
                        .andRangeQueryFilter(
                                RangeQueryFilter.of("updated")
                                        .betweenDate(String.valueOf(timeDTO.getStartDate()), String.valueOf(timeDTO.getEndDate()))
                        )
        );

        Map<String, Integer> allIssueCountsOfDate = new HashMap<>();
        for (DocumentBucket documentBucket : allIssueAggregations.deepestList()) {
            String date = documentBucket.valueByName("updated");
            Float key = documentBucket.floatCountByName("key");
            int integerOfKey = Math.round(key);
            allIssueCountsOfDate.put(transformDate(date), integerOfKey);
        }

        Map<String, Integer> notReqIssueCountsOfDate = new HashMap<>(allIssueCountsOfDate);
        for (Map.Entry<String, Integer> entry : reqIssueCountsOfDate.entrySet()) {
            String date = entry.getKey();
            Integer totalCount = notReqIssueCountsOfDate.getOrDefault(date, 0);
            Integer reqCount = entry.getValue();
            notReqIssueCountsOfDate.put(date, Math.max(0, totalCount - reqCount));
        }

        return DateIssueCountVO.builder()
                .reqIssuesDateCountMap(reqIssueCountsOfDate)
                .notReqIssuesCountMap(notReqIssueCountsOfDate)
                .build();
    }

    @Override
    public HeatMapVO getHeatMapDataByUpdated(TimeDTO timeDTO) {
        TimeDTO processedTimeDTO = processTimeRangeDTO(timeDTO);

        DateIssueCountVO dateHistogramCardinalityData = this.getDateHistogramCardinalityData(processedTimeDTO);
        List<HeatMapDateVO> reqIssueDateVO = processHeatMapData(dateHistogramCardinalityData.getReqIssuesDateCountMap());
        List<HeatMapDateVO> notReqIssueDateVO = processHeatMapData(dateHistogramCardinalityData.getNotReqIssuesCountMap());

        return HeatMapVO.builder()
                .requirement(reqIssueDateVO)
                .relationIssue(notReqIssueDateVO)
                .build();
    }

    private TimeDTO processTimeRangeDTO(TimeDTO originalDTO) {
        LocalDate now = LocalDate.now(ZoneId.of("UTC"));
        String startDate = originalDTO.getStartDate();
        String endDate = originalDTO.getEndDate();

        // 시작일, 종료일 모두 없는 경우 (현재로부터 1년)
        if (startDate == null && endDate == null) {
            return copyWithDates(originalDTO,
                    String.valueOf(now.minusYears(1L)),
                    String.valueOf(now));
        }
        // 시작일만 존재하는 경우 (시작일 ~ 지금(or 최대1년))
        if (startDate != null && endDate == null) {
            LocalDate startLocalDate = LocalDate.parse(startDate);
            LocalDate endLocalDate = startLocalDate.plusYears(1L);
            endLocalDate = endLocalDate.isAfter(now) ? now : endLocalDate;
            return copyWithDates(originalDTO,
                    startDate,
                    String.valueOf(endLocalDate));
        }
        // 종료일만 존재하는 경우 (종료일로부터 1년)
        if (startDate == null && endDate != null) {
            LocalDate endLocalDate = LocalDate.parse(endDate);
            return copyWithDates(originalDTO,
                    String.valueOf(endLocalDate.minusYears(1L)),
                    endDate);
        }

        return originalDTO;
    }

    private TimeDTO copyWithDates(TimeDTO originalDTO, String startDate, String endDate) {
        TimeDTO newDTO = new TimeDTO();
        newDTO.setPdServiceAndIsReq(originalDTO.getPdServiceAndIsReq());
        newDTO.setStartDate(startDate);
        newDTO.setEndDate(endDate);
        return newDTO;
    }

    private List<HeatMapDateVO> processHeatMapData(Map<String, Integer> issueCountsOfDate) {
        return issueCountsOfDate.entrySet().stream()
                .map(entry -> new HeatMapDateVO(entry.getKey(), entry.getValue()))
                .collect(Collectors.toList());
    }


    @Override
    public List<AlmIssueStatVO> getMultiCombinationChartData(TimeDTO timeDTO) {
        timeDTO.setSize(10000);
        List<AlmIssueStatVO> createdIssueStatusByDate = getCreatedIssueStatusByDate(timeDTO);
        List<AlmIssueStatVO> updatedIssueStatusByDate = getUpdatedIssueStatusByDate(timeDTO);

        Map<String, AlmIssueStatVO> mergedMap = new LinkedHashMap<>();
        // 1. created 데이터 먼저 처리
        createdIssueStatusByDate.stream()
                .filter(Objects::nonNull)
                .forEach(stat -> {
                    mergedMap.put(stat.getDate(), AlmIssueStatVO.builder()
                            .date(stat.getDate())
                            .totalRequirements(stat.getTotalRequirements())
                            .totalRelationIssues(stat.getTotalRelationIssues())
                            .requirementStatuses(new ArrayList<>())  // 기본값
                            .relationIssueStatuses(new ArrayList<>()) // 기본값
                            .build());
                });
        // 2. updated 데이터 병합
        updatedIssueStatusByDate.stream()
                .filter(Objects::nonNull)
                .forEach(stat -> {
                    mergedMap.compute(stat.getDate(), (key, existing) -> {
                        if (existing == null) {
                            // created 데이터가 없는 경우
                            return AlmIssueStatVO.builder()
                                    .date(stat.getDate())
                                    .totalRequirements(0L)  // 기본값
                                    .totalRelationIssues(0L) // 기본값
                                    .requirementStatuses(stat.getRequirementStatuses())
                                    .relationIssueStatuses(stat.getRelationIssueStatuses())
                                    .build();
                        } else {
                            // created 데이터가 있는 경우 - updated 정보 추가
                            return AlmIssueStatVO.builder()
                                    .date(existing.getDate())
                                    .totalRequirements(existing.getTotalRequirements())
                                    .totalRelationIssues(existing.getTotalRelationIssues())
                                    .requirementStatuses(stat.getRequirementStatuses())
                                    .relationIssueStatuses(stat.getRelationIssueStatuses())
                                    .build();
                        }
                    });
                });

        return mergedMap.values().stream()
                .sorted(Comparator.comparing(AlmIssueStatVO::getDate))
                .collect(Collectors.toList());
    }

    // 생성된 이슈 갯수 처리
    private List<AlmIssueStatVO> getCreatedIssueStatusByDate(TimeDTO timeDTO) {
        Long pdServiceId = timeDTO.getPdServiceAndIsReq().getPdServiceId();
        List<Long> versions = timeDTO.getPdServiceAndIsReq().getPdServiceVersions();
        timeDTO.setIncludeFields(new String[]{"created", "updated", "pdServiceId", "isReq"});
        timeDTO.setSources(
            List.of(
                    CompositeSourceDTO.builder()
                            .fieldAlias("update_date")
                            .field("updated")
                            .isAscending(false)
                            .build(),
                    CompositeSourceDTO.builder()
                            .fieldAlias("recent_id")
                            .field("recent_id")
                            .isAscending(false)
                            .build()
            )
        );
        timeDTO.setSortField("updated");

        List<AlmIssueEntity> allIssues = esCommonRepositoryWrapper.compositeAllHits(
                        SimpleQuery.composite(timeDTO)
                                .andTermsQueryFilter("linkedIssuePdServiceIds", List.of(pdServiceId))
                                .andTermsQueryFilter("linkedIssuePdServiceVersions", versions)
                                .andRangeQueryFilter(
                                        RangeQueryFilter.of("created")
                                                .betweenDate(String.valueOf(timeDTO.getStartDate()), String.valueOf(timeDTO.getEndDate()))
                                )
                                .andExistsQueryFilter("status")
                                .orderBy(
                                        SortDTO.builder().field("overallUpdatedDate").sortType("desc").build()
                                ))
                .toDocs();

        Map<String, AlmIssueStatVO> tempMap = new LinkedHashMap<>();

        allIssues.stream()
                .filter(Objects::nonNull)
                .forEach(entity -> {
                    String date = transformDate(entity.stringValueOfCreatedDate());
                    if(date == null) { return; }

                    boolean isReqIssue = isRequirementIssueWithMatchingService(entity, pdServiceId);

                    tempMap.compute(date, (key, existingStatVO) -> {
                        if(existingStatVO == null) {
                            return AlmIssueStatVO.builder()
                                    .date(date)
                                    .totalRequirements(isReqIssue ? 1L : 0L)
                                    .totalRelationIssues(isReqIssue ? 0L : 1L)
                                    .build();
                        }
                        else {
                            if (isReqIssue) {
                                existingStatVO.addTotalRequirements(1L);
                            } else {
                                existingStatVO.addTotalRelationIssues(1L);
                            }
                            return existingStatVO;
                        }
                    });
                });

        return new ArrayList<>(tempMap.values());
    }
    // status 처리
    private List<AlmIssueStatVO> getUpdatedIssueStatusByDate(TimeDTO timeDTO) {
        Long pdServiceId = timeDTO.getPdServiceAndIsReq().getPdServiceId();
        List<Long> versions = timeDTO.getPdServiceAndIsReq().getPdServiceVersions();

        timeDTO.setSources(
                List.of(
                        CompositeSourceDTO.builder()
                                .fieldAlias("update_date")
                                .field("updated")
                                .isAscending(false)
                                .build(),
                        CompositeSourceDTO.builder()
                                .fieldAlias("recent_id")
                                .field("recent_id")
                                .isAscending(false)
                                .build()
                )
        );
        timeDTO.setSortField("updated");
        timeDTO.setIncludeFields(new String[]{"created","updated", "pdServiceId", "isReq", "status.status_name.keyword"});

        List<AlmIssueEntity> allIssues = esCommonRepositoryWrapper.compositeAllHits(
                        SimpleQuery.composite(timeDTO)
                                .andTermsQueryFilter("linkedIssuePdServiceIds", List.of(pdServiceId))
                                .andTermsQueryFilter("linkedIssuePdServiceVersions", versions)
                                .andRangeQueryFilter(
                                        RangeQueryFilter.of("updated")
                                                .betweenDate(String.valueOf(timeDTO.getStartDate()), String.valueOf(timeDTO.getEndDate()))
                                )
                                .andExistsQueryFilter("status")
                                .orderBy(
                                        SortDTO.builder().field("overallUpdatedDate").sortType("desc").build()
                                )
                    )
                .toDocs();

        Map<String, AlmIssueStatVO> tempMap = new LinkedHashMap<>();

        allIssues.stream()
                .filter(Objects::nonNull)
                .forEach(entity -> {
                    String date = transformDate(entity.stringValueOfUpdatedDate());
                    if (date == null) return;

                    String status = extractStatusName(entity);
                    boolean isReqIssue = isRequirementIssueWithMatchingService(entity, pdServiceId);

                    tempMap.compute(date, (key, existingStatVO) ->
                            updateOrCreateStatVO(date, status, isReqIssue, existingStatVO)
                    );
                });

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

    @Override
    public List<ScatterChartVO> getScatterData(TimeDTO timeDTO) {
        TimeDTO processedTimeDTO = processTimeRangeDTO(timeDTO);

        DateIssueCountVO dateHistogramCardinalityData = this.getDateHistogramCardinalityData(processedTimeDTO);

        Map<String, Integer> reqIssuesDateCountMap = dateHistogramCardinalityData.getReqIssuesDateCountMap();
        Map<String, Integer> notReqIssuesCountMap = dateHistogramCardinalityData.getNotReqIssuesCountMap();

        Map<String, ScatterChartVO> scatterChartVOMap = new HashMap<>();
        reqIssuesDateCountMap.forEach((date, count) -> {
            ScatterChartVO scatterChartVO = scatterChartVOMap.computeIfAbsent(date,
                    key -> ScatterChartVO.builder()
                            .date(key)
                            .requirement(0L)
                            .relationIssue(0L)
                            .build()
            );
            scatterChartVO.addRequirement(count);
        });
        notReqIssuesCountMap.forEach((date, count) -> {
            ScatterChartVO scatterChartVO = scatterChartVOMap.computeIfAbsent(date,
                    key -> ScatterChartVO.builder()
                            .date(key)
                            .requirement(0L)
                            .relationIssue(0L)
                            .build()
            );

            scatterChartVO.addRelationIssue(count);
        });

        return scatterChartVOMap.values().stream().sorted(Comparator.comparing(ScatterChartVO::getDate)).toList();
    }

    @Override
    public List<VerticalTimeLineVO> getUpdatedReqIssueByDateRange(TimeDTO timeDTO) {
        // findUpdateHistory
        timeDTO.setSources(
                List.of(
                        CompositeSourceDTO.builder()
                                .fieldAlias("update_date")
                                .field("updated")
                                .isAscending(false)
                                .build(),
                        CompositeSourceDTO.builder()
                                .fieldAlias("recent_id")
                                .field("recent_id")
                                .isAscending(false)
                                .build()
                )
        );
        timeDTO.setSortField("updated");
        timeDTO.setIncludeFields(new String[]{"pdServiceVersions","summary.keyword", "project.project_name.keyword", "updated"});

        List<AlmIssueEntity> verticalTimelineIssueList = esCommonRepositoryWrapper.compositeAllHits(
                SimpleQuery.composite(timeDTO)
                        .andTermsQueryFilter("pdServiceVersions", timeDTO.getPdServiceAndIsReq().getPdServiceVersions())
                        .andTermQueryMust("pdServiceId", timeDTO.getPdServiceAndIsReq().getPdServiceId())
                        .andRangeQueryFilter(RangeQueryFilter.of("updated")
                                .betweenDate(timeDTO.getStartDate(),timeDTO.getEndDate()))
                        .andTermQueryMust("isReq", true)
                        .orderBy(SortDTO.builder()
                                .field("updated")
                                .sortType("desc").build()
                        )
        ).toDocs();

        return buildAlmIssueToVerticalTimelineVO(verticalTimelineIssueList);

    }

    @Override
    public List<RidgeLineVO> getRidgeLineData(TimeDTO timeDTO) {

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

        List<AlmIssueEntity> retrievedReqIssues = retrieveReqIssues(timeDTO);

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

        Map<String, KeyVerSummaryByIssueVO> keyToSummaryMap = buildKeyVerSummaryMapByIssues(retrievedReqIssues);
        Set<String> recentIdSetOfReqIssue = retrievedReqIssues.stream()
                .map(AlmIssueEntity::getRecentId)
                .collect(Collectors.toSet());

        // 3. (date -> set(rootReqKey)) 유니크 카운트
        Map<String, Map<String, IssueKeyUpdateCountVO>> dateIssueUpdateCountsMap = new HashMap<>();

        // 전체 - 제품서비스 및 버전에 대한 이슈 가져오기. (담당자 존재)
        List<AlmIssueEntity> allIssuesByUpdateHistory = findAllIssuesByUpdateHistory(timeDTO);

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

        for (AlmIssueEntity issue : allIssuesByUpdateHistory) {

            String updateDate = transformDate(issue.stringValueOfUpdatedDate());

            // 적합한 요구사항
            if (isRequirementIssueWithMatchingService(issue, pdServiceId)) {
                String key = issue.getKey();
                // 요구사항의 업데이트 내용 처리
                processDateIssueUpdateCountsMapByIssueKey(dateIssueUpdateCountsMap, updateDate, key);
            }

            // 적합 하위이슈
            if (!ObjectUtils.isEmpty(issue.getPdServiceId())
                    && ObjectUtils.nullSafeEquals(issue.getPdServiceId(), pdServiceId)
                    && issue.getIsReq().equals(Boolean.FALSE)) {

                String parentReqKey = issue.getParentReqKey();
                String parentReqRecentId = getParentReqRecentId(issue);

                // 하위이슈의 업데이트 내용 반영 (to 요구사항)
                if (!ObjectUtils.isEmpty(parentReqRecentId)
                        && recentIdSetOfReqIssue.contains(parentReqRecentId)) {
                    processDateIssueUpdateCountsMapByIssueKey(dateIssueUpdateCountsMap, updateDate, parentReqKey);
                }

            } //. 하위이슈 업데이트 내용 반영

            // 연결이슈들 업데이트 내용 반영 (to 요구사항)
            List<String> linkedIssues = issue.getLinkedIssues();

            if (ObjectUtils.isEmpty(linkedIssues)) {
                continue;
            }

            for (String linkedIssueRecentId : linkedIssues) {
                if (recentIdSetOfReqIssue.contains(linkedIssueRecentId)) {
                    String keyOfRecentId = ParseUtil.getLastSegment(linkedIssueRecentId);

                    processDateIssueUpdateCountsMapByIssueKey(dateIssueUpdateCountsMap, updateDate, keyOfRecentId);

                }
            } // .연결이슈들 업데이트 내용 반영

        }

        List<RidgeLineVO> ridgeLineData = new ArrayList<>();
        dateIssueUpdateCountsMap.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .forEach( e -> {
                    String date = e.getKey();
                    for (Map.Entry<String, IssueKeyUpdateCountVO> entry : e.getValue().entrySet()) {
                        String key = entry.getKey();
                        IssueKeyUpdateCountVO issueKeyUpdateCountVO = entry.getValue();
                        KeyVerSummaryByIssueVO keyVerSummaryByIssue = keyToSummaryMap.get(key);
                        String versionsWithComma = versionLinkWithComma(keyVerSummaryByIssue.getVersions().stream()
                                .map(String::valueOf).collect(Collectors.toList()));
                        ridgeLineData.add(RidgeLineVO.builder()
                                .updateDate(date)
                                .key(key)
                                .version(versionsWithComma)
                                .summary(keyVerSummaryByIssue.getSummary())
                                .updatedCount(issueKeyUpdateCountVO.getUpdateCount())
                                .build());
                    }
                });

        if (ridgeLineData.size() == 1) {
            return reRenderRidgeLineDate(ridgeLineData);
        } else {
            return ridgeLineData;
        }
    }

    private Map<String, KeyVerSummaryByIssueVO> buildKeyVerSummaryMapByIssues(List<AlmIssueEntity> issueEntityList) {
        if (ObjectUtils.isEmpty(issueEntityList)) {
            return Collections.emptyMap();
        }

        return issueEntityList.stream()
                .collect(Collectors.toMap(
                        AlmIssueEntity::getKey,  // key mapper
                        issue -> KeyVerSummaryByIssueVO.builder()  // value mapper
                                .key(issue.getKey())
                                .summary(issue.getSummary())
                                .versions(issue.getPdServiceVersions())
                                .build(),
                        (existing, incoming) -> {  // merge function
                            // 기존 versions와 새로운 versions를 합치고 중복 제거
                            List<Long> mergedVersions = Stream.concat(
                                            existing.getVersions().stream(),
                                            incoming.getVersions().stream()
                                    )
                                    .distinct()
                                    .sorted()
                                    .collect(Collectors.toList());

                            return KeyVerSummaryByIssueVO.builder()
                                    .key(existing.getKey())
                                    .summary(existing.getSummary())
                                    .versions(mergedVersions)
                                    .build();
                        }
                ));
    }

    private String getParentReqRecentId(AlmIssueEntity issue) {
        String parentReqIssueKey = issue.getParentReqKey();
        String connectIdWithProjectKey = ParseUtil.getPrefixIncludingLastDelimiter(issue.recentId());
        if (ObjectUtils.isEmpty(issue.getParentReqKey())) {
            return null;
        }
        if (ObjectUtils.isEmpty(issue.getRecentId())) {
            return null;
        }
        if (ObjectUtils.isEmpty(connectIdWithProjectKey)) {
            return null;
        }
        return connectIdWithProjectKey + parentReqIssueKey;
    }

    private void processDateIssueUpdateCountsMapByIssueKey(
            Map<String, Map<String, IssueKeyUpdateCountVO>> dateIssueUpdateCountsMap,
            String updateDate,
            String key) {

        dateIssueUpdateCountsMap
                .computeIfAbsent(updateDate, k -> new HashMap<>())
                .compute(key, (k, existing) -> {
                    if (existing == null) {
                        // 새로운 이슈: updateCount = 1로 초기화
                        return IssueKeyUpdateCountVO.builder()
                                .key(key)
                                .updateCount(1L)
                                .build();
                    } else {
                        // 기존 이슈: updateCount 증가
                        existing.addUpdateCount(1L);
                        return existing;
                    }
                });
    }

    private String versionLinkWithComma(List<String> versions) {
        if (versions == null || versions.isEmpty()) {return "";}
        if (versions.size() == 1) {return versions.getFirst();}
        else {
            return versions.stream()
                    .map(String::valueOf)
                    .collect(Collectors.joining(","));
        }
    }

    private List<VerticalTimeLineVO> buildAlmIssueToVerticalTimelineVO(List<AlmIssueEntity> almIssueEntityList) {
        List<VerticalTimeLineVO> verticalTimelineList = new ArrayList<>();

        for (AlmIssueEntity almIssueEntity : almIssueEntityList) {
            String projectName;
            if (ObjectUtils.isEmpty(almIssueEntity.getProject()) || ObjectUtils.isEmpty(almIssueEntity.getProject().getName())) {
                projectName = "";
            } else {
                projectName = almIssueEntity.getProject().getName();
            }

            verticalTimelineList.add(
                    VerticalTimeLineVO.builder()
                    .pdServiceVersions(almIssueEntity.getPdServiceVersions())
                    .projectName(projectName)
                    .updated(almIssueEntity.getUpdated())
                    .summary(almIssueEntity.getSummary())
                    .build()
            );
        }

        return verticalTimelineList;
    }

    // overall 로 변경(요구사항 및 하위이슈 업데이트 기준으로 서치) - 변경예정
    private List<AlmIssueEntity> findReqIssueWithOverallUpdateHistory(TimeDTO timeDTO) {
        return esCommonRepositoryWrapper.findHits(
                SimpleQuery.termQueryMust("pdServiceId", timeDTO.fetchPdServiceLink())
                        .andTermsQueryFilter("pdServiceVersions", timeDTO.fetchPdServiceVersionLinks())
                        .andTermQueryMust("isReq", timeDTO.fetchIsReq())
                        .andRangeQueryFilter(
                                RangeQueryFilter.of("overallUpdatedDate")
                                        .betweenDate(timeDTO.getStartDate(), timeDTO.getEndDate()))
                        .orderBy(
                                SortDTO.builder().field("overallUpdatedDate").sortType("desc").build()
                        )).toDocs();
    }

    private List<AlmIssueEntity> findAllIssuesByUpdateHistory(TimeDTO timeDTO) {
        return esCommonRepositoryWrapper.findHits(
                SimpleQuery.termsQueryFilter("linkedIssuePdServiceIds", List.of(timeDTO.fetchPdServiceLink()))
                        .andTermsQueryFilter("linkedIssuePdServiceVersions", timeDTO.fetchPdServiceVersionLinks())
                        .andTermQueryMust("isReq", timeDTO.fetchIsReq())
                        .andExistsQueryFilter("assignee")
                        .andRangeQueryFilter(
                                RangeQueryFilter.of("updated")
                                        .betweenDate(timeDTO.getStartDate(), timeDTO.getEndDate()))
                        .orderBy(
                                SortDTO.builder().field("updated").sortType("desc").build()
                        )).toDocs();
    }

    private List<RidgeLineVO> reRenderRidgeLineDate(List<RidgeLineVO> ridgeLineData) {

        if (ridgeLineData.size() == 1) {
            RidgeLineVO singleData = ridgeLineData.getFirst();

            LocalDate currentDate = LocalDate.parse(singleData.getUpdateDate());

            String previousDate = currentDate.minusDays(1).toString();
            String nextDate = currentDate.plusDays(1).toString();

            RidgeLineVO previousDayData = RidgeLineVO.builder()
                    .version(singleData.getVersion())
                    .updateDate(previousDate)
                    .key(singleData.getKey())
                    .updatedCount(0)
                    .summary(singleData.getSummary())
                    .build();

            RidgeLineVO nextDayData = RidgeLineVO.builder()
                    .version(singleData.getVersion())
                    .updateDate(nextDate)
                    .key(singleData.getKey())
                    .updatedCount(0)
                    .summary(singleData.getSummary())
                    .build();

            return List.of(previousDayData, singleData, nextDayData);
        }
        return ridgeLineData;
    }


    private String transformDate(String date) {
        String subDate = date.substring(0,10);
        LocalDate localDate = LocalDate.parse(subDate);
        return DateTimeFormatter.ofPattern("yyyy-MM-dd").format(localDate);
    }

    /**
     * Validates whether the given issue meets both of the following criteria:
     * 1. Must be a requirement-issue
     * 2. Must have a product service ID that matches the provided ID
     *
     * @param issue The issue to validate
     * @param pdServiceId The product service ID to match against
     * @return true if all conditions are met, false otherwise
     */
    private boolean isRequirementIssueWithMatchingService(AlmIssueEntity issue, Long pdServiceId) {

        if (ObjectUtils.isEmpty(issue.getPdServiceId())) {
            return false;
        }
        if (issue.getIsReq() == false) {
            return false;
        }
        if (!issue.getPdServiceId().equals(pdServiceId)) {
            return false;
        }
        // 명시적으로 표시
        if (issue.getIsReq() && issue.getPdServiceId().equals(pdServiceId)) {
            return true;
        }

        return false;
    }

    // 상태명 추출 로직 분리
    private String extractStatusName(AlmIssueEntity entity) {
        if (entity.getStatus() != null && entity.getStatus().getName() != null) {
            return entity.getStatus().getName();
        }
        return "UNKNOWN";
    }

    // compute 내부 로직 분리
    private AlmIssueStatVO updateOrCreateStatVO(String date,
                                                String status,
                                                boolean isReqIssue,
                                                AlmIssueStatVO existingStatVO) {
        // 기존 맵 가져오기 또는 새로 생성
        Map<String, Long> requirementStatusMap = getStatusMap(existingStatVO, true);
        Map<String, Long> relationIssueStatusMap = getStatusMap(existingStatVO, false);

        // 해당하는 맵에 카운트 증가
        if (isReqIssue) {
            requirementStatusMap.merge(status, 1L, Long::sum);
        } else {
            relationIssueStatusMap.merge(status, 1L, Long::sum);
        }

        return AlmIssueStatVO.builder()
                .date(date)
                .requirementStatuses(toStatusCountList(requirementStatusMap))
                .relationIssueStatuses(toStatusCountList(relationIssueStatusMap))
                .build();
    }

    // 기존 StatVO에서 Map 추출하는 로직 분리
    private Map<String, Long> getStatusMap(AlmIssueStatVO statVO, boolean isReqIssue) {
        if (statVO == null) {
            return new HashMap<>();
        }

        List<StatusCountVO> statusList = isReqIssue
                ? statVO.getRequirementStatuses()
                : statVO.getRelationIssueStatuses();

        if (statusList == null) {
            return new HashMap<>();
        }

        return statusList.stream()
                .collect(Collectors.toMap(
                        StatusCountVO::getStatus,
                        StatusCountVO::getCount,
                        (existing, replacement) -> existing // 중복 키 처리
                ));
    }

    // Map을 List<StatusCountVO>로 변환하는 로직 분리
    private List<StatusCountVO> toStatusCountList(Map<String, Long> statusMap) {
        return statusMap.entrySet().stream()
                .map(entry -> new StatusCountVO(entry.getKey(), entry.getValue()))
                .collect(Collectors.toList());
    }

    // 적합 요구사항만
    private List<AlmIssueEntity> retrieveReqIssues(TimeDTO timeDTO) {
        TimeDTO processedTimeDTO = applyDefaultTimeRangeByOneYear(timeDTO);

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

        return esCommonRepositoryWrapper.findRecentHits(
                SimpleQuery.termQueryMust("pdServiceId", pdServiceId)
                        .andTermsQueryFilter("pdServiceVersions", pdServiceVersions)
                        .andTermQueryFilter("isReq", Boolean.TRUE)
                        .andRangeQueryFilter(RangeQueryFilter.of("updated")
                                .betweenDate(processedTimeDTO.getStartDate(), processedTimeDTO.getEndDate()))
                        .andExistsQueryFilter("assignee")
        ).toDocs();
    }

    private TimeDTO applyDefaultTimeRangeByOneYear(TimeDTO originalDTO) {
        LocalDate now = LocalDate.now(ZoneId.of("UTC"));
        String startDate = originalDTO.getStartDate();
        String endDate = originalDTO.getEndDate();

        // 1. 시작일, 종료일 모두 없는 경우 (현재로부터 1년)
        if (startDate == null && endDate == null) {
            return copyWithDates(originalDTO,
                    String.valueOf(now.minusYears(1L)),
                    String.valueOf(now));
        }

        // 2. 시작일만 존재하는 경우 (시작일 ~ 지금(or 최대 1년))
        else if (startDate != null && endDate == null) {
            LocalDate startLocalDate = LocalDate.parse(startDate);
            LocalDate endLocalDate = startLocalDate.plusYears(1L);
            endLocalDate = endLocalDate.isAfter(now) ? now : endLocalDate;
            return copyWithDates(originalDTO,
                    startDate,
                    String.valueOf(endLocalDate));
        }

        // 3. 종료일만 존재하는 경우 (종료일로부터 1년)
        else if (startDate == null && endDate != null) {
            LocalDate endLocalDate = LocalDate.parse(endDate);
            return copyWithDates(originalDTO,
                    String.valueOf(endLocalDate.minusYears(1L)),
                    endDate);
        }

        // 4. 시작일과 종료일이 모두 존재하는 경우:
        //    요구사항에 따라, endDate 기준으로 startDate를 1년 전으로 강제 재설정
        else if (startDate != null && endDate != null) {
            LocalDate endLocalDate = LocalDate.parse(endDate);
            // endDate 기준으로 1년 전 날짜를 새로운 startDate로 설정
            String newStartDate = String.valueOf(endLocalDate.minusYears(1L));

            // 기존의 TimeDTO 내용을 유지하고, startDate만 재설정된 새로운 DTO 반환
            return copyWithDates(originalDTO,
                    newStartDate,
                    endDate);
        }
        else {
            return originalDTO;
        }
    }

    public void updateOrAddIssueCount(Set<IssueKeyUpdateCountVO> issueKeyUpdateCountVOS, String inputKey) {
        // 1. Set을 순회하며 inputKey와 일치하는 요소를 찾습니다.
        IssueKeyUpdateCountVO foundVO = null;
        for (IssueKeyUpdateCountVO vo : issueKeyUpdateCountVOS) {
            if (vo.getKey().equals(inputKey)) {
                foundVO = vo;
                break; // 찾았으니 반복 중단
            }
        }
        if (foundVO != null) {
            // 2. 요소가 있다면: updateCount를 1 증가시킵니다.
            // Set 내부의 객체 상태를 변경하는 것은 허용됩니다.
            foundVO.addUpdateCount(1L);
        } else {
            // 3. 요소가 없다면: key=inputKey, updateCount=1인 새로운 VO를 추가합니다.
            IssueKeyUpdateCountVO newVO = new IssueKeyUpdateCountVO(inputKey, 1L);
            issueKeyUpdateCountVOS.add(newVO);
        }
    }
}

