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

import static com.kamco.cd.training.postgres.entity.QDatasetClsObjEntity.datasetClsObjEntity;
import static com.kamco.cd.training.postgres.entity.QDatasetEntity.datasetEntity;
import static com.kamco.cd.training.postgres.entity.QDatasetObjEntity.datasetObjEntity;
import static com.kamco.cd.training.postgres.entity.QModelDatasetMappEntity.modelDatasetMappEntity;
import static com.kamco.cd.training.postgres.entity.QModelMasterEntity.modelMasterEntity;

import com.kamco.cd.training.common.enums.DetectionClassification;
import com.kamco.cd.training.common.enums.ModelType;
import com.kamco.cd.training.dataset.dto.DatasetClsObjDto;
import com.kamco.cd.training.dataset.dto.DatasetDto.*;
import com.kamco.cd.training.dataset.dto.DatasetObjDto;
import com.kamco.cd.training.postgres.entity.*;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class DatasetClsRepositoryImpl implements DatasetClsRepositoryCustom {

  private final JPAQueryFactory queryFactory;
  private final QDatasetEntity dataset = QDatasetEntity.datasetEntity;

  @PersistenceContext private final EntityManager em;

  /**
   * 데이터셋 목록 조회
   *
   * @param searchReq 검색 조건
   * @return 페이징 처리된 데이터셋 Entity 목록
   */
  @Override
  public Page<DatasetEntity> findDatasetList(SearchReq searchReq) {
    Pageable pageable = searchReq.toPageable();
    BooleanBuilder builder = new BooleanBuilder();

    // 제목
    if (StringUtils.isNotBlank(searchReq.getTitle())) {
      String contains = "%" + searchReq.getTitle() + "%";
      builder.and(dataset.title.likeIgnoreCase(contains));
    }

    // 구분
    if (StringUtils.isNotBlank(searchReq.getDataType())) {
      builder.and(dataset.dataType.eq(searchReq.getDataType()));
    }

    // Entity 직접 조회 (Projections 사용 지양)
    List<DatasetEntity> content =
        queryFactory
            .selectFrom(dataset)
            .where(builder.and(dataset.deleted.isFalse()).and(dataset.cdClsType.eq("CLS")))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(dataset.createdDttm.desc())
            .fetch();

    // Count 쿼리 별도 실행 (null safe handling)
    long total =
        Optional.ofNullable(
                queryFactory
                    .select(dataset.count())
                    .from(dataset)
                    .where(builder.and(dataset.deleted.isFalse()).and(dataset.cdClsType.eq("CLS")))
                    .fetchOne())
            .orElse(0L);

    return new PageImpl<>(content, pageable, total);
  }

  @Override
  public Optional<DatasetEntity> findByUuid(UUID id) {
    return Optional.ofNullable(
        queryFactory
            .select(dataset)
            .from(dataset)
            .where(dataset.uuid.eq(id), dataset.deleted.isFalse())
            .fetchOne());
  }

  @Override
  public List<SelectDataSet> getDatasetSelectG1List(DatasetReq req) {

    BooleanBuilder builder = new BooleanBuilder();

    builder.and(dataset.deleted.isFalse());

    if (StringUtils.isNotBlank(req.getDataType()) && !"CURRENT".equals(req.getDataType())) {
      builder.and(dataset.dataType.eq(req.getDataType()));
    }

    if (req.getIds() != null) {
      builder.and(dataset.id.in(req.getIds()));
    }

    return queryFactory
        .select(
            Projections.constructor(
                SelectDataSet.class,
                Expressions.constant(req.getModelNo()),
                dataset.id,
                dataset.uuid,
                dataset.dataType,
                dataset.title,
                dataset.roundNo,
                dataset.compareYyyy,
                dataset.targetYyyy,
                dataset.memo,
                new CaseBuilder()
                    .when(
                        datasetObjEntity.targetClassCd.eq(DetectionClassification.BUILDING.getId()))
                    .then(1L)
                    .otherwise(0L)
                    .sum(),
                new CaseBuilder()
                    .when(
                        datasetObjEntity.targetClassCd.eq(
                            DetectionClassification.CONTAINER.getId()))
                    .then(1L)
                    .otherwise(0L)
                    .sum()))
        .from(dataset)
        .leftJoin(datasetObjEntity)
        .on(dataset.id.eq(datasetObjEntity.datasetUid))
        .where(builder) // datasetObjEntity.targetClassCd.in("building", "container").and(
        .groupBy(
            dataset.id,
            dataset.uuid,
            dataset.dataType,
            dataset.title,
            dataset.roundNo,
            dataset.memo)
        .orderBy(dataset.createdDttm.desc())
        .fetch();
  }

  // TODO 미사용시작
  @Override
  public List<SelectTransferDataSet> getDatasetTransferSelectG1List(Long modelId) {

    QModelMasterEntity beforeMaster = new QModelMasterEntity("beforeMaster");
    QModelDatasetMappEntity beforeMapp = new QModelDatasetMappEntity("beforeMapp");
    QDatasetEntity beforeDataset = new QDatasetEntity("beforeDataset");
    QDatasetObjEntity beforeObj = new QDatasetObjEntity("beforeObj");

    return queryFactory
        .select(
            Projections.constructor(
                SelectTransferDataSet.class,

                // ===== 현재 =====
                modelMasterEntity.modelNo,
                dataset.id,
                dataset.uuid,
                dataset.dataType,
                dataset.title,
                dataset.roundNo,
                dataset.compareYyyy,
                dataset.targetYyyy,
                dataset.memo,
                new CaseBuilder()
                    .when(datasetObjEntity.targetClassCd.eq("building"))
                    .then(1)
                    .otherwise(0)
                    .sum(),
                new CaseBuilder()
                    .when(datasetObjEntity.targetClassCd.eq("container"))
                    .then(1)
                    .otherwise(0)
                    .sum(),

                // ===== before (join으로) =====
                beforeMaster.modelNo,
                beforeDataset.id,
                beforeDataset.uuid,
                beforeDataset.dataType,
                beforeDataset.title,
                beforeDataset.roundNo,
                beforeDataset.compareYyyy,
                beforeDataset.targetYyyy,
                beforeDataset.memo,
                new CaseBuilder()
                    .when(beforeObj.targetClassCd.eq("building"))
                    .then(1)
                    .otherwise(0)
                    .sum(),
                new CaseBuilder()
                    .when(beforeObj.targetClassCd.eq("container"))
                    .then(1)
                    .otherwise(0)
                    .sum()))
        .from(modelMasterEntity)

        // ===== 현재 dataset join =====
        .leftJoin(modelDatasetMappEntity)
        .on(modelDatasetMappEntity.modelUid.eq(modelMasterEntity.id))
        .leftJoin(dataset)
        .on(modelDatasetMappEntity.datasetUid.eq(dataset.id))
        .leftJoin(datasetObjEntity)
        .on(dataset.id.eq(datasetObjEntity.datasetUid))

        // ===== before 모델 join =====
        .leftJoin(beforeMaster)
        .on(beforeMaster.id.eq(modelMasterEntity.beforeModelId))
        .leftJoin(beforeMapp)
        .on(beforeMapp.modelUid.eq(beforeMaster.id))
        .leftJoin(beforeDataset)
        .on(beforeMapp.datasetUid.eq(beforeDataset.id))
        .leftJoin(beforeObj)
        .on(beforeDataset.id.eq(beforeObj.datasetUid))
        .where(modelMasterEntity.id.eq(modelId))
        .groupBy(
            modelMasterEntity.modelNo,
            dataset.id,
            dataset.uuid,
            dataset.dataType,
            dataset.title,
            dataset.roundNo,
            dataset.compareYyyy,
            dataset.targetYyyy,
            dataset.memo,
            beforeMaster.modelNo,
            beforeDataset.id,
            beforeDataset.uuid,
            beforeDataset.dataType,
            beforeDataset.title,
            beforeDataset.roundNo,
            beforeDataset.compareYyyy,
            beforeDataset.targetYyyy,
            beforeDataset.memo)
        .orderBy(dataset.createdDttm.desc())
        .fetch();
  }

  // TODO 미사용 끝

  @Override
  public List<SelectDataSet> getDatasetSelectG2G3List(DatasetReq req) {

    String building = DetectionClassification.BUILDING.getId();
    String container = DetectionClassification.CONTAINER.getId();
    String waste = DetectionClassification.WASTE.getId();
    String solar = DetectionClassification.SOLAR.getId();

    BooleanBuilder builder = new BooleanBuilder();
    builder.and(dataset.deleted.isFalse());

    NumberExpression<Long> selectedCnt = null;
    // G2
    NumberExpression<Long> wasteCnt =
        datasetObjEntity
            .targetClassCd
            .when(DetectionClassification.WASTE.getId())
            .then(1L)
            .otherwise(0L)
            .sum();

    // G3 (G1, G2, G4 제외)
    NumberExpression<Long> elseCnt =
        new CaseBuilder()
            .when(datasetObjEntity.targetClassCd.notIn(building, container, waste, solar))
            .then(1L)
            .otherwise(0L)
            .sum();

    if (req.getModelNo().equals(ModelType.G2.getId())) {
      selectedCnt = wasteCnt;
    } else {
      selectedCnt = elseCnt;
    }

    if (StringUtils.isNotBlank(req.getDataType())) {
      if (!"CURRENT".equals(req.getDataType())) {
        builder.and(dataset.dataType.eq(req.getDataType()));
      }
    }

    if (req.getIds() != null) {
      builder.and(dataset.id.in(req.getIds()));
    }

    return queryFactory
        .select(
            Projections.constructor(
                SelectDataSet.class,
                Expressions.constant(req.getModelNo()),
                dataset.id,
                dataset.uuid,
                dataset.dataType,
                dataset.title,
                dataset.roundNo,
                dataset.compareYyyy,
                dataset.targetYyyy,
                dataset.memo,
                selectedCnt.as("cnt")))
        .from(dataset)
        .leftJoin(datasetObjEntity)
        .on(dataset.id.eq(datasetObjEntity.datasetUid))
        .where(builder)
        .groupBy(
            dataset.id,
            dataset.uuid,
            dataset.dataType,
            dataset.title,
            dataset.roundNo,
            dataset.memo)
        .orderBy(dataset.createdDttm.desc())
        .fetch();
  }

  // TODO 미사용시작
  @Override
  public List<SelectTransferDataSet> getDatasetTransferSelectG2G3List(
      Long modelId, String modelNo) {

    // before join용
    QModelMasterEntity beforeMaster = new QModelMasterEntity("beforeMaster");
    QModelDatasetMappEntity beforeMapp = new QModelDatasetMappEntity("beforeMapp");
    QDatasetEntity beforeDataset = new QDatasetEntity("beforeDataset");
    QDatasetObjEntity beforeObj = new QDatasetObjEntity("beforeObj");

    BooleanBuilder builder = new BooleanBuilder();

    NumberExpression<Long> wasteCnt =
        datasetObjEntity.targetClassCd.when("waste").then(1L).otherwise(0L).sum();

    NumberExpression<Long> elseCnt =
        new CaseBuilder()
            .when(datasetObjEntity.targetClassCd.notIn("building", "container", "waste"))
            .then(1L)
            .otherwise(0L)
            .sum();

    NumberExpression<Long> selectedCnt = ModelType.G2.getId().equals(modelNo) ? wasteCnt : elseCnt;

    // before도 동일 로직으로 cnt 계산
    NumberExpression<Long> beforeWasteCnt =
        beforeObj.targetClassCd.when("waste").then(1L).otherwise(0L).sum();

    NumberExpression<Long> beforeElseCnt =
        new CaseBuilder()
            .when(beforeObj.targetClassCd.notIn("building", "container", "waste"))
            .then(1L)
            .otherwise(0L)
            .sum();

    NumberExpression<Long> beforeSelectedCnt =
        ModelType.G2.getId().equals(modelNo) ? beforeWasteCnt : beforeElseCnt;

    return queryFactory
        .select(
            Projections.constructor(
                SelectTransferDataSet.class,

                // ===== 현재 =====
                modelMasterEntity.modelNo, // modelNo 파라미터 사용 (req.getModelNo() 제거)
                dataset.id,
                dataset.uuid,
                dataset.dataType,
                dataset.title,
                dataset.roundNo,
                dataset.compareYyyy,
                dataset.targetYyyy,
                dataset.memo,
                selectedCnt, // classCount 자리에 들어가는 cnt (Long)

                // ===== before =====
                beforeMaster.modelNo,
                beforeDataset.id,
                beforeDataset.uuid,
                beforeDataset.dataType,
                beforeDataset.title,
                beforeDataset.roundNo,
                beforeDataset.compareYyyy,
                beforeDataset.targetYyyy,
                beforeDataset.memo,
                beforeSelectedCnt))
        .from(modelMasterEntity)

        // ===== 현재 dataset =====
        .leftJoin(modelDatasetMappEntity)
        .on(modelDatasetMappEntity.modelUid.eq(modelMasterEntity.id))
        .leftJoin(dataset)
        .on(modelDatasetMappEntity.datasetUid.eq(dataset.id))
        .leftJoin(datasetObjEntity)
        .on(dataset.id.eq(datasetObjEntity.datasetUid))

        // ===== before dataset =====
        .leftJoin(beforeMaster)
        .on(beforeMaster.id.eq(modelMasterEntity.beforeModelId))
        .leftJoin(beforeMapp)
        .on(beforeMapp.modelUid.eq(beforeMaster.id))
        .leftJoin(beforeDataset)
        .on(beforeMapp.datasetUid.eq(beforeDataset.id))
        .leftJoin(beforeObj)
        .on(beforeDataset.id.eq(beforeObj.datasetUid))
        .where(modelMasterEntity.id.eq(modelId).and(builder))

        // sum() 때문에 groupBy 필요
        .groupBy(
            dataset.id,
            dataset.uuid,
            dataset.dataType,
            dataset.title,
            dataset.roundNo,
            dataset.compareYyyy,
            dataset.targetYyyy,
            dataset.memo,
            beforeMaster.modelNo,
            beforeDataset.id,
            beforeDataset.uuid,
            beforeDataset.dataType,
            beforeDataset.title,
            beforeDataset.roundNo,
            beforeDataset.compareYyyy,
            beforeDataset.targetYyyy,
            beforeDataset.memo)
        .orderBy(dataset.createdDttm.desc())
        .fetch();
  }

  // TODO 미사용 끝

  @Override
  public Long getDatasetMaxStage(int compareYyyy, int targetYyyy) {
    return queryFactory
        .select(dataset.roundNo.max().coalesce(0L))
        .from(dataset)
        .where(dataset.compareYyyy.eq(compareYyyy), dataset.targetYyyy.eq(targetYyyy))
        .fetchOne();
  }

  @Override
  public Long insertDatasetMngData(DatasetMngRegDto mngRegDto) {

    UUID uuid = UUID.randomUUID();
    // 1. 기본 생성자로 빈 엔티티 객체 생성
    //    DatasetEntity datasetEntity = new DatasetEntity();
    //
    //    // 2. Setter를 이용해 데이터 채우기
    //    datasetEntity.setUid(mngRegDto.getUid());
    //    datasetEntity.setDataType(mngRegDto.getDataType());
    //    datasetEntity.setCompareYyyy(mngRegDto.getCompareYyyy());
    //    datasetEntity.setTargetYyyy(mngRegDto.getTargetYyyy());
    //    datasetEntity.setRoundNo(mngRegDto.getRoundNo());
    //    datasetEntity.setTotalSize(mngRegDto.getTotalSize());
    //    datasetEntity.setTitle(mngRegDto.getTitle());
    //    datasetEntity.setMemo(mngRegDto.getMemo());
    //    datasetEntity.setDatasetPath(mngRegDto.getDatasetPath());
    //    datasetEntity.setCdClsType(mngRegDto.getCdClsType());
    //
    //    datasetEntity.setUuid(UUID.randomUUID());
    //
    //
    //    // 2. JPA 영속성 컨텍스트에 저장 (이 시점에 DB에 들어가거나 ID가 생성됨)
    //    em.persist(datasetEntity);

    queryFactory
        .insert(dataset)
        .columns(
            dataset.uid,
            dataset.uuid,
            dataset.dataType,
            dataset.compareYyyy,
            dataset.targetYyyy,
            dataset.roundNo,
            dataset.totalSize,
            dataset.title,
            dataset.memo,
            dataset.datasetPath,
            dataset.cdClsType)
        .values(
            mngRegDto.getUid(),
            uuid,
            mngRegDto.getDataType(),
            mngRegDto.getCompareYyyy(),
            mngRegDto.getTargetYyyy(),
            mngRegDto.getRoundNo(),
            mngRegDto.getTotalSize(),
            mngRegDto.getTitle(),
            mngRegDto.getMemo(),
            mngRegDto.getDatasetPath(),
            mngRegDto.getCdClsType())
        .execute();

    // 3. uid 조회 필요 없이, 생성된 Key를 안전하게 바로 리턴
    // return datasetEntity.getId();
    return queryFactory.select(dataset.id).from(dataset).where(dataset.uuid.eq(uuid)).fetchOne();
  }

  @Override
  public List<String> findDatasetUid(List<Long> datasetIds) {
    return queryFactory.select(dataset.uid).from(dataset).where(dataset.id.in(datasetIds)).fetch();
  }

  @Override
  public Long findDatasetByUidExistsCnt(String uid) {
    return queryFactory
        .select(dataset.id.count())
        .from(dataset)
        .where(dataset.uid.eq(uid), dataset.deleted.isFalse())
        .fetchOne();
  }

  @Override
  public List<SelectDataSet> getDatasetSelectG4List(DatasetReq req) {
    BooleanBuilder builder = new BooleanBuilder();

    builder.and(dataset.deleted.isFalse());

    if (StringUtils.isNotBlank(req.getDataType()) && !"CURRENT".equals(req.getDataType())) {
      builder.and(dataset.dataType.eq(req.getDataType()));
    }

    if (req.getIds() != null) {
      builder.and(dataset.id.in(req.getIds()));
    }

    return queryFactory
        .select(
            Projections.constructor(
                SelectDataSet.class,
                Expressions.constant(req.getModelNo()),
                dataset.id,
                dataset.uuid,
                dataset.dataType,
                dataset.title,
                dataset.roundNo,
                dataset.compareYyyy,
                dataset.targetYyyy,
                dataset.memo,
                new CaseBuilder()
                    .when(datasetObjEntity.targetClassCd.eq(DetectionClassification.SOLAR.getId()))
                    .then(1L)
                    .otherwise(0L)
                    .sum()))
        .from(dataset)
        .leftJoin(datasetObjEntity)
        .on(dataset.id.eq(datasetObjEntity.datasetUid))
        .where(builder)
        .groupBy(
            dataset.id,
            dataset.uuid,
            dataset.dataType,
            dataset.title,
            dataset.roundNo,
            dataset.memo)
        .orderBy(dataset.createdDttm.desc())
        .fetch();
  }

  @Override
  public Page<DatasetClsObjEntity> searchDatasetClsObjectList(
      DatasetClsObjDto.ClsSearchReq searchReq) {
    {
      Pageable pageable = searchReq.toPageable();
      BooleanBuilder builder = new BooleanBuilder();

      if (StringUtils.isNotBlank(searchReq.getClassCd())) {
        builder.and(datasetClsObjEntity.classCd.equalsIgnoreCase(searchReq.getClassCd()));
      }
      if (searchReq.getYyyy() != null) {
        builder.and(datasetClsObjEntity.yyyy.eq(searchReq.getYyyy()));
      }
      if (StringUtils.isNotBlank(searchReq.getMapSheetNum())) {
        builder.and(datasetClsObjEntity.mapSheetNum.eq(searchReq.getMapSheetNum()));
      }

      DatasetEntity entity =
          queryFactory
              .selectFrom(datasetEntity)
              .where(datasetEntity.uuid.eq(searchReq.getUuid()))
              .fetchOne();

      if (Objects.isNull(entity)) {
        throw new EntityNotFoundException(
            "DatasetEntity not found for uuid: " + searchReq.getUuid());
      }

      List<DatasetClsObjEntity> content =
          queryFactory
              .selectFrom(datasetClsObjEntity)
              .where(
                  datasetClsObjEntity
                      .deleted
                      .isFalse()
                      .and(datasetClsObjEntity.datasetUid.eq(entity.getId()))
                      .and(builder)
                      .and(
                          datasetClsObjEntity.type.eq(DatasetClsObjDto.FolderType.TRAIN.getText())))
              .offset(pageable.getOffset())
              .limit(pageable.getPageSize())
              .fetch();

      // Count 쿼리 별도 실행 (null safe handling)
      long total =
          Optional.ofNullable(
                  queryFactory
                      .select(datasetClsObjEntity.count())
                      .from(datasetClsObjEntity)
                      .where(
                          datasetClsObjEntity
                              .deleted
                              .isFalse()
                              .and(datasetClsObjEntity.datasetUid.eq(entity.getId()))
                              .and(builder)
                              .and(
                                  datasetClsObjEntity.type.eq(
                                      DatasetClsObjDto.FolderType.TRAIN.getText())))
                      .fetchOne())
              .orElse(0L);

      return new PageImpl<>(content, pageable, total);
    }
  }

  @Override
  public void insertDatasetClsObj(DatasetObjDto.DatasetClsObjRegDto objRegDto) {
    try {
      em.createNativeQuery(
              """
        insert into tb_dataset_cls_obj
      (uuid, dataset_uid, type, yyyy, class_cd,
       map_sheet_num, tif_img_path, file_name)
      values
      (?, ?, ?, ?, ?, ?, ?, ?)
      """)
          .setParameter(1, UUID.randomUUID())
          .setParameter(2, objRegDto.getDatasetUid())
          .setParameter(3, objRegDto.getType())
          .setParameter(4, objRegDto.getYyyy())
          .setParameter(5, objRegDto.getClassCd())
          .setParameter(6, objRegDto.getMapSheetNum())
          .setParameter(7, objRegDto.getTifImgPath())
          .setParameter(8, objRegDto.getFileName())
          .executeUpdate();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public String getFilePathByUUIDPathType(UUID uuid) {
    return queryFactory
        .select(datasetClsObjEntity.tifImgPath)
        .from(datasetClsObjEntity)
        .where(datasetClsObjEntity.uuid.eq(uuid))
        .fetchOne();
  }
}
