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

import com.arms.api.admin.indexdata.model.dto.NdjsonMetaDTO;
import com.arms.api.admin.indexdata.model.dto.TemplateDTO;
import com.arms.api.admin.restore.model.dto.IndexCreateParamDTO;
import com.arms.api.admin.restore.model.dto.RestoreExecutionDTO;
import com.arms.api.admin.restore.model.dto.RestoreTemplateParamDTO;
import com.arms.api.admin.restore.model.vo.RestoreVerifyResultVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.opensearch.action.index.IndexRequest;
import org.opensearch.client.Request;
import org.opensearch.client.RequestOptions;
import org.opensearch.client.Response;
import org.opensearch.client.RestHighLevelClient;
import org.opensearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

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

@Service
@Slf4j
@RequiredArgsConstructor
public class IndexRestoreAsyncServiceImpl implements IndexRestoreAsyncService {

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

    @Value("${backup.directory:/mnt}")
    private String backupDirectory;

    @Override
    @Async
    public CompletableFuture<RestoreVerifyResultVO> executeRestoreWithVerify(RestoreExecutionDTO restoreExecutionDTO) {
        String jobId = restoreExecutionDTO.getJobId();
        String backupFilePath = restoreExecutionDTO.getBackupFilePath();

        try {
            String targetIndexName = restoreExecutionDTO.getTargetIndexName();

            Path inputAsPath = Paths.get(backupFilePath);
            Path backupPath;
            if (inputAsPath.isAbsolute()) {
                backupPath = inputAsPath;
            } else {
                backupPath = Paths.get(backupDirectory, backupFilePath);
            }

            if (!Files.exists(backupPath)) {
                log.error("[복구 실패] jobId: {}, 백업 경로가 존재하지 않습니다: {}", jobId, backupPath);
                return CompletableFuture.completedFuture(RestoreVerifyResultVO.builder()
                        .backupPath(backupFilePath)
                        .backupDocCount(0)
                        .restoredDocCount(0)
                        .verified(false)
                        .message("백업 경로가 존재하지 않습니다: " + backupPath)
                        .build());
            }

            List<Path> dataFiles;
            try (Stream<Path> pathStream = Files.list(backupPath)) {
                dataFiles = pathStream
                        .filter(p -> p.getFileName().toString().matches(".*_data(_\\d+)?\\.ndjson"))
                        .sorted()
                        .toList();
            }

            if (dataFiles.isEmpty()) {
                log.error("[복구 실패] jobId: {}, 백업 데이터 파일이 없습니다: {}", jobId, backupPath);
                return CompletableFuture.completedFuture(RestoreVerifyResultVO.builder()
                        .backupPath(backupFilePath)
                        .backupDocCount(0)
                        .restoredDocCount(0)
                        .verified(false)
                        .message("백업 데이터 파일이 없습니다: " + backupPath)
                        .build());
            }

            log.info("[복구 시작] jobId: {}, 경로: {}, 파일 수: {}", jobId, backupPath, dataFiles.size());

            List<Path> templateFiles;
            try (Stream<Path> pathStream = Files.list(backupPath)) {
                templateFiles = pathStream
                        .filter(p -> p.toString().endsWith("_template.json"))
                        .toList();
            }

            List<Path> mappingSettingsFiles;
            try (Stream<Path> pathStream = Files.list(backupPath)) {
                mappingSettingsFiles = pathStream
                        .filter(p -> p.toString().endsWith("_mapping_settings.json"))
                        .toList();
            }

            if (templateFiles.isEmpty() && mappingSettingsFiles.isEmpty()) {
                log.warn("[복구 경고] jobId: {}, 템플릿/매핑 파일이 없습니다. 데이터만으로 복구를 시도합니다: {}", jobId, backupPath);
            }

            boolean useTargetIndex = targetIndexName != null && !targetIndexName.isBlank();
            Map<String, Boolean> prefixAliasMap = new HashMap<>();
            Map<String, String> prefixMappingMap = new HashMap<>();

            // 템플릿 파일 처리
            for (Path templateFile : templateFiles) {
                String templateFileName = templateFile.getFileName().toString();
                String templatePrefix = templateFileName.replace("_template.json", "");
                byte[] templateBytes = Files.readAllBytes(templateFile);
                String templateJson = new String(templateBytes, StandardCharsets.UTF_8);

                TemplateDTO templateDTO = objectMapper.readValue(templateJson, TemplateDTO.class);
                prefixAliasMap.put(templatePrefix, hasAliases(templateDTO));

                String restoreTemplateName;
                String restoreTemplateJson;
                if (useTargetIndex) {
                    restoreTemplateName = targetIndexName + "-template";
                    TemplateDTO modifiedTemplateDTO = modifyTemplateForTarget(templateDTO, targetIndexName);
                    restoreTemplateJson = objectMapper.writeValueAsString(modifiedTemplateDTO);
                } else {
                    restoreTemplateName = templatePrefix + "-template";
                    restoreTemplateJson = templateJson;
                }

                RestoreTemplateParamDTO restoreTemplateParamDTO = new RestoreTemplateParamDTO();
                restoreTemplateParamDTO.setTemplateName(restoreTemplateName);
                restoreTemplateParamDTO.setTemplateJson(restoreTemplateJson);
                restoreTemplate(restoreTemplateParamDTO);
            }

            // 매핑/설정 파일 처리
            for (Path mappingSettingsFile : mappingSettingsFiles) {
                String mappingSettingsFileName = mappingSettingsFile.getFileName().toString();
                String mappingSettingsPrefix = mappingSettingsFileName.replace("_mapping_settings.json", "");

                if (prefixAliasMap.containsKey(mappingSettingsPrefix)) {
                    continue;
                }

                byte[] mappingSettingsBytes = Files.readAllBytes(mappingSettingsFile);
                String mappingSettingsJson = new String(mappingSettingsBytes, StandardCharsets.UTF_8);
                prefixMappingMap.put(mappingSettingsPrefix, mappingSettingsJson);
                prefixAliasMap.put(mappingSettingsPrefix, false);
                log.info("[매핑/설정 로드] prefix: {}", mappingSettingsPrefix);
            }

            String currentDate = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);

            // 데이터 복구
            long totalProcessed = 0L;
            for (Path dataFile : dataFiles) {
                String fileName = dataFile.getFileName().toString();
                String originalIndexName = fileName.replaceAll("_data(_\\d+)?\\.ndjson$", "");
                String prefix = extractIndexPrefix(originalIndexName);

                String restoreIndexName;
                if (useTargetIndex) {
                    boolean hasAlias = prefixAliasMap.getOrDefault(prefix, false);
                    if (hasAlias) {
                        restoreIndexName = targetIndexName + "-" + currentDate;
                    } else {
                        restoreIndexName = targetIndexName;
                    }
                } else {
                    restoreIndexName = originalIndexName;
                }

                if (prefixMappingMap.containsKey(prefix)) {
                    IndexCreateParamDTO indexCreateParamDTO = new IndexCreateParamDTO();
                    indexCreateParamDTO.setIndexName(restoreIndexName);
                    indexCreateParamDTO.setMappingSettingsJson(prefixMappingMap.get(prefix));
                    createIndexWithMappingSettings(indexCreateParamDTO);
                }

                long beforeCount = totalProcessed;
                totalProcessed = restoreIndexData(restoreIndexName, dataFile, totalProcessed);
                log.info("[복구 진행] jobId: {}, file: {}, 처리된 문서 수: {}",
                        jobId, fileName, totalProcessed - beforeCount);
            }

            log.info("[복구 완료] jobId: {}, 총 처리 문서 수: {}", jobId, totalProcessed);

            // 검증
            return CompletableFuture.completedFuture(verifyRestore(restoreExecutionDTO, dataFiles, totalProcessed));

        } catch (Exception e) {
            log.error("[복구 실패] jobId: {}, error: {}", jobId, e.getMessage(), e);
            return CompletableFuture.completedFuture(RestoreVerifyResultVO.builder()
                    .backupPath(backupFilePath)
                    .backupDocCount(0)
                    .restoredDocCount(0)
                    .verified(false)
                    .message("복구 실패: " + e.getMessage())
                    .build());
        }
    }

    private long restoreIndexData(String targetIndex, Path dataFile, long totalProcessed) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(dataFile, StandardCharsets.UTF_8)) {
            String metaLine;
            while ((metaLine = reader.readLine()) != null) {
                String dataLine = reader.readLine();
                if (dataLine == null) break;

                NdjsonMetaDTO ndjsonMetaDTO = objectMapper.readValue(metaLine, NdjsonMetaDTO.class);
                String docId = ndjsonMetaDTO.getIndexMeta().getId();

                IndexRequest indexRequest = new IndexRequest(targetIndex)
                        .id(docId)
                        .source(dataLine, XContentType.JSON);

                restHighLevelClient.index(indexRequest, largeBufferRequestOptions);
                totalProcessed++;
            }
        }
        return totalProcessed;
    }

    private RestoreVerifyResultVO verifyRestore(RestoreExecutionDTO restoreExecutionDTO, List<Path> dataFiles, long restoredDocCount) throws IOException {
        long backupDocCount = countBackupDocuments(dataFiles);
        boolean verified = (backupDocCount == restoredDocCount);

        String message;
        if (verified) {
            message = "검증 성공: 백업 문서 수와 복구된 문서 수가 일치합니다.";
        } else {
            message = "검증 실패: 백업 문서 수(" + backupDocCount + ")와 복구된 문서 수(" + restoredDocCount + ")가 일치하지 않습니다.";
        }

        return RestoreVerifyResultVO.builder()
                .backupPath(restoreExecutionDTO.getBackupFilePath())
                .backupDocCount(backupDocCount)
                .restoredDocCount(restoredDocCount)
                .verified(verified)
                .message(message)
                .build();
    }

    private long countBackupDocuments(List<Path> dataFiles) throws IOException {
        long totalDocs = 0L;
        for (Path file : dataFiles) {
            try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
                long lineCount = lines.count();
                totalDocs += lineCount / 2;
            }
        }
        return totalDocs;
    }

    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 restoreTemplate(RestoreTemplateParamDTO restoreTemplateParamDTO) {
        String templateName = restoreTemplateParamDTO.getTemplateName();
        String templateJson = restoreTemplateParamDTO.getTemplateJson();

        Request request = new Request("PUT", "/_template/" + templateName);
        request.setJsonEntity(templateJson);
        try {
            restHighLevelClient.getLowLevelClient().performRequest(request);
            log.info("[템플릿 복구] templateName: {}", templateName);
        } catch (Exception e) {
            log.warn("템플릿 복구 실패 (이미 존재할 수 있음): {} - {}", templateName, e.getMessage());
        }
    }

    private void createIndexWithMappingSettings(IndexCreateParamDTO indexCreateParamDTO) {
        String indexName = indexCreateParamDTO.getIndexName();
        String mappingSettingsJson = indexCreateParamDTO.getMappingSettingsJson();

        try {
            Request headRequest = new Request("HEAD", "/" + indexName);
            Response headResponse = restHighLevelClient.getLowLevelClient().performRequest(headRequest);
            if (headResponse.getStatusLine().getStatusCode() == 200) {
                log.debug("[인덱스 존재] indexName: {} - 이미 존재하여 생성 스킵", indexName);
                return;
            }
        } catch (Exception e) {
            log.debug("[인덱스 확인] indexName: {} - 존재하지 않음, 생성 진행", indexName);
        }

        try {
            Request createRequest = new Request("PUT", "/" + indexName);
            createRequest.setJsonEntity(mappingSettingsJson);
            restHighLevelClient.getLowLevelClient().performRequest(createRequest);
            log.info("[인덱스 생성] indexName: {} (mapping/settings 적용)", indexName);
        } catch (Exception e) {
            log.warn("[인덱스 생성 실패] indexName: {}, error: {}", indexName, e.getMessage());
        }
    }

    private boolean hasAliases(TemplateDTO templateDTO) {
        Map<String, Object> aliases = templateDTO.getAliases();
        return aliases != null && !aliases.isEmpty();
    }

    private TemplateDTO modifyTemplateForTarget(TemplateDTO templateDTO, String targetIndexName) {
        TemplateDTO modifiedTemplateDTO = new TemplateDTO();
        modifiedTemplateDTO.setIndexPatterns(List.of(targetIndexName + "-*"));
        modifiedTemplateDTO.setMappings(templateDTO.getMappings());
        modifiedTemplateDTO.setSettings(templateDTO.getSettings());
        modifiedTemplateDTO.setOrder(templateDTO.getOrder());
        modifiedTemplateDTO.setVersion(templateDTO.getVersion());

        Map<String, Object> oldAliases = templateDTO.getAliases();
        if (oldAliases != null && !oldAliases.isEmpty()) {
            Object firstAliasConfig = oldAliases.values().iterator().next();
            Map<String, Object> newAliases = new HashMap<>();
            newAliases.put(targetIndexName, Optional.ofNullable(firstAliasConfig).orElse(Map.of()));
            modifiedTemplateDTO.setAliases(newAliases);
        } else {
            modifiedTemplateDTO.setAliases(templateDTO.getAliases());
        }

        return modifiedTemplateDTO;
    }
}