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

import com.kamco.cd.training.postgres.entity.ModelHyperParamEntity;
import com.kamco.cd.training.postgres.entity.ModelMasterEntity;
import com.kamco.cd.training.postgres.repository.hyperparam.HyperParamRepository;
import com.kamco.cd.training.postgres.repository.model.ModelMngRepository;
import com.kamco.cd.training.train.dto.TrainingMetricsDto;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class TrainingMetricsService {

  @Value("${train.docker.response_dir}")
  private String responseDir;

  private final HyperParamRepository hyperParamRepository;
  private final ModelMngRepository modelMngRepository;

  /**
   * 하이퍼파라미터 UUID로 학습 메트릭 조회
   *
   * @param hyperParamUuid 하이퍼파라미터 UUID
   * @return 학습 메트릭 응답
   */
  public TrainingMetricsDto.Response getTrainingMetricsByHyperParam(UUID hyperParamUuid) {
    log.info("하이퍼파라미터 UUID로 학습 메트릭 조회 시작: {}", hyperParamUuid);

    // 1. 하이퍼파라미터로 모델 찾기
    ModelHyperParamEntity hyperParam =
        hyperParamRepository
            .findHyperParamByUuid(hyperParamUuid)
            .orElseThrow(
                () -> new IllegalArgumentException("하이퍼파라미터를 찾을 수 없습니다: " + hyperParamUuid));

    // 2. 해당 하이퍼파라미터를 사용하는 모델 찾기
    List<ModelMasterEntity> models = modelMngRepository.findByHyperParamId(hyperParam.getId());

    if (models.isEmpty()) {
      log.warn("하이퍼파라미터 ID {}를 사용하는 모델이 없습니다.", hyperParam.getId());
      return TrainingMetricsDto.Response.builder()
          .jobId(hyperParamUuid.toString())
          .basePath(null)
          .source("none")
          .status("EMPTY")
          .errorMessage("해당 하이퍼파라미터를 사용하는 모델이 없습니다.")
          .epochs(new ArrayList<>())
          .build();
    }

    // 3. 가장 최근 모델 선택 (step1이 완료된 것 우선)
    ModelMasterEntity targetModel =
        models.stream()
            .filter(m -> "COMPLETED".equals(m.getStep1State()))
            .findFirst()
            .orElse(models.get(0));

    log.info(
        "선택된 모델 UUID: {}, ModelNo: {}, Step1State: {}",
        targetModel.getUuid(),
        targetModel.getModelNo(),
        targetModel.getStep1State());

    // 4. 모델 UUID로 메트릭 조회
    return getTrainingMetricsByModelUuid(targetModel.getUuid());
  }

  /**
   * 모델 UUID로 학습 메트릭 조회
   *
   * @param modelUuid 모델 UUID
   * @return 학습 메트릭 응답
   */
  public TrainingMetricsDto.Response getTrainingMetricsByModelUuid(UUID modelUuid) {
    log.info("모델 UUID로 학습 메트릭 조회 시작: {}", modelUuid);

    // 실제 존재하는 basePath 찾기 (uuid 또는 uuid-out)
    PathInfo pathInfo = findActualBasePath(modelUuid);

    if (pathInfo == null || !Files.exists(pathInfo.basePath)) {
      log.warn(
          "모델 결과 디렉토리가 존재하지 않습니다. 시도한 경로: {} 또는 {}-out",
          responseDir + "/" + modelUuid,
          responseDir + "/" + modelUuid);
      return TrainingMetricsDto.Response.builder()
          .jobId(modelUuid.toString())
          .basePath(null)
          .source("none")
          .status("EMPTY")
          .errorMessage("모델 결과 디렉토리가 존재하지 않습니다.")
          .epochs(new ArrayList<>())
          .build();
    }

    Path basePathObj = pathInfo.basePath;
    String basePath = pathInfo.basePathString;

    // 1. CSV 파일 우선 시도
    Path metricsDir = basePathObj.resolve("metrics");
    Path valCsvPath = metricsDir.resolve("val.csv");
    Path trainCsvPath = metricsDir.resolve("train.csv");

    if (Files.exists(valCsvPath) && Files.exists(trainCsvPath)) {
      log.info("CSV 파일을 사용하여 메트릭 파싱: {}", metricsDir);
      try {
        List<TrainingMetricsDto.EpochMetrics> epochs = parseCsvFiles(trainCsvPath, valCsvPath);
        return TrainingMetricsDto.Response.builder()
            .jobId(modelUuid.toString())
            .basePath(basePath)
            .source("csv")
            .status("SUCCESS")
            .epochs(epochs)
            .build();
      } catch (Exception e) {
        log.error("CSV 파싱 중 오류 발생", e);
        // CSV 실패 시 log fallback
      }
    }

    // 2. processing.log fallback
    Path processingDir = basePathObj.resolve("processing");
    Path logPath = processingDir.resolve("processing.log");

    if (Files.exists(logPath)) {
      log.info("processing.log 파일을 사용하여 메트릭 파싱: {}", logPath);
      try {
        List<TrainingMetricsDto.EpochMetrics> epochs = parseProcessingLog(logPath);
        return TrainingMetricsDto.Response.builder()
            .jobId(modelUuid.toString())
            .basePath(basePath)
            .source("log")
            .status("SUCCESS")
            .epochs(epochs)
            .build();
      } catch (Exception e) {
        log.error("processing.log 파싱 중 오류 발생", e);
        return TrainingMetricsDto.Response.builder()
            .jobId(modelUuid.toString())
            .basePath(basePath)
            .source("log")
            .status("ERROR")
            .errorMessage("로그 파싱 중 오류 발생: " + e.getMessage())
            .epochs(new ArrayList<>())
            .build();
      }
    }

    // 3. 둘 다 없으면 EMPTY
    log.warn("메트릭 파일을 찾을 수 없습니다: {}", basePath);
    return TrainingMetricsDto.Response.builder()
        .jobId(modelUuid.toString())
        .basePath(basePath)
        .source("none")
        .status("EMPTY")
        .errorMessage("메트릭 파일(val.csv 또는 processing.log)을 찾을 수 없습니다.")
        .epochs(new ArrayList<>())
        .build();
  }

  /**
   * train.csv + val.csv 파싱
   *
   * @param trainCsvPath train.csv 경로
   * @param valCsvPath val.csv 경로
   * @return Epoch별 메트릭 리스트
   */
  private List<TrainingMetricsDto.EpochMetrics> parseCsvFiles(Path trainCsvPath, Path valCsvPath)
      throws IOException {

    log.debug("CSV 파일 파싱 시작: train={}, val={}", trainCsvPath, valCsvPath);

    // train.csv 파싱
    Map<Integer, TrainingMetricsDto.TrainMetrics> trainMetricsMap = parseTrainCsv(trainCsvPath);

    // val.csv 파싱
    Map<Integer, ValMetricsData> valMetricsMap = parseValCsv(valCsvPath);

    // 합치기
    List<TrainingMetricsDto.EpochMetrics> epochs = new ArrayList<>();

    for (Integer epoch : trainMetricsMap.keySet()) {
      TrainingMetricsDto.TrainMetrics trainMetrics = trainMetricsMap.get(epoch);
      ValMetricsData valData = valMetricsMap.get(epoch);

      if (valData != null) {
        epochs.add(
            TrainingMetricsDto.EpochMetrics.builder()
                .epoch(epoch)
                .train(trainMetrics)
                .summary(valData.summary)
                .classes(valData.classes)
                .build());
      }
    }

    log.debug("CSV 파싱 완료: {} epoch(s)", epochs.size());
    return epochs;
  }

  /**
   * train.csv 파싱
   *
   * @param trainCsvPath train.csv 경로
   * @return Epoch별 TrainMetrics Map
   */
  private Map<Integer, TrainingMetricsDto.TrainMetrics> parseTrainCsv(Path trainCsvPath)
      throws IOException {

    Map<Integer, TrainingMetricsDto.TrainMetrics> result = new HashMap<>();

    try (BufferedReader reader = Files.newBufferedReader(trainCsvPath)) {
      String headerLine = reader.readLine(); // 헤더 스킵
      if (headerLine == null) {
        return result;
      }

      String line;
      while ((line = reader.readLine()) != null) {
        String[] parts = line.split(",");
        if (parts.length >= 5) {
          try {
            int epoch = Integer.parseInt(parts[0].trim());
            int iteration = Integer.parseInt(parts[1].trim());
            double loss = Double.parseDouble(parts[2].trim());
            String lr = parts[3].trim();
            double time = Double.parseDouble(parts[4].trim());

            result.put(
                epoch,
                TrainingMetricsDto.TrainMetrics.builder()
                    .iteration(iteration)
                    .loss(loss)
                    .lr(lr)
                    .time(time)
                    .build());
          } catch (NumberFormatException e) {
            log.warn("train.csv 파싱 오류 (라인 스킵): {}", line);
          }
        }
      }
    }

    return result;
  }

  /**
   * val.csv 파싱
   *
   * @param valCsvPath val.csv 경로
   * @return Epoch별 ValMetricsData Map
   */
  private Map<Integer, ValMetricsData> parseValCsv(Path valCsvPath) throws IOException {

    Map<Integer, ValMetricsData> result = new HashMap<>();

    try (BufferedReader reader = Files.newBufferedReader(valCsvPath)) {
      String headerLine = reader.readLine(); // 헤더 스킵
      if (headerLine == null) {
        return result;
      }

      String line;
      while ((line = reader.readLine()) != null) {
        String[] parts = line.split(",");
        if (parts.length >= 13) {
          try {
            int epoch = Integer.parseInt(parts[0].trim());
            double aAcc = Double.parseDouble(parts[1].trim());
            double mFscore = Double.parseDouble(parts[2].trim());
            double mPrecision = Double.parseDouble(parts[3].trim());
            double mRecall = Double.parseDouble(parts[4].trim());
            double mIoU = Double.parseDouble(parts[5].trim());
            double mAcc = Double.parseDouble(parts[6].trim());

            double changedFscore = Double.parseDouble(parts[7].trim());
            double changedPrecision = Double.parseDouble(parts[8].trim());
            double changedRecall = Double.parseDouble(parts[9].trim());

            double unchangedFscore = Double.parseDouble(parts[10].trim());
            double unchangedPrecision = Double.parseDouble(parts[11].trim());
            double unchangedRecall = Double.parseDouble(parts[12].trim());

            TrainingMetricsDto.SummaryMetrics summary =
                TrainingMetricsDto.SummaryMetrics.builder()
                    .aAcc(aAcc)
                    .mFscore(mFscore)
                    .mPrecision(mPrecision)
                    .mRecall(mRecall)
                    .mIoU(mIoU)
                    .mAcc(mAcc)
                    .build();

            List<TrainingMetricsDto.ClassMetrics> classes = new ArrayList<>();
            classes.add(
                TrainingMetricsDto.ClassMetrics.builder()
                    .className("unchanged")
                    .fscore(unchangedFscore)
                    .precision(unchangedPrecision)
                    .recall(unchangedRecall)
                    .build());
            classes.add(
                TrainingMetricsDto.ClassMetrics.builder()
                    .className("changed")
                    .fscore(changedFscore)
                    .precision(changedPrecision)
                    .recall(changedRecall)
                    .build());

            result.put(epoch, new ValMetricsData(summary, classes));
          } catch (NumberFormatException e) {
            log.warn("val.csv 파싱 오류 (라인 스킵): {}", line);
          }
        }
      }
    }

    return result;
  }

  /**
   * processing.log 파싱 (fallback)
   *
   * @param logPath processing.log 경로
   * @return Epoch별 메트릭 리스트
   */
  private List<TrainingMetricsDto.EpochMetrics> parseProcessingLog(Path logPath)
      throws IOException {

    log.debug("processing.log 파싱 시작: {}", logPath);

    List<TrainingMetricsDto.EpochMetrics> epochs = new ArrayList<>();

    // 정규식 패턴
    Pattern epochPattern = Pattern.compile("Epoch\\(val\\)\\s*\\[(\\d+)\\]");
    Pattern summaryPattern =
        Pattern.compile(
            "aAcc:\\s*([\\d.]+)\\s+mFscore:\\s*([\\d.]+)\\s+mPrecision:\\s*([\\d.]+)\\s+mRecall:\\s*([\\d.]+)\\s+mIoU:\\s*([\\d.]+)\\s+mAcc:\\s*([\\d.]+)");
    Pattern classPattern =
        Pattern.compile(
            "\\|\\s*(unchanged|changed)\\s*\\|\\s*([\\d.]+)\\s*\\|\\s*([\\d.]+)\\s*\\|\\s*([\\d.]+)\\s*\\|\\s*([\\d.]+)\\s*\\|\\s*([\\d.]+)");

    try (BufferedReader reader = Files.newBufferedReader(logPath)) {
      String line;
      Integer currentEpoch = null;
      TrainingMetricsDto.SummaryMetrics currentSummary = null;
      List<TrainingMetricsDto.ClassMetrics> currentClasses = new ArrayList<>();

      while ((line = reader.readLine()) != null) {
        // Epoch 추출
        Matcher epochMatcher = epochPattern.matcher(line);
        if (epochMatcher.find()) {
          // 이전 epoch 데이터 저장
          if (currentEpoch != null && currentSummary != null) {
            epochs.add(
                TrainingMetricsDto.EpochMetrics.builder()
                    .epoch(currentEpoch)
                    .train(null) // log에는 train 정보 없음
                    .summary(currentSummary)
                    .classes(new ArrayList<>(currentClasses))
                    .build());
          }

          // 새 epoch 시작
          currentEpoch = Integer.parseInt(epochMatcher.group(1));
          currentSummary = null;
          currentClasses.clear();
          continue;
        }

        // Summary 추출
        Matcher summaryMatcher = summaryPattern.matcher(line);
        if (summaryMatcher.find()) {
          currentSummary =
              TrainingMetricsDto.SummaryMetrics.builder()
                  .aAcc(Double.parseDouble(summaryMatcher.group(1)))
                  .mFscore(Double.parseDouble(summaryMatcher.group(2)))
                  .mPrecision(Double.parseDouble(summaryMatcher.group(3)))
                  .mRecall(Double.parseDouble(summaryMatcher.group(4)))
                  .mIoU(Double.parseDouble(summaryMatcher.group(5)))
                  .mAcc(Double.parseDouble(summaryMatcher.group(6)))
                  .build();
          continue;
        }

        // Class 추출
        Matcher classMatcher = classPattern.matcher(line);
        if (classMatcher.find()) {
          currentClasses.add(
              TrainingMetricsDto.ClassMetrics.builder()
                  .className(classMatcher.group(1))
                  .fscore(Double.parseDouble(classMatcher.group(2)))
                  .precision(Double.parseDouble(classMatcher.group(3)))
                  .recall(Double.parseDouble(classMatcher.group(4)))
                  .build());
        }
      }

      // 마지막 epoch 데이터 저장
      if (currentEpoch != null && currentSummary != null) {
        epochs.add(
            TrainingMetricsDto.EpochMetrics.builder()
                .epoch(currentEpoch)
                .train(null)
                .summary(currentSummary)
                .classes(new ArrayList<>(currentClasses))
                .build());
      }
    }

    log.debug("processing.log 파싱 완료: {} epoch(s)", epochs.size());
    return epochs;
  }

  /**
   * 실제 존재하는 basePath 찾기 (uuid 또는 uuid-out 둘 다 확인)
   *
   * @param modelUuid 모델 UUID
   * @return PathInfo 또는 null
   */
  private PathInfo findActualBasePath(UUID modelUuid) {
    // 1순위: {uuid}-out 경로 확인
    String basePathWithSuffix = responseDir + "/" + modelUuid + "-out";
    Path pathWithSuffix = Paths.get(basePathWithSuffix);

    if (Files.exists(pathWithSuffix) && Files.isDirectory(pathWithSuffix)) {
      log.debug("경로 발견: {} (suffix -out 포함)", basePathWithSuffix);
      return new PathInfo(pathWithSuffix, basePathWithSuffix);
    }

    // 2순위: {uuid} 경로 확인 (suffix 없음)
    String basePathWithoutSuffix = responseDir + "/" + modelUuid;
    Path pathWithoutSuffix = Paths.get(basePathWithoutSuffix);

    if (Files.exists(pathWithoutSuffix) && Files.isDirectory(pathWithoutSuffix)) {
      log.debug("경로 발견: {} (suffix 없음)", basePathWithoutSuffix);
      return new PathInfo(pathWithoutSuffix, basePathWithoutSuffix);
    }

    // 둘 다 없으면 null 반환
    log.warn("경로를 찾을 수 없음: {} 또는 {}", basePathWithSuffix, basePathWithoutSuffix);
    return null;
  }

  /** 경로 정보를 담는 내부 클래스 */
  private static class PathInfo {
    Path basePath;
    String basePathString;

    PathInfo(Path basePath, String basePathString) {
      this.basePath = basePath;
      this.basePathString = basePathString;
    }
  }

  /** val.csv 파싱 결과를 담는 내부 클래스 */
  private static class ValMetricsData {
    TrainingMetricsDto.SummaryMetrics summary;
    List<TrainingMetricsDto.ClassMetrics> classes;

    ValMetricsData(
        TrainingMetricsDto.SummaryMetrics summary, List<TrainingMetricsDto.ClassMetrics> classes) {
      this.summary = summary;
      this.classes = classes;
    }
  }
}
