package com.arms.api.admin.backup.service;

import com.arms.api.admin.indexdata.model.dto.NdjsonMetaDTO;
import com.arms.api.admin.indexdata.model.dto.TemplateDTO;
import com.arms.api.admin.backup.model.dto.BackupExecutionDTO;
import com.arms.api.admin.backup.model.vo.BackupSummaryVO;
import com.arms.api.admin.backup.model.vo.BackupVerifyResultVO;
import com.arms.api.admin.backup.model.dto.IndexInfoDTO;
import com.arms.api.admin.backup.model.dto.SaveTemplateParamDTO;
import com.arms.egovframework.javaservice.esframework.model.vo.CatIndexVO;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.util.EntityUtils;
import org.opensearch.action.search.ClearScrollRequest;
import org.opensearch.action.search.SearchRequest;
import org.opensearch.action.search.SearchResponse;
import org.opensearch.action.search.SearchScrollRequest;
import org.opensearch.client.Request;
import org.opensearch.client.RequestOptions;
import org.opensearch.client.Response;
import org.opensearch.client.RestHighLevelClient;
import org.opensearch.common.unit.TimeValue;
import org.opensearch.index.query.QueryBuilders;
import org.opensearch.search.SearchHit;
import org.opensearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;

@Service
@Slf4j
@RequiredArgsConstructor
public class IndexBackupAsyncServiceImpl implements IndexBackupAsyncService {

    private final RestHighLevelClient restHighLevelClient;
    private final ObjectMapper objectMapper;
    private final RequestOptions largeBufferRequestOptions;

    @Value("${backup.batch-size:100}")
    private int batchSize;

    private static final long DEFAULT_SCROLL_TIMEOUT = 60000L;

    @Override
    @Async
    public CompletableFuture<BackupVerifyResultVO> executeBackupWithVerify(BackupExecutionDTO backupExecutionDTO) {
        String jobId = backupExecutionDTO.getJobId();
        Path outputPath = backupExecutionDTO.getOutputPath();

        try {
            // 인덱스 목록 조회
            List<CatIndexVO> indices = getMatchingIndices(
                    backupExecutionDTO.getIndexPattern(),
                    backupExecutionDTO.isIncludeCurrentMonth(),
                    backupExecutionDTO.isIncludeMergedIndex()
            );

            if (indices.isEmpty()) {
                log.error("[백업 실패] jobId: {}, 매칭되는 인덱스가 없습니다: {}", jobId, backupExecutionDTO.getIndexPattern());
                return CompletableFuture.completedFuture(BackupVerifyResultVO.builder()
                        .backupPath(outputPath.toString())
                        .verified(false)
                        .message("매칭되는 인덱스가 없습니다: " + backupExecutionDTO.getIndexPattern())
                        .build());
            }

            Files.createDirectories(outputPath);

            long totalDocs = indices.stream()
                    .mapToLong(catIndexVO -> Optional.ofNullable(catIndexVO.getDocsCount()).orElse(0L))
                    .sum();

            log.info("[백업 시작] jobId: {}, 인덱스 수: {}, 총 문서 수: {}, 경로: {}",
                    jobId, indices.size(), totalDocs, outputPath);

            // prefix별 샘플 인덱스 매핑
            Map<String, String> prefixToSampleIndex = new LinkedHashMap<>();
            for (CatIndexVO catIndexVO : indices) {
                String indexName = catIndexVO.getIndex();
                String prefix = extractIndexPrefix(indexName);
                prefixToSampleIndex.putIfAbsent(prefix, indexName);
            }

            // 템플릿 또는 매핑 백업
            for (Map.Entry<String, String> entry : prefixToSampleIndex.entrySet()) {
                SaveTemplateParamDTO saveTemplateParamDTO = new SaveTemplateParamDTO();
                saveTemplateParamDTO.setPrefix(entry.getKey());
                saveTemplateParamDTO.setIndexName(entry.getValue());
                saveTemplateOrMapping(backupExecutionDTO, saveTemplateParamDTO);
            }

            // 데이터 백업
            long processedDocs = 0L;
            for (CatIndexVO catIndexVO : indices) {
                String indexName = catIndexVO.getIndex();
                processedDocs = backupIndexData(backupExecutionDTO, indexName, processedDocs);
                log.info("[백업 진행] jobId: {}, indexName: {}, 처리된 문서 수: {}/{}",
                        jobId, indexName, processedDocs, totalDocs);
            }

            // 백업 요약 저장
            saveBackupSummary(backupExecutionDTO, indices, processedDocs);

            log.info("[백업 완료] jobId: {}, 경로: {}, 총 처리 문서 수: {}", jobId, outputPath, processedDocs);

            // 검증
            return CompletableFuture.completedFuture(verifyBackup(backupExecutionDTO, indices, totalDocs));

        } catch (Exception e) {
            log.error("[백업 실패] jobId: {}, error: {}", jobId, e.getMessage(), e);
            return CompletableFuture.completedFuture(BackupVerifyResultVO.builder()
                    .backupPath(outputPath.toString())
                    .verified(false)
                    .message("백업 실패: " + e.getMessage())
                    .build());
        }
    }

