package com.eactive.eai.rms.onl.dashboard.modern.service;

import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceType;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.onl.common.service.EaiServerInfoService;
import com.eactive.eai.rms.onl.dashboard.service.Dashboard2RepositoryService;
import com.eactive.eai.rms.onl.dashboard.service.DashboardService;
import com.eactive.eai.rms.onl.tracking.vo.MemStaticsVo;
import com.eactive.eai.rms.onl.vo.HostStatusVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 처리량 계산 서비스
 * 각 인스턴스별 초당 처리량을 계산하여 저장합니다.
 * 시계열 데이터를 유지하여 트렌드 분석에 사용합니다.
 */
@Slf4j
@Service
public class ThroughputCalculationService {

    @Autowired
    @Qualifier("eaiServerInfoService")
    private EaiServerInfoService eaiServerInfoService;

    @Autowired
    @Qualifier("dashboard2RepositoryService")
    private Dashboard2RepositoryService dashboard2Repository;

    @Autowired
    @Qualifier("dashboardService")
    private DashboardService dashboardService;

    // 인스턴스별 처리량 정보를 저장하는 작은 캐시
    private final ConcurrentHashMap<String, ThroughputCacheInfo> throughputCache = new ConcurrentHashMap<>();

    // 서비스 타입별 시계열 데이터를 저장하는 맵
    // 키: 서비스타입(EAI, FEP 등), 값: TimeSeriesData 객체
    private final ConcurrentHashMap<String, TimeSeriesData> timeSeriesMap = new ConcurrentHashMap<>();

    // 서버별 리소스 사용량 시계열 데이터를 저장하는 맵
    // 키: 호스트명, 값: ServerResourceData 객체
    private final ConcurrentHashMap<String, ServerResourceData> serverResourceMap = new ConcurrentHashMap<>();

    // 서비스 타입별 마지막 누적 값을 저장하는 맵
    private final ConcurrentHashMap<String, Map<String, Integer>> lastAccumulatedValuesMap = new ConcurrentHashMap<>();

    // 스케줄러
    private ScheduledExecutorService scheduler;

    // 시계열 데이터 저장 기간 (분)
    private static final int TIME_SERIES_RETENTION_MINUTES = 60;

    // 서버 정보 시계열 데이터 저장 기간 (분)
    private static final int SERVER_STATUS_TIME_SERIES_RETENTION_MINUTES = 2;

    // 시계열 데이터 샘플링 간격 (초)
    private static final int TIME_SERIES_SAMPLING_SECONDS = 1;

    // 처리량 정보를 담는 내부 클래스 (캐시용)
    private static class ThroughputCacheInfo {
        private long timestamp;      // 마지막 조회 시간
        private int totalCount;      // 마지막 누적 건수

        public ThroughputCacheInfo(long timestamp, int totalCount) {
            this.timestamp = timestamp;
            this.totalCount = totalCount;
        }
    }

    // 시계열 데이터를 담는 내부 클래스
    public static class TimeSeriesData {
        private List<Long> timestamps = new ArrayList<>();      // 타임스탬프 목록
        private List<Integer> success = new ArrayList<>();      // 성공 건수
        private List<Integer> fail = new ArrayList<>();         // 실패 건수
        private List<Integer> timeout = new ArrayList<>();      // 타임아웃 건수
        private List<Integer> syserror = new ArrayList<>();     // 시스템 에러 건수

        // 시계열 데이터 추가
        public void addDataPoint(long timestamp, int successCount, int failCount, int timeoutCount, int syserrorCount) {
            timestamps.add(timestamp);
            success.add(successCount);
            fail.add(failCount);
            timeout.add(timeoutCount);
            syserror.add(syserrorCount);

            // 오래된 데이터 정리 (최대 저장 기간을 초과하는 데이터 제거)
            long cutoffTime = System.currentTimeMillis() - (TIME_SERIES_RETENTION_MINUTES * 60 * 1000);
            while (!timestamps.isEmpty() && timestamps.get(0) < cutoffTime) {
                timestamps.remove(0);
                success.remove(0);
                fail.remove(0);
                timeout.remove(0);
                syserror.remove(0);
            }
        }

        // Getter 메서드
        public List<Long> getTimestamps() {
            return timestamps;
        }

        public List<Integer> getSuccess() {
            return success;
        }

        public List<Integer> getFail() {
            return fail;
        }

        public List<Integer> getTimeout() {
            return timeout;
        }

        public List<Integer> getSyserror() {
            return syserror;
        }
    }

