package com.arms.api.dashboard.service;

import com.arms.api.analysis.resource.model.dto.ResourceRequestDTO;
import com.arms.api.analysis.resource.service.AnalysisResource;
import com.arms.api.analysis.resource.model.vo.UniqueAssigneeVO;
import com.arms.api.dashboard.model.dto.DashboardDTO;
import com.arms.api.dashboard.model.vo.*;
import com.arms.api.issue.almapi.model.entity.AlmIssueEntity;
import com.arms.api.util.ParseUtil;
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.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.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toMap;

@Slf4j
@Service("DashboardService")
@AllArgsConstructor
public class DashboardImpl implements Dashboard {

    private final EsCommonRepositoryWrapper<AlmIssueEntity> esCommonRepositoryWrapper;
    private final AnalysisResource resourceService;


    @Override
    public List<TopNReqAssigneeReqLinkVO> topNAssigneeReqLinks(DashboardDTO dashboardDTO, Integer rankSize) {

        ResourceRequestDTO resourceDTO = new ResourceRequestDTO();
        resourceDTO.setPdServiceAndIsReq(dashboardDTO.getPdServiceAndIsReq());
        Map<String, UniqueAssigneeVO> uniquedAssigneeMap = listToMapConvert(resourceService.findAssigneesInfo(resourceDTO));

        DocumentAggregations aggregations = esCommonRepositoryWrapper.aggregateRecentDocs(
                SimpleQuery.aggregation(AggregationRequestDTO.builder()
                                .mainField("assignee.assignee_accountId.keyword")
                                .mainFieldAlias("accountId")
                                .subGroupFieldDTOS(
                                        List.of(SubGroupFieldDTO.builder()
                                                .subField("cReqLink")
                                                .subFieldAlias("cReqLink")
                                                .build()
                                        ))
                                .build())
                        .andTermQueryMust("pdServiceId", dashboardDTO.getPdServiceId())
                        .andTermsQueryFilter("pdServiceVersions", dashboardDTO.getPdServiceVersions())
                        .andExistsQueryFilter("assignee")
                        .andTermQueryMust("isReq", dashboardDTO.getIsReq())
        );

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

        Map<String, TopNReqAssigneeReqLinkVO> accountIdReqLinkVOMap = new HashMap<>();

        for (DocumentBucket doc : documentBuckets) {
            String accountId = doc.valueByName("accountId");

            if (accountIdReqLinkVOMap.get(accountId) == null) {

                UniqueAssigneeVO uniqueAssigneeVO = uniquedAssigneeMap.get(accountId);
                String emailId = ParseUtil.extractUsernameFromEmail(uniqueAssigneeVO.getEmailAddress());
                String nameWithEmailId = uniqueAssigneeVO.getName() + Optional.ofNullable(emailId).map(name -> "("+name+")").orElse("");

                accountIdReqLinkVOMap.put(accountId, TopNReqAssigneeReqLinkVO.builder()
                                .name(uniquedAssigneeMap.get(accountId).getName())
                                .nameWithEmailId(nameWithEmailId)
                                .emailAddress(uniqueAssigneeVO.getEmailAddress())
                                .reqCount(doc.countByName("accountId"))
                                .reqLinks(new ArrayList<>(List.of(doc.valueByName("cReqLink"))))
                        .build());
            } else {
                TopNReqAssigneeReqLinkVO topNReqAssigneeReqLinkVO = accountIdReqLinkVOMap.get(accountId);
                topNReqAssigneeReqLinkVO.addReqCount(doc.countByName("accountId"));
                topNReqAssigneeReqLinkVO.getReqLinks().add(doc.valueByName("cReqLink"));
            }
        }
        return accountIdReqLinkVOMap.values().stream()
                .sorted(
                    // 1차 키: reqCount 내림차순 (null은 뒤로)
                    Comparator.comparing(
                                TopNReqAssigneeReqLinkVO::getReqCount,
                                Comparator.nullsLast(Comparator.reverseOrder())
                            )
                            // 2차 키: name 오름차순 (null은 뒤로)
                            .thenComparing(
                                TopNReqAssigneeReqLinkVO::getName,
                                    Comparator.nullsLast(String::compareTo))
                )
                .limit(rankSize != null && rankSize > 0 ? rankSize : Long.MAX_VALUE)
                .toList();
    }


