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

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.postgres.core.ModelTestMetricsJobCoreService;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.BestPthInfo;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ModelMetricJsonDto;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ModelTestFileName;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ResponsePathDto;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.SimpleMetricJsonDto;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class ModelTestMetricsJobService {

  private final ModelTestMetricsJobCoreService modelTestMetricsJobCoreService;

  @Value("${spring.profiles.active}")
  private String profile;

  // 학습 결과가 저장될 호스트 디렉토리
  @Value("${train.docker.response_dir}")
  private String responseDir;

  @Value("${file.pt-path}")
  private String ptPathDir;

  @Value("${file.pt-FileName}")
  private String ptFileName;

  /** 결과 csv 파일 정보 등록 */
  public void findTestValidMetricCsvFiles() {

    List<ResponsePathDto> modelIds =
        modelTestMetricsJobCoreService.getTestMetricSaveNotYetModelIds();

    if (modelIds.isEmpty()) {
      return;
    }

    for (ResponsePathDto modelInfo : modelIds) {
      createFile(modelInfo);
    }
  }

  /** 단건 결과 csv 파일 정보 등록 */
  public void testValidMetricCsvFiles(Long modelId) {

    ResponsePathDto model = modelTestMetricsJobCoreService.getTestMetricSaveNotYetModelId(modelId);

    if (model == null) {
      return;
    }

    createFile(model);
  }

  /**
   * 베스트 에폭 zip파일 생성, 테스트결과 db등록
   *
   * @param modelInfo 모델 정보 (modelId, responsePath, uuid)
   */
  private void createFile(ResponsePathDto modelInfo) {

    String testPath = responseDir + "/" + modelInfo.getUuid() + "/metrics/test.csv";
    try (BufferedReader reader =
        Files.newBufferedReader(Paths.get(testPath), StandardCharsets.UTF_8)) {

      CSVParser parser = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(reader);

      List<Object[]> batchArgs = new ArrayList<>();

      for (CSVRecord record : parser) {

        String model = record.get("model");
        long TP = Long.parseLong(record.get("TP"));
        long FP = Long.parseLong(record.get("FP"));
        long FN = Long.parseLong(record.get("FN"));
        float precision = Float.parseFloat(record.get("precision"));
        float recall = Float.parseFloat(record.get("recall"));
        float f1_score = Float.parseFloat(record.get("f1_score"));
        float accuracy = Float.parseFloat(record.get("accuracy"));
        float iou = Float.parseFloat(record.get("iou"));
        long detection_count = Long.parseLong(record.get("detection_count"));
        long gt_count = Long.parseLong(record.get("gt_count"));

        batchArgs.add(
            new Object[] {
              modelInfo.getModelId(),
              model,
              TP,
              FP,
              FN,
              precision,
              recall,
              f1_score,
              accuracy,
              iou,
              detection_count,
              gt_count
            });
      }

      modelTestMetricsJobCoreService.insertModelMetricsTest(batchArgs);

      // test.csv 파일 읽어서 저장한 여부로만 사용하기
      modelTestMetricsJobCoreService.updateModelMetricsTrainSaveYn(modelInfo.getModelId(), "step2");

    } catch (IOException e) {
      throw new RuntimeException(e);
    }

    // 패키징할 파일 만들기
    modelTestMetricsJobCoreService.updatePackingStart(modelInfo.getModelId(), ZonedDateTime.now());

    // 간단한 형식의 JSON 생성 (ZIP 파일용)
    SimpleMetricJsonDto jsonDto =
        modelTestMetricsJobCoreService.getSimpleTestMetricPackingInfo(modelInfo.getModelId());
    try {
      writeJsonFile(
          jsonDto,
          Paths.get(
              responseDir + "/" + modelInfo.getUuid() + "/" + jsonDto.getModelVersion() + ".json"));
    } catch (IOException e) {
      throw new RuntimeException(e);
    }

    Path responsePath = Paths.get(responseDir + "/" + modelInfo.getUuid());

    ModelTestFileName fileInfo =
        modelTestMetricsJobCoreService.findModelTestFileNames(modelInfo.getModelId());

    Path zipPath =
        Paths.get(
            responseDir + "/" + modelInfo.getUuid() + "/" + fileInfo.getModelVersion() + ".zip");
    Set<String> targetNames =
        Set.of(
            "model_config.py",
            fileInfo.getBestEpochFileName() + ".pth",
            fileInfo.getModelVersion() + ".json");

    try {
      List<Path> files = new ArrayList<>();
      try (Stream<Path> s = Files.list(responsePath)) {
        files.addAll(
            s.filter(Files::isRegularFile)
                .filter(p -> targetNames.contains(p.getFileName().toString()))
                .collect(Collectors.toList()));
      }

      // PT 파일 정확하게 조회
      Path ptFile = Paths.get(ptPathDir, ptFileName);
      if (Files.exists(ptFile)) {
        files.add(ptFile);
      } else {
        log.warn("PT 파일을 찾을 수 없습니다: {}", ptFile);
        throw new IOException("PT 파일 누락: " + ptFile);
      }

      // 기본 ZIP 생성
      zipFiles(files, zipPath);
      log.info(" 기본 ZIP 생성 완료: {}", zipPath.getFileName());

      // 개별 best*.pth ZIP 생성
      int individualZipCount = createIndividualBestPthZips(modelInfo, responsePath);

      // 모든 ZIP 생성 성공 시 COMPLETED
      modelTestMetricsJobCoreService.updatePackingEnd(
          modelInfo.getModelId(), ZonedDateTime.now(), TrainStatusType.COMPLETED.getId());

      log.info(" 전체 ZIP 생성 완료: 기본 1개 + 개별 {}개", individualZipCount);

    } catch (IOException e) {
      modelTestMetricsJobCoreService.updatePackingEnd(
          modelInfo.getModelId(), ZonedDateTime.now(), TrainStatusType.ERROR.getId());
      log.error("ZIP 생성 중 오류 발생: modelId={}", modelInfo.getModelId(), e);
      throw new RuntimeException(e);
    }
  }

  /**
   * Response 폴더의 모든 best*.pth 파일을 각각 개별 ZIP 파일로 생성
   *
   * <p>각 PTH 파일의 Epoch과 메트릭 타입을 파싱하여 해당 Epoch의 메트릭 정보를 조회한 후, 개별 JSON 파일을 생성하고 ZIP으로 패키징합니다.
   *
   * @param modelInfo 모델 정보
   * @param responsePath Response 디렉토리 경로
   * @return 생성된 개별 ZIP 파일 개수
   */
  private int createIndividualBestPthZips(ResponsePathDto modelInfo, Path responsePath) {

    log.info("=== 개별 best*.pth ZIP 파일 생성 시작: modelId={} ===", modelInfo.getModelId());

    int successCount = 0;

    try {
      // 1. Response 폴더에서 모든 best*.pth 파일 찾기
      List<Path> bestPthFiles;
      try (Stream<Path> stream = Files.list(responsePath)) {
        bestPthFiles =
            stream
                .filter(Files::isRegularFile)
                .filter(p -> p.getFileName().toString().startsWith("best"))
                .filter(p -> p.getFileName().toString().endsWith(".pth"))
                .collect(Collectors.toList());
      }

      if (bestPthFiles.isEmpty()) {
        log.warn("best*.pth 파일을 찾을 수 없습니다: path={}", responsePath);
        return 0;
      }

      log.info("발견된 best*.pth 파일 개수: {}", bestPthFiles.size());

      // 2. PT 파일 경로 확인 (모든 ZIP에 공통으로 포함)
      Path ptFile = Paths.get(ptPathDir, ptFileName);
      if (!Files.exists(ptFile)) {
        log.warn("PT 파일을 찾을 수 없습니다: {}", ptFile);
        return 0;
      }

      // model_config.py 경로 (선택적)
      Path modelConfigPath = responsePath.resolve("model_config.py");

      // 3. 각 best*.pth 파일별로 개별 ZIP 생성
      for (Path bestPthFile : bestPthFiles) {
        String pthFileName = bestPthFile.getFileName().toString();
        log.info("처리 중인 best PTH 파일: {}", pthFileName);

        Path individualJsonPath = null;

        try {
          // 파싱 성공 여부와 관계없이 기본 간단한 JSON 사용
          // (개별 ZIP도 동일한 간단한 형식 적용)
          log.debug("PTH 파일: {}, 간단한 JSON 형식 사용", pthFileName);

          SimpleMetricJsonDto simpleJsonDto =
              modelTestMetricsJobCoreService.getSimpleTestMetricPackingInfo(modelInfo.getModelId());

          if (simpleJsonDto == null) {
            log.warn("메트릭 정보 없음, 건너뜀: {}", pthFileName);
            continue;
          }

          // 3-2. 개별 JSON 파일 생성
          individualJsonPath = createIndividualJson(responsePath, pthFileName, simpleJsonDto);
          log.debug("개별 JSON 생성: file={}", individualJsonPath.getFileName());

          // 3-3. 개별 ZIP 파일명 생성
          // 형식: {modelVersion}.{pthFileNameWithoutExt}.zip
          // 예: G1_000001.best_fscore_5.zip, G1_000001.best_precision_7.zip
          String pthFileNameWithoutExt = pthFileName.replace(".pth", "");

          // modelVersion 조회 (JSON에서 추출)
          ModelTestFileName fileInfo =
              modelTestMetricsJobCoreService.findModelTestFileNames(modelInfo.getModelId());
          String individualZipName =
              fileInfo.getModelVersion() + "." + pthFileNameWithoutExt + ".zip";
          Path individualZipPath = responsePath.resolve(individualZipName);

          // 3-4. ZIP에 포함될 파일 목록 구성
          List<Path> zipFileList = new ArrayList<>();
          zipFileList.add(bestPthFile); // best*.pth 파일
          zipFileList.add(individualJsonPath); // 개별 JSON 파일
          zipFileList.add(ptFile); // PT 파일

          // model_config.py 파일이 있으면 추가
          if (Files.exists(modelConfigPath)) {
            zipFileList.add(modelConfigPath);
          }

          // 3-5. 개별 ZIP 생성
          zipFiles(zipFileList, individualZipPath);

          log.info(
              "개별 ZIP 생성 완료: fileName={}, pthFile={}, size={} bytes",
              individualZipName,
              pthFileName,
              Files.size(individualZipPath));

          // 3-6. 임시 JSON 파일 정리
          try {
            Files.deleteIfExists(individualJsonPath);
            log.debug("임시 JSON 파일 삭제: {}", individualJsonPath.getFileName());
          } catch (IOException deleteEx) {
            log.warn("임시 JSON 파일 삭제 실패: {}", individualJsonPath, deleteEx);
          }

          successCount++;

        } catch (Exception e) {
          log.error("개별 ZIP 생성 실패: pthFile={}", pthFileName, e);

          // 실패한 임시 JSON 파일도 정리 시도
          if (individualJsonPath != null) {
            try {
              Files.deleteIfExists(individualJsonPath);
            } catch (IOException deleteEx) {
              log.warn("실패한 임시 JSON 파일 삭제 실패: {}", individualJsonPath, deleteEx);
            }
          }

          // 개별 ZIP 실패는 전체 프로세스를 중단하지 않음
        }
      }

      log.info("=== 개별 best*.pth ZIP 파일 생성 완료: 성공 {}/{}개 ===", successCount, bestPthFiles.size());

    } catch (IOException e) {
      log.error("개별 ZIP 생성 중 오류 발생", e);
      // 에러 발생해도 기존 ZIP은 이미 생성되었으므로 예외를 던지지 않음
    }

    return successCount;
  }

  /**
   * PTH 파일명에서 Epoch과 메트릭 타입을 추출
   *
   * <p>지원되는 파일명 패턴:
   *
   * <ul>
   *   <li>best_{metricType}_{epoch}.pth (예: best_fscore_5.pth)
   *   <li>best_epoch_{epoch}.pth (예: best_epoch_10.pth)
   *   <li>best_changed_{metricType}_{epoch}.pth (예: best_changed_fscore_5.pth)
   * </ul>
   *
   * @param fileName PTH 파일명
   * @return 파싱된 PTH 정보, 실패 시 null
   */
  private BestPthInfo parsePthFileName(String fileName) {
    try {
      // 패턴 1: best_changed_{metricType}_{epoch}.pth
      Pattern pattern1 = Pattern.compile("best_changed_([a-z_]+)_(\\d+)\\.pth");
      Matcher matcher1 = pattern1.matcher(fileName);
      if (matcher1.matches()) {
        String metricType = matcher1.group(1); // "fscore", "precision", "recall"
        Integer epoch = Integer.parseInt(matcher1.group(2));
        Path filePath = Paths.get(fileName);
        return new BestPthInfo(fileName, metricType, epoch, filePath);
      }

      // 패턴 2: best_{metricType}_{epoch}.pth
      Pattern pattern2 = Pattern.compile("best_([a-z_]+)_(\\d+)\\.pth");
      Matcher matcher2 = pattern2.matcher(fileName);
      if (matcher2.matches()) {
        String metricType = matcher2.group(1); // "fscore", "precision", "recall"
        Integer epoch = Integer.parseInt(matcher2.group(2));
        Path filePath = Paths.get(fileName);
        return new BestPthInfo(fileName, metricType, epoch, filePath);
      }

      // 패턴 3: best_epoch_{epoch}.pth (메트릭 타입 없음)
      Pattern pattern3 = Pattern.compile("best_epoch_(\\d+)\\.pth");
      Matcher matcher3 = pattern3.matcher(fileName);
      if (matcher3.matches()) {
        Integer epoch = Integer.parseInt(matcher3.group(1));
        Path filePath = Paths.get(fileName);
        // 메트릭 타입을 "epoch"로 설정 (기본값)
        return new BestPthInfo(fileName, "epoch", epoch, filePath);
      }

      log.warn("알 수 없는 PTH 파일명 패턴: {}", fileName);
      return null;

    } catch (Exception e) {
      log.error("PTH 파일명 파싱 중 오류: {}", fileName, e);
      return null;
    }
  }

  /**
   * 개별 JSON 파일 생성
   *
   * @param responsePath Response 디렉토리 경로
   * @param pthFileName PTH 파일명
   * @param jsonDto JSON 메타데이터
   * @return 생성된 JSON 파일 경로
   * @throws IOException JSON 쓰기 실패 시
   */
  private Path createIndividualJson(
      Path responsePath, String pthFileName, ModelMetricJsonDto jsonDto) throws IOException {
    String individualJsonName = pthFileName.replace(".pth", ".json");
    Path individualJsonPath = responsePath.resolve(individualJsonName);
    writeJsonFile(jsonDto, individualJsonPath);
    return individualJsonPath;
  }

  /**
   * 개별 JSON 파일 생성 (간단한 형식)
   *
   * @param responsePath Response 디렉토리 경로
   * @param pthFileName PTH 파일명
   * @param jsonDto 간단한 JSON 메타데이터
   * @return 생성된 JSON 파일 경로
   * @throws IOException JSON 쓰기 실패 시
   */
  private Path createIndividualJson(
      Path responsePath, String pthFileName, SimpleMetricJsonDto jsonDto) throws IOException {
    String individualJsonName = pthFileName.replace(".pth", ".json");
    Path individualJsonPath = responsePath.resolve(individualJsonName);
    writeJsonFile(jsonDto, individualJsonPath);
    return individualJsonPath;
  }

  private void writeJsonFile(Object data, Path outputPath) throws IOException {

    Path parent = outputPath.getParent();

    if (parent != null) {
      Files.createDirectories(parent);
    }

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.enable(SerializationFeature.INDENT_OUTPUT);

    try (OutputStream os =
        Files.newOutputStream(
            outputPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
      objectMapper.writeValue(os, data);
    }
  }

  private void zipFiles(List<Path> files, Path zipPath) throws IOException {

    Path parent = zipPath.getParent();
    if (parent != null) {
      Files.createDirectories(parent);
    }

    try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipPath))) {

      for (Path file : files) {

        ZipEntry entry = new ZipEntry(file.getFileName().toString());
        zos.putNextEntry(entry);

        Files.copy(file, zos);

        zos.closeEntry();
      }
    }
  }
}
