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.priority.model.IssuePriorityDTO;
import com.arms.api.issue.resolution.model.IssueResolutionData;
import com.arms.api.issue.status.model.dto.IssueStatusDTO;
import com.arms.api.issue.type.model.dto.IssueTypeDTO;
import com.arms.api.almproject.model.dto.ProjectDTO;
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.errors.ErrorCode;
import com.arms.api.util.errors.ErrorLogUtil;
import com.arms.api.issue.almapi.service.CategoryMappingService;
import com.arms.egovframework.javaservice.esframework.esquery.SimpleQuery;
import com.arms.egovframework.javaservice.esframework.repository.common.EsCommonRepositoryWrapper;
import com.atlassian.jira.rest.client.api.IssueRestClient;
import com.atlassian.jira.rest.client.api.JiraRestClient;
import com.atlassian.jira.rest.client.api.RestClientException;
import com.atlassian.jira.rest.client.api.domain.*;
import com.atlassian.jira.rest.client.api.domain.IssueLink;
import com.atlassian.jira.rest.client.api.domain.input.IssueInput;
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder;
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.joda.JodaModule;

import io.atlassian.util.concurrent.Promise;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.joda.time.DateTime;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.function.client.WebClient;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static com.arms.api.util.ParseUtil.timeFormat;

@Slf4j
@Component("ON_PREMISS")
@AllArgsConstructor
public class OnPremiseJiraIssueStrategy implements IssueStrategy {

    private static final long PROMISE_TIMEOUT_SECONDS = 30;

    private final EsCommonRepositoryWrapper<AlmIssueEntity> esCommonRepositoryWrapper;

    private final JiraUtil jiraUtil;

    private final JiraApi jiraApi;

    private final CategoryMappingService categoryMappingService;

    private final ServerInfoService serverInfoService;

    private final IssueSaveTemplate issueSaveTemplate;

