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

import com.kamco.cd.training.common.download.RangeDownloadResponder;
import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.SelectTransferDataSet;
import com.kamco.cd.training.model.dto.ModelConfigDto;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.DetailSummary;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.HyperSummary;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.MappingDataset;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelBestEpoch;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelFileInfo;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelTestMetrics;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelTrainMetrics;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ModelValidationMetrics;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferDetailDto;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.TransferHyperSummary;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ZipFileInfo;
import com.kamco.cd.training.model.dto.ModelTrainDetailDto.ZipFileListResponse;
import com.kamco.cd.training.model.dto.ModelTrainMngDto;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.Basic;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.CleanupResult;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.ModelProgressStepDto;
import com.kamco.cd.training.postgres.core.ModelTrainDetailCoreService;
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ModelTrainDetailService {

  private final ModelTrainDetailCoreService modelTrainDetailCoreService;
  private final ModelTrainMngCoreService mngCoreService;
  private final RangeDownloadResponder rangeDownloadResponder;

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

  /**
   * 모델 상세정보 요약
   *
   * @param uuid
   * @return
   */
  public DetailSummary getModelDetailSummary(UUID uuid) {
    return modelTrainDetailCoreService.getModelDetailSummary(uuid);
  }

  /**
   * 모델 하이퍼파라미터 요약
   *
   * @param uuid
   * @return
   */
  public HyperSummary getByModelHyperParamSummary(UUID uuid) {
    return modelTrainDetailCoreService.getByModelHyperParamSummary(uuid);
  }

  public List<MappingDataset> getByModelMappingDataset(UUID uuid) {
    return modelTrainDetailCoreService.getByModelMappingDataset(uuid);
  }

  public Basic findByModelByUUID(UUID uuid) {
    return modelTrainDetailCoreService.findByModelByUUID(uuid);
  }

  // TODO 미사용시작
  /**
   * 전이학습 모델선택 정보
   *
   * @param uuid
   * @return
   */
  public TransferDetailDto getTransferDetail(UUID uuid) {
    Basic modelInfo = modelTrainDetailCoreService.findByModelByUUID(uuid);

    // config 정보 조회
    ModelConfigDto.TransferBasic configInfo = mngCoreService.findModelTransferConfigByModelId(uuid);

    // 하이파라미터 정보 조회
    TransferHyperSummary hyperSummary = modelTrainDetailCoreService.getTransferHyperSummary(uuid);
    List<SelectTransferDataSet> dataSets = new ArrayList<>();

    DatasetReq datasetReq = new DatasetReq();
    List<Long> datasetIds = new ArrayList<>();

    List<MappingDataset> mappingDatasets =
        modelTrainDetailCoreService.getByModelMappingDataset(uuid);
    for (MappingDataset mappingDataset : mappingDatasets) {
      datasetIds.add(mappingDataset.getDatasetId());
    }
    datasetReq.setIds(datasetIds);
    datasetReq.setModelNo(modelInfo.getModelNo());

    if (modelInfo.getModelNo().equals(ModelType.G1.getId())) {
      dataSets = mngCoreService.getDatasetTransferSelectG1List(modelInfo.getId());
    } else {
      dataSets =
          mngCoreService.getDatasetTransferSelectG2G3List(
              modelInfo.getId(), modelInfo.getModelNo());
    }

    //    DatasetReq beforeDatasetReq = new DatasetReq();
    //    List<Long> beforeDatasetIds = new ArrayList<>();
    //    List<SelectDataSet> beforeDataSets = new ArrayList<>();
    //
    //    Long beforeModelId = modelInfo.getBeforeModelId();
    //    if (beforeModelId != null) {
    //      Basic beforeInfo = modelTrainDetailCoreService.findByModelBeforeId(beforeModelId);
    //      List<MappingDataset> beforeDatasets =
    //          modelTrainDetailCoreService.getByModelMappingDataset(beforeInfo.getUuid());
    //
    //      for (MappingDataset before : beforeDatasets) {
    //        beforeDatasetIds.add(before.getDatasetId());
    //      }
    //      beforeDatasetReq.setIds(beforeDatasetIds);
    //      beforeDatasetReq.setModelNo(modelInfo.getModelNo());
    //
    //      if (beforeInfo.getModelNo().equals(ModelType.G1.getId())) {
    //        beforeDataSets = mngCoreService.getDatasetSelectG1List(beforeDatasetReq);
    //      } else {
    //        beforeDataSets = mngCoreService.getDatasetSelectG2G3List(beforeDatasetReq);
    //      }
    //    }

    TransferDetailDto transferDetailDto = new TransferDetailDto();
    transferDetailDto.setEtcConfig(configInfo);
    transferDetailDto.setModelTrainHyper(hyperSummary);
    transferDetailDto.setModelTrainDataset(dataSets);

    return transferDetailDto;
  }

  // TODO 미사용 끝

  public List<ModelTrainMetrics> getModelTrainMetricResult(UUID uuid) {
    return modelTrainDetailCoreService.getModelTrainMetricResult(uuid);
  }

  public List<ModelValidationMetrics> getModelValidationMetricResult(UUID uuid) {
    return modelTrainDetailCoreService.getModelValidationMetricResult(uuid);
  }

  public List<ModelTestMetrics> getModelTestMetricResult(UUID uuid) {
    return modelTrainDetailCoreService.getModelTestMetricResult(uuid);
  }

  public ModelBestEpoch getModelTrainBestEpoch(UUID uuid) {
    return modelTrainDetailCoreService.getModelTrainBestEpoch(uuid);
  }

  public ModelFileInfo getModelTrainFileInfo(UUID uuid) {
    return modelTrainDetailCoreService.getModelTrainFileInfo(uuid);
  }

  public List<ModelProgressStepDto> findModelTrainProgressInfo(UUID uuid) {
    return modelTrainDetailCoreService.findModelTrainProgressInfo(uuid);
  }

  /**
   * 삭제될 파일목록 및 유지될 파일 목록
   *
   * @param uuid
   * @return
   */
  public CleanupResult previewCleanup(UUID uuid) {

    CleanupResult result = new CleanupResult();

    // ===== 모델 조회 =====
    ModelTrainMngDto.Basic model = modelTrainDetailCoreService.findByModelByUUID(uuid);

    if (model == null) {
      throw new CustomApiException(
          "NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다. UUID: " + uuid);
    }

    Path dir = Paths.get(responseDir, model.getUuid().toString()).toAbsolutePath().normalize();

    if (!Files.exists(dir) || !Files.isDirectory(dir)) {
      throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "디렉토리가 없습니다.");
    }

    if (!Files.isReadable(dir)) {
      throw new CustomApiException("FORBIDDEN", HttpStatus.FORBIDDEN, "디렉토리 읽기 권한이 없습니다.");
    }

    try (Stream<Path> stream = Files.list(dir)) {

      List<Path> files = stream.toList();

      if (files.isEmpty()) {
        throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "파일이 없습니다.");
      }

      // ===== keep 파일 찾기 =====
      Path keep =
          files.stream()
              .filter(
                  p -> {
                    String name = p.getFileName().toString();
                    return name.endsWith(".zip") && name.contains(model.getUuid().toString());
                  })
              .findFirst()
              .orElseThrow(
                  () ->
                      new CustomApiException(
                          "NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "zip 파일이 없습니다."));

      log.info("유지 파일: {}", keep.getFileName());

      // ===== 결과 세팅 =====
      result.setTotalCount(files.size());
      result.setKeptFile(keep.getFileName().toString());

      // ===== 삭제 대상 =====
      List<String> deleteTargets =
          files.stream()
              .filter(
                  p -> !p.toAbsolutePath().normalize().equals(keep.toAbsolutePath().normalize()))
              .map(p -> p.getFileName().toString())
              .toList();

      result.setDeleteTargets(deleteTargets);

      log.info(
          "previewCleanup 완료. total={}, deleteTargets={}",
          result.getTotalCount(),
          deleteTargets.size());

      return result;

    } catch (IOException e) {
      log.error("파일 목록 조회 실패: {}", dir, e);
      throw new CustomApiException(
          "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "파일 목록 조회 실패");
    }
  }

  public CleanupResult cleanup(UUID uuid) {
    // ===== 모델 조회 =====
    ModelTrainMngDto.Basic model = modelTrainDetailCoreService.findByModelByUUID(uuid);

    if (model == null) {
      throw new CustomApiException(
          "NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다. UUID: " + uuid);
    }

    if (!TrainStatusType.COMPLETED.getId().equals(model.getStep2Status())) {
      throw new CustomApiException("CONFLICT", HttpStatus.CONFLICT, "테스트가 완료되지 않았습니다.");
    }

    // ===== 경로 =====
    Path dir = Paths.get(responseDir, model.getUuid().toString()).toAbsolutePath().normalize();

    return executeCleanup(model, dir);
  }

  /**
   * 베스트 에폭 제외 파일 삭제, 베스트 에폭 zip 파일만 남김
   *
   * @param model model 정보
   * @param dir response 폴더 경로
   * @return 삭제 정보
   */
  public CleanupResult executeCleanup(ModelTrainMngDto.Basic model, Path dir) {
    CleanupResult result = new CleanupResult();

    if (!Files.exists(dir) || !Files.isDirectory(dir)) {
      throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "디렉토리가 없습니다.");
    }

    if (!Files.isReadable(dir)) {
      throw new CustomApiException("FORBIDDEN", HttpStatus.FORBIDDEN, "디렉토리 읽기 권한이 없습니다.");
    }

    if (!Files.isWritable(dir)) {
      throw new CustomApiException("FORBIDDEN", HttpStatus.FORBIDDEN, "디렉토리 삭제 권한이 없습니다.");
    }

    int bestEpoch = model.getBestEpoch();

    if (bestEpoch <= 0) {
      throw new CustomApiException(
          "BAD_REQUEST", HttpStatus.BAD_REQUEST, "잘못된 bestEpoch 값 입니다. : " + bestEpoch);
    }

    log.info("cleanup 시작. dir={}, bestEpoch={}", dir, bestEpoch);

    try (Stream<Path> stream = Files.list(dir)) {

      List<Path> files = stream.toList();

      if (files.isEmpty()) {
        throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "파일이 없습니다.");
      }

      // ===== keep 파일 찾기 =====
      Path keep = null;

      for (Path p : files) {
        String name = p.getFileName().toString();
        if (name.endsWith(".zip") && name.contains(model.getUuid().toString())) {
          keep = p;
          break;
        }
      }

      if (keep == null) {
        throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "zip 파일이 없습니다.");
      }

      log.info("유지 파일: {}", keep.getFileName());

      result.setTotalCount(files.size());
      result.setKeptFile(keep.getFileName().toString());

      int deletedCount = 0;
      List<String> failed = new ArrayList<>();

      // ===== 삭제 =====
      for (Path p : files) {

        if (p.equals(keep)) {
          continue;
        }

        try {

          // 심볼릭 링크 → 링크만 삭제
          if (Files.isSymbolicLink(p)) {
            Files.deleteIfExists(p);
            log.info("심볼릭 링크 삭제: {}", p.getFileName());
          }

          // 디렉토리 → 재귀 삭제
          else if (Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) {
            log.info("디렉토리 재귀 삭제: {}", p.getFileName());
            deleteDirectory(p);
          }

          // 일반 파일
          else {
            Files.deleteIfExists(p);
            log.info("파일 삭제: {}", p.getFileName());
          }

          deletedCount++;

        } catch (AccessDeniedException e) {
          failed.add(p.getFileName().toString());
          log.error("권한 없음: {}", p.getFileName(), e);

        } catch (IOException e) {
          failed.add(p.getFileName().toString());
          log.error("삭제 실패: {}", p.getFileName(), e);
        }
      }

      result.setDeletedCount(deletedCount);
      result.setFailedCount(failed.size());
      result.setFailedFiles(failed);

    } catch (IOException e) {
      log.error("파일 목록 조회 실패: {}", dir, e);
      throw new CustomApiException(
          "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "파일 목록 조회 실패");
    }

    log.info(
        "cleanup 완료. total={}, deleted={}, failed={}",
        result.getTotalCount(),
        result.getDeletedCount(),
        result.getFailedCount());

    return result;
  }

  // 디렉토리 재귀 삭제
  private void deleteDirectory(Path dir) throws IOException {

    if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
      return;
    }

    // dir 자체가 심볼릭 링크면 링크만 삭제
    if (Files.isSymbolicLink(dir)) {
      Files.delete(dir);
      return;
    }

    Files.walkFileTree(
        dir,
        EnumSet.noneOf(FileVisitOption.class), // NOFOLLOW_LINKS
        Integer.MAX_VALUE,
        new SimpleFileVisitor<>() {

          @Override
          public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
              throws IOException {

            Files.delete(file); // 링크면 링크만 삭제됨
            return FileVisitResult.CONTINUE;
          }

          @Override
          public FileVisitResult postVisitDirectory(Path directory, IOException exc)
              throws IOException {

            if (exc != null) throw exc;

            Files.delete(directory);
            return FileVisitResult.CONTINUE;
          }
        });
  }

  /**
   * 모델 UUID로 ZIP 파일 목록 조회 및 전체 URL 다운로드 링크 생성
   *
   * @param uuid 모델 UUID
   * @param downloadUuid 다운로드 추적 UUID
   * @param request HTTP 요청
   * @return ZIP 파일 목록 및 전체 URL 다운로드 링크
   */
  public ZipFileListResponse getZipFileListWithFullUrl(
      UUID uuid, String downloadUuid, HttpServletRequest request) {
    log.info("=== ZIP 파일 목록 조회 시작: modelUuid={}, downloadUuid={} ===", uuid, downloadUuid);

    // 1. 모델 정보 조회
    Basic modelInfo;
    try {
      modelInfo = findByModelByUUID(uuid);
      if (modelInfo == null) {
        log.warn("모델을 찾을 수 없음: modelUuid={}", uuid);
        throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid);
      }
      log.debug(
          "모델 정보 조회 성공: modelNo={}, modelVer={}", modelInfo.getModelNo(), modelInfo.getModelVer());
    } catch (NullPointerException e) {
      log.error("모델 조회 중 NullPointerException 발생: modelUuid={}", uuid, e);
      throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid);
    } catch (CustomApiException e) {
      // CustomApiException은 그대로 재throw
      throw e;
    } catch (Exception e) {
      log.error("모델 조회 중 예상치 못한 오류 발생: modelUuid={}", uuid, e);
      throw new CustomApiException(
          "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "모델 조회 중 오류가 발생했습니다.");
    }

    // 2. 실제 디렉토리 경로 찾기 (uuid 또는 uuid-out)
    Path baseDir;
    try {
      baseDir = findActualBasePath(uuid);

      if (baseDir == null || !Files.exists(baseDir)) {
        log.warn(
            "모델 결과 디렉토리를 찾을 수 없음: modelUuid={}, 시도한 경로: {} 또는 {}-out",
            uuid,
            responseDir + "/" + uuid,
            responseDir + "/" + uuid);
        throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델 결과 디렉토리가 존재하지 않습니다.");
      }

      log.debug("디렉토리 발견: basePath={}", baseDir.toString());
    } catch (CustomApiException e) {
      // CustomApiException은 그대로 재throw
      throw e;
    } catch (Exception e) {
      log.error("디렉토리 경로 확인 중 오류 발생: modelUuid={}", uuid, e);
      throw new CustomApiException(
          "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "디렉토리 경로 확인 중 오류가 발생했습니다.");
    }

    // 요청에서 도메인 정보 추출
    String scheme = request.getScheme(); // http 또는 https
    String serverName = request.getServerName(); // localhost 또는 도메인
    int serverPort = request.getServerPort(); // 8080 등
    String contextPath = request.getContextPath(); // /api 등

    // 기본 URL 구성
    String baseUrl;
    if ((scheme.equals("http") && serverPort == 80)
        || (scheme.equals("https") && serverPort == 443)) {
      baseUrl = scheme + "://" + serverName + contextPath;
    } else {
      baseUrl = scheme + "://" + serverName + ":" + serverPort + contextPath;
    }

    log.debug("다운로드 URL 베이스: {}", baseUrl);

    // 3. ZIP 파일 목록 검색
    List<ZipFileInfo> zipFiles = new ArrayList<>();
    long totalSize = 0L;

    try (Stream<Path> stream = Files.list(baseDir)) {
      List<Path> files =
          stream
              .filter(Files::isRegularFile)
              .filter(p -> p.getFileName().toString().endsWith(".zip"))
              .filter(p -> p.getFileName().toString().contains(uuid.toString()))
              .sorted(
                  Comparator.comparing(p -> p.getFileName().toString(), Comparator.reverseOrder()))
              .toList();

      log.debug("ZIP 파일 필터링 완료: 발견된 파일 개수={}", files.size());

      for (Path file : files) {
        try {
          String fileName = file.getFileName().toString();
          long fileSize = Files.size(file);
          totalSize += fileSize;

          // 파일명에서 버전 추출
          String version = extractVersionFromZipFileName(fileName);
          boolean isCurrent = version.equals(modelInfo.getModelVer());

          // 전체 도메인 URL 포함한 다운로드 링크 생성
          String downloadUrl = baseUrl + "/api/models/download/" + uuid + "?file=" + fileName;

          zipFiles.add(
              ZipFileInfo.builder()
                  .fileName(fileName)
                  .filePath(file.toString())
                  .version(version)
                  .fileSize(fileSize)
                  .fileSizeFormatted(formatFileSize(fileSize))
                  .lastModified(
                      Files.getLastModifiedTime(file).toInstant().atZone(ZoneId.systemDefault()))
                  .isCurrent(isCurrent)
                  .downloadUrl(downloadUrl)
                  .build());

          log.debug(
              "ZIP 파일 정보 추가: fileName={}, size={}, version={}, isCurrent={}",
              fileName,
              formatFileSize(fileSize),
              version,
              isCurrent);

        } catch (IOException e) {
          log.warn("ZIP 파일 정보 조회 실패 (건너뜀): file={}", file.getFileName(), e);
          // 개별 파일 실패 시 전체 프로세스를 중단하지 않고 계속 진행
        } catch (Exception e) {
          log.error("ZIP 파일 정보 처리 중 예상치 못한 오류 (건너뜀): file={}", file.getFileName(), e);
          // 예상치 못한 오류도 전체 프로세스를 중단하지 않음
        }
      }

      log.info(" ZIP 파일 목록 조회 완료: 총 {}개 파일, 전체 크기={}", zipFiles.size(), formatFileSize(totalSize));

    } catch (IOException e) {
      log.error("ZIP 파일 목록 조회 중 IO 오류 발생: basePath={}", baseDir, e);
      throw new CustomApiException(
          "INTERNAL_SERVER_ERROR",
          HttpStatus.INTERNAL_SERVER_ERROR,
          "ZIP 파일 목록 조회 중 IO 오류가 발생했습니다.");
    } catch (Exception e) {
      log.error("ZIP 파일 목록 조회 중 예상치 못한 오류 발생: basePath={}", baseDir, e);
      throw new CustomApiException(
          "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "ZIP 파일 목록 조회 중 오류가 발생했습니다.");
    }

    return ZipFileListResponse.builder()
        .modelUuid(uuid.toString())
        .modelNo(modelInfo.getModelNo())
        .modelVer(modelInfo.getModelVer())
        .basePath(baseDir.toString())
        .zipFiles(zipFiles)
        .totalFiles(zipFiles.size())
        .totalSize(totalSize)
        .totalSizeFormatted(formatFileSize(totalSize))
        .build();
  }

  /**
   * 모델 UUID로 ZIP 파일 목록 조회 및 다운로드 링크 생성
   *
   * @param uuid 모델 UUID
   * @param downloadUuid 다운로드 추적 UUID
   * @return ZIP 파일 목록 및 다운로드 링크
   */
  public ZipFileListResponse getZipFileList(UUID uuid, String downloadUuid) {
    log.info("ZIP 파일 목록 조회 시작: modelUuid={}, downloadUuid={}", uuid, downloadUuid);

    // 1. 모델 정보 조회
    Basic modelInfo;
    try {
      modelInfo = findByModelByUUID(uuid);
      if (modelInfo == null) {
        throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid);
      }
    } catch (NullPointerException e) {
      log.error("모델 조회 실패: {}", uuid, e);
      throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid);
    }

    // 2. 실제 디렉토리 경로 찾기 (uuid 또는 uuid-out)
    Path baseDir = findActualBasePath(uuid);

    if (baseDir == null || !Files.exists(baseDir)) {
      log.warn(
          "디렉토리를 찾을 수 없음: modelUuid={}, 시도한 경로: {} 또는 {}-out",
          uuid,
          responseDir + "/" + uuid,
          responseDir + "/" + uuid);
      throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델 결과 디렉토리가 존재하지 않습니다.");
    }

    log.info("디렉토리 발견: basePath={}", baseDir.toString());

    // 3. ZIP 파일 목록 검색
    List<ZipFileInfo> zipFiles = new ArrayList<>();
    long totalSize = 0L;

    try (Stream<Path> stream = Files.list(baseDir)) {
      List<Path> files =
          stream
              .filter(Files::isRegularFile)
              .filter(p -> p.getFileName().toString().endsWith(".zip"))
              .filter(p -> p.getFileName().toString().contains(uuid.toString()))
              .sorted(
                  Comparator.comparing(p -> p.getFileName().toString(), Comparator.reverseOrder()))
              .toList();

      for (Path file : files) {
        String fileName = file.getFileName().toString();
        long fileSize = Files.size(file);
        totalSize += fileSize;

        // 파일명에서 버전 추출
        String version = extractVersionFromZipFileName(fileName);
        boolean isCurrent = version.equals(modelInfo.getModelVer());

        // 다운로드 URL 생성
        String downloadUrl = String.format("/api/models/download/%s?file=%s", uuid, fileName);

        zipFiles.add(
            ZipFileInfo.builder()
                .fileName(fileName)
                .filePath(file.toString())
                .version(version)
                .fileSize(fileSize)
                .fileSizeFormatted(formatFileSize(fileSize))
                .lastModified(
                    Files.getLastModifiedTime(file).toInstant().atZone(ZoneId.systemDefault()))
                .isCurrent(isCurrent)
                .downloadUrl(downloadUrl)
                .build());
      }

      log.info("ZIP 파일 {}개 발견", zipFiles.size());

    } catch (IOException e) {
      log.error("ZIP 파일 목록 조회 실패: {}", baseDir, e);
      throw new CustomApiException(
          "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "ZIP 파일 목록 조회 실패");
    }

    return ZipFileListResponse.builder()
        .modelUuid(uuid.toString())
        .modelNo(modelInfo.getModelNo())
        .modelVer(modelInfo.getModelVer())
        .basePath(baseDir.toString())
        .zipFiles(zipFiles)
        .totalFiles(zipFiles.size())
        .totalSize(totalSize)
        .totalSizeFormatted(formatFileSize(totalSize))
        .build();
  }

  /**
   * 실제 디렉토리 경로 찾기 (uuid 또는 uuid-out)
   *
   * @param uuid 모델 UUID
   * @return 실제 경로 또는 null
   */
  private Path findActualBasePath(UUID uuid) {
    // 1순위: {uuid}-out
    Path pathWithSuffix = Paths.get(responseDir, uuid + "-out");
    if (Files.exists(pathWithSuffix) && Files.isDirectory(pathWithSuffix)) {
      log.debug("경로 발견: {} (suffix -out 포함)", pathWithSuffix);
      return pathWithSuffix;
    }

    // 2순위: {uuid}
    Path pathWithoutSuffix = Paths.get(responseDir, uuid.toString());
    if (Files.exists(pathWithoutSuffix) && Files.isDirectory(pathWithoutSuffix)) {
      log.debug("경로 발견: {} (suffix 없음)", pathWithoutSuffix);
      return pathWithoutSuffix;
    }

    return null;
  }

  /**
   * ZIP 파일명에서 버전 추출 예: G2.G2_000001.{uuid}.zip → G2_000001
   *
   * @param fileName ZIP 파일명
   * @return 모델 버전
   */
  private String extractVersionFromZipFileName(String fileName) {
    // 패턴: {modelNo}.{modelVer}.{uuid}.zip
    // 예: G2.G2_000001.d7dd54e9-1f20-46a7-9c26-89287a7d61b0.zip

    String[] parts = fileName.split("\\.");

    // parts[0] = G2 (modelNo)
    // parts[1] = G2_000001 (modelVer)
    // parts[2-7] = uuid parts
    // parts[8] = zip

    if (parts.length >= 2) {
      return parts[1]; // G2_000001
    }

    // fallback: 전체 파일명에서 .zip 제거
    return fileName.replace(".zip", "");
  }

  /**
   * 파일 크기를 읽기 쉬운 형식으로 변환
   *
   * @param size 파일 크기 (bytes)
   * @return 포맷된 문자열 (예: "1.26 GB")
   */
  private String formatFileSize(long size) {
    if (size < 1024) {
      return size + " B";
    } else if (size < 1024 * 1024) {
      return String.format("%.2f KB", size / 1024.0);
    } else if (size < 1024 * 1024 * 1024) {
      return String.format("%.2f MB", size / (1024.0 * 1024.0));
    } else {
      return String.format("%.2f GB", size / (1024.0 * 1024.0 * 1024.0));
    }
  }

  /**
   * 모델 UUID로 모든 ZIP 파일 다운로드 또는 목록 조회
   *
   * @param uuid 모델 UUID
   * @param downloadUuid 다운로드 추적 UUID
   * @param fileName 개별 파일명 (선택)
   * @param accept Accept 헤더 (application/json 또는 application/octet-stream)
   * @param request HTTP 요청
   * @return JSON 응답 또는 ZIP 파일 Binary 응답
   */
  public ResponseEntity<?> downloadZipFile(
      UUID uuid, String downloadUuid, String fileName, String accept, HttpServletRequest request)
      throws IOException {
    log.info(
        "ZIP 파일 다운로드/조회 시작: modelUuid={}, downloadUuid={}, file={}, accept={}",
        uuid,
        downloadUuid,
        fileName,
        accept);

    // 1. 모델 정보 조회
    Basic modelInfo;
    try {
      modelInfo = findByModelByUUID(uuid);
      if (modelInfo == null) {
        throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid);
      }
    } catch (NullPointerException e) {
      log.warn("모델 조회 실패: {}", uuid);
      throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다: " + uuid);
    }

    // 2. 실제 디렉토리 경로 찾기 (uuid 또는 uuid-out)
    Path baseDir = findActualBasePath(uuid);

    if (baseDir == null || !Files.exists(baseDir)) {
      log.warn(
          "디렉토리를 찾을 수 없음: modelUuid={}, 시도한 경로: {} 또는 {}-out",
          uuid,
          responseDir + "/" + uuid,
          responseDir + "/" + uuid);
      throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델 결과 디렉토리가 존재하지 않습니다.");
    }

    log.info("디렉토리 발견: basePath={}", baseDir);

    // 3. 모든 ZIP 파일 찾기
    List<Path> zipFiles = findAllZipFiles(baseDir, uuid);

    if (zipFiles.isEmpty()) {
      log.warn("ZIP 파일을 찾을 수 없음: modelUuid={}, basePath={}", uuid, baseDir);
      throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "ZIP 파일을 찾을 수 없습니다.");
    }

    log.info(
        "ZIP 파일 {}개 발견: basePath={}, files={}",
        zipFiles.size(),
        baseDir,
        zipFiles.stream().map(p -> p.getFileName().toString()).toList());

    // 4. Accept 헤더에 따라 분기
    if (accept != null && accept.contains("application/json")) {
      // JSON 응답: ZIP 파일 목록과 다운로드 링크 반환
      log.info("JSON 응답 모드: ZIP 파일 목록 반환");
      return ResponseEntity.ok(buildZipFileListResponse(modelInfo, baseDir, zipFiles, request));
    }

    // 5. Binary 응답: 파일 다운로드
    return handleBinaryDownload(modelInfo, baseDir, zipFiles, fileName, uuid, request);
  }

  /**
   * ZIP 파일 목록 응답 생성 (JSON)
   *
   * @param modelInfo 모델 정보
   * @param baseDir 기본 디렉토리
   * @param zipFiles ZIP 파일 목록
   * @param request HTTP 요청
   * @return ZipFileListResponse
   */
  private ZipFileListResponse buildZipFileListResponse(
      Basic modelInfo, Path baseDir, List<Path> zipFiles, HttpServletRequest request)
      throws IOException {

    // 요청에서 도메인 정보 추출
    String scheme = request.getScheme(); // http 또는 https
    String serverName = request.getServerName(); // localhost 또는 도메인
    int serverPort = request.getServerPort(); // 8080 등
    String contextPath = request.getContextPath(); // /api 등

    // 기본 URL 구성
    String baseUrl;
    if ((scheme.equals("http") && serverPort == 80)
        || (scheme.equals("https") && serverPort == 443)) {
      baseUrl = scheme + "://" + serverName + contextPath;
    } else {
      baseUrl = scheme + "://" + serverName + ":" + serverPort + contextPath;
    }

    List<ZipFileInfo> zipFileInfos = new ArrayList<>();
    long totalSize = 0L;

    for (Path zipFile : zipFiles) {
      String zipFileName = zipFile.getFileName().toString();
      long fileSize = Files.size(zipFile);
      totalSize += fileSize;

      // 파일명에서 버전 추출
      String version = extractVersionFromZipFileName(zipFileName);
      boolean isCurrent = version.equals(modelInfo.getModelVer());

      // 전체 도메인 URL 포함한 다운로드 링크 생성
      String downloadUrl =
          baseUrl + "/api/models/downloadzip/" + modelInfo.getUuid() + "?file=" + zipFileName;

      zipFileInfos.add(
          ZipFileInfo.builder()
              .fileName(zipFileName)
              .filePath(zipFile.toString())
              .version(version)
              .fileSize(fileSize)
              .fileSizeFormatted(formatFileSize(fileSize))
              .lastModified(
                  Files.getLastModifiedTime(zipFile).toInstant().atZone(ZoneId.systemDefault()))
              .isCurrent(isCurrent)
              .downloadUrl(downloadUrl)
              .build());
    }

    return ZipFileListResponse.builder()
        .modelUuid(modelInfo.getUuid().toString())
        .modelNo(modelInfo.getModelNo())
        .modelVer(modelInfo.getModelVer())
        .basePath(baseDir.toString())
        .zipFiles(zipFileInfos)
        .totalFiles(zipFileInfos.size())
        .totalSize(totalSize)
        .totalSizeFormatted(formatFileSize(totalSize))
        .build();
  }

  /**
   * Binary 다운로드 처리
   *
   * @param modelInfo 모델 정보
   * @param baseDir 기본 디렉토리
   * @param zipFiles ZIP 파일 목록
   * @param fileName 개별 파일명 (선택)
   * @param uuid 모델 UUID
   * @param request HTTP 요청
   * @return Binary 응답
   */
  private ResponseEntity<?> handleBinaryDownload(
      Basic modelInfo,
      Path baseDir,
      List<Path> zipFiles,
      String fileName,
      UUID uuid,
      HttpServletRequest request)
      throws IOException {

    // file 파라미터가 있으면 개별 파일 다운로드
    if (fileName != null && !fileName.isEmpty()) {
      log.info("개별 파일 다운로드 요청: fileName={}", fileName);

      Path targetFile =
          zipFiles.stream()
              .filter(p -> p.getFileName().toString().equals(fileName))
              .findFirst()
              .orElse(null);

      if (targetFile == null) {
        log.warn("요청한 파일을 찾을 수 없음: fileName={}, basePath={}", fileName, baseDir);
        throw new CustomApiException(
            "NOT_FOUND", HttpStatus.NOT_FOUND, "요청한 파일을 찾을 수 없습니다: " + fileName);
      }

      log.info(
          "개별 ZIP 파일 다운로드: fileName={}, fileSize={} bytes, basePath={}",
          targetFile.getFileName(),
          Files.size(targetFile),
          baseDir);

      return rangeDownloadResponder.buildZipResponse(targetFile, fileName, request);
    }

    // file 파라미터가 없으면 모든 파일 다운로드
    // 파일이 1개면 바로 다운로드
    if (zipFiles.size() == 1) {
      Path zipPath = zipFiles.get(0);
      log.info(
          "ZIP 파일 1개 다운로드: fileName={}, fileSize={} bytes, basePath={}",
          zipPath.getFileName(),
          Files.size(zipPath),
          baseDir);
      String downloadFileName = zipPath.getFileName().toString();
      return rangeDownloadResponder.buildZipResponse(zipPath, downloadFileName, request);
    }

    // 파일이 여러 개면 하나의 zip으로 묶어서 다운로드
    log.info("여러 ZIP 파일을 하나로 묶어서 다운로드: 총 {}개 파일", zipFiles.size());

    String combinedZipName = modelInfo.getModelNo() + "." + modelInfo.getModelVer() + ".all.zip";
    Path tempZipPath = createCombinedZipFile(zipFiles, combinedZipName, uuid, baseDir);

    try {
      long totalSize = Files.size(tempZipPath);
      log.info(
          "통합 ZIP 파일 생성 완료: fileName={}, fileSize={} bytes, basePath={}",
          tempZipPath.getFileName(),
          totalSize,
          baseDir);

      ResponseEntity<?> response =
          rangeDownloadResponder.buildZipResponse(tempZipPath, combinedZipName, request);

      // 다운로드 완료 후 임시 파일 삭제 (비동기)
      deleteTempFileAsync(tempZipPath);

      return response;

    } catch (IOException e) {
      log.warn("통합 ZIP 파일 처리 실패: {}", tempZipPath, e);
      // 에러 발생 시 임시 파일 삭제
      try {
        Files.deleteIfExists(tempZipPath);
        Path tempDir = tempZipPath.getParent();
        if (tempDir != null && Files.exists(tempDir)) {
          Files.deleteIfExists(tempDir);
        }
      } catch (IOException ex) {
        log.warn("임시 파일 삭제 실패: {}", tempZipPath, ex);
      }
      throw new CustomApiException(
          "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "ZIP 파일 생성 실패");
    }
  }

  /**
   * 모든 ZIP 파일 찾기
   *
   * @param baseDir 기본 디렉토리
   * @param uuid 모델 UUID
   * @return ZIP 파일 목록 (최신순 정렬)
   */
  private List<Path> findAllZipFiles(Path baseDir, UUID uuid) {
    try (Stream<Path> stream = Files.list(baseDir)) {
      return stream
          .filter(Files::isRegularFile)
          .filter(p -> p.getFileName().toString().endsWith(".zip"))
          .filter(p -> p.getFileName().toString().contains(uuid.toString()))
          .sorted(
              Comparator.comparing(
                  p -> {
                    try {
                      return Files.getLastModifiedTime(p);
                    } catch (IOException e) {
                      return FileTime.fromMillis(0);
                    }
                  },
                  Comparator.reverseOrder()))
          .toList();
    } catch (IOException e) {
      log.warn("ZIP 파일 검색 실패: basePath={}", baseDir, e);
      return new ArrayList<>();
    }
  }

  /**
   * 여러 ZIP 파일을 하나의 ZIP으로 묶기
   *
   * @param zipFiles 원본 ZIP 파일 목록
   * @param combinedZipName 통합 ZIP 파일명
   * @param uuid 모델 UUID
   * @param baseDir 기본 디렉토리 (로그용)
   * @return 통합 ZIP 파일 경로
   */
  private Path createCombinedZipFile(
      List<Path> zipFiles, String combinedZipName, UUID uuid, Path baseDir) throws IOException {
    // 임시 디렉토리에 통합 ZIP 생성
    Path tempDir = Files.createTempDirectory("kamco-download-" + uuid);
    Path combinedZipPath = tempDir.resolve(combinedZipName);

    log.debug("통합 ZIP 생성 시작: tempPath={}, 원본 파일 {}개", combinedZipPath, zipFiles.size());

    try (java.util.zip.ZipOutputStream zos =
        new java.util.zip.ZipOutputStream(Files.newOutputStream(combinedZipPath))) {

      for (Path zipFile : zipFiles) {
        String entryName = zipFile.getFileName().toString();
        log.debug("ZIP에 파일 추가: {}", entryName);

        java.util.zip.ZipEntry entry = new java.util.zip.ZipEntry(entryName);
        zos.putNextEntry(entry);

        Files.copy(zipFile, zos);
        zos.closeEntry();
      }

      zos.finish();
    }

    log.debug("통합 ZIP 생성 완료: {}", combinedZipPath);
    return combinedZipPath;
  }

  /**
   * 임시 파일 비동기 삭제
   *
   * @param tempFile 삭제할 임시 파일
   */
  private void deleteTempFileAsync(Path tempFile) {
    new Thread(
            () -> {
              try {
                // 다운로드 완료 후 대기
                Thread.sleep(10000); // 10초 대기
                Files.deleteIfExists(tempFile);
                // 임시 디렉토리도 삭제
                Path tempDir = tempFile.getParent();
                if (tempDir != null && Files.exists(tempDir)) {
                  Files.deleteIfExists(tempDir);
                }
                log.debug("임시 파일 삭제 완료: {}", tempFile);
              } catch (Exception e) {
                log.warn("임시 파일 삭제 실패: {}", tempFile, e);
              }
            })
        .start();
  }
}
