package com.arms.api.fluentd.service;

import com.arms.api.fluentd.model.dto.ScheduleHistoryDTO;
import com.arms.api.fluentd.model.vo.ScheduleHistoryVO;
import com.arms.api.fluentd.model.vo.ScheduleLogVO;
import com.arms.api.search_engine.model.dto.BaseSearchAggrDTO;
import com.arms.api.search_engine.model.dto.SearchDTO;
import com.arms.api.search_engine.model.vo.SearchAggrResultVO;
import com.arms.api.search_engine.model.vo.SearchDocFieldNameAndCount;
import com.arms.api.search_engine.model.vo.SearchResultVO;
import com.arms.api.fluentd.model.entity.FluentdEntity;
import com.arms.egovframework.javaservice.esframework.esquery.must.QueryStringMust;
import com.arms.egovframework.javaservice.esframework.model.dto.request.SearchRequestDTO;
import com.arms.egovframework.javaservice.esframework.model.dto.esquery.SortDTO;
import com.arms.egovframework.javaservice.esframework.esquery.filter.RangeQueryFilter;

import com.arms.egovframework.javaservice.esframework.model.dto.request.AggregationRequestDTO;
import com.arms.egovframework.javaservice.esframework.model.vo.DocumentBucket;
import com.arms.egovframework.javaservice.esframework.model.vo.DocumentResultWrapper;
import com.arms.egovframework.javaservice.esframework.model.vo.DocumentAggregations;

import com.arms.egovframework.javaservice.esframework.repository.common.EsCommonRepositoryWrapper;
import com.arms.egovframework.javaservice.esframework.esquery.SimpleQuery;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.opensearch.OpenSearchException;
import org.opensearch.OpenSearchStatusException;
import org.opensearch.index.query.Operator;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.arms.egovframework.javaservice.esframework.esquery.highlight.EsHighlightQuery.esHighlightAll;


@Slf4j
@Service("fluentdSearch")
@AllArgsConstructor
public class FluentdServiceImpl implements FluentdService {

    private final EsCommonRepositoryWrapper<FluentdEntity> esCommonRepositoryWrapper;

    @Override
    public SearchAggrResultVO aggregateByFluentdLogName(BaseSearchAggrDTO searchAggrDTO) {

        String startDate = convertTimeFormatOfFluentdLog(searchAggrDTO.getStartDate());
        String endDate = convertTimeFormatOfFluentdLog(searchAggrDTO.getEndDate());

        try {
            DocumentAggregations documentAggregations = esCommonRepositoryWrapper.aggregate(

                    SimpleQuery.aggregation(
                            AggregationRequestDTO.builder()
                                    .size(5)
                                    .mainField("@log_name")
                                    .mainFieldAlias("@log_name")
                                    .isAscending(false)
                                .build()
                    )
                    .andQueryStringFilter(searchAggrDTO.getSearchString())
                    .andRangeQueryFilter(RangeQueryFilter.of("@timestamp").betweenDate(startDate, endDate))
            );

            List<DocumentBucket> documentBuckets = documentAggregations.deepestList();
            List<SearchDocFieldNameAndCount> list = documentBuckets.stream()
                    .map(documentBucket -> SearchDocFieldNameAndCount
                            .builder()
                            .docFieldName(documentBucket.valueByName("@log_name"))
                            .docCount(documentBucket.countByName("@log_name"))
                            .build())
                    .toList();

            return SearchAggrResultVO.builder()
                    .totalHits(documentAggregations.getTotalHits())
                    .docFieldNameAndCounts(list)
                    .build();
        } catch (OpenSearchException e) {
            if (isIllegalArgumentException(e)) {
                log.warn("[ FluentdSearchImpl ::aggregateByFluentdLogName ] Aggregation failed due to illegal_argument_exception (text field aggregation etc). Returning empty VO.");
                return SearchAggrResultVO.builder()
                        .totalHits(0L)
                        .docFieldNameAndCounts(Collections.emptyList())
                        .build();
            }

            throw e; // 그 외 예외는 그대로 throw
        }

    }

    private boolean isIllegalArgumentException(Throwable t) {
        Throwable cause = t;
        while (cause != null) {
            if (cause instanceof OpenSearchStatusException || cause instanceof OpenSearchException) {
                String message = cause.getMessage();
                if (message != null && message.contains("illegal_argument_exception")) {
                    return true;
                }
            }
            cause = cause.getCause();
        }
        return false;
    }


