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

import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.postgres.core.ModelTrainJobCoreService;
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
import com.kamco.cd.training.train.dto.EvalRunRequest;
import com.kamco.cd.training.train.dto.ModelTrainJobDto;
import com.kamco.cd.training.train.dto.ModelTrainJobQueuedEvent;
import com.kamco.cd.training.train.dto.TrainRunRequest;
import com.kamco.cd.training.train.dto.TrainRunResult;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

/** job 실행 */
@Log4j2
@Component
@RequiredArgsConstructor
public class TrainJobWorker {

  private final ModelTrainJobCoreService modelTrainJobCoreService;
  private final ModelTrainMngCoreService modelTrainMngCoreService;
  private final ModelTrainMetricsJobService modelTrainMetricsJobService;
  private final ModelTestMetricsJobService modelTestMetricsJobService;
  private final DockerTrainService dockerTrainService;
  private final ObjectMapper objectMapper;

  @Async("trainJobExecutor")
  @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
  public void handle(ModelTrainJobQueuedEvent event) {

    log.info("[JOB] thread={}, jobId={}", Thread.currentThread().getName(), event.getJobId());

    Long jobId = event.getJobId();

    ModelTrainJobDto job =
        modelTrainJobCoreService
            .findById(jobId)
            .orElseThrow(() -> new IllegalArgumentException("Job not found: " + jobId));

    if (TrainStatusType.STOPPED.getId().equals(job.getStatusCd())) {
      return;
    }

    Long modelId = job.getModelId();
    Map<String, Object> params = job.getParamsJson();

    String jobType = params != null ? String.valueOf(params.get("jobType")) : null;

    boolean isEval = "EVAL".equals(jobType);

    String containerName =
        (isEval ? "eval-" : "train-") + jobId + "-" + params.get("uuid").toString().substring(0, 8);

    String type = isEval ? "TEST" : "TRAIN";
    String step = isEval ? "STEP2" : "STEP1";

    Integer totalEpoch = null;
    if (params.containsKey("totalEpoch")) {
      if (params.get("totalEpoch") != null) {
        totalEpoch = Integer.parseInt(params.get("totalEpoch").toString());
      }
    }

    // Phase 1: 준비 단계
    // Step1: 0% ~ 2.5%, Step2: 50% ~ 52.5%
    double preparingProgress = isEval ? 50.0 : 0.0;
    log.info(
        "[JOB] {} jobId={} | Phase: PREPARING | Progress: {}%",
        step, jobId, String.format("%.1f", preparingProgress + 2.5));

    // Phase 2: 컨테이너 시작
    // Step1: 2.5% ~ 5%, Step2: 52.5% ~ 55%
    double containerStartProgress = isEval ? 52.5 : 2.5;
    log.info(
        "[JOB] {} jobId={} | Phase: CONTAINER_STARTING | Progress: {}%",
        step, jobId, String.format("%.1f", containerStartProgress + 2.5));

    log.info("[JOB] markRunning start jobId={}, containerName={}", jobId, containerName);
    // 실행 시작 처리
    modelTrainJobCoreService.markRunning(
        jobId, containerName, null, "TRAIN_WORKER", totalEpoch, type);
    log.info("[JOB] markRunning done jobId={}", jobId);
    try {
      TrainRunResult result;

      if (isEval) {
        // step2 진행중 처리
        modelTrainMngCoreService.markStep2InProgress(modelId, jobId);
        String uuid = String.valueOf(params.get("uuid"));
        int epoch = (int) params.get("epoch");
        String datasetFolder = String.valueOf(params.get("datasetFolder"));
        String outputFolder = String.valueOf(params.get("outputFolder"));

        EvalRunRequest evalReq = new EvalRunRequest();
        evalReq.setUuid(uuid);
        evalReq.setEpoch(epoch);
        evalReq.setTimeoutSeconds(null);
        evalReq.setDatasetFolder(datasetFolder);
        evalReq.setOutputFolder(outputFolder);
        evalReq.setReqTmpYn((Boolean) params.get("reqTmpYn"));
        log.info("[JOB] selected test epoch={}", epoch);

        // Phase 3: 테스트 시작
        // Step2: 55% ~ 57.5%
        log.info("[JOB] STEP2 jobId={} | Phase: EVAL_STARTED | Progress: 57.5%", jobId);

        // 도커 실행 후 로그 수집
        result = dockerTrainService.runEvalSync(containerName, evalReq);
      } else {
        // step1 진행중 처리
        modelTrainMngCoreService.markStep1InProgress(modelId, jobId);
        TrainRunRequest trainReq = toTrainRunRequest(params);

        // Phase 3: 학습 시작
        // Step1: 5% ~ 7.5%
        log.info("[JOB] STEP1 jobId={} | Phase: TRAINING_STARTED | Progress: 7.5%", jobId);

        // 도커 실행 후 로그 수집
        result = dockerTrainService.runTrainSync(trainReq, containerName);
      }

      ModelTrainJobDto latest =
          modelTrainJobCoreService
              .findById(jobId)
              .orElseThrow(() -> new IllegalArgumentException("Job not found: " + jobId));

      if (TrainStatusType.STOPPED.getId().equals(latest.getStatusCd())) {
        return;
      }

      // Phase 4: 학습/테스트 완료
      // Step1: 42.5%, Step2: 92.5%
      double completedProgress = isEval ? 92.5 : 42.5;
      log.info(
          "[JOB] {} jobId={} | Phase: TRAINING_COMPLETED | Progress: {}%",
          step, jobId, String.format("%.1f", completedProgress));

      /**
       * 0 정상 종료 SUCCESS 1~125 학습 코드 에러 FAILED 137 OOMKill FAILED 143 SIGTERM (stop) STOP -1 우리 내부
       * 강제 중단 STOP
       */
      if (result.getExitCode() == 0) {
        // Phase 5: 결과 처리 중
        // Step1: 45%, Step2: 95%
        double processingProgress = isEval ? 95.0 : 45.0;
        log.info(
            "[JOB] {} jobId={} | Phase: PROCESSING_RESULTS | Progress: {}%",
            step, jobId, String.format("%.1f", processingProgress));

        // 성공 처리
        modelTrainJobCoreService.markSuccess(jobId, result.getExitCode());

        if (isEval) {
          // step2 완료처리
          modelTrainMngCoreService.markStep2Success(modelId);
          // 결과 csv 파일 정보 등록
          modelTestMetricsJobService.findTestValidMetricCsvFiles();
        } else {
          modelTrainMngCoreService.markStep1Success(modelId);
          // 결과 csv 파일 정보 등록
          modelTrainMetricsJobService.findTrainValidMetricCsvFiles();
        }

        // Phase 6: 완료
        // Step1: 50%, Step2: 100%
        double finalProgress = isEval ? 100.0 : 50.0;
        log.info(
            "[JOB] {} jobId={} | Phase: COMPLETED | Progress: {}%",
            step, jobId, String.format("%.1f", finalProgress));

      } else {

        String failMsg = result.getStatus() + "\n" + result.getLogs();
        log.info("training fail exitCode={} Msg ={}", result.getExitCode(), failMsg);

        if (result.getExitCode() == -1
            || result.getExitCode() == 143
            || result.getExitCode() == 137) {
          // 실패 처리
          modelTrainJobCoreService.markPaused(
              jobId, result.getExitCode(), result.getStatus() + "\n" + result.getLogs());

          if (isEval) {
            // 오류 정보 등록
            modelTrainMngCoreService.markStep2Stop(modelId, "exit=" + result.getExitCode());
          } else {
            // 오류 정보 등록
            modelTrainMngCoreService.markStep1Stop(modelId, "exit=" + result.getExitCode());
          }
        } else {
          // 실패 처리
          modelTrainJobCoreService.markFailed(
              jobId, result.getExitCode(), result.getStatus() + "\n" + result.getLogs());

          if (isEval) {
            // 오류 정보 등록
            modelTrainMngCoreService.markStep2Error(modelId, "exit=" + result.getExitCode());
          } else {
            // 오류 정보 등록
            modelTrainMngCoreService.markError(modelId, "exit=" + result.getExitCode());
          }
        }
      }

    } catch (Exception e) {
      modelTrainJobCoreService.markFailed(jobId, null, e.getMessage());

      if ("EVAL".equals(params.get("jobType"))) {
        // 오류 정보 등록
        modelTrainMngCoreService.markStep2Error(modelId, e.getMessage());
      } else {
        // 오류 정보 등록
        modelTrainMngCoreService.markError(modelId, e.getMessage());
      }
    }
  }

  private TrainRunRequest toTrainRunRequest(Map<String, Object> paramsJson) {
    if (paramsJson == null || paramsJson.isEmpty()) {
      return null;
    }

    return objectMapper.convertValue(paramsJson, TrainRunRequest.class);
  }
}