    private List<CatIndexVO> getMatchingIndices(String pattern, boolean includeCurrentMonth, boolean includeMergedIndex) {
        try {
            Request request = new Request("GET", "/_cat/indices/" + pattern + "?format=json");
            Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
            String responseBody = EntityUtils.toString(response.getEntity());
            List<CatIndexVO> indices = objectMapper.readValue(responseBody, new TypeReference<>() {});

            Stream<CatIndexVO> stream = indices.stream();

            // merge된 인덱스(-00) 포함 여부에 따라 필터링
            if (!includeMergedIndex) {
                stream = stream.filter(catIndexVO -> !catIndexVO.getIndex().matches(".*-\\d{4}-\\d{2}-00$"));
            }

            // 현재월 포함 여부에 따라 필터링
            if (!includeCurrentMonth) {
                String currentMonth = java.time.LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
                stream = stream.filter(catIndexVO -> !catIndexVO.getIndex().contains("-" + currentMonth + "-"));
            }

            return stream.toList();
        } catch (Exception e) {
            log.error("인덱스 목록 조회 실패 - pattern: {}, error: {}", pattern, e.getMessage());
            return Collections.emptyList();
        }
    }

    private String extractIndexPrefix(String indexName) {
        if (indexName.matches(".*-\\d{4}-\\d{2}-\\d{2}$")) {
            return indexName.substring(0, indexName.length() - 11);
        }
        return indexName;
    }

    private void saveTemplateOrMapping(BackupExecutionDTO backupExecutionDTO, SaveTemplateParamDTO saveTemplateParamDTO) {
        if (saveTemplate(backupExecutionDTO, saveTemplateParamDTO)) {
            return;
        }
        saveMappingSettings(backupExecutionDTO, saveTemplateParamDTO);
    }

    private boolean saveTemplate(BackupExecutionDTO backupExecutionDTO, SaveTemplateParamDTO saveTemplateParamDTO) {
        Path outputPath = backupExecutionDTO.getOutputPath();
        String prefix = saveTemplateParamDTO.getPrefix();

        List<String> templateNames = List.of(prefix, prefix + "-template");

        for (String templateName : templateNames) {
            try {
                Request request = new Request("GET", "/_template/" + templateName);
                Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
                String templateJson = EntityUtils.toString(response.getEntity());

                Map<String, TemplateDTO> fullResponse = objectMapper.readValue(templateJson, new TypeReference<>() {});

                if (!fullResponse.isEmpty()) {
                    TemplateDTO templateBody = fullResponse.get(templateName);
                    String templateBodyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(templateBody);

                    Path templateFile = outputPath.resolve(prefix + "_template.json");
                    Files.writeString(templateFile, templateBodyJson, StandardCharsets.UTF_8);
                    log.info("[템플릿 저장] templateName: {}, file: {}", templateName, templateFile);
                    return true;
                }
            } catch (Exception e) {
                log.debug("템플릿 조회 실패: {} - {}", templateName, e.getMessage());
            }
        }
        log.info("[템플릿 없음] 템플릿을 찾을 수 없어 mapping을 백업합니다: prefix={}", prefix);
        return false;
    }

