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

import com.kamco.cd.training.model.dto.ModelTrainMngDto;
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
import com.kamco.cd.training.train.dto.DockerInspectState;
import com.kamco.cd.training.train.dto.ModelTrainJobDto;
import com.kamco.cd.training.train.dto.OutputResult;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class TrainUtilService {

  private final ModelTrainMngCoreService modelTrainMngCoreService;

  /**
   * Docker 컨테이너가 쓰는 response(산출물) 디렉토리의 "호스트 측" 베이스 경로. 예) /data/train/response
   *
   * <p>컨테이너가 --rm 으로 삭제된 경우에도 이 경로에 val.csv / *.pth 등이 남아있으면 정상 종료 여부를 "파일 기반"으로 판정합니다.
   */
  @Value("${train.docker.response_dir}")
  private String responseDir;

  /**
   * docker inspect를 사용해서 컨테이너 상태를 조회합니다.
   *
   * <p>사용하는 템플릿: {{.State.Status}} {{.State.Running}} {{.State.ExitCode}}
   *
   * <p>예상 출력 예: - "running true 0" - "exited false 0" - "exited false 137"
   *
   * <p>주의: - 컨테이너가 없거나 inspect 실패 시 exitCode != 0 또는 output이 비어서 missing() 반환 - 무한 대기 방지를 위해 5초
   * 타임아웃을 둠
   */
  public DockerInspectState inspectContainer(String containerName)
      throws IOException, InterruptedException {

    ProcessBuilder pb =
        new ProcessBuilder(
            "docker",
            "inspect",
            "-f",
            "{{.State.Status}} {{.State.Running}} {{.State.ExitCode}}",
            containerName);

    pb.redirectErrorStream(true);

    Process p = pb.start();

    String output;
    try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
      output = br.readLine();
    }

    boolean finished = p.waitFor(5, TimeUnit.SECONDS);
    if (!finished) {
      p.destroyForcibly();
      throw new IOException("docker inspect timeout");
    }

    int code = p.exitValue();

    if (code != 0 || output == null || output.isBlank()) {
      return DockerInspectState.missing();
    }

    String[] parts = output.trim().split("\\s+");

    String status = parts.length > 0 ? parts[0] : "unknown";
    boolean running = parts.length > 1 && Boolean.parseBoolean(parts[1]);

    Integer exitCode = null;
    if (parts.length > 2) {
      try {
        exitCode = Integer.parseInt(parts[2]);
      } catch (Exception ignore) {
      }
    }

    return new DockerInspectState(true, running, exitCode, status);
  }

  /**
   * 컨테이너가 없을 때(responseDir 산출물만 남아있는 상태) 완료 여부를 파일 기반으로 판정합니다.
   *
   * <p>판정 규칙(보수적으로 설계): 1) total_epoch가 paramsJson에 있어야 함 (없으면 완료 판단 불가) 2) val.csv 존재 + 헤더 제외 라인 수
   * >= total_epoch 이어야 함 3) *.pth 파일이 total_epoch 이상 존재하거나, best*.pth(또는 *best*.pth)가 존재해야 함
   *
   * <p>왜 이렇게? - 어떤 학습은 epoch마다 pth를 남기고 - 어떤 학습은 best만 남기기도 해서 "pthCount >= total_epoch"만 쓰면 정상 종료를
   * 실패로 오판할 수 있음.
   */
  public OutputResult probeOutputs(ModelTrainJobDto job) {

    try {
      Path outDir = resolveOutputDir(job);
      if (outDir == null || !Files.isDirectory(outDir)) {
        return new OutputResult(false, "output-dir-missing");
      }

      Integer totalEpoch = extractTotalEpoch(job).orElse(null);
      if (totalEpoch == null || totalEpoch <= 0) {
        return new OutputResult(false, "total-epoch-missing");
      }

      Integer valInterval = extractValInterval(job).orElse(null);
      if (valInterval == null || valInterval <= 0) {
        return new OutputResult(false, "val-interval-missing");
      }

      Path valCsv = outDir.resolve("val.csv");
      if (!Files.exists(valCsv)) {
        return new OutputResult(false, "val.csv-missing");
      }

      long lines = countNonHeaderLines(valCsv);
      int expectedLines = totalEpoch / valInterval;

      if (lines >= expectedLines) {
        return new OutputResult(true, "ok");
      }

      return new OutputResult(false, "val.csv-lines-mismatch");

    } catch (Exception e) {
      return new OutputResult(false, "probe-error");
    }
  }

  /**
   * 테스트 완료후 zip 파일 있는지 확인
   *
   * @param uuid
   * @return
   */
  public boolean existsZipFile(UUID uuid) {
    Path path = Paths.get(responseDir, uuid.toString());

    if (!Files.isDirectory(path)) {
      return false;
    }

    String pattern = "*" + uuid + "*.zip";

    try (DirectoryStream<Path> stream = Files.newDirectoryStream(path, pattern)) {
      return stream.iterator().hasNext();
    } catch (IOException e) {
      return false;
    }
  }

  /**
   * responseDir 아래에서 job 산출물 디렉토리를 찾습니다.
   *
   * <p>가장 중요한 커스터마이징 포인트: - 실제 운영 환경에서 산출물이 어떤 경로 규칙으로 저장되는지에 따라 여기만 수정하면 됩니다.
   *
   * <p>현재 기본 탐색 순서: 1) {responseDir}/{jobId} 2) {responseDir}/{modelId} 3)
   * {responseDir}/{containerName} 4) 마지막 fallback: responseDir 자체
   *
   * <p>추천: - 여러분 규칙이 "{responseDir}/{modelId}/{jobId}" 같은 형태라면 base.resolve(modelId).resolve(jobId)
   * 형태를 1순위로 두세요.
   */
  private Path resolveOutputDir(ModelTrainJobDto job) {
    ModelTrainMngDto.Basic model = modelTrainMngCoreService.findModelById(job.getModelId());

    Path base = Paths.get(responseDir, model.getUuid().toString(), "metrics");

    return Files.isDirectory(base) ? base : null;
  }

  /**
   * paramsJson에서 total_epoch 값을 추출합니다.
   *
   * <p>키 후보: - "total_epoch" (snake_case) - "totalEpoch" (camelCase)
   *
   * <p>예: paramsJson = {"jobType":"TRAIN","total_epoch":50,...}
   */
  private Optional<Integer> extractTotalEpoch(ModelTrainJobDto job) {
    Map<String, Object> params = job.getParamsJson();
    if (params == null) return Optional.empty();

    Object v = params.get("total_epoch");
    if (v == null) v = params.get("totalEpoch");

    try {
      return v == null ? Optional.empty() : Optional.of(Integer.parseInt(String.valueOf(v)));
    } catch (Exception e) {
      return Optional.empty();
    }
  }

  /** paramsJson에서 valInterval 추출 */
  private Optional<Integer> extractValInterval(ModelTrainJobDto job) {
    Map<String, Object> params = job.getParamsJson();
    if (params == null) return Optional.empty();

    Object v = params.get("valInterval");

    try {
      return v == null ? Optional.empty() : Optional.of(Integer.parseInt(String.valueOf(v)));
    } catch (Exception e) {
      return Optional.empty();
    }
  }

  /**
   * CSV 파일에서 "헤더(첫 줄)"를 제외한 라인 수를 계산합니다.
   *
   * <p>가정: - val.csv 첫 줄은 헤더 - 이후 라인들이 epoch별 기록(또는 유사한 누적 기록)
   *
   * <p>주의: - 파일 인코딩은 UTF-8로 가정 - 빈 줄은 제외
   */
  private long countNonHeaderLines(Path csv) throws IOException {
    try (Stream<String> lines = Files.lines(csv, StandardCharsets.UTF_8)) {
      return lines.skip(1).filter(s -> s != null && !s.isBlank()).count();
    }
  }
}
