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

import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.model.dto.ModelTrainMngDto;
import com.kamco.cd.training.postgres.core.ModelTrainJobCoreService;
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
import com.kamco.cd.training.train.dto.ModelTrainJobDto;
import com.kamco.cd.training.train.dto.ModelTrainJobQueuedEvent;
import com.kamco.cd.training.train.dto.ModelTrainLinkDto;
import com.kamco.cd.training.train.dto.TrainRunRequest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Log4j2
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TrainJobService {

  private final ModelTrainJobCoreService modelTrainJobCoreService;
  private final ModelTrainMngCoreService modelTrainMngCoreService;
  private final DockerTrainService dockerTrainService;
  private final ObjectMapper objectMapper;
  private final ApplicationEventPublisher eventPublisher;
  private final TmpDatasetService tmpDatasetService;
  private final DataSetCountersService dataSetCounters;

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

  public Long getModelIdByUuid(UUID uuid) {
    return modelTrainMngCoreService.findModelIdByUuid(uuid);
  }

  /**
   * 실행 예약 (QUEUE 등록)
   *
   * @param modelId
   * @return
   */
  @Transactional
  public Long enqueue(Long modelId) {

    // 폴더 카운트
    dataSetCounters.getCount(modelId);

    // 마스터 존재 확인(없으면 예외)
    modelTrainMngCoreService.findModelById(modelId);

    // 파라미터 조회
    TrainRunRequest trainRunRequest = modelTrainMngCoreService.findTrainRunRequest(modelId);

    if (trainRunRequest == null) {
      throw new IllegalArgumentException("Model not found: " + modelId);
    }

    int nextAttemptNo = modelTrainJobCoreService.findMaxAttemptNo(modelId) + 1;

    @SuppressWarnings("unchecked")
    Map<String, Object> paramsMap = objectMapper.convertValue(trainRunRequest, Map.class);
    paramsMap.put("jobType", "TRAIN");
    paramsMap.put("uuid", trainRunRequest.getUuid());
    paramsMap.put("totalEpoch", trainRunRequest.getEpochs());

    Long jobId =
        modelTrainJobCoreService.createQueuedJob(
            modelId, nextAttemptNo, paramsMap, ZonedDateTime.now());

    modelTrainMngCoreService.clearLastError(modelId);
    modelTrainMngCoreService.markInProgress(modelId, jobId);

    // 커밋 이후 Worker 실행 트리거(리스너에서 AFTER_COMMIT로 받아야 함)
    eventPublisher.publishEvent(new ModelTrainJobQueuedEvent(jobId));
    return jobId;
  }

  /**
   * 재시작
   *
   * <p>- STOPPED / ERROR 상태에서만 가능 (IN_PROGRESS면 예외) - 이전 params_json 재사용 - 새 attempt 생성
   */
  @Transactional
  public Long restart(Long modelId) {
    return createNextAttempt(modelId, ResumeMode.NONE);
  }

  /**
   * 이어하기
   *
   * @param modelId
   * @return
   */
  @Transactional
  public Long resume(Long modelId) {
    return createNextAttempt(modelId, ResumeMode.REQUIRE);
  }

  /**
   * 중단
   *
   * <p>- job 상태 CANCELED - master 상태 STOPPED
   *
   * <p>※ 실제 docker stop은 Worker/Runner가 수행(운영 안정)
   */
  @Transactional
  public void cancel(Long modelId) {

    ModelTrainMngDto.Basic master = modelTrainMngCoreService.findModelById(modelId);

    Long jobId = master.getCurrentAttemptId();
    if (jobId == null) {
      throw new IllegalStateException("실행중인 작업이 없습니다.");
    }

    var job =
        modelTrainJobCoreService
            .findById(jobId)
            .orElseThrow(() -> new IllegalStateException("Job not found"));

    String containerName = job.getContainerName();

    // 1) 컨테이너 강제 종료 + 제거 (없거나 이미 죽었어도 괜찮게)
    if (containerName != null && !containerName.isBlank()) {
      dockerTrainService.killContainer(containerName);
    }

    // 2) 상태 업데이트 (항상 수행)
    modelTrainJobCoreService.markCanceled(jobId);
    modelTrainMngCoreService.markStopped(modelId);
  }

  /**
   * 학습 이어하기
   *
   * @param modelId 모델 id
   * @param mode NONE 새로 시작, REQUIRE 이어하기
   * @return
   */
  private Long createNextAttempt(Long modelId, ResumeMode mode) {

    ModelTrainMngDto.Basic master = modelTrainMngCoreService.findModelById(modelId);

    ModelTrainJobDto lastJob =
        modelTrainJobCoreService
            .findLatestByModelId(modelId)
            .orElseThrow(() -> new IllegalStateException("이전 실행 이력이 없습니다."));

    int nextAttemptNo = modelTrainJobCoreService.findMaxAttemptNo(modelId) + 1;

    // 이전 params_json 재사용 (재현성)
    Map<String, Object> params = lastJob.getParamsJson();
    if (params == null || params.isEmpty()) {
      throw new IllegalStateException("이전 실행 params_json이 없습니다.");
    }

    // mode에 따라 resume 옵션 주입/제거
    Map<String, Object> nextParams = new java.util.LinkedHashMap<>(params);

    if (mode == ResumeMode.NONE) {
      // 이어하기 관련 키가 있다면 제거 (완전 새로 시작 보장)
      nextParams.remove("resumeFrom");
      nextParams.remove("resume");
    } else if (mode == ResumeMode.REQUIRE) {
      // 체크포인트 탐지해서 resumeFrom 세팅
      String resumeFrom = findResumeFromOrNull(nextParams);
      if (resumeFrom == null) {
        throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "이어하기 체크포인트가 없습니다.");
      }
      nextParams.put("resumeFrom", resumeFrom);
      nextParams.put("resume", true);
    }

    Long jobId =
        modelTrainJobCoreService.createQueuedJob(
            modelId, nextAttemptNo, nextParams, ZonedDateTime.now());

    modelTrainMngCoreService.clearLastError(modelId);
    modelTrainMngCoreService.markInProgress(modelId, jobId);

    eventPublisher.publishEvent(new ModelTrainJobQueuedEvent(jobId));
    return jobId;
  }

  private enum ResumeMode {
    NONE, // 새로 시작
    REQUIRE // 이어하기
  }

  /**
   * 이어하기 체크포인트 탐지해서 resumeFrom 세팅
   *
   * @param paramsJson
   * @return
   */
  public String findResumeFromOrNull(Map<String, Object> paramsJson) {
    if (paramsJson == null) return null;

    Object out = paramsJson.get("outputFolder");
    if (out == null) return null;

    String outputFolder = String.valueOf(out).trim();
    if (outputFolder.isEmpty()) return null;

    Path outDir = Paths.get(responseDir, outputFolder);
    log.info("resume outDir response path: {}", outDir);

    Path last = outDir.resolve("last_checkpoint");
    log.info("resume last response path: {}", last);

    if (!Files.isRegularFile(last)) return null;

    try {
      // last_checkpoint 내용 그대로 읽기
      String containerPath = Files.readString(last).trim();
      log.info("resume containerPath: {}", containerPath);

      if (containerPath.isEmpty()) return null;

      // 호스트 경로로 변환해서 실제 파일 존재 확인
      String hostPathStr = containerPath.replace("/checkpoints", responseDir);
      Path hostPath = Paths.get(hostPathStr);

      log.info("resume hostPath: {}", hostPath);

      if (!Files.isRegularFile(hostPath)) return null;

      // 3컨테이너 경로 그대로 반환
      return containerPath;

    } catch (Exception e) {
      log.error("resume error", e);
      return null;
    }
  }

  /**
   * 학습에 필요한 데이터셋 파일을 임시폴더 하나에 합치기
   *
   * @param modelUuid
   * @return
   */
  @Transactional
  public UUID createTmpFile(UUID modelUuid) {
    UUID tmpUuid = UUID.randomUUID();
    String raw = tmpUuid.toString().toUpperCase().replace("-", "");

    // model id 가져오기
    Long modelId = modelTrainMngCoreService.findModelIdByUuid(modelUuid);
    // model 에 연결된 dataset id 가져오기
    List<Long> datasetIds = modelTrainMngCoreService.findModelDatasetMapp(modelId);
    List<String> uids = modelTrainMngCoreService.findDatasetUid(datasetIds);

    try {
      // 데이터셋 심볼링크 생성
      //      String pathUid = tmpDatasetService.buildTmpDatasetSymlink(raw, uids);
      // train path 모델 클래스별 조회
      List<ModelTrainLinkDto> trainList = modelTrainMngCoreService.findDatasetTrainPath(modelId);
      // validation path 모델 클래스별 조회
      List<ModelTrainLinkDto> valList = modelTrainMngCoreService.findDatasetValPath(modelId);
      // test path 모델 클래스별 조회
      List<ModelTrainLinkDto> testList = modelTrainMngCoreService.findDatasetTestPath(modelId);

      log.info(
          "createTmpFile class list trainList = {} valList = {} testList = {}",
          trainList.size(),
          valList.size(),
          testList.size());

      // train 데이터셋 심볼링크 생성
      tmpDatasetService.buildTmpDatasetHardlink(raw, "train", trainList);
      // val 데이터셋 심볼링크 생성
      tmpDatasetService.buildTmpDatasetHardlink(raw, "val", valList);
      // test 데이터셋 심볼링크 생성
      tmpDatasetService.buildTmpDatasetHardlink(raw, "test", testList);

      ModelTrainMngDto.UpdateReq updateReq = new ModelTrainMngDto.UpdateReq();
      updateReq.setRequestPath(raw);

      // 학습모델을 수정한다.
      modelTrainMngCoreService.updateModelMaster(modelId, updateReq);
    } catch (IOException e) {
      log.error(
          "createTmpFile failed. modelUuid={}, modelId={}, tmpRaw={}, datasetIdsSize={}, uidsSize={}",
          modelUuid,
          modelId,
          raw,
          (datasetIds == null ? null : datasetIds.size()),
          (uids == null ? null : uids.size()),
          e);

      // 런타임 예외로 래핑하되, 메시지에 핵심 정보 포함
      throw new CustomApiException(
          "INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "임시 데이터셋 생성에 실패했습니다.");
    }
    return modelUuid;
  }
}