    @Override
    public List<TopNReqAssigneeVO> topNReqAssignee(DashboardDTO dashboardDTO, Integer rankSize) {
        List<TopNReqAssigneeVO> returnList = new ArrayList<>();

        ResourceRequestDTO resourceDTO = new ResourceRequestDTO();
        resourceDTO.setPdServiceAndIsReq(dashboardDTO.getPdServiceAndIsReq());
        Map<String, UniqueAssigneeVO> uniquedAssigneeMap = listToMapConvert(resourceService.findAssigneesInfo(resourceDTO));

        // ReqIssue 이므로, linkedIssuePdServiceIds, linkedIssuePdServiceVersions 사용 불필요.
        DocumentAggregations aggregations = esCommonRepositoryWrapper.aggregateRecentDocs(
                SimpleQuery.aggregation(AggregationRequestDTO.builder()
                                .mainField("assignee.assignee_accountId.keyword")
                                .mainFieldAlias("accountId")
                                .build())
                        .andTermQueryMust("pdServiceId", dashboardDTO.getPdServiceId())
                        .andTermsQueryFilter("pdServiceVersions", dashboardDTO.getPdServiceVersions())
                        .andExistsQueryFilter("assignee")
                        .andTermQueryMust("isReq", dashboardDTO.getIsReq())
        );

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

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

        for (DocumentBucket doc : documentBuckets) {
            String accountId = doc.valueByName("accountId");
            Long reqCount = doc.countByName("accountId");

            UniqueAssigneeVO uniqueAssigneeVO = uniquedAssigneeMap.get(accountId);
            String emailId = ParseUtil.extractUsernameFromEmail(uniqueAssigneeVO.getEmailAddress());
            String nameWithEmailId = uniqueAssigneeVO.getName() + Optional.ofNullable(emailId).map(name -> "("+name+")").orElse("");

            returnList.add(
                    TopNReqAssigneeVO.builder()
                        .accountId(accountId)
                        .reqCount(reqCount)
                        .name(uniqueAssigneeVO.getName())
                        .emailAddress(uniqueAssigneeVO.getEmailAddress())
                        .nameWithEmailId(nameWithEmailId)
                        .build()
            );
        }

        return returnList.stream()
                .sorted(
                        // 1차 키: reqCount 내림차순 (null은 뒤로)
                        Comparator.comparing(
                                        TopNReqAssigneeVO::getReqCount,
                                        Comparator.nullsLast(Comparator.reverseOrder())
                                )
                                // 2차 키: name 오름차순 (null은 뒤로)
                                .thenComparing(
                                        TopNReqAssigneeVO::getName,
                                        Comparator.nullsLast(String::compareTo)
                                )
                )
                .limit(rankSize != null && rankSize > 0 ? rankSize : Long.MAX_VALUE)
                .toList();
    }


    @Override
    public Map<String, 요구사항_지라이슈상태_주별_집계> aggregateWeeklyReqIssueStatus(DashboardDTO dashboardDTO) {

        LocalDate now = LocalDate.now(ZoneId.of("UTC"));
        LocalDate monthAgo = now.minusWeeks(4);

        요구사항_지라이슈상태_주별_집계 historicalData = getPriorCumulativeData(dashboardDTO, monthAgo);
        Map<String, Long> totalStatuses = new HashMap<>(historicalData.getStatuses());
        long totalIssues = historicalData.getTotalIssues();
        long totalRequirements = historicalData.getTotalRequirements();

        // 2. 검색 범위 내의 데이터를 가져온다. 현재 검색 범위는 차트 UI를 고려하여, 4~5주 정도로 적용
        DocumentAggregations documentAggregations
            = esCommonRepositoryWrapper.aggregateRecentDocsByWeek(
                SimpleQuery.aggregation(
                        AggregationRequestDTO.builder()
                            .mainField("created")
                            .mainFieldAlias("created")
                            .subGroupFieldDTOS(
                                    List.of(
                                        SubGroupFieldDTO.builder()
                                                .subField("status.status_name.keyword")
                                                .subFieldAlias("statusName")
                                            .build()
                                    )
                            )
                            .build()
                    )
                    .andTermQueryMust("pdServiceId", dashboardDTO.getPdServiceId())
                    .andTermsQueryFilter("pdServiceVersions", dashboardDTO.getPdServiceVersions())
                    .andTermQueryMust("isReq", true)
                    .andRangeQueryFilter(RangeQueryFilter.of("created").lte(now).gte(monthAgo))
            );

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

        List<ReqIssueStatusByWeekVO> issueStatusByCreatedDateVOS = deepestList.stream()
                .map(documentBucket ->
                        ReqIssueStatusByWeekVO.builder()
                                .created(transformDate(documentBucket.valueByName("created")))
                                .statusName(documentBucket.valueByName("statusName"))
                                .count((documentBucket.countByName("statusName")))
                                .build()
        ).toList();

        Map<String, List<ReqIssueStatusByWeekVO>> issueStatusByCreated =
                issueStatusByCreatedDateVOS.stream()
                        .collect(Collectors.groupingBy(
                                ReqIssueStatusByWeekVO::getCreated,
                                TreeMap::new, // 사전 순(날짜 순)으로 정렬 보장
                                Collectors.toList()
                        ));

        Map<String, 요구사항_지라이슈상태_주별_집계> aggrResultMap = new TreeMap<>();

        if (issueStatusByCreated.isEmpty()) {

            요구사항_지라이슈상태_주별_집계 defaultAggregation = 요구사항_지라이슈상태_주별_집계.builder()
                    .totalIssues(totalIssues)
                    .totalRequirements(totalRequirements)
                    .statuses(new HashMap<>(totalStatuses))
                    .build();

            aggrResultMap.put("최근 한 달 간의 데이터가 존재하지 않아 누적 데이터를 표시합니다.", defaultAggregation);

        } else {
            for (Map.Entry<String, List<ReqIssueStatusByWeekVO>> entry: issueStatusByCreated.entrySet()) {

                String date = entry.getKey(); // key
                List<ReqIssueStatusByWeekVO> vos = entry.getValue(); // value

                long totalRequirements1 = vos.stream()
                        .mapToLong(ReqIssueStatusByWeekVO::getCount)
                        .sum();

                // Group by statusName and sum counts
                Map<String, Long> statuses = vos.stream()
                        .collect(Collectors.groupingBy(
                                ReqIssueStatusByWeekVO::getStatusName,
                                Collectors.summingLong(ReqIssueStatusByWeekVO::getCount)
                        ));

                요구사항_지라이슈상태_주별_집계 주별_집계 = 요구사항_지라이슈상태_주별_집계.builder()
                        .totalRequirements(totalRequirements1)
                        .statuses(statuses)
                        .totalIssues(totalRequirements1)
                        .build();

                aggrResultMap.put(date, 주별_집계);
            }

            for (요구사항_지라이슈상태_주별_집계 monthlyAggregation : aggrResultMap.values()) {
                totalIssues += monthlyAggregation.getTotalIssues();
                totalRequirements += monthlyAggregation.getTotalRequirements();
                monthlyAggregation.setTotalIssues(totalIssues);
                monthlyAggregation.setTotalRequirements(totalRequirements);

                Map<String, Long> currentStatuses = monthlyAggregation.getStatuses();
                for(Map.Entry<String, Long> entry : currentStatuses.entrySet()) {
                    totalStatuses.merge(entry.getKey(), entry.getValue(), Long::sum);
                }

                monthlyAggregation.setStatuses(new HashMap<>(totalStatuses));
            }
        }


        return aggrResultMap;
    }