    /* ***
     * 수정사항: null 체크하여 에러 처리 필요
     *** */
    @Override
    public AlmIssueVO createIssue(AlmIssueDTO almIssueDTO) {

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

        try( JiraRestClient restClient = jiraUtil.createJiraOnPremiseCommunicator(serverInfoVO.getUri(),
                serverInfoVO.getUserId(),
                serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()))) {
            IssueCreationFieldsDTO fieldsDTO = almIssueDTO.getFields();
            if (fieldsDTO == null) {
                String errorMessage = String.format("%s[%s] :: 이슈 생성 필드 데이터가 존재 하지 않습니다.",
                        serverInfoVO.getType(), serverInfoVO.getUri());
                log.error(errorMessage);
                throw new IllegalArgumentException(ErrorCode.REQUEST_BODY_ERROR_CHECK.getErrorMsg() + " :: " + errorMessage);
            }

            String projectKeyOrId = null;
            Long issueTypeId = null;
            String summary = null;

            if (fieldsDTO.getProject() != null && StringUtils.isNotBlank(fieldsDTO.getProject().getKey())) {
                projectKeyOrId = fieldsDTO.getProject().getKey();
            }

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

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

            if (StringUtils.isNotEmpty(fieldsDTO.getSummary())) {
                summary = fieldsDTO.getSummary();
            }

            IssueInputBuilder issueInputBuilder = new IssueInputBuilder(projectKeyOrId, issueTypeId, summary);

            if (StringUtils.isNotEmpty(fieldsDTO.getDescription())) {
                issueInputBuilder.setDescription(fieldsDTO.getDescription());
            }

            if (fieldsDTO.getReporter() != null && StringUtils.isNotEmpty(fieldsDTO.getReporter().getAccountId())) {
                issueInputBuilder.setReporterName(fieldsDTO.getReporter().getAccountId());
            }

            if (fieldsDTO.getPriority() != null && StringUtils.isNotEmpty(fieldsDTO.getPriority().getId())) {
                issueInputBuilder.setPriorityId(Long.valueOf(fieldsDTO.getPriority().getId()));
            }

            if (fieldsDTO.getDueDate() != null) {
                DateTime dateTime = new DateTime(fieldsDTO.getDueDate());
                issueInputBuilder.setDueDate(dateTime);
            }

            IssueInput issueInput = issueInputBuilder.build();

            BasicIssue basicIssue = restClient.getIssueClient().createIssue(issueInput)
                    .get(PROMISE_TIMEOUT_SECONDS, TimeUnit.SECONDS);


            if (basicIssue == null) {
                String errorMessage = String.format("%s[%s], 프로젝트[%s], 이슈유형[%s], 생성 필드 :: %s 생성된 이슈가 존재 하지 않습니다.",
                                                    serverInfoVO.getType(), serverInfoVO.getUri(), projectKeyOrId, issueTypeId, issueInput.toString());
                log.error(errorMessage);
                throw new IllegalArgumentException(ErrorCode.ISSUE_CREATION_ERROR.getErrorMsg() + errorMessage);
            }

            return AlmIssueVO.builder()
                    .id(String.valueOf(basicIssue.getId()))
                    .key(basicIssue.getKey())
                    .self(String.valueOf(basicIssue.getSelf()))
                    .build();
        }  catch (InterruptedException | ExecutionException | IOException | TimeoutException e){
            Thread.currentThread().interrupt();
            throw new RuntimeException("Jira 이슈 생성 API 호출 오류", e);
        }catch (Exception e) {
            throw new IllegalArgumentException(ErrorCode.ISSUE_CREATION_ERROR.getErrorMsg() + e.getMessage());
        }
    }

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

        String issueKeyOrId = almIssueDTO.getIssueKeyOrId();

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

        Map<String, Object> 결과 = new HashMap<>();
        IssueCreationFieldsDTO 필드_데이터 = almIssueDTO.getFields();

        try(JiraRestClient restClient = jiraUtil.createJiraOnPremiseCommunicator(serverInfoVO.getUri(),
                serverInfoVO.getUserId(),
                serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()))) {
            IssueInputBuilder 입력_생성 = new IssueInputBuilder();

            if (필드_데이터.getSummary() != null) {
                입력_생성.setSummary(필드_데이터.getSummary());
            }

            if (필드_데이터.getDescription() != null) {
                입력_생성.setDescription(필드_데이터.getDescription());
            }

            if (필드_데이터.getLabels() != null) {
                입력_생성.setFieldValue("labels", 필드_데이터.getLabels());
            }

            IssueInput 수정_데이터 = 입력_생성.build();
            Promise<Void> 수정결과 = restClient.getIssueClient().updateIssue(issueKeyOrId, 수정_데이터);
            수정결과.get(PROMISE_TIMEOUT_SECONDS, TimeUnit.SECONDS);

            결과.put("success", true);
            결과.put("message", "이슈 수정 성공");
        }
        catch (InterruptedException | ExecutionException | IOException | TimeoutException e) {
            String 에러로그 = ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                    "온프레미스 지라(" + serverInfoVO.getUri() +") ::  생성 필드 :: "
                            + 필드_데이터.toString() + ", :: 이슈_수정하기 중 오류 :: " + issueKeyOrId);
            log.error(에러로그);
            Thread.currentThread().interrupt();
            결과.put("success", false);
            결과.put("message", 에러로그);
        }
        return 결과;
    }

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

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

        Map<String, Object> 결과 = new HashMap<>();
        String 이슈전환_아이디 = null;

        String issueKeyOrId = almIssueDTO.getIssueKeyOrId();
        String statusId = almIssueDTO.getStatusId();

        try(JiraRestClient restClient = jiraUtil.createJiraOnPremiseCommunicator(serverInfoVO.getUri(),
                serverInfoVO.getUserId(),
                serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()))) {

            Issue 지라이슈 = restClient.getIssueClient().getIssue(issueKeyOrId)
                    .get(PROMISE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            이슈전환_아이디 = 이슈전환_아이디_조회하기(almIssueDTO);

            if (이슈전환_아이디 != null) {
                TransitionInput transitionInput = new TransitionInput(Integer.parseInt(이슈전환_아이디));
                Promise<Void> 변경결과 = restClient.getIssueClient().transition(지라이슈, transitionInput);
                변경결과.get(PROMISE_TIMEOUT_SECONDS, TimeUnit.SECONDS);

                결과.put("success", true);
                결과.put("message", "이슈 상태 변경 성공");
            }
            else {
                String 에러로그 = "온프레미스 지라(" + serverInfoVO.getUri() + ") :: 이슈 키(" + issueKeyOrId + ") :: 상태 아이디(" + statusId + ") :: 해당 업무 흐름으로 변경이 불가능 합니다.";
                log.error(에러로그);

                결과.put("success", false);
                결과.put("message", "변경할 이슈 상태가 존재하지 않습니다.");
            }
        }
        catch (InterruptedException | ExecutionException | IOException | TimeoutException e) {
            String 에러로그 = ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                    "온프레미스 지라(" + serverInfoVO.getUri() + ") :: 이슈 키(" + issueKeyOrId + ") :: 상태 아이디(" + statusId + ") :: 전환 아이디(" + 이슈전환_아이디 + ") :: 이슈_상태_변경하기에 실패하였습니다.");
            Thread.currentThread().interrupt();
            결과.put("success", false);
            결과.put("message", 에러로그);
        }

        return 결과;
    }

    private 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 = "/rest/api/2/issue/" + issueKeyOrId + "/transitions";

            JiraIssueMigData 이슈전환_데이터 = jiraUtil.get(webClient, endpoint, JiraIssueMigData.class).block();

            return Optional.ofNullable(이슈전환_데이터)
                    .map(JiraIssueMigData::getTransitions)
                    .orElse(Collections.emptyList()).stream()
                    .filter(데이터 -> {
                        if (데이터.getTo() != null && !StringUtils.isBlank(데이터.getTo().getId())) {
                            return statusId.equals(데이터.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) {

        String issueKeyOrId = almIssueDTO.getIssueKeyOrId();

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

        Map<String, Object> 결과 = new HashMap<String, Object>();

        try(JiraRestClient restClient = jiraUtil.createJiraOnPremiseCommunicator(serverInfoVO.getUri(),
                serverInfoVO.getUserId(),
                serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()))) {
            boolean 하위이슈_삭제유무 = jiraApi.getParameter().isDeleteSubtasks();
            Promise<Void> 삭제결과 = restClient.getIssueClient().deleteIssue(issueKeyOrId, 하위이슈_삭제유무);
            삭제결과.get(PROMISE_TIMEOUT_SECONDS, TimeUnit.SECONDS);

            결과.put("success", true);
            결과.put("message", "이슈 삭제 성공");
        }
        catch (InterruptedException | ExecutionException | IOException | TimeoutException e) {
            String 에러로그 = ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                    "온프레미스 지라(" + serverInfoVO.getUri() + ") :: 이슈_삭제하기 중 오류 :: " + issueKeyOrId);
            log.error(에러로그);
            Thread.currentThread().interrupt();

            결과.put("success", false);
            결과.put("message", 에러로그);
        }

        return 결과;
    }


    @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) {

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

        String issueKeyOrId = almIssueDTO.getIssueKeyOrId();

        try( JiraRestClient restClient = jiraUtil.createJiraOnPremiseCommunicator(serverInfoVO.getUri(),
                serverInfoVO.getUserId(),
                serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()))) {

            Issue issue = restClient.getIssueClient().getIssue(issueKeyOrId)
                    .get(PROMISE_TIMEOUT_SECONDS, TimeUnit.SECONDS);

            if(convertFlag){
                return this.convertOnPremiseJiraIssueToAlmIssue(OnPremissJiraIssueVO.builder().issue(issue).serverInfoVO(serverInfoVO).build());
            }
            return AlmIssueVO.builder().key(issue.getKey()).build();
        }
        catch (InterruptedException | ExecutionException | IOException |TimeoutException e) {
            ErrorLogUtil.exceptionLogging(e, this.getClass().getName(),
                    "온프레미스 지라(" + serverInfoVO.getUri() + ") ::  이슈_키_또는_아이디 :: " + issueKeyOrId + ", 이슈_상세정보_가져오기 중 오류");
            Thread.currentThread().interrupt();
            throw new IllegalArgumentException("이슈_상세정보_가져오기에 실패하였습니다.");
        }
    }

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

        Map<String,Issue> lruMap = new LRUMap<>(10000);
        return issueSaveTemplate.discoveryIncrementALmIssueAndGetReqAlmIssueEntities(
                almIssueIncrementDTO, (dto)-> this.discoveryIssueAndGetReqEntities(dto, lruMap));
    }

    private AlmIssueVOCollection discoveryIssueAndGetReqEntities(AlmIssueIncrementDTO almIssueIncrementDTO, Map<String,Issue> lruMap) {

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

        try (JiraRestClient restClient = jiraUtil.createJiraOnPremiseCommunicator(serverInfoVO.getUri(),
                serverInfoVO.getUserId(),
                serverInfoService.getDecryptPasswordOrToken(serverInfoVO.getPasswordOrToken()))){

            List<Issue> issuesFromJiraIssue = fetchIssuesFromDate(almIssueIncrementDTO, restClient);

            List<AlmIssueEntity> almIssueEntities
                    = esCommonRepositoryWrapper.findRecentHits(
                    SimpleQuery.termsQueryFilter("key", OnPremissJiraIssueVOCollection
                                    .fromIssues(issuesFromJiraIssue).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)) {

                        try{

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

                            Issue issue = issueList(lruMap, issueDTO, restClient);

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

                        }catch (RestClientException e){
                            com.google.common.base.Optional<Integer> statusCode = e.getStatusCode();
                            if(statusCode.isPresent() && statusCode.get() == 404){

                                log.info("onPremise 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());
                                }

                            }else{
                                throw new IllegalArgumentException(e.getMessage(), e);
                            }
                        }

                        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(issuesFromJiraIssue::add);
                }

            }

            List<OnPremissJiraIssueVO> onPremissJiraIssueVOS = issuesFromJiraIssue.stream().distinct()
                    .flatMap(jiraIssueRawDataVO
                            -> traceAndMapToRequirement(lruMap, jiraIssueRawDataVO, serverInfoVO, restClient).stream())
                    .toList();

            OnPremissJiraIssueVOCollection onPremissJiraIssueVOCollection = OnPremissJiraIssueVOCollection.of(
                onPremissJiraIssueVOS
                    .stream()
                    .map(onPremissJiraIssueVO->{
                        if(!issuesFromJiraIssue.contains(onPremissJiraIssueVO.getIssue())){
                            return onPremissJiraIssueVO.markAsExcludedFromSave();
                        }else{
                            return onPremissJiraIssueVO;
                        }
                    }).toList());

            List<OnPremissJiraIssueVO> onPremissJiraIssueVOs = onPremissJiraIssueVOCollection.appliedLinkedIssuePdServiceVO();

            return this.convertIncrementOnPremiseJiraIssueToAlmIssues(onPremissJiraIssueVOs);

        } catch (InterruptedException | ExecutionException | IOException e) {
            log.error("증분 데이터 수집에 실패 하였습니다.",e);
            Thread.currentThread().interrupt();
            ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                    "온프레미스 지라(" + serverInfoVO.getUri() + ") 증분 이슈 수집 중 오류");

            return new AlmIssueVOCollection(new ArrayList<>());
        }

    }

    private AlmIssueVOCollection convertIncrementOnPremiseJiraIssueToAlmIssues(List<OnPremissJiraIssueVO> onPremissJiraIssueVOs){
        return new AlmIssueVOCollection(onPremissJiraIssueVOs
                .stream()
                .map(this::convertOnPremiseJiraIssueToAlmIssue)
                .toList());
    }

    private AlmIssueVO convertOnPremiseJiraIssueToAlmIssue(OnPremissJiraIssueVO onPremissJiraIssueVO) {

        AlmIssueVO.AlmIssueVOBuilder almIssueVOBuilder = AlmIssueVO.builder();
        IssueFieldData issueFieldData = new IssueFieldData();

        Issue issue = onPremissJiraIssueVO.getIssue();
        ServerInfoVO serverInfoVO = onPremissJiraIssueVO.getServerInfoVO();

        Optional.ofNullable(onPremissJiraIssueVO.relationRecentIds())
                .ifPresent(almIssueVOBuilder::linkedIssue);

        almIssueVOBuilder
            .almIssueWithRequirementVO(onPremissJiraIssueVO.getAlmIssueWithRequirementVO())
            .id(String.valueOf(issue.getId()))
            .excludeFromSave(onPremissJiraIssueVO.isExcludeFromSave())
            .key(issue.getKey())
            .self(String.valueOf(issue.getSelf()));

        try {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.registerModule(new JodaModule());

            String rawData = objectMapper.writeValueAsString(issue);
            almIssueVOBuilder.rawData(rawData);
        }
        catch (JsonProcessingException e) {
            String 에러로그 = ErrorLogUtil.exceptionLoggingAndReturn(e, this.getClass().getName(),
                    "온프레미스 지라 :: 지라이슈_데이터로_변환 중 오류 :: " + issue.getKey());
            log.error(에러로그);
        }

        for (IssueField issueField : issue.getFields()) {
            if (issueField.getId().equals("parent")) {
                JSONObject value = (JSONObject)issueField.getValue();
                try {
                    Optional.ofNullable(value.get("key"))
                        .ifPresent(a->{
                            almIssueVOBuilder.upperKey(String.valueOf(a));
                        });
                } catch (JSONException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        // 필드
        // 초기화
        ProjectDTO 프로젝트 = ProjectDTO.builder().build();
        UserData 보고자 = new UserData();
        UserData 담당자 = new UserData();

        // 프로젝트
        if (issue.getProject() != null) {

            프로젝트.setSelf(String.valueOf(issue.getProject().getSelf()));
            프로젝트.setId(String.valueOf(issue.getProject().getId()));
            프로젝트.setKey(issue.getProject().getKey());
            프로젝트.setName(issue.getProject().getName());

            issueFieldData.setProject(프로젝트);
        }

        // 이슈 유형
        if (issue.getIssueType() != null) {

            String 이슈유형_주소 = String.valueOf(issue.getIssueType().getSelf());
            String 이슈유형_아이디 = String.valueOf(issue.getIssueType().getId());
            String 이슈유형_이름 = issue.getIssueType().getName();
            String 이슈유형_내용 = issue.getIssueType().getDescription();
            Boolean 이슈유형_서브테스크여부 = issue.getIssueType().isSubtask();

            IssueTypeDTO 이슈유형 = new IssueTypeDTO();
            이슈유형.setSelf(이슈유형_주소);
            이슈유형.setId(이슈유형_아이디);
            이슈유형.setName(이슈유형_이름);
            이슈유형.setDescription(이슈유형_내용);
            이슈유형.setSubtask(이슈유형_서브테스크여부);

            issueFieldData.setIssuetype(이슈유형);
        }

        // 생성자

        // 보고자
        if (issue.getReporter() != null) {

            보고자.setAccountId(issue.getReporter().getName());
            보고자.setEmailAddress(issue.getReporter().getEmailAddress());
            보고자.setDisplayName(issue.getReporter().getDisplayName());

            issueFieldData.setReporter(보고자);
        }

        // 담당자
        if (issue.getAssignee() != null) {

            담당자.setAccountId(issue.getAssignee().getName());
            담당자.setEmailAddress(issue.getAssignee().getEmailAddress());
            담당자.setDisplayName(issue.getAssignee().getDisplayName());

            issueFieldData.setAssignee(담당자);
        }

        // 라벨
        if (issue.getLabels() != null) {
            Set<String> 라벨_목록 = issue.getLabels();
            List<String> 이슈라벨 = new ArrayList<>(라벨_목록);
            issueFieldData.setLabels(이슈라벨);
        }

        // 우선 순위
        if (issue.getPriority() != null) {

            String 이슈우선순위_주소 = String.valueOf(issue.getPriority().getSelf());
            String 이슈우선순위_아이디 = String.valueOf(issue.getPriority().getId());
            String 이슈우선순위_이름 = issue.getPriority().getName();

            IssuePriorityDTO 이슈우선순위 = new IssuePriorityDTO();
            이슈우선순위.setSelf(이슈우선순위_주소);
            이슈우선순위.setId(이슈우선순위_아이디);
            이슈우선순위.setName(이슈우선순위_이름);
            // description

            issueFieldData.setPriority(이슈우선순위);
        }

        // 상태 값
        if (issue.getStatus() != null) {

            String 이슈상태_주소 = String.valueOf(issue.getStatus().getSelf());
            String 이슈상태_아이디 = String.valueOf(issue.getStatus().getId());
            String 이슈상태_이름 = issue.getStatus().getName();
            String 이슈상태_설명 =  issue.getStatus().getDescription();

            IssueStatusDTO 이슈상태 = new IssueStatusDTO();
            이슈상태.setSelf(이슈상태_주소);
            이슈상태.setId(이슈상태_아이디);
            이슈상태.setName(이슈상태_이름);
            이슈상태.setDescription(이슈상태_설명);

            issueFieldData.setStatus(이슈상태);
        }

        // 해결책
        if (issue.getResolution() != null) {

            String 이슈해결책_주소 = String.valueOf(issue.getResolution().getSelf());
            String 이슈해결책_아이디 = String.valueOf(issue.getResolution().getId());
            String 이슈해결책_이름 = issue.getResolution().getName();
            String 이슈해결책_설명 = issue.getResolution().getDescription();

            IssueResolutionData 이슈해결책 = new IssueResolutionData();
            이슈해결책.setSelf(이슈해결책_주소);
            이슈해결책.setId(이슈해결책_아이디);
            이슈해결책.setName(이슈해결책_이름);
            이슈해결책.setDescription(이슈해결책_설명);

            issueFieldData.setResolution(이슈해결책);
        }

        // resolutiondate
        for (IssueField 필드 : issue.getFields()) {
            if (필드 != null && !필드.getId().isEmpty() && 필드.getId().equals("resolutiondate")) {
                if (필드.getValue().toString() != null) {
                    issueFieldData.setResolutiondate(필드.getValue().toString());
                }
                break;
            }
        }

        // created
        if (issue.getCreationDate() != null) {
            String 이슈생성날짜 = String.valueOf(issue.getCreationDate());
            issueFieldData.setCreated(이슈생성날짜);
        }

        if (issue.getUpdateDate() != null) {
            String 이슈수정날짜 = String.valueOf(issue.getUpdateDate());
            issueFieldData.setUpdated(이슈수정날짜);
        }

        // worklogs
        // BasicUser 타입에서 이메일 데이터를 받아올 수 없어서 고민 중...
        if (issue.getWorklogs() != null) {

            List<IssueWorkLogData> 이슈워크로그_목록 = new ArrayList<>();

            Iterable<Worklog> 전체이슈워크로그 = issue.getWorklogs();

            for (Worklog 워크로그 : 전체이슈워크로그) {
                UserData 작성자 = new UserData();

                String 이슈워크로그_주소 = 워크로그.getSelf().toString();
                BasicUser 이슈워크로그_작성자 = 워크로그.getAuthor();
                String 이슈워크로그_작성자아이디 = 이슈워크로그_작성자.getName();
                //String 이슈워크로그_작성자이메일 = 이슈워크로그_작성자.getSelf().toString();
                BasicUser 이슈워크로그_수정작성자 = 워크로그.getUpdateAuthor();
                String 이슈워크로그_수정작성자아이디 = 이슈워크로그_수정작성자.getName();
                //String 이슈워크로그_수정작성자이메일 = 이슈워크로그_수정작성자.getSelf().toString();
                String 이슈워크로그_생성날짜 = 워크로그.getCreationDate().toString();
                String 이슈워크로그_수정날짜 = 워크로그.getUpdateDate().toString();
                String 이슈워크로그_시작날짜 = 워크로그.getStartDate().toString();
                String 이슈워크로그_소요시간_포맷 = timeFormat(워크로그.getMinutesSpent());
                Integer 이슈워크로그_소요시간 = 워크로그.getMinutesSpent() * 60;
                String[] 이슈워크로그_아이디 = 이슈워크로그_주소.split("/");

                IssueWorkLogData 이슈워크로그 = new IssueWorkLogData();
                이슈워크로그.setSelf(이슈워크로그_주소);

                작성자.setAccountId(이슈워크로그_작성자아이디);
                //작성자.setEmailAddress(이슈워크로그_작성자이메일);
                이슈워크로그.setAuthor(작성자);

                작성자.setAccountId(이슈워크로그_수정작성자아이디);
                //작성자.setEmailAddress(이슈워크로그_수정작성자이메일);
                이슈워크로그.setUpdateAuthor(작성자);

                이슈워크로그.setCreated(이슈워크로그_생성날짜);
                이슈워크로그.setUpdated(이슈워크로그_수정날짜);
                이슈워크로그.setStarted(이슈워크로그_시작날짜);
                이슈워크로그.setTimeSpent(이슈워크로그_소요시간_포맷);
                이슈워크로그.setTimeSpentSeconds(이슈워크로그_소요시간);
                이슈워크로그.setId(이슈워크로그_아이디[이슈워크로그_아이디.length - 1]);
                이슈워크로그.setIssueId(String.valueOf(issue.getId()));

                이슈워크로그_목록.add(이슈워크로그);
            }
            issueFieldData.setWorklogs(이슈워크로그_목록);
        }

        // timespent
        if (issue.getTimeTracking().getTimeSpentMinutes() != null) {
            Integer 이슈소요시간 = issue.getTimeTracking().getTimeSpentMinutes() * 60;
            issueFieldData.setTimespent(이슈소요시간);
        }

        if (issue.getSummary() != null) {
            issueFieldData.setSummary(issue.getSummary());
        }

        almIssueVOBuilder.fields(issueFieldData)
            .armsStateCategory(getMappingCategory(serverInfoVO, issueFieldData.getAlmStatusId()))
            .linkedIssuePdServiceIds(onPremissJiraIssueVO.getLinkedIssuePdServiceIds())
            .linkedIssuePdServiceVersions(onPremissJiraIssueVO.getLinkedIssuePdServiceVersions());

        return almIssueVOBuilder.build();
    }

    private List<Issue> fetchIssuesFromDate(AlmIssueIncrementDTO almIssueIncrementDTO, JiraRestClient jiraRestClient) throws ExecutionException, InterruptedException {

        String startDate = almIssueIncrementDTO.getStartDate();

        String endDate = almIssueIncrementDTO.getEndDate();

        String projectKey = almIssueIncrementDTO.getProjectKey();

        String updateJql = jiraApi.getParameter().getJql().getUpdated();

        if(startDate!=null&&endDate!=null){
            updateJql = jiraApi.getParameter().getJql().getManualDate();
            updateJql = updateJql.replace("{수동날짜}","updated > '"+startDate+"' AND updated < '"+jiraUtil.getNextDate(endDate)+"' AND project = '"+projectKey+"'");
        }

        String jql = updateJql;

        int page = 0;

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

        Set<String> fields = new HashSet<>(List.of("*all"));

        boolean hasMore = true;

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

        while (hasMore) {

            List<Issue> results = StreamUtil.toStream(jiraRestClient.getSearchClient()
                    .searchJql(jql, maxResults, page, fields)
                    .get().getIssues()).toList();

            syncRenamedIssueKeys(almIssueIncrementDTO, jiraRestClient, results);

            int fetchCount = results.size();

            if (fetchCount < maxResults) {
                hasMore = false;
            } else {
                page++;
            }
            issueResult.addAll(results);
        }

        return issueResult;

    }

    private void syncRenamedIssueKeys(AlmIssueIncrementDTO almIssueIncrementDTO, JiraRestClient jiraRestClient, List<Issue> issues) {

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

        for (Issue issue : issues) {

            List<ChangelogGroup> changelog;
            try {
                changelog = StreamSupport.stream(Objects.requireNonNull(jiraRestClient.getIssueClient()
                        .getIssue(issue.getKey(), List.of(IssueRestClient.Expandos.CHANGELOG))
                        .get(PROMISE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
                        .getChangelog()).spliterator(), false).toList();
            } catch (InterruptedException | ExecutionException | TimeoutException e) {
                Thread.currentThread().interrupt();
                log.warn("Jira 이슈 변경로그 조회 오류: {}", issue.getKey(), e);
                continue;
            }

            if (ObjectUtils.isEmpty(changelog)) continue;

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

            changelog.stream()
                .sorted(Comparator.comparing(ChangelogGroup::getCreated).reversed())
                .forEach(history ->
                    StreamSupport.stream(history.getItems().spliterator(),false)
                        .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);
                                    esCommonRepositoryWrapper.modifyWithIndexName(almIssueEntity,hitDoc.getIndex());
                                    almIssueRecentFalseList.add(almIssueEntity);
                                });
                            }
                        )

                );

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

    private List<OnPremissJiraIssueVO> traceAndMapToRequirement(
            Map<String,Issue> lruMap, Issue currentIssue,
            ServerInfoVO serverInfoVO, JiraRestClient restClient) {

        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();
                Issue issue = issueList(lruMap, issueDTO, restClient);

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

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

                    Iterable<Subtask> subtasks = issue.getSubtasks();
                    if (subtasks != null) {
                        for (Subtask subtask : Objects.requireNonNull(subtasks)) {
                            queue.add(this.createIssueDTO(subtask));
                        }
                    }

                    linkedIssueQueueRegister(issue, queue, visited);
                    visited.add(issue.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(), restClient);

            return OnPremissJiraIssueVO.builder()
                    .issue(currentIssue)
                    .almIssueWithRequirementVO(Optional.ofNullable(isSelfCreatedIssue(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();
        }).filter(a->a.getAlmIssueWithRequirementVO()!=null).toList();
    }

    private AlmIssueWithRequirementVO isSelfCreatedIssue(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, Issue> lruMap, String startKey, JiraRestClient restClient) {
        String findKey = startKey;
        String parentKey = null;

        while (findKey != null) {
            Issue issue = issueList(lruMap, IssueDTO.builder().key(findKey).build(), restClient);
            String currentParentKey = getParentKeyFromIssue(issue);
            if (currentParentKey != null) {
                parentKey = currentParentKey;
                findKey = parentKey;
            } else {
                break;
            }
        }
        return parentKey;
    }

    private String getParentKeyFromIssue(Issue issue) {
        return Optional.ofNullable(issue.getFields()).map(fields -> {
            for (IssueField issueField : fields) {
                if (issueField.getId().equals("parent")) {
                    JSONObject value = (JSONObject) issueField.getValue();
                    try {
                        return String.valueOf(value.get("key"));
                    } catch (JSONException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            return null;
        }).orElse(null);
    }

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

    private IssueDTO createIssueDTO(Subtask subtask) {
        return IssueDTO.builder()
                .key(String.valueOf(subtask.getIssueKey()))
                .fromKey(String.valueOf(subtask.getIssueKey()))
                .build();
    }

    private IssueDTO createIssueParentTraceDTO(Issue issue) {
        return IssueDTO.builder()
                .key(Optional.ofNullable(issue.getFields()).map(a->{
                    for (IssueField issueField : a) {
                        if (issueField.getId().equals("parent")) {
                            JSONObject value = (JSONObject)issueField.getValue();
                            try {
                                return String.valueOf(value.get("key"));
                            } catch (JSONException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                    return issue.getKey();
                }).orElseGet(issue::getKey))
                .fromKey(String.valueOf(issue.getKey()))
                .build();
    }


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

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

        issueRelations.forEach(element->{

            IssueDTO relation = IssueDTO.builder().key(element.getTargetIssueKey()).fromKey(issue.getKey()).build();

            if (!visited.contains(relation.getKey())) {
                queue.add(relation);
            }

            IssueDTO reverseRelation = IssueDTO.builder().key(issue.getKey()).fromKey(element.getTargetIssueKey()).build();

            if (!visited.contains(reverseRelation.getKey())) {
                queue.add(reverseRelation);
            }

        });
    }

    private Issue issueList(Map<String,Issue> lruMap, IssueDTO issueDTO, JiraRestClient jiraRestClient) {

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

        Issue issue = lruMap.get(issueDTO.getKey());

        if(issue !=null){
            return issue;
        }else{
            try {
                return jiraRestClient.getIssueClient().getIssue(issueDTO.getKey(), List.of(IssueRestClient.Expandos.CHANGELOG))
                        .get(PROMISE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            } catch (TimeoutException | InterruptedException | ExecutionException e) {
                if (e instanceof InterruptedException) {
                    Thread.currentThread().interrupt();
                }
                if(e.getCause() instanceof RestClientException restClientException){
                    throw restClientException;
                }
                throw new RuntimeException("Jira API 호출 오류: " + issueDTO.getKey(), e);
            }
        }
    }

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

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

        for (Issue 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 {
                IssueDTO issueDTO = IssueDTO.builder().key(key).build();
                Issue issue = issueList(lruMap, issueDTO, jiraRestClient);
                lruMap.put(issue.getKey(), issue);

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

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

                Iterable<Subtask> subtasks = issue.getSubtasks();
                if (subtasks != null) {
                    for (Subtask subtask : subtasks) {
                        String subtaskKey = subtask.getIssueKey();
                        if (!visited.contains(subtaskKey)) {
                            queue.add(subtaskKey);
                        }
                    }
                }

                Iterable<com.atlassian.jira.rest.client.api.domain.IssueLink> issueLinks = issue.getIssueLinks();
                if (issueLinks != null) {
                    for (com.atlassian.jira.rest.client.api.domain.IssueLink link : issueLinks) {
                        String targetKey = link.getTargetIssueKey();
                        if (!visited.contains(targetKey)) {
                            queue.add(targetKey);
                        }
                    }
                }

            } catch (RestClientException e) {
                com.google.common.base.Optional<Integer> statusCode = e.getStatusCode();
                if (statusCode.isPresent() && statusCode.get() == 404) {
                    log.info("exploreRelatedIssuesByFetchedIssues - not found key: {}", key);
                } else {
                    log.warn("exploreRelatedIssuesByFetchedIssues - error fetching issue key: {}", key, e);
                }
            } catch (Exception e) {
                log.warn("exploreRelatedIssuesByFetchedIssues - error fetching issue key: {}", key, e);
            }
        }
        return result;
    }

    private String getMappingCategory(ServerInfoVO serverInfoVO, String statusId) {
        return categoryMappingService.getMappingCategory(serverInfoVO,  statusId);
    }


}