    // 서버 리소스 사용량 시계열 데이터를 담는 내부 클래스
    public static class ServerResourceData {
        private List<Long> timestamps = new ArrayList<>();      // 타임스탬프 목록
        private List<Integer> cpu = new ArrayList<>();          // CPU 사용률
        private List<Integer> memory = new ArrayList<>();       // 메모리 사용률
        private List<Integer> disk = new ArrayList<>();         // 디스크 사용률

        // 시계열 데이터 추가
        public void addDataPoint(long timestamp, int cpuUsage, int memoryUsage, int diskUsage) {
            timestamps.add(timestamp);
            cpu.add(cpuUsage);
            memory.add(memoryUsage);
            disk.add(diskUsage);
        }

        public void removeExpiredDataPoint(){
            // 오래된 데이터 정리 (최대 저장 기간을 초과하는 데이터 제거)
            long cutoffTime = System.currentTimeMillis() - (SERVER_STATUS_TIME_SERIES_RETENTION_MINUTES * 60 * 1000);
            while (!timestamps.isEmpty() && timestamps.get(0) < cutoffTime) {
                timestamps.remove(0);
                cpu.remove(0);
                memory.remove(0);
                disk.remove(0);
            }
        }

        // Getter 메서드
        public List<Long> getTimestamps() {
            return timestamps;
        }

        public List<Integer> getCpu() {
            return cpu;
        }

        public List<Integer> getMemory() {
            return memory;
        }

        public List<Integer> getDisk() {
            return disk;
        }

        // 최근 N개의 데이터 포인트 반환
        public ServerResourceData getRecentDataPoints(int count) {
            ServerResourceData result = new ServerResourceData();
            int size = timestamps.size();
            int startIndex = Math.max(0, size - count);

            for (int i = startIndex; i < size; i++) {
                result.timestamps.add(timestamps.get(i));
                result.cpu.add(cpu.get(i));
                result.memory.add(memory.get(i));
                result.disk.add(disk.get(i));
            }

            return result;
        }
    }

    /**
     * 초기화 메서드
     * 서비스 로드 시 스케줄러를 시작합니다.
     */
    @PostConstruct
    public void init() {
        log.info("처리량 계산 서비스 초기화");

        // 스케줄러 생성
        scheduler = Executors.newScheduledThreadPool(2);

        // 시계열 데이터 수집 작업 추가 (지정된 간격마다 실행)
        scheduler.scheduleWithFixedDelay(this::collectTimeSeriesData, 5, TIME_SERIES_SAMPLING_SECONDS, TimeUnit.SECONDS);

        // 서버 리소스 사용량 데이터 수집 작업 추가 (10초마다 실행)
        scheduler.scheduleWithFixedDelay(this::collectServerResourceData, 3, 10, TimeUnit.SECONDS);

        log.info("처리량 계산 스케줄러 시작됨");

        // 서비스 타입 초기화
        for (DataSourceType server : DataSourceTypeManager.getOnlineDataSourceTypes()) {
            timeSeriesMap.putIfAbsent(server.getName(), new TimeSeriesData());
        }

        // BAT(배치) 서버 추가
        DataSourceType batServer = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.BAT);
        if (batServer != null) {
            timeSeriesMap.putIfAbsent(batServer.getName(), new TimeSeriesData());
        }

