package com.kamco.cd.kamcoback.postgres.repository.Inference;

import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceGeomEntity;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class InferenceResultRepositoryImpl implements InferenceResultRepositoryCustom {

  private final JPAQueryFactory queryFactory;

  @PersistenceContext private final EntityManager em;

  /** tb_map_sheet_anal_data_inference */
  private final QMapSheetAnalDataInferenceEntity inferenceEntity =
      QMapSheetAnalDataInferenceEntity.mapSheetAnalDataInferenceEntity;

  /** tb_map_sheet_anal_data_inference_geom */
  private final QMapSheetAnalDataInferenceGeomEntity inferenceGeomEntity =
      QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity;

  // ===============================
  // Upsert (Native only)
  // ===============================

  @Override
  public int upsertGroupsFromMapSheetAnal() {
    String sql =
        """
        INSERT INTO tb_map_sheet_anal_inference (
            compare_yyyy,
            target_yyyy,
            anal_map_sheet,
            stage,
            anal_title
        )
        SELECT
                    r.input1 AS compare_yyyy,
                    r.input2 AS target_yyyy,
                    r.map_id AS anal_map_sheet,
                    r.stage,
                    CONCAT(r.stage ,'_', r.input1 ,'_', r.input2 ,'_', r.map_id) as anal_title
                FROM inference_results r
                GROUP BY r.stage, r.input1, r.input2, r.map_id
        ON CONFLICT (compare_yyyy, target_yyyy, anal_map_sheet, stage)
        DO UPDATE SET
            updated_dttm  = now()
        """;

    return em.createNativeQuery(sql).executeUpdate();
  }

  /**
   * inference_results 테이블을 기준으로 분석 데이터 단위(stage, compare_yyyy, target_yyyy, map_sheet_num)를
   * 생성/갱신한다.
   *
   * <p>- 최초 생성 시 file_created_yn = false - detecting_cnt는 inference_results 건수 기준
   *
   * @return 반영된 행 수
   */
  @Override
  public int upsertGroupsFromInferenceResults() {

    String sql =
        """
        INSERT INTO tb_map_sheet_anal_data_inference (
            stage,
            compare_yyyy,
            target_yyyy,
            map_sheet_num,
            created_dttm,
            updated_dttm,
            file_created_yn,
            detecting_cnt
        )
        SELECT
            r.stage,
            r.input1 AS compare_yyyy,
            r.input2 AS target_yyyy,
            r.map_id AS map_sheet_num,
            now()    AS created_dttm,
            now()    AS updated_dttm,
            false    AS file_created_yn,
            count(*) AS detecting_cnt
        FROM inference_results r
        GROUP BY r.stage, r.input1, r.input2, r.map_id
        ON CONFLICT (stage, compare_yyyy, target_yyyy, map_sheet_num)
        DO UPDATE SET
            updated_dttm  = now(),
            detecting_cnt = EXCLUDED.detecting_cnt
        """;

    return em.createNativeQuery(sql).executeUpdate();
  }

  /**
   * inference_results 테이블을 기준으로 도형 단위(uuid) 분석 결과를 생성/갱신한다.
   *
   * <p>- uuid 기준 중복 제거(DISTINCT ON) - 최신 updated_dttm 우선 - geometry는 WKB / WKT 모두 처리 - 최초 생성 시
   * file_created_yn = false
   *
   * @return 반영된 행 수
   */
  @Override
  public int upsertGeomsFromInferenceResults() {

    String sql =
        """
        INSERT INTO tb_map_sheet_anal_data_inference_geom (
            uuid, stage, cd_prob, compare_yyyy, target_yyyy, map_sheet_num,
            class_before_cd, class_before_prob, class_after_cd, class_after_prob,
            geom, area, data_uid, created_dttm, updated_dttm,
            file_created_yn
        )
        SELECT
            x.uuid, x.stage, x.cd_prob, x.compare_yyyy, x.target_yyyy, x.map_sheet_num,
            x.class_before_cd, x.class_before_prob, x.class_after_cd, x.class_after_prob,
            x.geom, x.area, x.data_uid, x.created_dttm, x.updated_dttm,
            false AS file_created_yn
        FROM (
          SELECT DISTINCT ON (r.uuid)
              r.uuid,
              r.stage,
              r.cd_prob,
              r.input1 AS compare_yyyy,
              r.input2 AS target_yyyy,
              r.map_id AS map_sheet_num,
              r.before_class AS class_before_cd,
              r.before_probability AS class_before_prob,
              r.after_class AS class_after_cd,
              r.after_probability AS class_after_prob,
              CASE
                WHEN r.geometry IS NULL THEN NULL
                WHEN left(r.geometry, 2) = '01'
                  THEN ST_SetSRID(ST_GeomFromWKB(decode(r.geometry, 'hex')), 5186)
                ELSE ST_GeomFromText(r.geometry, 5186)
              END AS geom,
              r.area,
              di.data_uid,
              r.created_dttm,
              r.updated_dttm
          FROM inference_results r
          JOIN tb_map_sheet_anal_data_inference di
            ON di.stage = r.stage
           AND di.compare_yyyy = r.input1
           AND di.target_yyyy  = r.input2
           AND di.map_sheet_num = r.map_id
          ORDER BY r.uuid, r.updated_dttm DESC NULLS LAST, r.uid DESC
        ) x
        ON CONFLICT (uuid)
        DO UPDATE SET
            stage             = EXCLUDED.stage,
            cd_prob           = EXCLUDED.cd_prob,
            compare_yyyy      = EXCLUDED.compare_yyyy,
            target_yyyy       = EXCLUDED.target_yyyy,
            map_sheet_num     = EXCLUDED.map_sheet_num,
            class_before_cd   = EXCLUDED.class_before_cd,
            class_before_prob = EXCLUDED.class_before_prob,
            class_after_cd    = EXCLUDED.class_after_cd,
            class_after_prob  = EXCLUDED.class_after_prob,
            geom              = EXCLUDED.geom,
            area              = EXCLUDED.area,
            data_uid          = EXCLUDED.data_uid,
            updated_dttm      = now()
        """;

    return em.createNativeQuery(sql).executeUpdate();
  }

  // ===============================
  // Jobs
  // ===============================

  /**
   * 파일 생성이 완료되지 않은 분석 데이터(data_uid) 목록을 조회한다.
   *
   * @param limit 최대 조회 건수
   * @return data_uid 목록
   */
  @Override
  public List<Long> findPendingDataUids(int limit) {
    return queryFactory
        .select(inferenceEntity.id)
        .from(inferenceEntity)
        .where(inferenceEntity.fileCreatedYn.isFalse().or(inferenceEntity.fileCreatedYn.isNull()))
        .orderBy(inferenceEntity.id.asc())
        .limit(limit)
        .fetch();
  }

  // ===============================
  // Reset / Mark
  // ===============================

  /**
   * 분석 데이터의 파일 생성 상태를 재생성 가능 상태로 초기화한다.
   *
   * <p>- file_created_yn = false - file_created_dttm = null
   *
   * @return 갱신된 행 수
   */
  @Override
  public int resetInferenceCreated(Long dataUid) {
    ZonedDateTime now = ZonedDateTime.now();

    return (int)
        queryFactory
            .update(inferenceEntity)
            .set(inferenceEntity.fileCreatedYn, false)
            .set(inferenceEntity.fileCreatedDttm, (ZonedDateTime) null)
            .set(inferenceEntity.updatedDttm, now)
            .where(inferenceEntity.id.eq(dataUid))
            .execute();
  }

  /**
   * 분석 데이터의 파일 생성 완료 상태를 반영한다.
   *
   * @return 갱신된 행 수
   */
  @Override
  public int markInferenceCreated(Long dataUid) {
    ZonedDateTime now = ZonedDateTime.now();

    return (int)
        queryFactory
            .update(inferenceEntity)
            .set(inferenceEntity.fileCreatedYn, true)
            .set(inferenceEntity.fileCreatedDttm, now)
            .set(inferenceEntity.updatedDttm, now)
            .where(inferenceEntity.id.eq(dataUid))
            .execute();
  }

  /**
   * 분석 데이터에 속한 모든 도형의 파일 생성 상태를 초기화한다.
   *
   * @return 갱신된 행 수
   */
  @Override
  public int resetGeomCreatedByDataUid(Long dataUid) {
    ZonedDateTime now = ZonedDateTime.now();

    return (int)
        queryFactory
            .update(inferenceGeomEntity)
            .set(inferenceGeomEntity.fileCreatedYn, false)
            .set(inferenceGeomEntity.fileCreatedDttm, (ZonedDateTime) null)
            .set(inferenceGeomEntity.updatedDttm, now)
            .where(inferenceGeomEntity.dataUid.eq(dataUid))
            .execute();
  }

  /**
   * 파일 생성이 완료된 도형(geo_uid)을 생성 완료 상태로 반영한다.
   *
   * @param geoUids 생성 완료된 도형 UID 목록
   * @return 갱신된 행 수
   */
  @Override
  public int markGeomCreatedByGeoUids(List<Long> geoUids) {
    if (geoUids == null || geoUids.isEmpty()) {
      return 0;
    }

    ZonedDateTime now = ZonedDateTime.now();

    return (int)
        queryFactory
            .update(inferenceGeomEntity)
            .set(inferenceGeomEntity.fileCreatedYn, true)
            .set(inferenceGeomEntity.fileCreatedDttm, now)
            .set(inferenceGeomEntity.updatedDttm, now)
            .where(inferenceGeomEntity.geoUid.in(geoUids))
            .execute();
  }

  // ===============================
  // Export source (Entity only)
  // ===============================

  /**
   * SHP / GeoJSON 파일 생성을 위한 도형 데이터 조회
   *
   * <p>- 특정 분석 데이터(data_uid)에 속한 도형 - geometry 존재 - 파일 미생성 상태만 대상
   */
  @Override
  public List<MapSheetAnalDataInferenceGeomEntity> findGeomEntitiesByDataUid(
      Long dataUid, int limit) {
    return queryFactory
        .selectFrom(inferenceGeomEntity)
        .where(
            inferenceGeomEntity.dataUid.eq(dataUid),
            inferenceGeomEntity.geom.isNotNull(),
            inferenceGeomEntity
                .fileCreatedYn
                .isFalse()
                .or(inferenceGeomEntity.fileCreatedYn.isNull()))
        .orderBy(inferenceGeomEntity.geoUid.asc())
        .limit(limit)
        .fetch();
  }
}
