package com.kamco.cd.training.common.service;

import com.kamco.cd.training.common.dto.MonitorDto;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Log4j2
public class SystemMonitorService {

  // =========================
  // CPU 이전값 (누적값 → delta 계산용)
  // =========================
  private long prevIdle = 0;
  private long prevTotal = 0;

  // =========================
  // 최근 30초 히스토리
  // - CPU: 30개 (1초 * 30)
  // - GPU: GPU별 30개
  // =========================
  private final Deque<Double> cpuHistory = new ArrayDeque<>();
  private final Map<Integer, Deque<Integer>> gpuHistory = new java.util.HashMap<>();

  // =========================
  // GPU dmon reader (스트리밍)
  // =========================
  private final GpuDmonReader gpuReader;

  // =========================
  // 캐시 (API 응답용)
  // - 매 요청마다 계산하지 않기 위해 사용
  // - volatile로 동시성 안전 보장
  // =========================
  private volatile MonitorDto cached = new MonitorDto();

  // =========================
  // 1초마다 실행되는 수집 스케줄러
  // =========================
  @Scheduled(fixedRate = 1000)
  public void collect() {
    // 디버깅용
    // log.info("collect instance = {}", this);
    try {

      // =====================
      // 1. CPU 수집
      // =====================
      double cpu = readCpu();

      cpuHistory.add(cpu);

      // 30개 유지 (rolling window)
      if (cpuHistory.size() > 30) cpuHistory.poll();

      // =====================
      // 2. GPU (dmon 데이터 사용)
      // =====================
      Map<Integer, Integer> gpuMap = gpuReader.getGpuUtilMap();

      for (Map.Entry<Integer, Integer> entry : gpuMap.entrySet()) {
        int index = entry.getKey();
        int util = entry.getValue();

        gpuHistory.computeIfAbsent(index, k -> new ArrayDeque<>()).add(util);

        Deque<Integer> q = gpuHistory.get(index);
        if (q.size() > 30) q.poll();
      }

      // =====================
      // 3. 캐시 업데이트
      // =====================
      updateCache(gpuMap);

    } catch (Exception e) {
      log.error("collect error", e);
    }
  }

  // =========================
  // CPU 사용률 계산 (/proc/stat)
  // =========================
  private double readCpu() throws Exception {

    if (!isLinux()) return 0;

    try (BufferedReader br = new BufferedReader(new FileReader("/proc/stat"))) {

      String line = br.readLine();
      String[] p = line.split("\\s+");

      long user = Long.parseLong(p[1]);
      long nice = Long.parseLong(p[2]);
      long system = Long.parseLong(p[3]);
      long idle = Long.parseLong(p[4]);
      long iowait = Long.parseLong(p[5]);
      long irq = Long.parseLong(p[6]);
      long softirq = Long.parseLong(p[7]);

      long total = user + nice + system + idle + iowait + irq + softirq;
      long idleAll = idle + iowait;

      if (prevTotal == 0) {
        prevTotal = total;
        prevIdle = idleAll;
        return 0;
      }

      long totalDiff = total - prevTotal;
      long idleDiff = idleAll - prevIdle;

      prevTotal = total;
      prevIdle = idleAll;

      if (totalDiff == 0) return 0;

      return (1.0 - (double) idleDiff / totalDiff) * 100;

    } catch (Exception e) {
      log.warn("CPU read fail", e);
      return 0;
    }
  }

  private boolean isLinux() {
    return System.getProperty("os.name").toLowerCase().contains("linux");
  }

  // =========================
  // Memory (/proc/meminfo)
  // - 현재값 (평균 아님)
  // =========================
  private String readMemory() throws Exception {

    // linux 환경일때만 실행
    if (!isLinux()) {
      return "N/A";
    }

    BufferedReader br = new BufferedReader(new FileReader("/proc/meminfo"));

    long total = 0;
    long available = 0;

    String line;
    while ((line = br.readLine()) != null) {
      if (line.startsWith("MemTotal")) {
        total = Long.parseLong(line.replaceAll("\\D+", ""));
      } else if (line.startsWith("MemAvailable")) {
        available = Long.parseLong(line.replaceAll("\\D+", ""));
      }
    }

    br.close();

    long used = total - available;

    // kB → GB
    double usedGB = used / (1024.0 * 1024);
    double totalGB = total / (1024.0 * 1024);

    return String.format("%.1f/%.0fGB", usedGB, totalGB);
  }

  // =========================
  // 캐시 업데이트
  // =========================
  private void updateCache(Map<Integer, Integer> gpuMap) throws Exception {

    MonitorDto dto = new MonitorDto();

    // =====================
    // CPU 평균 (30초)
    // =====================
    dto.cpu = (int) cpuHistory.stream().mapToDouble(Double::doubleValue).average().orElse(0);

    // =====================
    // Memory (현재값)
    // =====================
    dto.memory = readMemory();

    // =====================
    // GPU 평균 (30초)
    // =====================
    for (Map.Entry<Integer, Deque<Integer>> entry : gpuHistory.entrySet()) {

      int index = entry.getKey();
      Deque<Integer> q = entry.getValue();

      int avg = (int) q.stream().mapToInt(i -> i).average().orElse(0);

      dto.gpus.add(new MonitorDto.Gpu(index, avg));
    }

    // =====================
    // 캐시 교체 (atomic)
    // =====================
    this.cached = dto;
  }

  // =========================
  // 외부 조회 (Controller에서 호출)
  // =========================
  public MonitorDto get() {
    return cached;
  }
}
