package com.arms.api.issue.almapi.strategy;

import com.arms.api.issue.almapi.model.dto.*;
import com.arms.api.issue.almapi.model.entity.AlmIssueEntity;
import com.arms.api.issue.almapi.model.vo.*;
import com.arms.api.issue.almapi.model.vo.cloudjiraspec.*;
import com.arms.api.serverinfo.model.vo.ServerInfoVO;
import com.arms.api.serverinfo.service.ServerInfoService;
import com.arms.api.util.LRUMap;
import com.arms.api.util.StreamUtil;
import com.arms.api.util.alm.JiraApi;
import com.arms.api.util.alm.JiraUtil;
import com.arms.api.util.alm.dto.CloudJiraIssueCreationFieldMetadata;
import com.arms.api.util.aspect.SlackSendAlarm;
import com.arms.api.util.errors.ErrorCode;
import com.arms.api.util.errors.ErrorLogUtil;
import com.arms.api.issue.almapi.service.CategoryMappingService;
import com.arms.api.util.response.CommonResponse;

import com.arms.egovframework.javaservice.esframework.esquery.SimpleQuery;
import com.arms.egovframework.javaservice.esframework.repository.common.EsCommonRepositoryWrapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.DateUtils;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

@Slf4j
@Component("CLOUD")
@AllArgsConstructor
public class CloudJiraIssueStrategy implements IssueStrategy {

    private final JiraUtil jiraUtil;

    private final JiraApi jiraApi;

    private final CategoryMappingService categoryMappingService;

    private final EsCommonRepositoryWrapper<AlmIssueEntity> esCommonRepositoryWrapper;

    private final ServerInfoService serverInfoService;

    private final IssueSaveTemplate issueSaveTemplate;

    @Override
    public AlmIssueVO createIssue(AlmIssueDTO almIssueDTO) {

        IssueCreationFieldsDTO issueCreationFieldsDTO = almIssueDTO.getFields();

        ServerInfoVO serverInfoVO = serverInfoService.verifyServerInfo(almIssueDTO.getServerId());

        if (issueCreationFieldsDTO == null) {
            String errorMessage = String.format("%s :: %s 이슈 생성 필드 데이터가 존재 하지 않습니다.",
                                                serverInfoVO.getType(), serverInfoVO.getUri());
            throw new IllegalArgumentException(ErrorCode.REQUEST_BODY_ERROR_CHECK.getErrorMsg() + " :: " + errorMessage);
        }

        String projectKeyOrId = null;
        String issueTypeId = null;

        if (issueCreationFieldsDTO.getProject() != null && StringUtils.isNotEmpty(issueCreationFieldsDTO.getProject().getId())) {
            projectKeyOrId = issueCreationFieldsDTO.getProject().getId();
        }

        if (issueCreationFieldsDTO.getIssuetype() != null && StringUtils.isNotEmpty(issueCreationFieldsDTO.getIssuetype().getId())) {
            issueTypeId = issueCreationFieldsDTO.getIssuetype().getId();
        }

        if (StringUtils.isEmpty(projectKeyOrId) || StringUtils.isEmpty(issueTypeId)) {
            String errorMessage = String.format("%s :: %s 이슈 생성 필드 확인에 필요한 프로젝트 아이디, 이슈유형 아이디가 존재하지 않습니다.",
                                                serverInfoVO.getType(), serverInfoVO.getUri());
            throw new IllegalArgumentException(errorMessage);
        }

        WebClient webClient = jiraUtil.createJiraCloudCommunicator(serverInfoVO.getUri(), serverInfoVO.getUserId(), serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()));

        /* ***
         * 프로젝트 와 이슈 유형에 따라 이슈 생성 시 들어가는 fields의 내용을 확인하는 부분
         *** */
        Map<String, CloudJiraIssueCreationFieldMetadata.FieldMetadata> fieldMetadataMap
                = jiraUtil.checkFieldMetadata(webClient, projectKeyOrId, issueTypeId);
        CloudJiraIssueFieldDTO fieldDTO = this.validateAndAddFields(issueCreationFieldsDTO, fieldMetadataMap, serverInfoVO, projectKeyOrId, issueTypeId);

        CloudJiraIssueCreationDTO issueCreationDTO = new CloudJiraIssueCreationDTO();
        issueCreationDTO.setFields(fieldDTO);