    private void saveMappingSettings(BackupExecutionDTO backupExecutionDTO, SaveTemplateParamDTO saveTemplateParamDTO) {
        Path outputPath = backupExecutionDTO.getOutputPath();
        String prefix = saveTemplateParamDTO.getPrefix();
        String indexName = saveTemplateParamDTO.getIndexName();

        try {
            Request request = new Request("GET", "/" + indexName);
            Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
            String responseJson = EntityUtils.toString(response.getEntity());

            Map<String, IndexInfoDTO> fullResponse = objectMapper.readValue(responseJson, new TypeReference<>() {});

            if (fullResponse.containsKey(indexName)) {
                IndexInfoDTO indexInfo = fullResponse.get(indexName);

                Map<String, Object> mappings = indexInfo.getMappings();
                Map<String, Object> settings = Optional.ofNullable(indexInfo.getSettingsWrapper())
                        .map(IndexInfoDTO.SettingsWrapper::getIndex)
                        .orElse(null);

                IndexInfoDTO indexInfoDTO = new IndexInfoDTO();
                indexInfoDTO.setAliases(Map.of());

                if (settings != null) {
                    Map<String, Object> cleanSettings = cleanIndexSettings(settings);
                    IndexInfoDTO.SettingsWrapper settingsWrapper = new IndexInfoDTO.SettingsWrapper();
                    settingsWrapper.setIndex(cleanSettings);
                    indexInfoDTO.setSettingsWrapper(settingsWrapper);
                }
                if (mappings != null) {
                    indexInfoDTO.setMappings(mappings);
                }

                String mappingSettingsJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(indexInfoDTO);
                Path mappingSettingsFile = outputPath.resolve(prefix + "_mapping_settings.json");
                Files.writeString(mappingSettingsFile, mappingSettingsJson, StandardCharsets.UTF_8);
                log.info("[매핑/설정 저장] indexName: {}, file: {}", indexName, mappingSettingsFile);
            }
        } catch (Exception e) {
            log.error("[매핑/설정 저장 실패] indexName: {}, error: {}", indexName, e.getMessage());
        }
    }

    private Map<String, Object> cleanIndexSettings(Map<String, Object> settings) {
        Map<String, Object> cleaned = new LinkedHashMap<>(settings);
        cleaned.remove("creation_date");
        cleaned.remove("uuid");
        cleaned.remove("version");
        cleaned.remove("provided_name");
        cleaned.remove("resize");
        cleaned.remove("routing");
        return cleaned;
    }