    private 요구사항_지라이슈상태_주별_집계 getPriorCumulativeData(DashboardDTO dashboardDTO, LocalDate monthAgo) {
        Long pdServiceId = dashboardDTO.getPdServiceId();
        List<Long> pdServiceVersions = dashboardDTO.getPdServiceVersions();

        // 총 이슈 개수를 구하기 위한 쿼리 -> 요구사항 집계를 위해서, isReq 추가
        DocumentAggregations documentAggregations
            = esCommonRepositoryWrapper.aggregateRecentDocs(
                SimpleQuery.aggregation(
                        AggregationRequestDTO.builder()
                            .mainField("status.status_name.keyword")
                            .mainFieldAlias("statusName")
                        .build()
                )
                        .andTermQueryMust("pdServiceId", dashboardDTO.getPdServiceId())
                        .andTermQueryMust("isReq", true)
                        .andTermsQueryFilter("pdServiceVersions", dashboardDTO.getPdServiceVersions())
                .andRangeQueryFilter(RangeQueryFilter.of("created").lt(monthAgo))
            );
        Long totalIssuesCount = documentAggregations.getTotalHits(); // 히트 총수

        // 총 요구사항 개수를 구하기 위한 쿼리
        Long totalRequirementsCount
            = esCommonRepositoryWrapper.findRecentHits(
                    SimpleQuery.termQueryMust("isReq", true)
                        .andTermQueryMust("pdServiceId", dashboardDTO.getPdServiceId())
                        .andTermsQueryFilter("pdServiceVersions", dashboardDTO.getPdServiceVersions())
                        .andRangeQueryFilter(RangeQueryFilter.of("created").lt(monthAgo))
                ).getTotalHits();

        // 이슈 상태 집계 결과 가져오기
        Map<String, Long> statusMap = new HashMap<>();

        for (DocumentBucket documentBucket : documentAggregations.deepestList()) {
            String statusName = documentBucket.valueByName("statusName");
            long docCount = documentBucket.countByName("statusName");
            statusMap.put(statusName, docCount);
        }

        return new 요구사항_지라이슈상태_주별_집계(totalIssuesCount, statusMap, totalRequirementsCount);
    }


    private String transformDate(String date) {
        OffsetDateTime offsetDateTime = OffsetDateTime.parse(date);
        return DateTimeFormatter
                .ofPattern("yyyy-MM-dd")
                .format(offsetDateTime);
    }


    private Map<String, UniqueAssigneeVO> listToMapConvert(List<UniqueAssigneeVO> list) {
        return list.stream()
                .collect(toMap(UniqueAssigneeVO::getAccountId, Function.identity(),
                // 충돌 시 어떤 값을 유지할지 결정: 여기선 첫 번째 값을 유지
                (existing, incoming) -> existing
        ));

    }

}
