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

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kamco.cd.training.common.enums.LearnDataRegister;
import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.common.service.FormatStorage;
import com.kamco.cd.training.common.utils.FIleChecker;
import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode;
import com.kamco.cd.training.config.api.ApiResponseDto.ResponseObj;
import com.kamco.cd.training.dataset.dto.DatasetClsObjDto;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.dto.DatasetDto.AddDeliveriesReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.AddReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetObjRegDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage;
import com.kamco.cd.training.model.dto.FileDto.FoldersDto;
import com.kamco.cd.training.model.dto.FileDto.SrchFoldersDto;
import com.kamco.cd.training.postgres.core.DatasetClsCoreService;
import jakarta.validation.Valid;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class DatasetClsService {

  private final DatasetClsCoreService datasetClsCoreService;
  private final DatasetBatchService datasetBatchService;

  private static final List<String> LABEL_DIRS = List.of("label-json", "label", "input1", "input2");
  private static final List<String> REQUIRED_DIRS = Arrays.asList("train", "val", "test");
  private static final List<String> CHECK_DIRS = List.of("label", "input1", "input2");

  /**
   * 데이터셋 목록 조회
   *
   * @param searchReq 검색 조건
   * @return 데이터셋 목록
   */
  public Page<DatasetDto.Basic> searchDatasets(DatasetDto.SearchReq searchReq) {
    log.info("데이터셋 목록 조회 - 조건: {}", searchReq);
    return datasetClsCoreService.findDatasetList(searchReq);
  }

  /**
   * 데이터셋 상세 조회
   *
   * @param id 상세 조회할 목록 Id
   * @return 데이터셋 상세 정보
   */
  public DatasetDto.Basic getDatasetDetail(UUID id) {
    return datasetClsCoreService.getOneByUuid(id);
  }

  // TODO 미사용시작
  /**
   * 데이터셋 등록
   *
   * @param registerReq 등록 요청
   * @return 등록된 데이터셋 ID
   */
  @Transactional
  public Long registerDataset(DatasetDto.RegisterReq registerReq) {
    log.info("데이터셋 등록 - 요청: {}", registerReq);
    DatasetDto.Basic saved = datasetClsCoreService.save(registerReq);
    log.info("데이터셋 등록 완료 - ID: {}", saved.getId());
    return saved.getId();
  }

  // TODO 미사용 끝
  /**
   * 데이터셋 수정
   *
   * @param updateReq 수정 요청
   * @return 수정된 데이터셋 ID
   */
  @Transactional
  public void updateDataset(UUID uuid, DatasetDto.UpdateReq updateReq) {
    datasetClsCoreService.update(uuid, updateReq);
  }

  /**
   * 데이터셋 삭제 (다건)
   *
   * @param uuid 삭제 요청
   */
  @Transactional
  public void deleteDatasets(UUID uuid) {
    datasetClsCoreService.deleteDatasets(uuid);
  }

  /**
   * 데이터셋 통계 요약
   *
   * @param summaryReq 요약 요청
   * @return 통계 요약
   */
  public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) {
    log.info("데이터셋 통계 요약 - 요청: {}", summaryReq);
    return datasetClsCoreService.getDatasetSummary(summaryReq);
  }

  public Page<DatasetClsObjDto.Basic> searchDatasetClsObjectList(
      DatasetClsObjDto.ClsSearchReq searchReq) {
    return datasetClsCoreService.searchDatasetClsObjectList(searchReq);
  }

  public UUID deleteDatasetObjByUuid(UUID uuid) {
    return datasetClsCoreService.deleteDatasetObjByUuid(uuid);
  }

  /**
   * 데이터셋 object class 조회
   *
   * @param uuid dataset uuid
   * @param type compare, target
   */
  public List<DatasetClass> getDatasetObjByUuid(UUID uuid, String type) {
    return datasetClsCoreService.findDatasetObjClassByUuid(uuid, type);
  }

  /**
   * 사용 가능 공간 조회
   *
   * @return
   */
  public DatasetStorage getUsableBytes() {
    // 현재 실행 위치가 속한 디스크 기준
    try {
      FormatStorage.DiskUsage usage = FormatStorage.getDiskUsage(Path.of("."));
      log.debug("경로 : {}", usage.path());
      log.debug("총 저장공간 : {}", usage.totalText());
      log.debug("남은 저장공간 : {}", usage.usableText());
      log.debug("사용률 : {}", usage.usedPercent());

      DatasetStorage datasetStorage = new DatasetStorage();
      datasetStorage.setUsableBytes(usage.usableText());
      return datasetStorage;
    } catch (Exception e) {
      throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND);
    }
  }

  /**
   * 분류모델 업로드
   *
   * @param addReq
   * @return
   */
  @Transactional
  public ResponseObj insertDataset(@Valid AddReq addReq) {

    Long datasetUid = null; // master id 값, 등록하면서 가져올 예정

    try {
      // 압축 해제
      FIleChecker.unzip(addReq.getFileName(), addReq.getFilePath());

      // 압축 해제한 폴더 하위에 train,val,test 폴더 모두 존재하는지 확인
      validateClsTrainValTestDirs(addReq.getFilePath() + addReq.getFileName().replace(".zip", ""));

      // 해제한 폴더 읽어서 데이터 저장
      datasetUid =
          this.processAndInsertDataset(
              addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), addReq);
      log.info("######testttest#### datasetUid: {}", datasetUid);

    } catch (IOException e) {
      log.error(e.getMessage());
      return new ResponseObj(ApiResponseCode.INTERNAL_SERVER_ERROR, e.getMessage());
    }

    datasetClsCoreService.updateDatasetUploadStatus(datasetUid, LearnDataRegister.COMPLETED);
    return new ResponseObj(ApiResponseCode.OK, "업로드 성공하였습니다.");
  }

  private List<Map<String, Object>> getUnzipDatasetFiles(String unzipRootPath, String subDir) {

    Path root = Paths.get(unzipRootPath).resolve(subDir);
    Map<String, Map<String, Object>> grouped = new HashMap<>();

    for (String dirName : LABEL_DIRS) {
      Path dir = root.resolve(dirName);

      if (!Files.isDirectory(dir)) {
        throw new CustomApiException(
            ApiResponseCode.NOT_FOUND_DATA.getId(),
            HttpStatus.CONFLICT,
            "폴더가 존재하지 않습니다. 업로드 된 파일을 확인하세요. : " + dir);
      }

      try (Stream<Path> stream = Files.list(dir)) {
        stream
            .filter(Files::isRegularFile)
            .forEach(
                path -> {
                  String fileName = path.getFileName().toString();
                  String baseName = getBaseName(fileName);

                  // baseName 기준 Map 생성
                  Map<String, Object> data =
                      grouped.computeIfAbsent(baseName, k -> new HashMap<>());

                  // 공통 메타
                  data.put("baseName", baseName);

                  // 폴더별 처리
                  if ("label-json".equals(dirName)) {
                    // json 파일이면 파싱
                    try {
                      data.put("label-json", readJson(path));
                    } catch (Exception e) {
                      log.error("파일 JSON 읽기 실패. skip. file={}", path, e);
                      return; // skip
                    }
                    data.put("geojson_path", path.toAbsolutePath().toString());
                  } else {
                    data.put(dirName, path.toAbsolutePath().toString());
                  }
                });

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

    return new ArrayList<>(grouped.values());
  }

  @Transactional
  public Long insertTrainTestData(
      Map<String, Object> map, AddReq addReq, int idx, Long datasetUid, String subDir) {
    ObjectMapper mapper = new ObjectMapper();
    String comparePath = (String) map.get("input1");
    String targetPath = (String) map.get("input2");
    String labelPath = (String) map.get("label");
    String geojsonPath = (String) map.get("geojson_path");
    Object labelJson = map.get("label-json");
    JsonNode json;

    if (labelJson instanceof JsonNode jn) {
      json = jn;
    } else {
      try {
        json = mapper.readTree(labelJson.toString());
      } catch (IOException e) {
        throw new RuntimeException("label_json parse error", e);
      }
    }

    String fileName = Paths.get(comparePath).getFileName().toString();
    String[] fileNameStr = fileName.split("_");
    String compareYyyy = fileNameStr[1];
    String targetYyyy = fileNameStr[2];
    String mapSheetNum = fileNameStr[3];

    if (idx == 0 && subDir.equals("train")) {
      String title = compareYyyy + "-" + targetYyyy; // 제목 : 비교년도-기준년도
      String dataType = LearnDataType.PRODUCTION.getId(); // 만들어 넣는 건 다 제작
      Long stage =
          datasetClsCoreService.getDatasetMaxStage(
                  Integer.parseInt(compareYyyy), Integer.parseInt(targetYyyy))
              + 1;
      String uid = addReq.getFileName().replace(".zip", "");

      DatasetMngRegDto mngRegDto =
          DatasetMngRegDto.builder()
              .uid(uid)
              .dataType(dataType)
              .compareYyyy(Integer.parseInt(compareYyyy))
              .targetYyyy(Integer.parseInt(targetYyyy))
              .title(title)
              .memo(addReq.getMemo())
              .roundNo(stage)
              .totalSize(addReq.getFileSize())
              .datasetPath(addReq.getFilePath() + uid)
              .build();

      datasetUid = datasetClsCoreService.insertDatasetMngData(mngRegDto); // tb_dataset 에 insert
    }

    // datasetUid 로 obj 도 등록하기
    // Json 갯수만큼 for문 돌려서 insert 해야 함, features에 빈값
    if (json != null && json.path("features") != null && !json.path("features").isEmpty()) {

      for (JsonNode feature : json.path("features")) {
        JsonNode prop = feature.path("properties");
        String compareClassCd = prop.path("before").asText(null);
        String targetClassCd = prop.path("after").asText(null);

        // 한 개씩 자른 geojson을 FeatureCollection 으로 만들어서 넣기
        ObjectNode root = mapper.createObjectNode();
        root.put("type", "FeatureCollection");
        ArrayNode features = mapper.createArrayNode();
        features.add(feature);
        root.set("features", features);

        DatasetObjRegDto objRegDto =
            DatasetObjRegDto.builder()
                .datasetUid(datasetUid)
                .compareYyyy(Integer.parseInt(compareYyyy))
                .compareClassCd(compareClassCd)
                .targetYyyy(Integer.parseInt(targetYyyy))
                .targetClassCd(targetClassCd)
                .comparePath(comparePath)
                .targetPath(targetPath)
                .labelPath(labelPath)
                .mapSheetNum(mapSheetNum)
                .geojson(root)
                .geojsonPath(geojsonPath)
                .fileName(fileName)
                .build();

        if (subDir.equals("train")) {
          datasetClsCoreService.insertDatasetObj(objRegDto);
        } else if (subDir.equals("val")) {
          datasetClsCoreService.insertDatasetValObj(objRegDto);
        } else {
          datasetClsCoreService.insertDatasetTestObj(objRegDto);
        }
      }
    }

    return datasetUid;
  }

  /**
   * CLS zip file DB화
   *
   * @param unzipRootPath
   * @param addReq
   * @return
   */
  @Transactional
  public Long processAndInsertDataset(String unzipRootPath, AddReq addReq) {
    Path datasetRoot = Paths.get(unzipRootPath).resolve("dataset");

    log.info("datasetRoot: {}", datasetRoot);
    Long datasetUid = null;
    boolean isMasterInserted = false; // Master(Mng) 테이블에 최초 1회만 등록하기 위한 플래그

    // 1. 순회할 상위 폴더 목록
    for (String subDir : REQUIRED_DIRS) {
      Path subDirPath = datasetRoot.resolve(subDir);
      if (!Files.exists(subDirPath) || !Files.isDirectory(subDirPath)) {
        continue; // 해당 폴더가 없으면 다음으로 패스
      }

      try (Stream<Path> classDirs = Files.list(subDirPath)) {
        // 2. 클래스명 폴더 목록 추출 (예: building, container, water 등)
        List<Path> classDirList = classDirs.filter(Files::isDirectory).toList();

        for (Path classDir : classDirList) {
          // DB에 넣을 classCd (폴더명) 추출
          String classCd = classDir.getFileName().toString();

          try (Stream<Path> files = Files.list(classDir)) {
            // 3. 해당 클래스 폴더 안의 실제 파일(tif) 목록 추출
            List<Path> filePaths =
                files
                    .filter(Files::isRegularFile)
                    .sorted(Comparator.comparing(Path::getFileName)) // 정렬 로직 추가
                    .toList();

            if (filePaths.isEmpty()) continue; // 폴더가 비어있으면 에러 방지를 위해 패스

            // ★ 맨 앞의 첫 번째 파일명에서 compareYyyy 추출 및 targetYyyy 계산
            String firstFileName = filePaths.getFirst().getFileName().toString();
            String compareYyyy = firstFileName.split("_")[0]; // 예: "2021"
            String targetYyyy = String.valueOf(Integer.parseInt(compareYyyy) + 1); // 예: "2022"

            for (Path file : filePaths) {
              String fileName = file.getFileName().toString();
              String absolutePath = file.toAbsolutePath().toString();
              String yyyy = fileName.split("_")[0];

              // 4. 파일명 파싱 (확장자 제거 후 Split)
              // ※ 주의: 첨부하신 이미지의 파일명(2021_33607077_idx93)을 기준으로 인덱스를 재조정했습니다.
              String nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
              String[] fileNameStr = nameWithoutExt.split("_");
              String mapSheetNum = fileNameStr.length > 1 ? fileNameStr[1] : ""; // 33607077

              // 5. Master(Mng) 데이터 등록 (전체 순회 중 최초 1회만 실행)
              if (!isMasterInserted) {
                String title = compareYyyy + "-" + targetYyyy;
                Long stage =
                    datasetClsCoreService.getDatasetMaxStage(
                            Integer.parseInt(compareYyyy), Integer.parseInt(targetYyyy))
                        + 1;
                String uid = addReq.getFileName().replace(".zip", "");

                DatasetMngRegDto mngRegDto =
                    DatasetMngRegDto.builder()
                        .uid(uid)
                        .dataType(LearnDataType.PRODUCTION.getId())
                        .compareYyyy(Integer.parseInt(compareYyyy))
                        .targetYyyy(Integer.parseInt(targetYyyy))
                        .title(title)
                        .memo(addReq.getMemo())
                        .roundNo(stage)
                        .totalSize(addReq.getFileSize())
                        .datasetPath(addReq.getFilePath() + uid)
                        .cdClsType("CLS")
                        .build();

                datasetUid = datasetClsCoreService.insertDatasetMngData(mngRegDto);
                isMasterInserted = true;
              }

              // 6. Obj(상세) 데이터 등록
              DatasetObjDto.DatasetClsObjRegDto objRegDto =
                  DatasetObjDto.DatasetClsObjRegDto.builder()
                      .datasetUid(datasetUid)
                      .type(subDir)
                      .yyyy(Integer.parseInt(yyyy))
                      .classCd(classCd)
                      .mapSheetNum(mapSheetNum)
                      .tifImgPath(absolutePath) // 실제 파일의 절대 경로
                      .fileName(fileName)
                      .build();

              // 7. 폴더 위치(train/val/test)에 맞는 테이블에 Insert
              datasetClsCoreService.insertDatasetClsObj(objRegDto);
            }
          }
        }
      } catch (IOException e) {
        throw new RuntimeException("디렉토리 읽기 중 오류가 발생했습니다: " + subDirPath, e);
      }
    }

    log.info("$$$$$$$$$$datasetUid: {}", datasetUid);
    return datasetUid;
  }

  /**
   * Cls File 목록
   *
   * @param unzipRootPath
   * @param subDir
   * @return
   */
  private List<String> getUnzipDatasetClsFiles(String unzipRootPath, String subDir) {

    Path root = Paths.get(unzipRootPath).resolve("dataset");
    Map<String, Map<String, Object>> grouped = new HashMap<>();

    Path dir = root.resolve(subDir);

    if (!Files.isDirectory(dir)) {
      throw new CustomApiException(
          ApiResponseCode.NOT_FOUND_DATA.getId(),
          HttpStatus.CONFLICT,
          "폴더가 존재하지 않습니다. 업로드 된 파일을 확인하세요. : " + dir);
    }

    try (Stream<Path> stream = Files.list(dir)) {
      return stream
          .filter(Files::isRegularFile) // 파일만 필터링 (하위 폴더 제외)
          .map(path -> path.toAbsolutePath().toString()) // 절대 경로 문자열로 변환
          .toList(); // List로 수집 (Java 16 미만인 경우 .collect(Collectors.toList()) 사용)
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  private String getBaseName(String fileName) {
    int idx = fileName.lastIndexOf('.');
    return (idx > 0) ? fileName.substring(0, idx) : fileName;
  }

  private JsonNode readJson(Path jsonPath) {
    try {
      ObjectMapper mapper = new ObjectMapper();
      return mapper.readTree(jsonPath.toFile());
    } catch (IOException e) {
      throw new RuntimeException("JSON 읽기 실패: " + jsonPath, e);
    }
  }

  public String getFilePathByUUIDPathType(UUID uuid, String pathType) {
    return datasetClsCoreService.getFilePathByUUIDPathType(uuid, pathType);
  }

  private String escape(String path) {
    // 쉘 커맨드에서 안전하게 사용할 수 있도록 문자열을 작은따옴표로 감싸면서, 내부의 작은따옴표를 이스케이프 처리
    return "'" + path.replace("'", "'\"'\"'") + "'";
  }

  private static String normalizeLinuxPath(String path) {
    return path.replace("\\", "/");
  }

  public ResponseEntity<Resource> getFilePathByFile(String remoteFilePath) {

    try {
      Path path = Paths.get(remoteFilePath);
      InputStream inputStream = Files.newInputStream(path);

      InputStreamResource resource =
          new InputStreamResource(inputStream) {
            @Override
            public long contentLength() {
              return -1; // 알 수 없으면 -1
            }
          };

      String fileName = Paths.get(remoteFilePath.replace("\\", "/")).getFileName().toString();

      return ResponseEntity.ok()
          .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
          .contentType(MediaType.APPLICATION_OCTET_STREAM)
          .body(resource);

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

  /** unzipRootDir: 압축 해제된 폴더 경로 (ex: /data/xxx/myzipname) */
  public static void validateTrainValTestDirs(String unzipRootDir) {
    Path root = Paths.get(unzipRootDir);

    // 루트 폴더 자체 존재 확인
    if (!Files.exists(root) || !Files.isDirectory(root)) {
      throw new CustomApiException(
          ApiResponseCode.NOT_FOUND_DATA.getId(),
          HttpStatus.CONFLICT,
          "압축 해제 폴더가 존재하지 않습니다: " + unzipRootDir);
    }

    // 필요한 폴더들 존재/디렉토리 여부 확인
    List<String> missing =
        REQUIRED_DIRS.stream()
            .filter(d -> !Files.isDirectory(root.resolve(d)))
            .collect(Collectors.toList());

    if (!missing.isEmpty()) {
      throw new CustomApiException(
          ApiResponseCode.NOT_FOUND_DATA.getId(),
          HttpStatus.CONFLICT,
          "데이터 폴더 구조가 올바르지 않습니다. 누락된 폴더: "
              + String.join(", ", missing)
              + " (필수: train, val, test)");
    }
  }

  /** unzipRootDir: 압축 해제된 폴더 경로 (ex: /zip파일폴더/dataset/train,val,test) */
  public static void validateClsTrainValTestDirs(String unzipRootDir) {
    Path root = Paths.get(unzipRootDir).resolve("dataset");

    // 루트 폴더 자체 존재 확인
    if (!Files.exists(root) || !Files.isDirectory(root)) {
      throw new CustomApiException(
          ApiResponseCode.NOT_FOUND_DATA.getId(),
          HttpStatus.CONFLICT,
          "압축 해제 폴더가 존재하지 않습니다: " + root.toAbsolutePath().toString());
    }

    // 필요한 폴더들 존재/디렉토리 여부 확인
    List<String> missing =
        REQUIRED_DIRS.stream()
            .filter(d -> !Files.isDirectory(root.resolve(d)))
            .collect(Collectors.toList());

    if (!missing.isEmpty()) {
      throw new CustomApiException(
          ApiResponseCode.NOT_FOUND_DATA.getId(),
          HttpStatus.CONFLICT,
          "데이터 폴더 구조가 올바르지 않습니다. 누락된 폴더: "
              + String.join(", ", missing)
              + " (필수: train, val, test)");
    }
  }

  public static void validateDirFileCount(String unzipRootDir) {
    Path root = Paths.get(unzipRootDir);

    for (String split : REQUIRED_DIRS) {

      Path splitPath = root.resolve(split);

      Map<String, Long> fileCountMap = new HashMap<>();

      for (String subDir : CHECK_DIRS) { // input1, input2, label 폴더만 수행하기

        Path subDirPath = splitPath.resolve(subDir);

        if (!Files.isDirectory(subDirPath)) {
          throw new CustomApiException(
              ApiResponseCode.NOT_FOUND_DATA.getId(),
              HttpStatus.CONFLICT,
              split + " 폴더 하위에 " + subDir + " 폴더가 존재하지 않습니다.");
        }

        long count;
        try (Stream<Path> files = Files.list(subDirPath)) {
          count = files.filter(Files::isRegularFile).count();
          log.info("dir: " + subDirPath + ", count: " + count);
        } catch (IOException e) {
          throw new CustomApiException(
              ApiResponseCode.NOT_FOUND_DATA.getId(),
              HttpStatus.CONFLICT,
              split + "/" + subDir + " 파일 개수 확인 중 오류 발생");
        }

        fileCountMap.put(subDir, count);
      }

      // 모든 폴더 파일 개수가 동일한지 확인
      Set<Long> uniqueCounts = new HashSet<>(fileCountMap.values());

      if (uniqueCounts.size() != 1) {
        throw new CustomApiException(
            ApiResponseCode.NOT_FOUND_DATA.getId(),
            HttpStatus.CONFLICT,
            split + " 데이터 파일 개수가 일치하지 않습니다. " + fileCountMap.toString());
      }
    }
  }

  /**
   * 폴더 조회
   *
   * @param srchDto 폴더 경로
   * @return 폴더 리스트
   * @throws IOException
   */
  public FoldersDto getFolderAll(SrchFoldersDto srchDto) throws IOException {

    File dir = new File(srchDto.getDirPath() == null ? "/" : srchDto.getDirPath());

    // 존재 + 디렉토리 체크
    if (!dir.exists() || !dir.isDirectory()) {
      throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST, "잘못된 경로입니다.");
    }

    // 권한 없을때
    if (!dir.canRead()) {
      throw new CustomApiException(
          ApiResponseCode.FORBIDDEN.getId(), HttpStatus.FORBIDDEN, "디렉토리에 접근할 권한이 없습니다.");
    }

    String canonicalPath = dir.getCanonicalPath();

    File[] files = dir.listFiles();

    if (files == null) {
      return new FoldersDto(canonicalPath, 0, 0, Collections.emptyList());
    }

    List<FIleChecker.Folder> folders = new ArrayList<>();

    int folderTotCnt = 0;
    int folderErrTotCnt = 0;

    for (File f : files) {

      // 숨김 제외
      if (f.isHidden()) continue;

      if (f.isDirectory()) {

        // 폴더 개수 증가
        folderTotCnt++;

        // 폴더 유효성 여부 (기본 true, 이후 검증 로직으로 변경 가능)
        boolean isValid = true;

        // 유효하지 않은 폴더 카운트 증가
        if (!isValid) folderErrTotCnt++;

        // 현재 폴더 이름 (ex: train, images 등)
        String folderNm = f.getName();

        // 부모 경로 (ex: /data/datasets)
        String parentPath = f.getParent();

        // 부모 폴더 이름 (ex: datasets)
        String parentFolderNm = new File(parentPath).getName();

        // 전체 절대 경로 (ex: /data/datasets/train)
        String fullPath = f.getAbsolutePath();

        // 폴더 깊이 (경로 기준 depth)
        // ex: /a/b/c → depth = 3
        int depth = f.toPath().getNameCount();

        // 하위 폴더 개수
        long childCnt = FIleChecker.getChildFolderCount(f);

        // 마지막 수정 시간 (문자열 포맷)
        String lastModified = FIleChecker.getLastModified(f);

        // Folder DTO 생성 및 리스트에 추가
        folders.add(
            new FIleChecker.Folder(
                folderNm, // 폴더명
                parentFolderNm, // 부모 폴더명
                parentPath, // 부모 경로
                fullPath, // 전체 경로
                depth, // 깊이
                childCnt, // 하위 폴더 개수
                lastModified, // 수정일시
                isValid // 유효성 여부
                ));
      }
    }

    // 폴더 정렬
    folders.sort(
        Comparator.comparing(FIleChecker.Folder::getFolderNm, String.CASE_INSENSITIVE_ORDER));

    return new FoldersDto(canonicalPath, folderTotCnt, folderErrTotCnt, folders);
  }

  /**
   * 납품 데이터 등록
   *
   * @param req 폴더경로, 메모
   * @return 성공/실패 여부0
   */
  public void insertDeliveriesDataset(AddDeliveriesReq req, Long datasetUid) {

    long startTime = System.currentTimeMillis();

    // 처리
    processType(req.getFilePath(), datasetUid, "train");
    processType(req.getFilePath(), datasetUid, "val");
    processType(req.getFilePath(), datasetUid, "test");

    log.info("========== 전체 완료. 총 소요시간: {} ms ==========", System.currentTimeMillis() - startTime);
  }

  /**
   * 납품 데이터 등록 처리
   *
   * @param path
   * @param datasetUid
   * @param type
   */
  private void processType(String path, Long datasetUid, String type) {
    long start = System.currentTimeMillis();

    log.info("[납품 데이터 등록 처리][{}] 시작", type.toUpperCase());

    List<Map<String, Object>> list = getUnzipDatasetFiles(path, type);

    int batchSize = 1000;
    int total = list.size();
    int processed = 0;

    for (int i = 0; i < total; i += batchSize) {

      List<Map<String, Object>> batch = list.subList(i, Math.min(i + batchSize, total));

      try {
        log.info("[납품 데이터 등록 처리][{}] batch 시작: {} ~ {}", type, i, i + batch.size());

        datasetBatchService.saveBatch(batch, datasetUid, type);

        processed += batch.size();
      } catch (Exception e) {
        log.error("batch 실패 row 데이터: {}", batch);
        log.error(
            "[납품 데이터 등록 처리][{}] batch 실패. range: {} ~ {}, datasetUid={}",
            type,
            i,
            i + batch.size(),
            datasetUid,
            e);
      }
    }

    log.info(
        "[납품 데이터 등록 처리][{}] 완료. 총 {}건, 소요시간: {} ms",
        type,
        total,
        System.currentTimeMillis() - start);
  }

  public void validateExistsUidChk(String filePath) {
    Path selectedPath = Paths.get(filePath);
    String uid = selectedPath.getFileName().toString();

    // 같은 uid 로 등록한 파일이 있는지 확인
    Long existsCnt = datasetClsCoreService.findDatasetByUidExistsCnt(uid);
    if (existsCnt > 0) {
      throw new CustomApiException(
          ApiResponseCode.DUPLICATE_DATA.getId(),
          HttpStatus.CONFLICT,
          "이미 등록된 회차 데이터 파일입니다. 확인 부탁드립니다.");
    }
  }

  public String getFilePathByUUIDPathTif(UUID uuid) {
    return datasetClsCoreService.getFilePathByUUIDPathTif(uuid);
  }
}