    @Override
    public SearchResultVO<FluentdEntity> searchFluentdLog(SearchDTO searchDTO) {

        String startDate = convertTimeFormatOfFluentdLog(searchDTO.getStartDate());
        String endDate = convertTimeFormatOfFluentdLog(searchDTO.getEndDate());

        String filterFieldName = getFilterFieldName(searchDTO.getFilter().getName());

        DocumentResultWrapper<FluentdEntity> hits = esCommonRepositoryWrapper.findHits(
                SimpleQuery
                        .search(searchDTO)
                        .andQueryStringFilter(searchDTO.getSearchString())
                        .andTermsQueryFilter(filterFieldName, searchDTO.getFilter().getValues())
                        .andRangeQueryFilter(RangeQueryFilter.of("@timestamp").betweenDate(startDate, endDate))
                        .orderBy(SortDTO.builder().field("@timestamp").sortType("desc").build())
                        .highlight(esHighlightAll())

        );

        return SearchResultVO.<FluentdEntity>builder()
                .totalResultDocs(hits.getTotalHits())
                .searchResultDocs(hits.toHitDocs())
                .build();
    }


    @Override
    public void keepAliveConnection(){
        log.info("keepAliveConnection");
        SearchRequestDTO searchRequestDTO = new SearchRequestDTO();
        searchRequestDTO.setSize(1);
        esCommonRepositoryWrapper.findHits(SimpleQuery.search(searchRequestDTO).andTermQueryFilter("recent_id", "313"));
    }

    private String getFilterFieldName(String filterName) {
        if (filterName != null && !filterName.isBlank() && filterName.equals("log")) {
            return "@log_name";
        }
        return null;
    }

    private String convertTimeFormatOfFluentdLog(String inputTime) {
        if (inputTime != null && !inputTime.isEmpty()) {
            LocalDateTime localDateTime  = LocalDateTime.parse(inputTime, DateTimeFormatter.ISO_DATE_TIME);

            ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneOffset.ofHours(0));

            return zonedDateTime.format(DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSSZZZZZ"));
        } else {
            return null;
        }

    }

    @Override
    public List<ScheduleHistoryVO> getScheduleHistory(SearchDTO searchDTO) {
        String scheduleName = searchDTO.getSearchString();
        String queryStringWithStartedAt = scheduleName + " started at";
        String queryStringWithFinishedAt = scheduleName + " finished at";

        SearchDTO searchDTOForStartAt = new SearchDTO();
        searchDTOForStartAt.setSearchString(queryStringWithStartedAt);

        SearchDTO searchDTOForFinishedAt = new SearchDTO();
        searchDTOForFinishedAt.setSearchString(queryStringWithFinishedAt);

        // 1. started at | finished at 검색
        List<FluentdEntity> scheduleStartedAtLogs = this.getScheduleLog(searchDTOForStartAt);
        List<FluentdEntity> scheduleFinishedAtLogs = this.getScheduleLog(searchDTOForFinishedAt);

        // 2. parsing to VO list (List<ScheduleLogVO>)
        List<ScheduleLogVO> startedAtLogs = parseScheduleLogs(scheduleName, scheduleStartedAtLogs, "START");
        List<ScheduleLogVO> finishedAtLogs = parseScheduleLogs(scheduleName, scheduleFinishedAtLogs, "FINISH");

        // 3. threadName 비교해서 Merge 그리고 ScheduleHistoryVO 생성
        return mergeToScheduleHistory(startedAtLogs, finishedAtLogs);
    }

    // 3. threadName 비교해서 Merge -> ScheduleHistoryVO 생성.
    private List<ScheduleHistoryVO> mergeToScheduleHistory(List<ScheduleLogVO> startedLogs, List<ScheduleLogVO> finishedLogs) {

        Map<String, ScheduleHistoryDTO> historyDTOMap = new HashMap<>();

        for (ScheduleLogVO startedLog : startedLogs) {
            if (startedLog.getThreadId() == null) {
                log.warn("mergeToScheduleHistory.startedLog(scheduleName: {}, started at {}) :: threadId is null. Skip and continue",
                        startedLog.getScheduleName(), startedLog.getEventTime());
                continue;
            }
            String threadId = startedLog.getThreadId();

            historyDTOMap.compute(threadId, (existingKey, existingDTO) -> {
                if (existingDTO == null) {
                    return ScheduleHistoryDTO.builder()
                            .threadId(threadId)
                            .threadName(startedLog.getThreadName())
                            .scheduleName(startedLog.getScheduleName())
                            .startedAt(Optional.ofNullable(startedLog.getEventTime()).orElse("")).build();
                } else {
                    existingDTO.setStartedAt(startedLog.getEventTime());
                    return existingDTO;
                }
            });
        }
        for (ScheduleLogVO finishedLog : finishedLogs) {
            if (finishedLog.getThreadId() == null) {
                log.warn("mergeToScheduleHistory.startedLog(scheduleName: {}, finished at {}) :: threadId is null. Skip and continue",
                        finishedLog.getScheduleName(), finishedLog.getEventTime());
                continue;
            }
            String threadId = finishedLog.getThreadId();

            historyDTOMap.compute(threadId, (existingKey, existingDTO) -> {
                if (existingDTO == null) {
                    return ScheduleHistoryDTO.builder()
                            .threadId(threadId)
                            .threadName(finishedLog.getThreadName())
                            .scheduleName(finishedLog.getScheduleName())
                            .finishedAt(Optional.ofNullable(finishedLog.getEventTime()).orElse(""))
                            .elapsedTime(Optional.ofNullable(finishedLog.getElapsedTime()).orElse("")).build();
                } else {
                    existingDTO.setFinishedAt(Optional.ofNullable(finishedLog.getEventTime()).orElse(""));
                    existingDTO.setElapsedTime(Optional.ofNullable(finishedLog.getElapsedTime()).orElse(""));
                    return existingDTO;
                }
            });
        }

        return convertDtoMapToVoList(historyDTOMap);
    }