    private long backupIndexData(BackupExecutionDTO backupExecutionDTO, String indexName, long processedDocs) throws IOException {
        Path outputPath = backupExecutionDTO.getOutputPath();
        long scrollTimeout = Optional.ofNullable(backupExecutionDTO.getScrollTimeout()).orElse(DEFAULT_SCROLL_TIMEOUT);

        log.info("배치 요청 제한 사이즈 :{}", batchSize);

        String safeIndexName = indexName.replace("*", "_");
        int fileIndex = 0;

        SearchRequest searchRequest = new SearchRequest(indexName);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchAllQuery());
        sourceBuilder.size(batchSize);
        searchRequest.source(sourceBuilder);
        searchRequest.scroll(TimeValue.timeValueMillis(scrollTimeout));

        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, largeBufferRequestOptions);
        String scrollId = searchResponse.getScrollId();
        SearchHit[] hits = searchResponse.getHits().getHits();

        while (hits != null && hits.length > 0) {
            Path dataFile = outputPath.resolve(String.format("%s_data_%04d.ndjson", safeIndexName, fileIndex++));

            try (BufferedWriter writer = Files.newBufferedWriter(dataFile, StandardCharsets.UTF_8)) {
                for (SearchHit searchHit : hits) {
                    NdjsonMetaDTO.IndexMeta indexMeta = new NdjsonMetaDTO.IndexMeta();
                    indexMeta.setIndexName(searchHit.getIndex());
                    indexMeta.setId(searchHit.getId());

                    NdjsonMetaDTO ndjsonMetaDTO = new NdjsonMetaDTO();
                    ndjsonMetaDTO.setIndexMeta(indexMeta);

                    writer.write(objectMapper.writeValueAsString(ndjsonMetaDTO));
                    writer.newLine();
                    writer.write(searchHit.getSourceAsString());
                    writer.newLine();

                    processedDocs++;
                }
            }

            SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
            scrollRequest.scroll(TimeValue.timeValueMillis(scrollTimeout));
            searchResponse = restHighLevelClient.scroll(scrollRequest, largeBufferRequestOptions);
            scrollId = searchResponse.getScrollId();
            hits = searchResponse.getHits().getHits();
        }

        ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
        clearScrollRequest.addScrollId(scrollId);
        restHighLevelClient.clearScroll(clearScrollRequest, largeBufferRequestOptions);

        return processedDocs;
    }

    private void saveBackupSummary(BackupExecutionDTO backupExecutionDTO, List<CatIndexVO> indices, long totalDocs) {
        try {
            Path outputPath = backupExecutionDTO.getOutputPath();
            Path summaryFile = outputPath.resolve("backup_summary.json");

            List<BackupSummaryVO.BackupIndexInfoVO> backupIndexInfoVOList = indices.stream()
                    .map(catIndexVO -> BackupSummaryVO.BackupIndexInfoVO.builder()
                            .indexName(catIndexVO.getIndex())
                            .docsCount(Optional.ofNullable(catIndexVO.getDocsCount()).orElse(0L))
                            .storeSize(catIndexVO.getStoreSize())
                            .build())
                    .toList();

            BackupSummaryVO backupSummaryVO = BackupSummaryVO.builder()
                    .jobId(backupExecutionDTO.getJobId())
                    .backupTime(java.time.LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                    .backupPath(outputPath.toAbsolutePath().toString())
                    .indexCount(indices.size())
                    .totalDocs(totalDocs)
                    .backupIndexInfoVOList(backupIndexInfoVOList)
                    .build();

            String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(backupSummaryVO);
            Files.writeString(summaryFile, json, StandardCharsets.UTF_8);
            log.info("[백업 요약 저장] file: {}", summaryFile);

        } catch (Exception e) {
            log.warn("[백업 요약 저장 실패] error: {}", e.getMessage());
            throw new RuntimeException(e);
        }
    }

    private BackupVerifyResultVO verifyBackup(BackupExecutionDTO backupExecutionDTO, List<CatIndexVO> indices, long sourceDocCount) throws IOException {
        Path outputPath = backupExecutionDTO.getOutputPath();
        long backupDocCount = countBackupDocuments(outputPath);
        boolean verified = (backupDocCount == sourceDocCount);

        List<String> backedUpIndices = indices.stream()
                .map(CatIndexVO::getIndex)
                .toList();

        String message;
        if (verified) {
            message = String.format("검증 성공 - 백업: %d, 원본: %d", backupDocCount, sourceDocCount);
            log.info("[백업 검증 성공] jobId: {}, {}", backupExecutionDTO.getJobId(), message);
        } else {
            message = String.format("검증 실패 - 백업: %d, 원본: %d, 차이: %d",
                    backupDocCount, sourceDocCount, Math.abs(backupDocCount - sourceDocCount));
            log.warn("[백업 검증 실패] jobId: {}, {}", backupExecutionDTO.getJobId(), message);
        }

        return BackupVerifyResultVO.builder()
                .backupPath(outputPath.toString())
                .backupDocCount(backupDocCount)
                .sourceDocCount(sourceDocCount)
                .verified(verified)
                .message(message)
                .backedUpIndices(backedUpIndices)
                .build();
    }

    private long countBackupDocuments(Path backupPath) throws IOException {
        if (!Files.exists(backupPath) || !Files.isDirectory(backupPath)) {
            return 0L;
        }

        long totalDocs = 0L;
        try (Stream<Path> files = Files.list(backupPath)) {
            List<Path> ndjsonFiles = files
                    .filter(p -> p.toString().endsWith(".ndjson"))
                    .toList();

            for (Path file : ndjsonFiles) {
                try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
                    long lineCount = lines.count();
                    totalDocs += lineCount / 2;
                }
            }
        }
        return totalDocs;
    }
}