        // BAP(일괄전송) 서버 추가
        DataSourceType bapServer = DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.BAP);
        if (bapServer != null) {
            timeSeriesMap.putIfAbsent(bapServer.getName(), new TimeSeriesData());
        }

        // 전체 데이터용 TOTAL 엔트리 추가
        timeSeriesMap.putIfAbsent("TOTAL", new TimeSeriesData());
    }

    /**
     * 종료 메서드
     * 서비스 종료 시 스케줄러를 정리합니다.
     */
    @PreDestroy
    public void destroy() {
        if (scheduler != null && !scheduler.isShutdown()) {
            scheduler.shutdown();
            log.info("처리량 계산 스케줄러 종료됨");
        }
    }

    /**
     * 서버 리소스 사용량 데이터 수집
     * 10초마다 실행되어 각 서버의 CPU, 메모리, 디스크 사용량을 수집합니다.
     */
    public void collectServerResourceData() {
        try {
            // 수집 시간
            long currentTime = System.currentTimeMillis();

            // 모든 온라인 데이터소스 타입에 대해 처리
            List<DataSourceType> servers = DataSourceTypeManager.getOnlineDataSourceTypes();

            // GW 추가
            if( DataSourceTypeManager.getDataSourceType("FGW") != null ){
                servers.add(DataSourceTypeManager.getDataSourceType("FGW"));
            }

            if( DataSourceTypeManager.getDataSourceType("CGW") != null ){
                servers.add(DataSourceTypeManager.getDataSourceType("CGW"));
            }

            for (DataSourceType server : servers) {
                // 서버 컨텍스트 설정
                DataSourceContextHolder.setDataSourceType(server);

                // 서버에 대한 인스턴스 목록 조회
                List<Map<String, String>> ipList = eaiServerInfoService.getEaiServerIpListDB();

                // 호스트명으로 인스턴스 그룹화
                Map<String, List<Map<String, String>>> instancesByHost = new HashMap<>();
                for (Map<String, String> instance : ipList) {
                    String hostname = instance.get("HOSTNAME");
                    instancesByHost.computeIfAbsent(hostname, k -> new ArrayList<>()).add(instance);
                }

                // 각 호스트별로 리소스 사용량 수집
                for (Map.Entry<String, List<Map<String, String>>> entry : instancesByHost.entrySet()) {
                    String hostname = entry.getKey();
                    List<Map<String, String>> hostInstances = entry.getValue();

                    // 호스트 상태 조회
                    HostStatusVo hostVo = dashboardService.findHostStatus(hostInstances);
                    if (hostVo != null) {

                        // 서버 리소스 데이터 객체 가져오기 또는 생성
                        ServerResourceData resourceData = serverResourceMap.computeIfAbsent(hostname, k -> new ServerResourceData());

                        // 데이터 포인트 추가
                        resourceData.addDataPoint(currentTime, hostVo.getCpu(), hostVo.getMemory(), hostVo.getDisk());
                    }
                }
            }

            // 기간 만료 데이터 삭제
            serverResourceMap.values().forEach(ServerResourceData::removeExpiredDataPoint);

        } catch (Exception e) {
            log.error("서버 리소스 사용량 데이터 수집 중 오류 발생", e);
        }
    }

    /**
     * 시계열 데이터 수집
     * 정해진 간격마다 실행되어 각 서비스 타입별 처리 건수를 수집합니다.
     */
    public void collectTimeSeriesData() {
        try {
            // 수집 시간
            long currentTime = System.currentTimeMillis();

            // 서비스 타입별 현재 누적 데이터
            Map<String, Integer> currentSuccessByService = new HashMap<>();
            Map<String, Integer> currentFailByService = new HashMap<>();
            Map<String, Integer> currentTimeoutByService = new HashMap<>();
            Map<String, Integer> currentSyserrorByService = new HashMap<>();

            // 전체 합계
            int totalSuccess = 0;
            int totalFail = 0;
            int totalTimeout = 0;
            int totalSyserror = 0;

            // 모든 온라인 데이터소스 타입에 대해 처리
            List<DataSourceType> servers = DataSourceTypeManager.getOnlineDataSourceTypes();

            for (DataSourceType server : servers) {
                // 서버 컨텍스트 설정
                DataSourceContextHolder.setDataSourceType(server);

                // 서버의 트랜잭션 정보 조회
                MemStaticsVo memStatics = dashboard2Repository.getMemTradeVoByService(server.getName());

                if (memStatics != null) {
                    // 현재 누적 값 계산
                    int currentSuccess = memStatics.getProcTotal() - memStatics.getTimeoutTotal() - memStatics.getErrorTotal() - memStatics.getSysErrorTotal();
                    int currentFail = memStatics.getErrorTotal();
                    int currentTimeout = memStatics.getTimeoutTotal();
                    int currentSyserror = memStatics.getSysErrorTotal();

                    // 서비스별 데이터 저장
                    currentSuccessByService.put(server.getName(), currentSuccess);
                    currentFailByService.put(server.getName(), currentFail);
                    currentTimeoutByService.put(server.getName(), currentTimeout);
                    currentSyserrorByService.put(server.getName(), currentSyserror);

                    // 전체 합계에 추가
                    totalSuccess += currentSuccess;
                    totalFail += currentFail;
                    totalTimeout += currentTimeout;
                    totalSyserror += currentSyserror;
                }
            }

            // 각 서비스 타입별 시계열 데이터 업데이트
            for (String serviceType : timeSeriesMap.keySet()) {
                if (serviceType.equals("TOTAL")) {
                    // TOTAL 데이터는 아래에서 별도로 처리
                    continue;
                }

                // 현재 누적 값
                int currentSuccess = currentSuccessByService.getOrDefault(serviceType, 0);
                int currentFail = currentFailByService.getOrDefault(serviceType, 0);
                int currentTimeout = currentTimeoutByService.getOrDefault(serviceType, 0);
                int currentSyserror = currentSyserrorByService.getOrDefault(serviceType, 0);

                // 마지막 누적 값 가져오기
                Map<String, Integer> lastValues = lastAccumulatedValuesMap.getOrDefault(serviceType, new HashMap<>());

                // 차이 계산
                int deltaSuccess = currentSuccess - lastValues.getOrDefault("success", 0);
                int deltaFail = currentFail - lastValues.getOrDefault("fail", 0);
                int deltaTimeout = currentTimeout - lastValues.getOrDefault("timeout", 0);
                int deltaSyserror = currentSyserror - lastValues.getOrDefault("syserror", 0);

                // 인스턴스 재기동 감지 (누적치가 감소한 경우)
                boolean isReset = false;
                if (deltaSuccess < 0 || deltaFail < 0 || deltaTimeout < 0 || deltaSyserror < 0) {
                    isReset = true;
                }

                // 시계열 데이터에 추가할 값 결정
                // 재기동된 경우 건수는 0으로 처리
                int addSuccess = isReset ? 0 : Math.max(0, deltaSuccess);
                int addFail = isReset ? 0 : Math.max(0, deltaFail);
                int addTimeout = isReset ? 0 : Math.max(0, deltaTimeout);
                int addSyserror = isReset ? 0 : Math.max(0, deltaSyserror);

                // 시계열 데이터 추가
                TimeSeriesData timeSeriesData = timeSeriesMap.get(serviceType);
                if (timeSeriesData != null) {
                    timeSeriesData.addDataPoint(currentTime, addSuccess, addFail, addTimeout, addSyserror);
                }

                // 마지막 누적 값 업데이트 (현재 값으로 항상 갱신)
                Map<String, Integer> newLastValues = new HashMap<>();
                newLastValues.put("success", currentSuccess);
                newLastValues.put("fail", currentFail);
                newLastValues.put("timeout", currentTimeout);
                newLastValues.put("syserror", currentSyserror);
                lastAccumulatedValuesMap.put(serviceType, newLastValues);
            }

            // TOTAL 시계열 데이터 업데이트
            TimeSeriesData totalTimeSeriesData = timeSeriesMap.get("TOTAL");
            if (totalTimeSeriesData != null) {
                // 마지막 누적 값 가져오기
                Map<String, Integer> lastTotalValues = lastAccumulatedValuesMap.getOrDefault("TOTAL", new HashMap<>());

                // 차이 계산
                int deltaSuccess = totalSuccess - lastTotalValues.getOrDefault("success", 0);
                int deltaFail = totalFail - lastTotalValues.getOrDefault("fail", 0);
                int deltaTimeout = totalTimeout - lastTotalValues.getOrDefault("timeout", 0);
                int deltaSyserror = totalSyserror - lastTotalValues.getOrDefault("syserror", 0);

                // 인스턴스 재기동 감지
                boolean isReset = false;
                if (deltaSuccess < 0 || deltaFail < 0 || deltaTimeout < 0 || deltaSyserror < 0) {
                    isReset = true;
                }

                // 시계열 데이터에 추가할 값 결정
                int addSuccess = isReset ? 0 : Math.max(0, deltaSuccess);
                int addFail = isReset ? 0 : Math.max(0, deltaFail);
                int addTimeout = isReset ? 0 : Math.max(0, deltaTimeout);
                int addSyserror = isReset ? 0 : Math.max(0, deltaSyserror);

                totalTimeSeriesData.addDataPoint(currentTime, addSuccess, addFail, addTimeout, addSyserror);

                // 마지막 누적 값 업데이트
                Map<String, Integer> newLastTotalValues = new HashMap<>();
                newLastTotalValues.put("success", totalSuccess);
                newLastTotalValues.put("fail", totalFail);
                newLastTotalValues.put("timeout", totalTimeout);
                newLastTotalValues.put("syserror", totalSyserror);
                lastAccumulatedValuesMap.put("TOTAL", newLastTotalValues);
            }

        } catch (Exception e) {
            log.error("시계열 데이터 수집 중 오류 발생", e);
        }
    }

    /**
     * 특정 인스턴스의 처리량 조회
     * 실시간으로 계산하여 반환합니다.
     * @param hostName 호스트명
     * @param instanceName 인스턴스명
     * @return 초당 처리량 문자열 (예: "10.5/s")
     */
    public String getThroughput(String hostName, String instanceName) {
        try {
            // 현재 처리 건수 조회
            MemStaticsVo memStatics = dashboard2Repository.getMemTradeVoByHost(hostName, instanceName);

            if (memStatics != null) {
                // 현재 시간과 누적 건수
                long currentTime = System.currentTimeMillis();
                int currentTotal = memStatics.getProcTotal();
                String instanceKey = hostName + "_" + instanceName;

                // 이전 조회 데이터 가져오기
                ThroughputCacheInfo prevInfo = throughputCache.get(instanceKey);

                double throughput = 0.0;
                boolean isReset = false;

                if (prevInfo != null) {
                    // 경과 시간 계산 (초 단위)
                    double elapsedSeconds = (currentTime - prevInfo.timestamp) / 1000.0;

                    // 최소 0.1초 이상 경과했을 때만 계산
                    if (elapsedSeconds >= 0.1) {
                        int processedCount = currentTotal - prevInfo.totalCount;

                        // 처리 건수가 음수이면 인스턴스 재기동으로 간주
                        if (processedCount < 0) {
                            processedCount = 0;
                            isReset = true;
                        }

                        throughput = processedCount / elapsedSeconds;
                    }
                }

                // 캐시 업데이트
                throughputCache.put(instanceKey, new ThroughputCacheInfo(currentTime, currentTotal));

                // 처리량 문자열 반환
                return String.format("%.1f/s", throughput);
            }
        } catch (Exception e) {
            log.error("처리량 계산 중 오류 발생", e);
        }

        return "0/s";
    }

    /**
     * 특정 서버의 리소스 사용량 시계열 데이터 조회
     * @param hostname 호스트명
     * @param count 조회할 데이터 포인트 수 (최대 7개)
     * @return 서버 리소스 사용량 시계열 데이터
     */
    public ServerResourceData getServerResourceData(String hostname, int count) {
        // 데이터 포인트 수 제한 (최대 7개)
        count = Math.min(count, 7);

        ServerResourceData resourceData = serverResourceMap.get(hostname);
        if (resourceData != null) {
            return resourceData.getRecentDataPoints(count);
        } else {
            return new ServerResourceData();
        }
    }

    /**
     * 특정 서비스 타입의 시계열 데이터 조회
     * @param serviceType 서비스 타입 (EAI, FEP 등)
     * @param minutes 조회할 분 단위 기간 (최대 60분)
     * @return 시계열 데이터
     */
    public TimeSeriesData getTimeSeriesData(String serviceType, int minutes) {
        // 기간 제한 (1~60분)
        minutes = Math.min(Math.max(minutes, 1), TIME_SERIES_RETENTION_MINUTES);

        // 기본적으로 요청된 서비스 타입의 시계열 데이터 사용
        TimeSeriesData originalData = timeSeriesMap.get(serviceType);

        // 서비스 타입이 없거나 "ALL"인 경우 TOTAL 데이터 사용
        if (originalData == null || "ALL".equalsIgnoreCase(serviceType) || "all".equals(serviceType)) {
            originalData = timeSeriesMap.get("TOTAL");
        }

        // 데이터가 없으면 빈 데이터 반환
        if (originalData == null) {
            return new TimeSeriesData();
        }

        // 기간에 맞게 데이터 필터링
        TimeSeriesData filteredData = new TimeSeriesData();
        long cutoffTime = System.currentTimeMillis() - (minutes * 60 * 1000);

        List<Long> originalTimestamps = originalData.getTimestamps();
        List<Integer> originalSuccess = originalData.getSuccess();
        List<Integer> originalFail = originalData.getFail();
        List<Integer> originalTimeout = originalData.getTimeout();
        List<Integer> originalSyserror = originalData.getSyserror();

        // 최근 데이터부터 시작하여 지정된 기간 내의 데이터만 필터링
        for (int i = originalTimestamps.size() - 1; i >= 0; i--) {
            if (originalTimestamps.get(i) >= cutoffTime) {
                filteredData.timestamps.add(0, originalTimestamps.get(i));
                filteredData.success.add(0, originalSuccess.get(i));
                filteredData.fail.add(0, originalFail.get(i));
                filteredData.timeout.add(0, originalTimeout.get(i));
                filteredData.syserror.add(0, originalSyserror.get(i));
            } else {
                // 시간 범위를 벗어나면 종료
                break;
            }
        }

        return filteredData;
    }
}