package com.kamco.cd.training.postgres.repository.train;

import static com.kamco.cd.training.postgres.entity.QModelMasterEntity.modelMasterEntity;
import static com.kamco.cd.training.postgres.entity.QModelMetricsTestEntity.modelMetricsTestEntity;
import static com.kamco.cd.training.postgres.entity.QModelMetricsTrainEntity.modelMetricsTrainEntity;
import static com.kamco.cd.training.postgres.entity.QModelMetricsValidationEntity.modelMetricsValidationEntity;

import com.kamco.cd.training.common.enums.TrainStatusType;
import com.kamco.cd.training.postgres.entity.ModelMetricsTestEntity;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ModelMetricJsonDto;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ModelTestFileName;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.Properties;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.ResponsePathDto;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.SimpleMetricJsonDto;
import com.kamco.cd.training.train.dto.ModelTrainMetricsDto.SimpleProperties;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.ZonedDateTime;
import java.util.List;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.jdbc.core.JdbcTemplate;

public class ModelTestMetricsJobRepositoryImpl extends QuerydslRepositorySupport
    implements ModelTestMetricsJobRepositoryCustom {

  private final JPAQueryFactory queryFactory;
  private final JdbcTemplate jdbcTemplate;

  public ModelTestMetricsJobRepositoryImpl(
      JPAQueryFactory queryFactory, JdbcTemplate jdbcTemplate) {
    super(ModelMetricsTestEntity.class);
    this.queryFactory = queryFactory;
    this.jdbcTemplate = jdbcTemplate;
  }

  @Override
  public void updateModelMetricsTrainSaveYn(Long modelId, String stepNo) {
    queryFactory
        .update(modelMasterEntity)
        .set(
            stepNo.equals("step1")
                ? modelMasterEntity.step1MetricSaveYn
                : modelMasterEntity.step2MetricSaveYn,
            true)
        .where(modelMasterEntity.id.eq(modelId))
        .execute();
  }

  @Override
  public List<ResponsePathDto> getTestMetricSaveNotYetModelIds() {
    return queryFactory
        .select(
            Projections.constructor(
                ResponsePathDto.class,
                modelMasterEntity.id,
                modelMasterEntity.responsePath,
                modelMasterEntity.uuid))
        .from(modelMasterEntity)
        .where(
            modelMasterEntity.step2EndDttm.isNotNull(),
            modelMasterEntity.step2State.eq(TrainStatusType.COMPLETED.getId()),
            modelMasterEntity
                .step2MetricSaveYn
                .isNull()
                .or(modelMasterEntity.step2MetricSaveYn.isFalse()))
        .fetch();
  }

  @Override
  public ResponsePathDto getTestMetricSaveNotYetModelId(Long modelId) {
    return queryFactory
        .select(
            Projections.constructor(
                ResponsePathDto.class,
                modelMasterEntity.id,
                modelMasterEntity.responsePath,
                modelMasterEntity.uuid))
        .from(modelMasterEntity)
        .where(
            modelMasterEntity.id.eq(modelId),
            modelMasterEntity
                .step2MetricSaveYn
                .isNull()
                .or(modelMasterEntity.step2MetricSaveYn.isFalse()))
        .fetchOne();
  }

  @Override
  public void insertModelMetricsTest(List<Object[]> batchArgs) {
    // AS-IS
    //    String sql =
    //        """
    //          insert into tb_model_metrics_test
    //          (model_id, model, tp, fp, fn, precisions, recall, f1_score, accuracy, iou,
    //           detection_count, gt_count
    //           )
    //          values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    //      """;
    //
    //    jdbcTemplate.batchUpdate(sql, batchArgs);

    // TO-BE: modelId, model(best_fscore_10) 같은 데이터가 있으면 update, 없으면 insert
    String updateSql =
        """
  UPDATE tb_model_metrics_test
  SET tp=?, fp=?, fn=?, precisions=?, recall=?, f1_score=?, accuracy=?, iou=?,
      detection_count=?, gt_count=?
  WHERE model_id=? AND model=?
""";

    String insertSql =
        """
  INSERT INTO tb_model_metrics_test
    (model_id, model, tp, fp, fn, precisions, recall, f1_score, accuracy, iou,
     detection_count, gt_count)
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";

    // row 단위 처리 (batch 안에서 upsert)
    for (Object[] row : batchArgs) {
      // row 순서: (model_id, model, tp, fp, fn, precisions, recall, f1_score, accuracy, iou,
      // detection_count, gt_count)
      int updated =
          jdbcTemplate.update(
              updateSql, row[2], row[3], row[4], row[5], row[6], row[7], row[8], row[9], row[10],
              row[11], row[0], row[1]);

      if (updated == 0) {
        jdbcTemplate.update(insertSql, row);
      }
    }
  }

  @Override
  public ModelMetricJsonDto getTestMetricPackingInfo(Long modelId) {
    return queryFactory
        .select(
            Projections.constructor(
                ModelMetricJsonDto.class,
                modelMasterEntity.modelNo,
                modelMasterEntity.modelVer,
                Projections.constructor(
                    Properties.class,
                    modelMetricsTestEntity.f1Score,
                    modelMetricsTestEntity.precisions,
                    modelMetricsTestEntity.recall,
                    modelMetricsTestEntity.iou,
                    modelMetricsTrainEntity.loss)))
        .from(modelMetricsTestEntity)
        .innerJoin(modelMasterEntity)
        .on(modelMetricsTestEntity.model.id.eq(modelMasterEntity.id))
        .innerJoin(modelMetricsTrainEntity)
        .on(
            modelMetricsTestEntity.model.eq(modelMetricsTrainEntity.model),
            modelMasterEntity.bestEpoch.eq(modelMetricsTrainEntity.epoch))
        .where(modelMetricsTestEntity.model.id.eq(modelId))
        .orderBy(modelMetricsTestEntity.createdDttm.desc())
        .fetchFirst();
  }

  @Override
  public SimpleMetricJsonDto getSimpleTestMetricPackingInfo(Long modelId) {
    return queryFactory
        .select(
            Projections.constructor(
                SimpleMetricJsonDto.class,
                modelMasterEntity.modelNo,
                modelMasterEntity.modelVer,
                Projections.constructor(
                    SimpleProperties.class,
                    modelMetricsTestEntity.f1Score,
                    modelMetricsTestEntity.precisions,
                    modelMetricsTestEntity.recall,
                    modelMetricsTestEntity.iou,
                    modelMetricsTrainEntity.loss)))
        .from(modelMetricsTestEntity)
        .innerJoin(modelMasterEntity)
        .on(modelMetricsTestEntity.model.id.eq(modelMasterEntity.id))
        .innerJoin(modelMetricsTrainEntity)
        .on(
            modelMetricsTestEntity.model.eq(modelMetricsTrainEntity.model),
            modelMasterEntity.bestEpoch.eq(modelMetricsTrainEntity.epoch))
        .where(modelMetricsTestEntity.model.id.eq(modelId))
        .orderBy(modelMetricsTestEntity.createdDttm.desc())
        .fetchFirst();
  }

  @Override
  public ModelTestFileName findModelTestFileNames(Long modelId) {
    return queryFactory
        .select(
            Projections.constructor(
                ModelTestFileName.class, modelMetricsTestEntity.model1, modelMasterEntity.modelVer))
        .from(modelMetricsTestEntity)
        .innerJoin(modelMasterEntity)
        .on(modelMetricsTestEntity.model.id.eq(modelMasterEntity.id))
        .where(modelMetricsTestEntity.model.id.eq(modelId))
        .fetchOne();
  }

  @Override
  public void updatePackingStart(Long modelId, ZonedDateTime now) {
    queryFactory
        .update(modelMasterEntity)
        .set(modelMasterEntity.packingStrtDttm, ZonedDateTime.now())
        .set(modelMasterEntity.packingState, TrainStatusType.READY.getId())
        .where(modelMasterEntity.id.eq(modelId))
        .execute();
  }

  @Override
  public void updatePackingEnd(Long modelId, ZonedDateTime now, String failSuccState) {
    queryFactory
        .update(modelMasterEntity)
        .set(modelMasterEntity.packingEndDttm, ZonedDateTime.now())
        .set(modelMasterEntity.packingState, failSuccState)
        .where(modelMasterEntity.id.eq(modelId))
        .execute();
  }

  @Override
  public ModelMetricJsonDto getMetricsByEpoch(Long modelId, Integer epoch, String metricType) {
    return queryFactory
        .select(
            Projections.constructor(
                ModelMetricJsonDto.class,
                modelMasterEntity.modelNo,
                modelMasterEntity.modelVer,
                com.querydsl.core.types.dsl.Expressions.constant(metricType),
                com.querydsl.core.types.dsl.Expressions.constant(epoch),
                Projections.constructor(
                    Properties.class,
                    // Changed 메트릭
                    modelMetricsValidationEntity.changedFscore,
                    modelMetricsValidationEntity.changedPrecision,
                    modelMetricsValidationEntity.changedRecall,
                    // Unchanged 메트릭
                    modelMetricsValidationEntity.unchangedFscore,
                    modelMetricsValidationEntity.unchangedPrecision,
                    modelMetricsValidationEntity.unchangedRecall,
                    // Mean 메트릭
                    modelMetricsValidationEntity.mFscore,
                    modelMetricsValidationEntity.mPrecision,
                    modelMetricsValidationEntity.mRecall,
                    modelMetricsValidationEntity.mIou,
                    modelMetricsValidationEntity.mAcc,
                    // Overall Accuracy
                    modelMetricsValidationEntity.aAcc,
                    // Train 메트릭 (Loss, LR, Duration)
                    modelMetricsTrainEntity.loss,
                    modelMetricsTrainEntity.lr,
                    modelMetricsTrainEntity.durationTime),
                Projections.constructor(
                    com.kamco.cd.training.train.dto.ModelTrainMetricsDto.TestMetrics.class,
                    modelMetricsTestEntity.model1,
                    modelMetricsTestEntity.tp,
                    modelMetricsTestEntity.fp,
                    modelMetricsTestEntity.fn,
                    modelMetricsTestEntity.precisions,
                    modelMetricsTestEntity.recall,
                    modelMetricsTestEntity.f1Score,
                    modelMetricsTestEntity.accuracy,
                    modelMetricsTestEntity.iou,
                    modelMetricsTestEntity.detectionCount,
                    modelMetricsTestEntity.gtCount)))
        .from(modelMetricsValidationEntity)
        .innerJoin(modelMasterEntity)
        .on(modelMetricsValidationEntity.model.id.eq(modelMasterEntity.id))
        .leftJoin(modelMetricsTrainEntity)
        .on(
            modelMetricsTrainEntity.model.id.eq(modelMasterEntity.id),
            modelMetricsTrainEntity.epoch.eq(epoch))
        .leftJoin(modelMetricsTestEntity)
        .on(modelMetricsTestEntity.model.id.eq(modelId))
        .where(
            modelMetricsValidationEntity.model.id.eq(modelId),
            modelMetricsValidationEntity.epoch.eq(epoch))
        .fetchOne();
  }
}
