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

import com.kamco.cd.training.common.dto.HyperParam;
import com.kamco.cd.training.common.enums.HyperParamSelectType;
import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.common.enums.TrainType;
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.SelectDataSet;
import com.kamco.cd.training.hyperparam.dto.HyperParamDto;
import com.kamco.cd.training.model.dto.ModelConfigDto;
import com.kamco.cd.training.model.dto.ModelTrainMngDto;
import com.kamco.cd.training.model.dto.ModelTrainMngDto.SearchReq;
import com.kamco.cd.training.postgres.core.HyperParamCoreService;
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
import com.kamco.cd.training.train.service.TrainJobService;
import java.io.IOException;
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.util.EnumSet;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

  private final ModelTrainMngCoreService modelTrainMngCoreService;
  private final HyperParamCoreService hyperParamCoreService;
  private final TrainJobService trainJobService;
  private final ModelTrainDetailService modelTrainDetailService;

  @Value("${train.docker.base_path}")
  private String basePath;

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

  @Value("${train.docker.symbolic_link_dir}")
  private String symbolicDir;

  /**
   * 모델학습 조회
   *
   * @param searchReq 검색 조건
   * @return 페이징 처리된 모델 목록
   */
  public Page<ModelTrainMngDto.ListDto> getModelList(SearchReq searchReq) {
    return modelTrainMngCoreService.findByModelList(searchReq);
  }

  /**
   * 모델학습 삭제
   *
   * <p>순서: 1. tmp 구조 검증 (예외 발생 가능) 2. DB 삭제 (트랜잭션) 3. 파일 삭제 (실패해도 로그만)
   */
  @Transactional
  public void deleteModelTrain(UUID uuid) {

    log.info("deleteModelTrain 시작. uuid={}", uuid);

    // ===== 1. 모델 조회 =====
    ModelTrainMngDto.Basic model = modelTrainMngCoreService.findModelByUuid(uuid);

    if (model == null) {
      throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델 없음");
    }

    if (model.getRequestPath() == null) {
      throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "임시파일 경로 없음");
    }

    // ===== 2. 경로 생성 =====
    Path tmpBase = Path.of(symbolicDir).toAbsolutePath().normalize();
    Path tmp = tmpBase.resolve(model.getRequestPath()).normalize();

    Path responseBase = Paths.get(responseDir).toAbsolutePath().normalize();
    Path response = responseBase.resolve(model.getUuid().toString()).normalize();

    // ===== 3. 경로 탈출 방지 =====
    if (!tmp.startsWith(tmpBase)) {
      throw new CustomApiException("INVALID_PATH", HttpStatus.BAD_REQUEST, "잘못된 tmp 경로");
    }

    if (!response.startsWith(responseBase)) {
      throw new CustomApiException("INVALID_PATH", HttpStatus.BAD_REQUEST, "잘못된 response 경로");
    }

    // ===== 4. 상태 로그 =====
    log.info(
        "tmp 상태: exists={}, isDir={}, isSymlink={}",
        Files.exists(tmp, LinkOption.NOFOLLOW_LINKS),
        Files.isDirectory(tmp, LinkOption.NOFOLLOW_LINKS),
        Files.isSymbolicLink(tmp));

    log.info(
        "response 상태: exists={}, isDir={}, isSymlink={}",
        Files.exists(response, LinkOption.NOFOLLOW_LINKS),
        Files.isDirectory(response, LinkOption.NOFOLLOW_LINKS),
        Files.isSymbolicLink(response));

    // ===== 5. tmp 구조 검증 =====
    validateTmpStructure(tmp);

    // ===== 6. DB 삭제 =====
    modelTrainMngCoreService.deleteModel(uuid);
    log.info("DB 삭제 완료. uuid={}", uuid);

    // ===== 7. tmp 삭제 =====
    log.info("tmp 삭제 시작: {}", tmp);
    try {
      deleteTmpDirectory(tmp);
      log.info("tmp 삭제 완료: {}", tmp);
    } catch (Exception e) {
      log.error("tmp 삭제 실패 (DB는 이미 삭제됨): {}", tmp, e);
    }

    // ===== 8. response 삭제 =====
    log.info("response 삭제 시작: {}", response);
    try {
      // 테스트 완료되었으면 베스트 에폭은 삭제안함
      if (TrainStatusType.COMPLETED.getId().equals(model.getStep2Status())) {
        modelTrainDetailService.executeCleanup(model, response);
      } else {
        deleteResponseDirectory(response);
      }

      log.info("response 삭제 완료: {}", response);
    } catch (Exception e) {
      log.error("response 삭제 실패 (DB는 이미 삭제됨): {}", response, e);
    }

    log.info("deleteModelTrain 완료. uuid={}", uuid);
  }

  /** tmp 디렉토리 삭제 */
  private void deleteTmpDirectory(Path dir) throws IOException {

    if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
      log.warn("삭제 대상 없음: {}", dir);
      return;
    }

    Files.walkFileTree(
        dir,
        EnumSet.noneOf(FileVisitOption.class),
        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;
          }
        });
  }

  /** response 디렉토리 삭제 */
  private void deleteResponseDirectory(Path dir) throws IOException {

    if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
      log.warn("삭제 대상 없음: {}", dir);
      return;
    }

    Files.walkFileTree(
        dir,
        EnumSet.noneOf(FileVisitOption.class),
        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;
          }
        });
  }

  /** tmp 내부 구조 검증 - 내부는 반드시 symlink만 허용 */
  private void validateTmpStructure(Path dir) {

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

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

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

              // 파일은 전부 허용 (일반 + symlink)
              return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs)
                throws IOException {

              // 루트 제외 + symlink 디렉토리 금지
              if (!directory.equals(dir) && Files.isSymbolicLink(directory)) {
                log.error("tmp 내부에 symlink 디렉토리 존재: {}", directory);
                throw new CustomApiException(
                    "BAD_REQUEST", HttpStatus.BAD_REQUEST, "tmp 내부에 symlink 디렉토리는 허용되지 않습니다.");
              }

              return FileVisitResult.CONTINUE;
            }
          });
    } catch (IOException e) {
      throw new CustomApiException(
          "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "tmp 구조 검증 실패");
    }
  }

  /**
   * 모델학습 등록
   *
   * @param req
   * @return
   */
  @Transactional
  public UUID createModelTrain(ModelTrainMngDto.AddReq req) {
    HyperParam hyperParam = req.getHyperParam();
    HyperParamDto.Basic hyper = new HyperParamDto.Basic();

    // 전이 학습은 모델 선택 필수
    if (TrainType.TRANSFER.getId().equals(req.getTrainType())) {
      if (req.getBeforeModelId() == null) {
        throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST, "모델을 선택해 주세요.");
      }
    }

    // 하이파라미터 신규저장
    if (HyperParamSelectType.NEW.getId().equals(req.getHyperParamType())) {
      // 하이퍼파라미터 등록
      hyper = hyperParamCoreService.createHyperParam(hyperParam);
      req.setHyperUuid(hyper.getUuid());
    }

    // 모델학습 테이블 저장
    ModelTrainMngDto.Basic modelDto = modelTrainMngCoreService.saveModel(req);

    Long modelId = modelDto.getId();
    UUID modelUuid = modelDto.getUuid();

    // 모델학습 데이터셋 저장
    modelTrainMngCoreService.saveModelDataset(modelId, req);

    // 모델 데이터셋 mapping 저장
    modelTrainMngCoreService.saveModelDatasetMap(
        modelId, req.getTrainingDataset().getDatasetList());

    // 모델 config 저장
    modelTrainMngCoreService.saveModelConfig(modelId, req.getModelConfig());

    // 데이터셋 임시파일 생성
    List<Long> datasetList = null;
    if (req.getTrainingDataset() != null) {
      datasetList = req.getTrainingDataset().getDatasetList();
    }

    boolean isSingleDataset = datasetList != null && datasetList.size() == 1;

    // 데이터셋 1개만 선택한 경우는 symbolic link 미생성 해도 됨 -> train 호출 시 그냥 데이터셋 request 경로로 호출
    if (isSingleDataset) {
      trainJobService.updateRequestPath(modelUuid, datasetList);
    } else {
      trainJobService.createTmpFile(modelUuid);
    }
    return modelUuid;
  }

  /**
   * 모델학습 모델학습 uuid config정보 조회
   *
   * @param uuid 모델학습 uuid
   * @return
   */
  public ModelConfigDto.Basic getModelConfigByModelId(UUID uuid) {
    return modelTrainMngCoreService.findModelConfigByModelId(uuid);
  }

  /**
   * 모델별 데이터셋 목록 조회
   *
   * @param req
   * @return
   */
  public List<SelectDataSet> getDatasetSelectList(DatasetReq req) {
    if (req.getModelNo().equals(ModelType.G1.getId())) {
      return modelTrainMngCoreService.getDatasetSelectG1List(req);
    } else if (req.getModelNo().equals(ModelType.G4.getId())) {
      return modelTrainMngCoreService.getDatasetSelectG4List(req);
    } else {
      return modelTrainMngCoreService.getDatasetSelectG2G3List(req);
    }
  }

  public Long findModelStep1InProgressCnt() {
    return modelTrainMngCoreService.findModelStep1InProgressCnt();
  }

  public ModelTrainMngDto.InProgressModel findInprogressModel() {
    return modelTrainMngCoreService.findInprogressModel();
  }

  public ModelTrainMngDto.ProgressPercent findTrainProgressPercent(UUID uuid) {
    return modelTrainMngCoreService.findTrainProgressPercent(uuid);
  }
}