        String endpoint = jiraApi.getEndpoint().getIssue().getBase();
        AlmIssueVO almIssueVO;
        try {
            almIssueVO = jiraUtil.post(webClient, endpoint, issueCreationDTO, AlmIssueVO.class).block();
            log.info("{}[{}], 프로젝트 : {}, 이슈유형 : {}, 생성 필드 : {}, 이슈 생성하기",
                    serverInfoVO.getType(), serverInfoVO.getUri(), projectKeyOrId, issueTypeId, issueCreationDTO);
        }
        catch (Exception e) {
            String errorMessage = ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                                    String.format("%s[%s],  프로젝트[%s], 이슈유형[%s], 생성 필드 :: 이슈 생성하기 중 오류",
                                                serverInfoVO.getType(), serverInfoVO.getUri(), projectKeyOrId, issueTypeId));
            throw new IllegalArgumentException(errorMessage);
        }

        if (almIssueVO == null) {
            String errorMessage = String.format("%s[%s], 프로젝트[%s], 이슈유형[%s], 생성 필드 :: {}, 생성된 이슈 데이터가 NULL 입니다.",
                                                serverInfoVO.getType(), serverInfoVO.getUri(), projectKeyOrId, issueTypeId);
            log.error(errorMessage);
            throw new IllegalArgumentException(errorMessage);
        }

        return almIssueVO;
    }

    @Override
    public Map<String, Object> updateIssue(AlmIssueDTO almIssueDTO) {

        ServerInfoVO serverInfoVO = serverInfoService.verifyServerInfo(almIssueDTO.getServerId());

        String issueKeyOrId = almIssueDTO.getIssueKeyOrId();

        try {

            WebClient webClient = jiraUtil.createJiraCloudCommunicator(serverInfoVO.getUri(), serverInfoVO.getUserId(), serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()));

            String endpoint = jiraApi.getEndpoint().getIssue().getBase() + "/" + issueKeyOrId;
            Map<String, Object> 결과 = new HashMap<>();

            IssueCreationFieldsDTO 필드_데이터 = almIssueDTO.getFields();

            CloudJiraIssueCreationDTO 수정_데이터 = new CloudJiraIssueCreationDTO();
            CloudJiraIssueFieldDTO 클라우드_필드_데이터 = new CloudJiraIssueFieldDTO();

            if (필드_데이터.getSummary() != null) {
                클라우드_필드_데이터.setSummary(필드_데이터.getSummary());
            }

            if (필드_데이터.getDescription() != null) {
                클라우드_필드_데이터.setDescription(내용_변환(필드_데이터.getDescription()));
            }

            if (필드_데이터.getLabels() != null) {
                클라우드_필드_데이터.setLabels(필드_데이터.getLabels());
            }

            수정_데이터.setFields(클라우드_필드_데이터);

            CommonResponse.ApiResult<?> 응답_결과 = jiraUtil.executePut(webClient, endpoint, 수정_데이터);

            if (응답_결과.isSuccess()) {
                결과.put("success", 응답_결과.isSuccess());
                결과.put("message", "이슈 수정 성공");
            }
            else {
                결과.put("success", 응답_결과.isSuccess());
                결과.put("message", 응답_결과.getError().getMessage());
            }

            return 결과;
        }
        catch (Exception e) {
            String 에러로그 = ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                    "클라우드 지라("+ serverInfoVO.getUri() +") :: 이슈 키("+ issueKeyOrId+ ") :: 이슈_수정하기에 실패하였습니다.");
            throw new IllegalArgumentException(ErrorCode.ISSUE_MODIFICATION_ERROR.getErrorMsg() + " :: " + 에러로그);
        }

    }

    @Override
    public Map<String, Object> updateIssueStatus(AlmIssueDTO almIssueDTO) {

        String 이슈전환_아이디 = null;

        ServerInfoVO serverInfoVO = serverInfoService.verifyServerInfo(almIssueDTO.getServerId());

        String issueKeyOrId = almIssueDTO.getIssueKeyOrId();

        try {
            WebClient webClient = jiraUtil.createJiraCloudCommunicator(serverInfoVO.getUri(), serverInfoVO.getUserId(), serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()));


            String endpoint = jiraApi.getEndpoint().getIssue().getBase() + "/" + issueKeyOrId + "/transitions";
            Map<String, Object> 결과 = new HashMap<>();

            // Transition 조회
            이슈전환_아이디 = 이슈전환_아이디_조회하기(almIssueDTO);

            // 이슈 상태 변경
            if (이슈전환_아이디 != null) {

                JiraIssueMigData.Transition 전환 = JiraIssueMigData.Transition.builder()
                        .id(이슈전환_아이디)
                        .build();
                JiraIssueMigData 수정_데이터 = JiraIssueMigData.builder()
                        .transition(전환)
                        .build();

                CommonResponse.ApiResult<?> 응답_결과 = jiraUtil.executePost(webClient, endpoint, 수정_데이터);

                if (응답_결과.isSuccess()) {
                    결과.put("success", 응답_결과.isSuccess());
                    결과.put("message", "이슈 상태 변경 성공");
                }
                else {
                    결과.put("success", 응답_결과.isSuccess());
                    결과.put("message", 응답_결과.getError().getMessage());
                }
            }
            else {
                String 에러로그 = "클라우드 지라(" + serverInfoVO.getUri() + ") :: 이슈 키(" + issueKeyOrId + ") :: 상태 아이디(" + almIssueDTO.getStatusId() + ") :: 해당 업무 흐름으로 변경이 불가능 합니다.";
                log.error(에러로그);

                결과.put("success", false);
                결과.put("message", "변경할 이슈 상태가 존재하지 않습니다.");
            }

            return 결과;

        }
        catch (Exception e) {
            String 에러로그 = ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                    "클라우드 지라(" + serverInfoVO.getUri() + ") :: 이슈 키(" + issueKeyOrId + ") :: 상태 아이디(" + almIssueDTO.getStatusId() + ") :: 전환 아이디(" + 이슈전환_아이디 + ") :: 이슈_상태_변경하기에 실패하였습니다.");
            throw new IllegalArgumentException(ErrorCode.ISSUE_TRANSITION_ERROR.getErrorMsg() + " :: " + 에러로그);
        }
    }

    public String 이슈전환_아이디_조회하기(AlmIssueDTO almIssueDTO) {

        String issueKeyOrId = almIssueDTO.getIssueKeyOrId();

        String statusId = almIssueDTO.getStatusId();

        ServerInfoVO serverInfoVO = serverInfoService.verifyServerInfo(almIssueDTO.getServerId());

        try {

            WebClient webClient = jiraUtil.createJiraCloudCommunicator(serverInfoVO.getUri(), serverInfoVO.getUserId(), serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()));

            String endpoint = jiraApi.getEndpoint().getIssue().getBase() + "/" + issueKeyOrId + "/transitions";

            JiraIssueMigData jiraIssueMigData = jiraUtil.get(webClient, endpoint, JiraIssueMigData.class).block();

            return Optional.ofNullable(jiraIssueMigData)
                    .map(JiraIssueMigData::getTransitions)
                    .orElse(Collections.emptyList()).stream()
                    .filter(data -> {
                        if (data.getTo() != null && !StringUtils.isBlank(data.getTo().getId())) {
                            return statusId.equals(data.getTo().getId());
                        }
                        return false;
                    })
                    .findFirst()
                    .map(JiraIssueMigData.Transition::getId)
                    .orElse(null);

        } catch (Exception e) {
            String 에러로그 = ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                    "클라우드 지라(" + serverInfoVO.getUri() + ") :: 이슈 키(" + issueKeyOrId + ") :: 상태 아이디(" + statusId + ") :: 이슈전환_아이디_조회하기에 실패하였습니다.");
            throw new IllegalArgumentException(ErrorCode.ISSUE_TRANSITION_RETRIEVAL_ERROR.getErrorMsg() + " :: " + 에러로그);
        }
    }

    @Override
    public Map<String, Object> deleteIssue(AlmIssueDTO almIssueDTO) {

        ServerInfoVO serverInfoVO = serverInfoService.verifyServerInfo(almIssueDTO.getServerId());

        String issueKeyOrId = almIssueDTO.getIssueKeyOrId();

        try {
            WebClient webClient = jiraUtil.createJiraCloudCommunicator(serverInfoVO.getUri(), serverInfoVO.getUserId(), serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()));

            boolean 하위이슈_삭제유무 = jiraApi.getParameter().isDeleteSubtasks();
            String endpoint = jiraApi.getEndpoint().getIssue().getBase() + "/" + issueKeyOrId + "?deleteSubtasks=" + 하위이슈_삭제유무;
            Map<String, Object> 결과 = new HashMap<>();

            CommonResponse.ApiResult<?> 응답_결과 = jiraUtil.executeDelete(webClient, endpoint);

            if (응답_결과.isSuccess()) {
                결과.put("success", 응답_결과.isSuccess());
                결과.put("message", "이슈 삭제 성공");
            }
            else {
                결과.put("success", 응답_결과.isSuccess());
                결과.put("message", 응답_결과.getError().getMessage());
            }

            return 결과;
        }
        catch (Exception e) {
            String 에러로그 = ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                    "클라우드 지라(" + serverInfoVO.getUri() + ") :: 이슈 키(" + issueKeyOrId + ") :: 이슈_수정하기에 실패하였습니다.");
            throw new IllegalArgumentException(ErrorCode.ISSUE_MODIFICATION_ERROR.getErrorMsg() + " :: " + 에러로그);
        }
    }

    public CloudJiraIssueFieldDTO.내용 내용_변환(String 입력_데이터) {

        List<CloudJiraIssueFieldDTO.콘텐츠> 콘텐츠_리스트 = new ArrayList<>();

        String URL_구분자 = "자세한 요구사항 내용 확인 ⇒";
        String 추출된_URL = extractRequirementUrl(입력_데이터);

        if (추출된_URL != null && 입력_데이터.contains(URL_구분자)) {
            // URL 구분자 기준으로 텍스트 분리
            int URL_구분자_위치 = 입력_데이터.indexOf(URL_구분자);
            String 앞부분_텍스트 = 입력_데이터.substring(0, URL_구분자_위치).trim();

            // URL 이후 텍스트 추출
            int URL_끝_위치 = 입력_데이터.indexOf(추출된_URL) + 추출된_URL.length();
            String 뒷부분_텍스트 = 입력_데이터.substring(URL_끝_위치).trim();

            // 1. 첫 번째 paragraph - 안내 문구
            if (!앞부분_텍스트.isEmpty()) {
                콘텐츠_리스트.add(텍스트_콘텐츠_생성(앞부분_텍스트));
            }

            // 2. 두 번째 paragraph - URL 링크 포함
            List<CloudJiraIssueFieldDTO.콘텐츠_아이템> 링크_콘텐츠_아이템_리스트 = new ArrayList<>();

            // "자세한 요구사항 내용 확인 ⇒ " 텍스트
            링크_콘텐츠_아이템_리스트.add(CloudJiraIssueFieldDTO.콘텐츠_아이템.builder()
                    .text(URL_구분자 + " ")
                    .type("text")
                    .build());

            // URL 링크
            CloudJiraIssueFieldDTO.마크_속성 링크_속성 = CloudJiraIssueFieldDTO.마크_속성.builder()
                    .href(추출된_URL)
                    .build();
            CloudJiraIssueFieldDTO.마크 링크_마크 = CloudJiraIssueFieldDTO.마크.builder()
                    .type("link")
                    .attrs(링크_속성)
                    .build();
            링크_콘텐츠_아이템_리스트.add(CloudJiraIssueFieldDTO.콘텐츠_아이템.builder()
                    .text(추출된_URL)
                    .type("text")
                    .marks(List.of(링크_마크))
                    .build());

            콘텐츠_리스트.add(CloudJiraIssueFieldDTO.콘텐츠.builder()
                    .content(링크_콘텐츠_아이템_리스트)
                    .type("paragraph")
                    .build());

            // 3. 세 번째 paragraph - 나머지 내용
            if (!뒷부분_텍스트.isEmpty()) {
                콘텐츠_리스트.add(텍스트_콘텐츠_생성(뒷부분_텍스트));
            }
        } else {
            // URL이 없으면 전체를 하나의 paragraph로
            콘텐츠_리스트.add(텍스트_콘텐츠_생성(입력_데이터));
        }

        return CloudJiraIssueFieldDTO.내용.builder()
                .content(콘텐츠_리스트)
                .type("doc")
                .version(1)
                .build();
    }

    private CloudJiraIssueFieldDTO.콘텐츠 텍스트_콘텐츠_생성(String 텍스트) {
        CloudJiraIssueFieldDTO.콘텐츠_아이템 콘텐츠_아이템 = CloudJiraIssueFieldDTO.콘텐츠_아이템.builder()
                .text(텍스트)
                .type("text")
                .build();
        return CloudJiraIssueFieldDTO.콘텐츠.builder()
                .content(List.of(콘텐츠_아이템))
                .type("paragraph")
                .build();
    }

    @Override
    public boolean isExistIssue(AlmIssueDTO almIssueDTO)  {
        try {
            AlmIssueVO issue = this.getIssueVO(almIssueDTO, false);
            return issue != null && issue.getKey().equals(almIssueDTO.getIssueKeyOrId());
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public AlmIssueVO getIssueVO(AlmIssueDTO almIssueDTO)  {
        return this.getIssueVO(almIssueDTO, true);
    }

    private AlmIssueVO getIssueVO(AlmIssueDTO almIssueDTO , boolean convertFlag) {

        String issueKeyOrId = almIssueDTO.getIssueKeyOrId();

        ServerInfoVO serverInfoVO = serverInfoService.verifyServerInfo(almIssueDTO.getServerId());

        String endpoint = jiraApi.getEndpoint().getIssue().getBase() + "/" + issueKeyOrId + "?" + jiraApi.getParameter().getFields();

        WebClient webClient = jiraUtil.createJiraCloudCommunicator(serverInfoVO.getUri(), serverInfoVO.getUserId(), serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()));

        try {
            CloudJiraIssueRawDataVO cloudJiraIssueRawDataVO
                = jiraUtil.getWithRawData(webClient, endpoint, CloudJiraIssueRawDataVO.class);
            if(convertFlag){

                return CloudJiraIssueVO.builder()
                            .cloudJiraIssueRawDataVO(cloudJiraIssueRawDataVO)
                            .build()
                        .toAlmIssueVO();
            }
            return AlmIssueVO.builder()
                    .key(Optional.ofNullable(cloudJiraIssueRawDataVO)
                            .map(CloudJiraIssueRawDataVO::getKey)
                            .orElseThrow(()-> new IllegalArgumentException("이슈_상세정보_가져오기에 실패하였습니다.")))
                    .build();
        } catch (Exception e) {
            ErrorLogUtil.exceptionLogging(e, this.getClass().getName(),
                    "클라우드 지라(" + serverInfoVO.getUri() + ") :: 이슈 키(" + issueKeyOrId + ") :: 이슈_상세정보_가져오기에 실패하였습니다.");
            throw new IllegalArgumentException("이슈_상세정보_가져오기에 실패하였습니다.");
        }
    }

    @Override
    public List<AlmIssueEntity> discoveryIssueAndGetReqEntities(AlmIssueIncrementDTO almIssueIncrementDTO) {

        Map<String,CloudJiraIssueRawDataVO> lruMap = new LRUMap<>(10000);
        Map<String,List<CloudJiraIssueRawDataVO>> lruParentKeyTraceMap = new LRUMap<>(10000);

        return issueSaveTemplate.discoveryIncrementALmIssueAndGetReqAlmIssueEntities(
                almIssueIncrementDTO, (dto) -> this.discoveryIssueAndGetReqEntities(dto, lruMap , lruParentKeyTraceMap));
    }

    private AlmIssueVOCollection discoveryIssueAndGetReqEntities(AlmIssueIncrementDTO almIssueIncrementDTO
            , Map<String,CloudJiraIssueRawDataVO> lruMap
            , Map<String,List<CloudJiraIssueRawDataVO>> parentKeyTraceMap
    ) {

        ServerInfoVO serverInfoVO = serverInfoService.verifyServerInfo(almIssueIncrementDTO.getServerId());

        WebClient webClient = jiraUtil.createJiraCloudCommunicator(
                serverInfoVO.getUri(), serverInfoVO.getUserId(), serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()));
        try {

            List<CloudJiraIssueRawDataVO> cloudJiraIssueRawDataVOS = fetchIssuesFromDate(almIssueIncrementDTO, webClient);

            List<AlmIssueEntity> almIssueEntities
                = esCommonRepositoryWrapper.findRecentHits(
                    SimpleQuery.termsQueryFilter("key", CloudJiraIssueVOCollection
                                                                .fromCloudJiraIssueRawDataVO(cloudJiraIssueRawDataVOS).keys())
                            .andTermQueryFilter("jira_server_id", serverInfoVO.getConnectId())
                ).toDocs();

            List<AlmIssueEntity> rootIssues = Stream.concat(
                    almIssueEntities.stream()
                            .filter(AlmIssueEntity::izReqTrue),
                    esCommonRepositoryWrapper.findRecentHits(
                            SimpleQuery
                                .termsQueryFilter("key",
                                    almIssueEntities.stream()
                                            .filter(AlmIssueEntity::izReqFalse)
                                            .map(AlmIssueEntity::getParentReqKey)
                                            .filter(parentReqKey -> !ObjectUtils.isEmpty(parentReqKey))
                                            .toList()
                                )
                                .andTermQueryFilter("jira_server_id", serverInfoVO.getConnectId())
                    ).toDocs().stream()
            ).distinct().toList();

            Set<String> visited = new HashSet<>();

            for (AlmIssueEntity almIssueEntity : rootIssues) {

                Queue<AlmIssueEntity> queue = new LinkedList<>();

                queue.add(almIssueEntity);

                while (!queue.isEmpty()) {

                    AlmIssueEntity queueValue = queue.poll();

                    String key = queueValue.getKey();

                    if (!visited.contains(key)&&!ObjectUtils.isEmpty(key)) {

                        String endpoint = jiraApi.getEndpoint().getIssue().getBase() + "/" + key + "?" + jiraApi.getParameter().getFields();

                        try{

                            IssueDTO issueDTO = IssueDTO.builder().key(key).build();

                            CloudJiraIssueRawDataVO cloudJiraIssueRawDataVO = issueList(lruMap, issueDTO, webClient, endpoint);

                            lruMap.put(cloudJiraIssueRawDataVO.getKey(),cloudJiraIssueRawDataVO);

                        }catch (WebClientResponseException.NotFound e){
                            log.info("cloud jira not found key : {} ",key);

                            List<SearchHit<AlmIssueEntity>> hitDocs = esCommonRepositoryWrapper.findRecentHits(
                                    SimpleQuery.termQueryFilter("recent_id", queueValue.getRecentId())
                            ).toHitDocs();

                            for (SearchHit<AlmIssueEntity> hitDoc : hitDocs) {
                                AlmIssueEntity content = hitDoc.getContent();
                                content.setRecent(false);
                                log.info("delete:key:{}",content.getKey());
                                esCommonRepositoryWrapper.modifyWithIndexName(content, hitDoc.getIndex());
                            }
                        }

                        List<AlmIssueEntity> docsByParentReqKey = esCommonRepositoryWrapper.findRecentHits(
                                SimpleQuery.termQueryFilter("parentReqKey", key)
                                        .andTermQueryFilter("jira_server_id", serverInfoVO.getConnectId())
                        ).toDocs();

                        queue.addAll(docsByParentReqKey);

                        List<AlmIssueEntity> docsByLinkedIssues = esCommonRepositoryWrapper.findRecentHits(
                            SimpleQuery.termQueryFilter("linkedIssues", queueValue.getRecentId())
                        ).toDocs();

                        queue.addAll(docsByLinkedIssues);
                    }

                    visited.add(key);

                    Optional.ofNullable(lruMap.get(key)).ifPresent(cloudJiraIssueRawDataVOS::add);
                }
            }

            CloudJiraIssueVOCollection cloudJiraIssueVOCollection = CloudJiraIssueVOCollection.of(
                cloudJiraIssueRawDataVOS.stream().distinct()
                    .flatMap(cloudJiraIssueRawDataVO ->
                            traceAndMapToRequirement(lruMap, parentKeyTraceMap, cloudJiraIssueRawDataVO, serverInfoVO, webClient).stream())
                    .map(cloudJiraIssueVO->{
                        if(!cloudJiraIssueRawDataVOS.contains(cloudJiraIssueVO.getCloudJiraIssueRawDataVO())){
                            return cloudJiraIssueVO.markAsExcludedFromSave();
                        }else{
                            return cloudJiraIssueVO;
                        }
                    }).toList()
            );

            List<CloudJiraIssueVO> cloudJiraIssueVOS = cloudJiraIssueVOCollection.appliedLinkedIssuePdServiceVO();

            return this.convertIncrementCloudJiraIssueToAlmIssue(cloudJiraIssueVOS);

        } catch (Exception e) {

            log.error("증분 데이터 수집에 실패 하였습니다.",e);

            ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                    "클라우드 지라(" + serverInfoVO.getUri() + ") 증분 이슈 수집 중 오류");

            throw new IllegalArgumentException("증분 데이터 수집에 실패 하였습니다.");
        }

    }

    private AlmIssueVOCollection convertIncrementCloudJiraIssueToAlmIssue(List<CloudJiraIssueVO> cloudJiraIssueVOs){
        return new AlmIssueVOCollection(cloudJiraIssueVOs
                .stream()
                .map(this::convertIncrementCloudJiraIssueToAlmIssue)
                .toList());
    }

    private AlmIssueVO convertIncrementCloudJiraIssueToAlmIssue(CloudJiraIssueVO cloudJiraIssueVO) {
        String armsStateCategory = getArmsStateCategory(cloudJiraIssueVO.getServerInfoVO(), cloudJiraIssueVO.cloudJiraIssueField());
        return cloudJiraIssueVO.toAlmIssueVOByStateCategory(armsStateCategory);
    }

    private List<CloudJiraIssueVO> traceAndMapToRequirement(
            Map<String,CloudJiraIssueRawDataVO> lruMap,
            Map<String,List<CloudJiraIssueRawDataVO>> parentKeyTraceMap,
            CloudJiraIssueRawDataVO currentIssue,
            ServerInfoVO serverInfoVO,
            WebClient webClient) {

        Queue<IssueDTO> queue = new LinkedList<>();
        queue.add(createIssueDTO(currentIssue));
        lruMap.put(currentIssue.getKey(),currentIssue);

        Set<String> recentIds = new HashSet<>();
        Set<String> visited = new HashSet<>();

        while (!queue.isEmpty()) {

            try {

                IssueDTO issueDTO = queue.poll();
                String endpoint = jiraApi.getEndpoint().getIssue().getBase() + "/" + issueDTO.getKey() + "?" + jiraApi.getParameter().getFields();
                CloudJiraIssueRawDataVO cloudJiraIssueRawDataVO = issueList(lruMap, issueDTO, webClient, endpoint);

                if (cloudJiraIssueRawDataVO.getKey() != null) {
                    lruMap.put(cloudJiraIssueRawDataVO.getKey(),cloudJiraIssueRawDataVO);
                    String recentId = serverInfoVO.getConnectId() + "_" + cloudJiraIssueRawDataVO.getFields().getProject().getKey() + "_" + cloudJiraIssueRawDataVO.getKey();
                    recentIds.add(recentId);
                }

                if (!visited.contains(cloudJiraIssueRawDataVO.getKey())) {
                    queue.add(createIssueParentTraceDTO(cloudJiraIssueRawDataVO));

                    this.fetchSubTaskIssuesByKey(parentKeyTraceMap,cloudJiraIssueRawDataVO.getKey(), webClient)
                        .forEach(issue-> queue.add(this.createIssueDTO(issue)));

                    linkedIssueQueueRegister(cloudJiraIssueRawDataVO, queue, visited);
                    visited.add(cloudJiraIssueRawDataVO.getKey());
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        return
            esCommonRepositoryWrapper.findRecentHits(
                SimpleQuery
                    .termsQueryFilter("recent_id", recentIds.stream().toList())
                    .andTermQueryFilter("isReq", true)
            )
            .toDocs()
            .stream()
            .map(reqIssueEntity->{

                String rootParentKey = findRootParentKey(lruMap, currentIssue.getKey(), webClient);

                return CloudJiraIssueVO.builder()
                        .cloudJiraIssueRawDataVO(currentIssue)
                        .almIssueWithRequirementVO(Optional.ofNullable(customReqIssue(reqIssueEntity,rootParentKey)).orElseGet(()->{
                            if(rootParentKey==null){
                                AlmIssueEntity almIssueEntity = esCommonRepositoryWrapper.findRecentHits(
                                        SimpleQuery
                                                .termQueryFilter("key", String.valueOf(currentIssue.getKey()))
                                                .andTermQueryFilter("jira_server_id", serverInfoVO.getConnectId())
                                                .andTermQueryFilter("isReq", true)
                                ).fetchOnlyOne();
                                if(almIssueEntity.getKey()!=null){
                                    return new AlmIssueWithRequirementVO(almIssueEntity);
                                }
                            }
                            if(rootParentKey!=null){
                                AlmIssueEntity almIssueEntity = esCommonRepositoryWrapper.findRecentHits(
                                        SimpleQuery
                                                .termQueryFilter("key", rootParentKey)
                                                .andTermQueryFilter("jira_server_id", serverInfoVO.getConnectId())
                                                .andTermQueryFilter("isReq", true)
                                ).fetchOnlyOne();
                                if(almIssueEntity.getKey()!=null){
                                    return new AlmIssueWithRequirementVO(almIssueEntity);
                                }
                            }

                            return new AlmIssueWithRequirementVO(reqIssueEntity);
                        }))
                        .serverInfoVO(serverInfoVO)
                        .build();
            }).toList();

    }

    private AlmIssueWithRequirementVO customReqIssue(AlmIssueEntity almIssueEntity, String rootParentKey){
        AlmIssueWithRequirementVO reqDTO = new AlmIssueWithRequirementVO(almIssueEntity, rootParentKey);
        AlmIssueEntity recentDocByRecentId = esCommonRepositoryWrapper.findRecentDocByRecentId(reqDTO.recentId());
        if (rootParentKey != null && (recentDocByRecentId.getKey() == null || recentDocByRecentId.izReqFalse())) {
            return reqDTO;
        }
        return null;
    }

    private String findRootParentKey(Map<String,CloudJiraIssueRawDataVO> lruMap, String startKey, WebClient webClient){
        String findKey = startKey;
        String parentKey = null;

        while (findKey != null) {
            String endpoint = jiraApi.getEndpoint().getIssue().getBase() + "/" + findKey + "?" + jiraApi.getParameter().getFields();
            CloudJiraIssueRawDataVO issue = issueList(lruMap, IssueDTO.builder().key(findKey).build(), webClient, endpoint);
            if (isExistParentKey(issue)) {
                parentKey = issue.getParentKey();
                findKey = parentKey;
            }else{
                break;
            }
        }
        return parentKey;
    }

    private boolean isExistParentKey(CloudJiraIssueRawDataVO issue) {
        return issue.getParentKey() != null;
    }

    private IssueDTO createIssueDTO(CloudJiraIssueRawDataVO issue) {
        return IssueDTO.builder()
                .key(String.valueOf(issue.getKey()))
                .fromKey(String.valueOf(issue.getKey()))
                .build();
    }

    private IssueDTO createIssueParentTraceDTO(CloudJiraIssueRawDataVO issue) {
        return IssueDTO.builder()
                .key(String.valueOf(Optional.ofNullable(issue.getFields()).map(a->{
                    ParentDTO parent = issue.getFields().getParent();
                    if(parent!=null){
                        return parent.getKey();
                    }
                    return issue.getKey();
                }).orElseGet(issue::getKey)))
                .fromKey(String.valueOf(issue.getKey()))
                .build();
    }

    private void linkedIssueQueueRegister(CloudJiraIssueRawDataVO issue, Queue<IssueDTO> queue, Set<String> visited) {

        List<IssueLink> issueRelations = StreamUtil.toStream(issue.getFields().getIssuelinks()).toList();

        issueRelations.forEach(element->{

            OutwardIssue outwardIssue = element.getOutwardIssue();

            if(outwardIssue!=null){
                String relationKey = outwardIssue.getKey();
                IssueDTO relation = new IssueDTO(String.valueOf(relationKey), "relation", String.valueOf(issue.getKey()));
                if (!visited.contains(relation.getKey())) {
                    queue.add(relation);
                }
            }

            InwardIssue inwardIssue = element.getInwardIssue();

            if(inwardIssue!=null){
                String relationKey = inwardIssue.getKey();
                IssueDTO relation = new IssueDTO(String.valueOf(relationKey), "relation", String.valueOf(issue.getKey()));
                if (!visited.contains(relation.getKey())) {
                    queue.add(relation);
                }
            }

        });

    }

    private List<CloudJiraIssueRawDataVO> fetchIssuesFromDate(AlmIssueIncrementDTO almIssueIncrementDTO, WebClient webClient) {

        String startDate = almIssueIncrementDTO.getStartDate();

        String endDate = almIssueIncrementDTO.getEndDate();

        String projectKey = almIssueIncrementDTO.getProjectKey();

        int maxResults = jiraApi.getParameter().getMaxResults();

        String endPoint = jiraApi.getEndpoint().getIssue()
                .getIncrement().getManualDate().replace("{수동날짜}","updated > '"+startDate+"' AND updated < '"+jiraUtil.getNextDate(endDate)+"' AND project = '"+projectKey+"'")
                + "&maxResults=" + maxResults+  "&expand=changelog";

        boolean hasMore = true;

        List<CloudJiraIssueRawDataVO> issueResult = new ArrayList<>();

        String nextPageToken = null;

        while (hasMore) {

            CloudJiraIssues cloudJiraIssues
                    = jiraUtil.getWithRawData(webClient, endPoint+Optional.ofNullable(nextPageToken).orElse(""), CloudJiraIssues.class);

            if(cloudJiraIssues==null){
                break;
            }

            List<CloudJiraIssueRawDataVO> issues
                    = Optional.of(cloudJiraIssues).map(CloudJiraIssues::getIssues)
                        .orElseThrow(()-> new IllegalArgumentException("이슈_상세정보_가져오기에 실패하였습니다."));

            syncRenamedIssueKeys(almIssueIncrementDTO, issues);

            issueResult.addAll(issues);

            if (issueResult.isEmpty() || cloudJiraIssues.getIsLast()) {
                hasMore = false;
            }else{
                nextPageToken = "&nextPageToken="+Optional.ofNullable(cloudJiraIssues.getNextPageToken()).orElse("");
            }
        }

        return issueResult;

    }

    private void syncRenamedIssueKeys(AlmIssueIncrementDTO almIssueIncrementDTO, List<CloudJiraIssueRawDataVO> issues) {
        ServerInfoVO serverInfoVO = serverInfoService.verifyServerInfo(almIssueIncrementDTO.getServerId());

        for(CloudJiraIssueRawDataVO issue : issues){

            ChangeLogVO changelog = issue.getChangelog();

            if (changelog == null) continue;

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

            changelog.getHistories()
                .stream()
                .sorted(Comparator.comparing(HistoriesDTO::getCreated).reversed())
                .forEach(history->
                        Optional.ofNullable(history.getItems())
                            .orElse(List.of())
                            .stream()
                            .filter(a->"Key".equals(a.getField()))
                            .forEach(itemsDTO->{
                                    String fromKey = itemsDTO.getFromString();
                                    List<SearchHit<AlmIssueEntity>> hitDocs = esCommonRepositoryWrapper.findRecentHits(
                                            SimpleQuery
                                                .termQueryFilter("key", fromKey)
                                                .andTermQueryFilter("jira_server_id", serverInfoVO.getConnectId())
                                    ).toHitDocs();

                                    hitDocs.forEach(hitDoc->{
                                        AlmIssueEntity almIssueEntity = hitDoc.getContent();
                                        almIssueEntity.setRecent(false);
                                        log.info("delete:key:{}",almIssueEntity.getKey());
                                        esCommonRepositoryWrapper.modifyWithIndexName(almIssueEntity,hitDoc.getIndex());
                                        almIssueRecentFalseList.add(almIssueEntity);
                                    });
                                }
                            )
                );

            almIssueRecentFalseList
                .stream()
                .filter(a->a.getRecentId()!=null)
                .findFirst()
                .ifPresent(almIssueEntity->{
                    almIssueEntity.modifyProjectKeyAndKey(issue.getFields().getAlmProjectKey(),issue.getKey());
                    esCommonRepositoryWrapper.save(almIssueEntity);
                });
        }
    }


    private CloudJiraIssueRawDataVO issueList(Map<String,CloudJiraIssueRawDataVO> lruMap, IssueDTO issueDTO, WebClient webClient, String endpoint) {

        log.info("key cache:key=={},{}",issueDTO.getKey(),lruMap.get(issueDTO.getKey())!=null);

        CloudJiraIssueRawDataVO cloudJiraIssueRawDataVO = lruMap.get(issueDTO.getKey());

        if(cloudJiraIssueRawDataVO !=null){
            return cloudJiraIssueRawDataVO;
        }else{
            return Objects.requireNonNull(jiraUtil.getWithRawData(webClient, endpoint, CloudJiraIssueRawDataVO.class));
        }
    }

    //일단 현재는 사용 안하지만 필요할 수 있어서 남김
    private List<CloudJiraIssueRawDataVO> exploreRelatedIssuesByFetchedIssues(
            List<CloudJiraIssueRawDataVO> fetchedIssues,
            Map<String, CloudJiraIssueRawDataVO> lruMap,
            Map<String, List<CloudJiraIssueRawDataVO>> parentKeyTraceMap,
            WebClient webClient) {

        Set<String> visited = new HashSet<>();
        Queue<String> queue = new LinkedList<>();
        List<CloudJiraIssueRawDataVO> result = new ArrayList<>(fetchedIssues);

        for (CloudJiraIssueRawDataVO issue : fetchedIssues) {
            String key = issue.getKey();
            queue.add(key);
            lruMap.put(key, issue);
        }

        while (!queue.isEmpty()) {
            String key = queue.poll();

            if (visited.contains(key) || ObjectUtils.isEmpty(key)) {
                continue;
            }
            visited.add(key);

            try {
                String endpoint = jiraApi.getEndpoint().getIssue().getBase() + "/" + key + "?" + jiraApi.getParameter().getFields();
                IssueDTO issueDTO = IssueDTO.builder().key(key).build();
                CloudJiraIssueRawDataVO issue = issueList(lruMap, issueDTO, webClient, endpoint);
                lruMap.put(issue.getKey(), issue);

                if (!result.contains(issue)) {
                    result.add(issue);
                }

                String parentKey = issue.getParentKey();
                if (parentKey != null && !visited.contains(parentKey)) {
                    queue.add(parentKey);
                }

                List<CloudJiraIssueRawDataVO> subtasks = fetchSubTaskIssuesByKey(parentKeyTraceMap, key, webClient);
                for (CloudJiraIssueRawDataVO subtask : subtasks) {
                    String subtaskKey = subtask.getKey();
                    if (!visited.contains(subtaskKey)) {
                        queue.add(subtaskKey);
                    }
                }

                if (issue.getFields() != null && issue.getFields().getIssuelinks() != null) {
                    for (IssueLink link : issue.getFields().getIssuelinks()) {
                        if (link.getOutwardIssue() != null) {
                            String outwardKey = link.getOutwardIssue().getKey();
                            if (!visited.contains(outwardKey)) {
                                queue.add(outwardKey);
                            }
                        }
                        if (link.getInwardIssue() != null) {
                            String inwardKey = link.getInwardIssue().getKey();
                            if (!visited.contains(inwardKey)) {
                                queue.add(inwardKey);
                            }
                        }
                    }
                }

            } catch (WebClientResponseException.NotFound e) {
                log.info("exploreRelatedIssuesByFetchedIssues - not found key: {}", key);
            } catch (Exception e) {
                log.warn("exploreRelatedIssuesByFetchedIssues - error fetching issue key: {}", key, e);
            }
        }
        return result;
    }

    private List<CloudJiraIssueRawDataVO> fetchSubTaskIssuesByKey( Map<String,List<CloudJiraIssueRawDataVO>> parentKeyTraceMap, String parentKey, WebClient webClient) {

        log.info("parentKey cache:parentKey=={},{}",parentKey,parentKeyTraceMap.get(parentKey)!=null);

        if(parentKeyTraceMap.containsKey(parentKey)){
            return parentKeyTraceMap.get(parentKey);
        }

        int maxResults = jiraApi.getParameter().getMaxResults();

        String endPoint = jiraApi.이슈키_대체하기(jiraApi.getEndpoint().getIssue().getFull().getSubtask(), parentKey)
                + "&maxResults=" + maxResults+  "&expand=changelog";

        boolean hasMore = true;

        List<CloudJiraIssueRawDataVO> issueResult = new ArrayList<>();

        String nextPageToken = null;

        while (hasMore) {

            CloudJiraIssues cloudJiraIssues
                    = jiraUtil.getWithRawData(webClient, endPoint+Optional.ofNullable(nextPageToken).orElse(""), CloudJiraIssues.class);

            if(cloudJiraIssues==null){
                break;
            }

            issueResult.addAll(Optional.of(cloudJiraIssues).map(CloudJiraIssues::getIssues).orElseThrow(()-> new IllegalArgumentException("이슈_상세정보_가져오기에 실패하였습니다.")));

            if (issueResult.isEmpty()||cloudJiraIssues.getIsLast()) {
                hasMore = false;
            }else{
                nextPageToken = "&nextPageToken="+Optional.ofNullable(cloudJiraIssues.getNextPageToken()).orElse("");
            }
        }

        parentKeyTraceMap.put(parentKey,issueResult);

        return issueResult;

    }


    public CloudJiraIssueFieldDTO validateAndAddFields(IssueCreationFieldsDTO IssueCreationFieldsDTO,
                                                       Map<String, CloudJiraIssueCreationFieldMetadata.FieldMetadata> fieldMetadataMap,
                                                       ServerInfoVO serverInfoVO, String projectKeyOrId, String issueTypeId) {

        if (fieldMetadataMap == null) {
            String 에러로그 = "필드검증_및_추가하기 필드_메타데이터_목록 Null 오류 클라우드 지라(" + serverInfoVO.getUri() + ") ::  프로젝트 :: "+projectKeyOrId+
                    " :: 이슈유형 :: " + issueTypeId+ " :: 생성 필드 :: "+ IssueCreationFieldsDTO.toString();
            log.error(에러로그);
            throw new IllegalArgumentException(에러로그);
        }

        CloudJiraIssueFieldDTO CloudJiraIssueFieldDTO = new CloudJiraIssueFieldDTO();

        if (fieldMetadataMap.containsKey(jiraApi.getMetadata().getFields().getProject())
                && IssueCreationFieldsDTO.getProject() != null) {

            CloudJiraIssueFieldDTO.setProject(IssueCreationFieldsDTO.getProject());
            fieldMetadataMap.remove(jiraApi.getMetadata().getFields().getProject());
        }

        if (fieldMetadataMap.containsKey(jiraApi.getMetadata().getFields().getIssuetype())
                && IssueCreationFieldsDTO.getIssuetype() != null) {

            CloudJiraIssueFieldDTO.setIssuetype(IssueCreationFieldsDTO.getIssuetype());
            fieldMetadataMap.remove(jiraApi.getMetadata().getFields().getIssuetype());
        }

        if (fieldMetadataMap.containsKey(jiraApi.getMetadata().getFields().getSummary())
                && IssueCreationFieldsDTO.getSummary() != null) {

            CloudJiraIssueFieldDTO.setSummary(IssueCreationFieldsDTO.getSummary());
            fieldMetadataMap.remove(jiraApi.getMetadata().getFields().getSummary());
        }

        if (fieldMetadataMap.containsKey(jiraApi.getMetadata().getFields().getDescription())
                && IssueCreationFieldsDTO.getDescription() != null) {

            CloudJiraIssueFieldDTO.setDescription(내용_변환(IssueCreationFieldsDTO.getDescription()));
            fieldMetadataMap.remove(jiraApi.getMetadata().getFields().getDescription());
        }

        if (fieldMetadataMap.containsKey(jiraApi.getMetadata().getFields().getReporter())) {
            /***
             * reporter 필드는 필수 여부와 상관없이 추가하지 않아도 API를 요청한 사용자로 자동으로 추가된다.
             ***/
            fieldMetadataMap.remove(jiraApi.getMetadata().getFields().getReporter());
        }

        if (fieldMetadataMap.containsKey(jiraApi.getMetadata().getFields().getPriority())
                && IssueCreationFieldsDTO.getPriority() != null) {

            CloudJiraIssueFieldDTO.setPriority(IssueCreationFieldsDTO.getPriority());
            fieldMetadataMap.remove(jiraApi.getMetadata().getFields().getPriority());
        }

        if (fieldMetadataMap.containsKey(jiraApi.getMetadata().getFields().getDuedate())
                && IssueCreationFieldsDTO.getDueDate() != null) {

            String 종료기한일자 = DateUtils.formatDate(IssueCreationFieldsDTO.getDueDate(), "yyyy-MM-dd");
            CloudJiraIssueFieldDTO.setDuedate(종료기한일자);
            fieldMetadataMap.remove(jiraApi.getMetadata().getFields().getDuedate());
        }

        /***
         * A-RMS에서 지원하는 필드 메타데이터 목록을 모두 remove 시킨 후 필수로 지정된 필드가 있을 경우 오류 반환
         ***/
        fieldMetadataMap.forEach((key, 필드_메타) -> {
            if (필드_메타.isRequired()) {
                String 에러로그 = "필수 필드 확인 및 추가 중 오류 [" + key + "] 필드가 필수로 지정되어있습니다. " +
                        "A-RMS에서 지원하지 않는 필드입니다. ";
                String 관련정보 = "클라우드 지라(" + serverInfoVO.getUri() + ") ::  프로젝트 :: "+projectKeyOrId+
                        " :: 이슈유형 :: " + issueTypeId+ " :: 생성 필드 :: "+ IssueCreationFieldsDTO.toString();
                log.error(에러로그 + 관련정보);
                throw new IllegalArgumentException(에러로그 + 관련정보);
            }
        });

        return CloudJiraIssueFieldDTO;
    }

    private String getArmsStateCategory(ServerInfoVO serverInfoVO, IssueFieldData IssueFieldData) {
        return categoryMappingService.getMappingCategory(serverInfoVO, IssueFieldData.getAlmProjectKey(), IssueFieldData.getAlmIssueTypeId(), IssueFieldData.getAlmStatusId());
    }

    @Override
    @Async
    @SlackSendAlarm(messageOnStart="클라우드 지라 API 테스트")
    public void cloudJiraTestApiRequest(){

        AlmIssueIncrementDTO almIssueIncrementDTO = AlmIssueIncrementDTO.builder()
                .startDate("2025-11-10")
                .endDate("2025-11-10")
                .serverId("936493109998294575")
                .projectKey("ARMSTEST")
                .build();

        ServerInfoVO serverInfoVO = serverInfoService.verifyServerInfo(almIssueIncrementDTO.getServerId());

        WebClient webClient = jiraUtil.createJiraCloudCommunicator(
                serverInfoVO.getUri(), serverInfoVO.getUserId(), serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()));

        List<CloudJiraIssueRawDataVO> cloudJiraIssueRawDataVOS = this.fetchIssuesFromDate(almIssueIncrementDTO, webClient);

        if(cloudJiraIssueRawDataVOS.isEmpty()){
            throw new IllegalArgumentException("fetchIssuesFromDate :: API 확인 필요");
        }

        List<CloudJiraIssueRawDataVO> cloudJiraIssueRawDataVOS1 = this.fetchSubTaskIssuesByKey(new HashMap<>(), "ARMSTEST-106", webClient);

        if(cloudJiraIssueRawDataVOS1.isEmpty()){
            throw new IllegalArgumentException("fetchSubTaskIssuesByKey :: API 확인 필요");
        }
    }

    private String extractRequirementUrl(String text) {

        // "자세한 요구사항 내용 확인 ⇒" 뒤의 URL 추출
        Pattern pattern = Pattern.compile("자세한 요구사항 내용 확인 ⇒\\s*([^\\s\\n]+)");
        Matcher matcher = pattern.matcher(text);

        if (matcher.find()) {
            return matcher.group(1);
        }
        return null;
    }

}