    // 2. parsing 해서 VO로 만듦. (List<ScheduleLogVO>)
    private List<ScheduleLogVO> parseScheduleLogs(String scheduleName, List<FluentdEntity> scheduleLogs, String eventType) {
        List<ScheduleLogVO> logVOList = new ArrayList<>();

        if (scheduleLogs == null || scheduleLogs.isEmpty()) {
            log.info("[FluentdServiceImpl :: parseScheduleLogs] :: scheduleName => {}, scheduleLogs is null or empty", scheduleName);
            return Collections.emptyList();
        }


        for (FluentdEntity scheduleLog : scheduleLogs) {
            String logString = scheduleLog.getLog();
            if (logString == null || logString.isBlank()) {
                log.warn("A parseScheduleLog.logString (from scheduleLogs) is empty. skip and continue");
                continue;
            }

            ScheduleLogVO.ScheduleLogVOBuilder builder = ScheduleLogVO.builder();
            builder.scheduleName(scheduleName);

            // Thread name
            extractGroup(logString, "\\[([^\\]]+)]\\sINFO", 1)
                    .ifPresentOrElse(builder::threadName, () -> log.info("Thread name not found."));

            // Thread UUID
            extractGroup(logString, "- \\[([a-fA-F0-9]{8})\\] ::", 1)
                    .ifPresentOrElse(builder::threadId, () -> log.info("Thread UUID not found."));


            if ("START".equalsIgnoreCase(eventType)) {
                builder.eventType("START");

                extractGroup(logString, "started at (\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})", 1)
                        .ifPresentOrElse(builder::eventTime, () -> log.info("Start time not found."));
            } else if ("FINISH".equalsIgnoreCase(eventType)) {
                builder.eventType("FINISH");

                extractGroup(logString, "finished at (\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})", 1)
                        .ifPresentOrElse(builder::eventTime, () -> log.info("Finish time not found."));

                extractGroup(logString, "completed in (\\d+) ms", 1)
                        .ifPresentOrElse(builder::elapsedTime, () -> log.info("Elapsed time not found."));
            } else {
                log.warn("Unknown event type '{}'. Skipping this log entry.", eventType);
                continue;
            }

            logVOList.add(builder.build());
        }

        return logVOList;
    }

    // 1. started at | finished at 검색
    private List<FluentdEntity> getScheduleLog(SearchDTO searchDTO) {
        String startDate = convertTimeFormatOfFluentdLog(searchDTO.getStartDate());
        String endDate = convertTimeFormatOfFluentdLog(searchDTO.getEndDate());

        DocumentResultWrapper<FluentdEntity> hits = esCommonRepositoryWrapper.findHits(
                SimpleQuery.search(searchDTO)
                    .andQueryStringMust(QueryStringMust.of(searchDTO.getSearchString()).operator(Operator.AND))
                    .andRangeQueryFilter(RangeQueryFilter.of("@timestamp").betweenDate(startDate, endDate))
                    .orderBy(SortDTO.builder().field("@timestamp").sortType("desc").build())
        );
        return hits.toDocs();
    }

    private Optional<String> extractGroup(String input, String regex, int group) {
        Matcher matcher = Pattern.compile(regex).matcher(input);
        return matcher.find() ? Optional.ofNullable(matcher.group(group)) : Optional.empty();
    }

    private List<ScheduleHistoryVO> convertDtoMapToVoList(Map<String, ScheduleHistoryDTO> historyDTOMap) {
        if (historyDTOMap == null || historyDTOMap.isEmpty()) {
            return new ArrayList<>();
        }

        return historyDTOMap.values().stream()
                .map(dto ->
                        new ScheduleHistoryVO(
                            dto.getThreadId(),
                            dto.getThreadName(),
                            dto.getScheduleName(),
                            dto.getStartedAt(),
                            dto.getFinishedAt(),
                            dto.getElapsedTime())
                )
                .collect(Collectors.toList());
    }
}
