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

import com.arms.api.admin.monitoring.model.vo.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.util.EntityUtils;
import org.opensearch.client.Request;
import org.opensearch.client.Response;
import org.opensearch.client.RestHighLevelClient;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class EsMonitoringServiceImpl implements EsMonitoringService {

    private final RestHighLevelClient restHighLevelClient;
    private final ObjectMapper objectMapper;

    // 장애 판단 기준값
    private static final int HEAP_WARNING_THRESHOLD = 75;
    private static final int HEAP_CRITICAL_THRESHOLD = 85;
    private static final int DISK_WARNING_THRESHOLD = 85;
    private static final int DISK_CRITICAL_THRESHOLD = 90;
    private static final int CPU_CRITICAL_THRESHOLD = 90;

    @Override
    public ClusterHealthVO getClusterHealth() {
        try {
            Request request = new Request("GET", "/_cluster/health");
            Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
            String responseBody = EntityUtils.toString(response.getEntity());
            JsonNode json = objectMapper.readTree(responseBody);

            String status = json.path("status").asText();
            boolean isHealthy = !"red".equalsIgnoreCase(status);

            int unassignedShards = json.path("unassigned_shards").asInt(0);
            int pendingTasks = json.path("number_of_pending_tasks").asInt(0);

            List<String> messages = new ArrayList<>();
            if ("red".equalsIgnoreCase(status)) {
                messages.add("CRITICAL: Cluster is RED");
            } else if ("yellow".equalsIgnoreCase(status)) {
                messages.add("WARNING: Cluster is YELLOW");
            }
            if (unassignedShards > 0) {
                messages.add("Unassigned shards: " + unassignedShards);
            }
            if (pendingTasks > 0) {
                messages.add("Pending tasks: " + pendingTasks);
            }
            String healthMessage = messages.isEmpty() ? "OK" : String.join(", ", messages);

            return ClusterHealthVO.builder()
                    .clusterName(json.path("cluster_name").asText())
                    .status(status.toUpperCase())
                    .timedOut(json.path("timed_out").asBoolean())
                    .numberOfNodes(json.path("number_of_nodes").asInt())
                    .numberOfDataNodes(json.path("number_of_data_nodes").asInt())
                    .activePrimaryShards(json.path("active_primary_shards").asInt())
                    .activeShards(json.path("active_shards").asInt())
                    .relocatingShards(json.path("relocating_shards").asInt())
                    .initializingShards(json.path("initializing_shards").asInt())
                    .unassignedShards(unassignedShards)
                    .delayedUnassignedShards(json.path("delayed_unassigned_shards").asInt())
                    .numberOfPendingTasks(pendingTasks)
                    .numberOfInFlightFetch(json.path("number_of_in_flight_fetch").asInt())
                    .taskMaxWaitingInQueueMillis(json.path("task_max_waiting_in_queue_millis").asLong())
                    .activeShardsPercentAsNumber(json.path("active_shards_percent_as_number").asDouble())
                    .isHealthy(isHealthy)
                    .healthMessage(healthMessage)
                    .build();
        } catch (IOException e) {
            log.error("Failed to get cluster health", e);
            return ClusterHealthVO.builder()
                    .isHealthy(false)
                    .healthMessage("Failed to connect to Elasticsearch: " + e.getMessage())
                    .build();
        }
    }

    @Override
    public List<NodeStatsVO> getNodeStats() {
        List<NodeStatsVO> result = new ArrayList<>();
        try {
            Request request = new Request("GET", "/_nodes/stats");
            Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
            String responseBody = EntityUtils.toString(response.getEntity());
            JsonNode json = objectMapper.readTree(responseBody);

            JsonNode nodes = json.path("nodes");
            Iterator<Map.Entry<String, JsonNode>> nodeIterator = nodes.fields();

            while (nodeIterator.hasNext()) {
                Map.Entry<String, JsonNode> entry = nodeIterator.next();
                String nodeId = entry.getKey();
                JsonNode nodeData = entry.getValue();

                result.add(parseNodeStats(nodeId, nodeData));
            }
        } catch (IOException e) {
            log.error("Failed to get node stats", e);
        }
        return result;
    }

    @Override
    public List<ThreadPoolStatsVO> getThreadPoolStats() {
        List<ThreadPoolStatsVO> result = new ArrayList<>();
        try {
            Request request = new Request("GET", "/_nodes/stats/thread_pool");
            Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
            String responseBody = EntityUtils.toString(response.getEntity());
            JsonNode json = objectMapper.readTree(responseBody);

            JsonNode nodes = json.path("nodes");
            Iterator<Map.Entry<String, JsonNode>> nodeIterator = nodes.fields();

            while (nodeIterator.hasNext()) {
                Map.Entry<String, JsonNode> entry = nodeIterator.next();
                JsonNode nodeData = entry.getValue();
                String nodeName = nodeData.path("name").asText();
                String nodeId = entry.getKey();

                JsonNode threadPools = nodeData.path("thread_pool");

                // write thread pool
                JsonNode writePool = threadPools.path("write");
                if (!writePool.isMissingNode()) {
                    result.add(parseThreadPoolStats(nodeName, nodeId, "write", writePool));
                }

                // search thread pool
                JsonNode searchPool = threadPools.path("search");
                if (!searchPool.isMissingNode()) {
                    result.add(parseThreadPoolStats(nodeName, nodeId, "search", searchPool));
                }
            }
        } catch (IOException e) {
            log.error("Failed to get thread pool stats", e);
        }
        return result;
    }

    @Override
    public IndexingStatsVO getIndexingStats() {
        try {
            Request request = new Request("GET", "/_stats/indexing,search");
            Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
            String responseBody = EntityUtils.toString(response.getEntity());
            JsonNode json = objectMapper.readTree(responseBody);

            JsonNode total = json.path("_all").path("total");
            JsonNode indexing = total.path("indexing");
            JsonNode search = total.path("search");

            long indexFailed = indexing.path("index_failed").asLong(0);
            boolean hasIndexFailed = indexFailed > 0;
            String healthMessage = hasIndexFailed ?
                    "WARNING: " + indexFailed + " index failures" : "OK";

            return IndexingStatsVO.builder()
                    .indexTotal(indexing.path("index_total").asLong(0))
                    .indexTimeInMillis(indexing.path("index_time_in_millis").asLong(0))
                    .indexCurrent(indexing.path("index_current").asLong(0))
                    .indexFailed(indexFailed)
                    .queryTotal(search.path("query_total").asLong(0))
                    .queryTimeInMillis(search.path("query_time_in_millis").asLong(0))
                    .queryCurrent(search.path("query_current").asLong(0))
                    .fetchTotal(search.path("fetch_total").asLong(0))
                    .fetchTimeInMillis(search.path("fetch_time_in_millis").asLong(0))
                    .fetchCurrent(search.path("fetch_current").asLong(0))
                    .hasIndexFailed(hasIndexFailed)
                    .healthMessage(healthMessage)
                    .build();
        } catch (IOException e) {
            log.error("Failed to get indexing stats", e);
            return IndexingStatsVO.builder()
                    .hasIndexFailed(false)
                    .healthMessage("Failed to get stats: " + e.getMessage())
                    .build();
        }
    }

    @Override
    public EsMonitoringSummaryVO getMonitoringSummary() {
        ClusterHealthVO clusterHealth = getClusterHealth();
        List<NodeStatsVO> nodeStats = getNodeStats();
        List<ThreadPoolStatsVO> threadPoolStats = getThreadPoolStats();
        IndexingStatsVO indexingStats = getIndexingStats();

        List<String> issues = new ArrayList<>();
        String overallStatus = "HEALTHY";

        // 클러스터 상태 체크
        if (clusterHealth.getStatus() != null) {
            if ("RED".equals(clusterHealth.getStatus())) {
                issues.add("Cluster status is RED");
                overallStatus = "CRITICAL";
            } else if ("YELLOW".equals(clusterHealth.getStatus())) {
                issues.add("Cluster status is YELLOW");
                overallStatus = "WARNING";
            }
        }

        // Unassigned 샤드 체크
        if (clusterHealth.getUnassignedShards() != null && clusterHealth.getUnassignedShards() > 0) {
            issues.add("Unassigned shards: " + clusterHealth.getUnassignedShards());
            if (!"CRITICAL".equals(overallStatus)) {
                overallStatus = "WARNING";
            }
        }

        // Thread Pool Rejected 체크
        for (ThreadPoolStatsVO tp : threadPoolStats) {
            if (tp.getHasRejected() != null && tp.getHasRejected()) {
                issues.add("Thread pool rejected: " + tp.getNodeName() + "/" + tp.getThreadPoolName() +
                        " - " + tp.getRejected() + " rejected");
                overallStatus = "CRITICAL";
            }
        }

        // 노드 상태 체크
        for (NodeStatsVO node : nodeStats) {
            if (node.getHeapUsedPercent() != null && node.getHeapUsedPercent() > HEAP_CRITICAL_THRESHOLD) {
                issues.add("High heap usage on " + node.getNodeName() + ": " + node.getHeapUsedPercent() + "%");
                overallStatus = "CRITICAL";
            } else if (node.getHeapUsedPercent() != null && node.getHeapUsedPercent() > HEAP_WARNING_THRESHOLD) {
                issues.add("Warning heap usage on " + node.getNodeName() + ": " + node.getHeapUsedPercent() + "%");
                if (!"CRITICAL".equals(overallStatus)) {
                    overallStatus = "WARNING";
                }
            }

            if (node.getDiskUsedPercent() != null && node.getDiskUsedPercent() > DISK_CRITICAL_THRESHOLD) {
                issues.add("High disk usage on " + node.getNodeName() + ": " + node.getDiskUsedPercent() + "%");
                overallStatus = "CRITICAL";
            } else if (node.getDiskUsedPercent() != null && node.getDiskUsedPercent() > DISK_WARNING_THRESHOLD) {
                issues.add("Warning disk usage on " + node.getNodeName() + ": " + node.getDiskUsedPercent() + "%");
                if (!"CRITICAL".equals(overallStatus)) {
                    overallStatus = "WARNING";
                }
            }
        }

        // 인덱싱 실패 체크
        if (indexingStats.getHasIndexFailed() != null && indexingStats.getHasIndexFailed()) {
            issues.add("Index failures detected: " + indexingStats.getIndexFailed());
            if (!"CRITICAL".equals(overallStatus)) {
                overallStatus = "WARNING";
            }
        }

        boolean isHealthy = "HEALTHY".equals(overallStatus);

        return EsMonitoringSummaryVO.builder()
                .timestamp(LocalDateTime.now())
                .clusterHealth(clusterHealth)
                .nodeStats(nodeStats)
                .threadPoolStats(threadPoolStats)
                .indexingStats(indexingStats)
                .isHealthy(isHealthy)
                .overallStatus(overallStatus)
                .issues(issues)
                .build();
    }

    @Override
    public Boolean isHealthy() {
        try {
            Request request = new Request("GET", "/_cluster/health");
            Response response = restHighLevelClient.getLowLevelClient().performRequest(request);
            String responseBody = EntityUtils.toString(response.getEntity());
            JsonNode json = objectMapper.readTree(responseBody);

            String status = json.path("status").asText();
            return !"red".equalsIgnoreCase(status);
        } catch (IOException e) {
            log.error("Failed to check health", e);
            return false;
        }
    }

    private NodeStatsVO parseNodeStats(String nodeId, JsonNode nodeData) {
        String nodeName = nodeData.path("name").asText();
        String ip = nodeData.path("ip").asText();

        // JVM
        JsonNode jvm = nodeData.path("jvm").path("mem");
        int heapUsedPercent = jvm.path("heap_used_percent").asInt(0);
        Long heapUsedBytes = jvm.path("heap_used_in_bytes").asLong(0);
        Long heapMaxBytes = jvm.path("heap_max_in_bytes").asLong(0);

        // OS
        JsonNode os = nodeData.path("os");
        JsonNode cpu = os.path("cpu");
        int cpuPercent = cpu.path("percent").asInt(0);

        JsonNode loadAvg = cpu.path("load_average");
        Double loadAverage1m = loadAvg.path("1m").asDouble(0);
        Double loadAverage5m = loadAvg.path("5m").asDouble(0);
        Double loadAverage15m = loadAvg.path("15m").asDouble(0);

        // File System
        JsonNode fs = nodeData.path("fs").path("total");
        long diskTotalBytes = fs.path("total_in_bytes").asLong(0);
        long diskFreeBytes = fs.path("free_in_bytes").asLong(0);
        long diskUsedBytes = diskTotalBytes - diskFreeBytes;
        int diskUsedPercent = diskTotalBytes > 0 ? (int) ((diskUsedBytes * 100) / diskTotalBytes) : 0;

        // Roles
        JsonNode roles = nodeData.path("roles");
        StringBuilder roleStr = new StringBuilder();
        if (roles.isArray()) {
            for (JsonNode role : roles) {
                if (!roleStr.isEmpty()) roleStr.append(",");
                roleStr.append(role.asText());
            }
        }

        // 장애 판단
        List<String> healthIssues = new ArrayList<>();
        boolean isHealthy = true;

        if (heapUsedPercent > HEAP_CRITICAL_THRESHOLD) {
            healthIssues.add("Heap usage critical: " + heapUsedPercent + "%");
            isHealthy = false;
        }
        if (diskUsedPercent > DISK_CRITICAL_THRESHOLD) {
            healthIssues.add("Disk usage critical: " + diskUsedPercent + "%");
            isHealthy = false;
        }
        if (cpuPercent > CPU_CRITICAL_THRESHOLD) {
            healthIssues.add("CPU usage critical: " + cpuPercent + "%");
            isHealthy = false;
        }

        String healthMessage = healthIssues.isEmpty() ? "OK" : String.join(", ", healthIssues);

        return NodeStatsVO.builder()
                .nodeName(nodeName)
                .nodeId(nodeId)
                .ip(ip)
                .nodeRole(roleStr.toString())
                .heapUsedBytes(heapUsedBytes)
                .heapMaxBytes(heapMaxBytes)
                .heapUsedPercent(heapUsedPercent)
                .cpuPercent(cpuPercent)
                .loadAverage1m(loadAverage1m)
                .loadAverage5m(loadAverage5m)
                .loadAverage15m(loadAverage15m)
                .diskTotalBytes(diskTotalBytes)
                .diskFreeBytes(diskFreeBytes)
                .diskUsedBytes(diskUsedBytes)
                .diskUsedPercent(diskUsedPercent)
                .isHealthy(isHealthy)
                .healthMessage(healthMessage)
                .build();
    }

    private ThreadPoolStatsVO parseThreadPoolStats(String nodeName, String nodeId, String poolName, JsonNode poolData) {
        int active = poolData.path("active").asInt(0);
        int queue = poolData.path("queue").asInt(0);
        long rejected = poolData.path("rejected").asLong(0);
        int largest = poolData.path("largest").asInt(0);
        long completed = poolData.path("completed").asLong(0);

        boolean hasRejected = rejected > 0;
        String healthMessage = hasRejected ?
                "CRITICAL: " + rejected + " requests rejected" : "OK";

        return ThreadPoolStatsVO.builder()
                .nodeName(nodeName)
                .nodeId(nodeId)
                .threadPoolName(poolName)
                .active(active)
                .queue(queue)
                .rejected(rejected)
                .largest(largest)
                .completed(completed)
                .hasRejected(hasRejected)
                .healthMessage(healthMessage)
                .build();
    }
}