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

import static com.kamco.cd.kamcoback.postgres.entity.QAuditLogEntity.auditLogEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QErrorLogEntity.errorLogEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMemberEntity.memberEntity;
import static com.kamco.cd.kamcoback.postgres.entity.QMenuEntity.menuEntity;

import com.kamco.cd.kamcoback.log.dto.AuditLogDto;
import com.kamco.cd.kamcoback.log.dto.ErrorLogDto;
import com.kamco.cd.kamcoback.log.dto.EventStatus;
import com.kamco.cd.kamcoback.log.dto.EventType;
import com.kamco.cd.kamcoback.postgres.entity.AuditLogEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMenuEntity;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.core.types.dsl.StringExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import io.micrometer.common.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;

public class AuditLogRepositoryImpl extends QuerydslRepositorySupport
    implements AuditLogRepositoryCustom {

  private final JPAQueryFactory queryFactory;
  private final StringExpression NULL_STRING = Expressions.stringTemplate("cast(null as text)");

  public AuditLogRepositoryImpl(JPAQueryFactory queryFactory) {
    super(AuditLogEntity.class);
    this.queryFactory = queryFactory;
  }

  @Override
  public Page<AuditLogDto.DailyAuditList> findLogByDaily(
      AuditLogDto.searchReq searchReq, LocalDate startDate, LocalDate endDate) {
    StringExpression groupDateTime =
        Expressions.stringTemplate("to_char({0}, 'YYYY-MM-DD')", auditLogEntity.createdDate);

    Pageable pageable = searchReq.toPageable();
    List<AuditLogDto.DailyAuditList> foundContent =
        queryFactory
            .select(
                Projections.constructor(
                    AuditLogDto.DailyAuditList.class,
                    readCount().as("readCount"),
                    cudCount().as("cudCount"),
                    printCount().as("printCount"),
                    downloadCount().as("downloadCount"),
                    auditLogEntity.count().as("totalCount"),
                    groupDateTime.as("baseDate")))
            .from(auditLogEntity)
            .where(eventEndedAtBetween(startDate, endDate))
            .groupBy(groupDateTime)
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(groupDateTime.desc())
            .fetch();

    Long countQuery =
        queryFactory
            .select(groupDateTime.countDistinct())
            .from(auditLogEntity)
            .where(eventEndedAtBetween(startDate, endDate))
            .fetchOne();

    return new PageImpl<>(foundContent, pageable, countQuery);
  }

  @Override
  public Page<AuditLogDto.MenuAuditList> findLogByMenu(
      AuditLogDto.searchReq searchReq, String searchValue) {
    Pageable pageable = searchReq.toPageable();
    List<AuditLogDto.MenuAuditList> foundContent =
        queryFactory
            .select(
                Projections.constructor(
                    AuditLogDto.MenuAuditList.class,
                    auditLogEntity.menuUid.as("menuId"),
                    menuEntity.menuNm.max().as("menuName"),
                    readCount().as("readCount"),
                    cudCount().as("cudCount"),
                    printCount().as("printCount"),
                    downloadCount().as("downloadCount"),
                    auditLogEntity.count().as("totalCount")))
            .from(auditLogEntity)
            .leftJoin(menuEntity)
            .on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
            .where(menuNameEquals(searchValue))
            .groupBy(auditLogEntity.menuUid)
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(auditLogEntity.createdDate.max().desc())
            .fetch();

    // count query group by 를 지정하면 하나의 row 가 아니라 그룹핑된 여러 row 가 나올 수 있다.
    // select query 의 group by 대상의 컬럼을 count query 에선 select distinct 로 처리 한다.
    Long countQuery =
        queryFactory
            .select(auditLogEntity.menuUid.countDistinct())
            .from(auditLogEntity)
            .leftJoin(menuEntity)
            .on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
            .where(menuNameEquals(searchValue))
            .fetchOne();

    return new PageImpl<>(foundContent, pageable, countQuery);
  }

  @Override
  public Page<AuditLogDto.UserAuditList> findLogByAccount(
      AuditLogDto.searchReq searchReq, String searchValue) {
    Pageable pageable = searchReq.toPageable();
    List<AuditLogDto.UserAuditList> foundContent =
        queryFactory
            .select(
                Projections.constructor(
                    AuditLogDto.UserAuditList.class,
                    auditLogEntity.userUid.as("accountId"),
                    memberEntity.employeeNo.as("loginId"),
                    memberEntity.name.as("username"),
                    readCount().as("readCount"),
                    cudCount().as("cudCount"),
                    printCount().as("printCount"),
                    downloadCount().as("downloadCount"),
                    auditLogEntity.count().as("totalCount")))
            .from(auditLogEntity)
            .leftJoin(memberEntity)
            .on(auditLogEntity.userUid.eq(memberEntity.id))
            .where(loginIdOrUsernameContains(searchValue))
            .groupBy(auditLogEntity.userUid, memberEntity.employeeNo, memberEntity.name)
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            //            .orderBy(auditLogEntity.eventEndedAt.max().desc())
            .fetch();

    Long countQuery =
        queryFactory
            .select(auditLogEntity.userUid.countDistinct())
            .from(auditLogEntity)
            .leftJoin(memberEntity)
            .on(auditLogEntity.userUid.eq(memberEntity.id))
            .where(loginIdOrUsernameContains(searchValue))
            .fetchOne();

    return new PageImpl<>(foundContent, pageable, countQuery);
  }

  @Override
  public Page<AuditLogDto.DailyDetail> findLogByDailyResult(
      AuditLogDto.searchReq searchReq, LocalDate logDate) {
    Pageable pageable = searchReq.toPageable();
    QMenuEntity parent = new QMenuEntity("parent");
    // 1depth menu name
    StringExpression parentMenuName =
        new CaseBuilder()
            .when(parent.menuUid.isNull())
            .then(menuEntity.menuNm)
            .otherwise(parent.menuNm);

    // 2depth menu name
    StringExpression menuName =
        new CaseBuilder()
            .when(parent.menuUid.isNull())
            .then(NULL_STRING)
            .otherwise(menuEntity.menuNm);

    List<AuditLogDto.DailyDetail> foundContent =
        queryFactory
            .select(
                Projections.constructor(
                    AuditLogDto.DailyDetail.class,
                    auditLogEntity.id.as("logId"),
                    memberEntity.name.as("userName"),
                    memberEntity.employeeNo.as("loginId"),
                    menuEntity.menuNm.as("menuName"),
                    auditLogEntity.eventType.as("eventType"),
                    Projections.constructor(
                        AuditLogDto.LogDetail.class,
                        Expressions.constant("한국자산관리공사"), // serviceName
                        parentMenuName.as("parentMenuName"),
                        menuName,
                        menuEntity.menuUrl.as("menuUrl"),
                        menuEntity.description.as("menuDescription"),
                        menuEntity.menuOrder.as("sortOrder"),
                        menuEntity.isUse.as("used"))))
            .from(auditLogEntity)
            .leftJoin(menuEntity)
            .on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
            .leftJoin(menuEntity.parent, parent)
            .leftJoin(memberEntity)
            .on(auditLogEntity.userUid.eq(memberEntity.id))
            .where(eventEndedAtEqDate(logDate))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(auditLogEntity.createdDate.desc())
            .fetch();

    Long countQuery =
        queryFactory
            .select(auditLogEntity.id.countDistinct())
            .from(auditLogEntity)
            .leftJoin(menuEntity)
            .on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
            .leftJoin(menuEntity.parent, parent)
            .leftJoin(memberEntity)
            .on(auditLogEntity.userUid.eq(memberEntity.id))
            .where(eventEndedAtEqDate(logDate))
            .fetchOne();

    return new PageImpl<>(foundContent, pageable, countQuery);
  }

  @Override
  public Page<AuditLogDto.MenuDetail> findLogByMenuResult(
      AuditLogDto.searchReq searchReq, String menuUid) {
    Pageable pageable = searchReq.toPageable();
    QMenuEntity parent = new QMenuEntity("parent");
    // 1depth menu name
    StringExpression parentMenuName =
        new CaseBuilder()
            .when(parent.menuUid.isNull())
            .then(menuEntity.menuNm)
            .otherwise(parent.menuNm);

    // 2depth menu name
    StringExpression menuName =
        new CaseBuilder()
            .when(parent.menuUid.isNull())
            .then(NULL_STRING)
            .otherwise(menuEntity.menuNm);

    List<AuditLogDto.MenuDetail> foundContent =
        queryFactory
            .select(
                Projections.constructor(
                    AuditLogDto.MenuDetail.class,
                    auditLogEntity.id.as("logId"),
                    Expressions.stringTemplate(
                            "to_char({0}, 'YYYY-MM-DD')", auditLogEntity.createdDate)
                        .as("logDateTime"), // ??
                    memberEntity.name.as("userName"),
                    memberEntity.employeeNo.as("loginId"),
                    auditLogEntity.eventType.as("eventType"),
                    Projections.constructor(
                        AuditLogDto.LogDetail.class,
                        Expressions.constant("한국자산관리공사"), // serviceName
                        parentMenuName.as("parentMenuName"),
                        menuName,
                        menuEntity.menuUrl.as("menuUrl"),
                        menuEntity.description.as("menuDescription"),
                        menuEntity.menuOrder.as("sortOrder"),
                        menuEntity.isUse.as("used"))))
            .from(auditLogEntity)
            .leftJoin(menuEntity)
            .on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
            .leftJoin(menuEntity.parent, parent)
            .leftJoin(memberEntity)
            .on(auditLogEntity.userUid.eq(memberEntity.id))
            .where(menuUidEq(menuUid))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(auditLogEntity.createdDate.desc())
            .fetch();

    Long countQuery =
        queryFactory
            .select(auditLogEntity.id.countDistinct())
            .from(auditLogEntity)
            .leftJoin(menuEntity)
            .on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
            .leftJoin(menuEntity.parent, parent)
            .leftJoin(memberEntity)
            .on(auditLogEntity.userUid.eq(memberEntity.id))
            .where(menuUidEq(menuUid))
            .fetchOne();

    return new PageImpl<>(foundContent, pageable, countQuery);
  }

  @Override
  public Page<AuditLogDto.UserDetail> findLogByAccountResult(
      AuditLogDto.searchReq searchReq, Long userUid) {
    Pageable pageable = searchReq.toPageable();
    QMenuEntity parent = new QMenuEntity("parent");
    // 1depth menu name
    StringExpression parentMenuName =
        new CaseBuilder()
            .when(parent.menuUid.isNull())
            .then(menuEntity.menuNm)
            .otherwise(parent.menuNm);

    // 2depth menu name
    StringExpression menuName =
        new CaseBuilder()
            .when(parent.menuUid.isNull())
            .then(NULL_STRING)
            .otherwise(menuEntity.menuNm);

    List<AuditLogDto.UserDetail> foundContent =
        queryFactory
            .select(
                Projections.constructor(
                    AuditLogDto.UserDetail.class,
                    auditLogEntity.id.as("logId"),
                    Expressions.stringTemplate(
                            "to_char({0}, 'YYYY-MM-DD')", auditLogEntity.createdDate)
                        .as("logDateTime"),
                    menuEntity.menuNm.as("menuName"),
                    auditLogEntity.eventType.as("eventType"),
                    Projections.constructor(
                        AuditLogDto.LogDetail.class,
                        Expressions.constant("한국자산관리공사"), // serviceName
                        parentMenuName.as("parentMenuName"),
                        menuName,
                        menuEntity.menuUrl.as("menuUrl"),
                        menuEntity.description.as("menuDescription"),
                        menuEntity.menuOrder.as("sortOrder"),
                        menuEntity.isUse.as("used"))))
            .from(auditLogEntity)
            .leftJoin(menuEntity)
            .on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
            .leftJoin(menuEntity.parent, parent)
            .leftJoin(memberEntity)
            .on(auditLogEntity.userUid.eq(memberEntity.id))
            .where(userUidEq(userUid))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(auditLogEntity.createdDate.desc())
            .fetch();

    Long countQuery =
        queryFactory
            .select(auditLogEntity.id.countDistinct())
            .from(auditLogEntity)
            .leftJoin(menuEntity)
            .on(auditLogEntity.menuUid.eq(menuEntity.menuUid))
            .leftJoin(menuEntity.parent, parent)
            .leftJoin(memberEntity)
            .on(auditLogEntity.userUid.eq(memberEntity.id))
            .where(userUidEq(userUid))
            .fetchOne();

    return new PageImpl<>(foundContent, pageable, countQuery);
  }

  private BooleanExpression eventEndedAtBetween(LocalDate startDate, LocalDate endDate) {
    if (Objects.isNull(startDate) || Objects.isNull(endDate)) {
      return null;
    }
    LocalDateTime startDateTime = startDate.atStartOfDay();
    LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
    return auditLogEntity
        .createdDate
        .goe(ZonedDateTime.from(startDateTime))
        .and(auditLogEntity.createdDate.lt(ZonedDateTime.from(endDateTime)));
  }

  private BooleanExpression menuNameEquals(String searchValue) {
    if (StringUtils.isBlank(searchValue)) {
      return null;
    }
    return menuEntity.menuNm.contains(searchValue);
  }

  private BooleanExpression loginIdOrUsernameContains(String searchValue) {
    if (StringUtils.isBlank(searchValue)) {
      return null;
    }
    return memberEntity
        .employeeNo
        .contains(searchValue)
        .or(memberEntity.name.contains(searchValue));
  }

  private BooleanExpression eventStatusEqFailed() {
    return auditLogEntity.eventStatus.eq(EventStatus.FAILED);
  }

  private BooleanExpression eventTypeEq(EventType eventType) {
    if (Objects.isNull(eventType)) {
      return null;
    }
    return auditLogEntity.eventType.eq(eventType);
  }

  private BooleanExpression errorLevelEq(ErrorLogDto.LogErrorLevel level) {
    if (Objects.isNull(level)) {
      return null;
    }
    return errorLogEntity.errorLevel.eq(ErrorLogDto.LogErrorLevel.valueOf(level.name()));
  }

  private BooleanExpression eventEndedAtEqDate(LocalDate logDate) {
    StringExpression eventEndedDate =
        Expressions.stringTemplate("to_char({0}, 'YYYY-MM-DD')", auditLogEntity.createdDate);
    LocalDateTime comparisonDate = logDate.atStartOfDay();

    return eventEndedDate.eq(comparisonDate.toString());
  }

  private BooleanExpression menuUidEq(String menuUid) {
    return auditLogEntity.menuUid.eq(menuUid);
  }

  private BooleanExpression userUidEq(Long userUid) {
    return auditLogEntity.userUid.eq(userUid);
  }

  private NumberExpression<Integer> readCount() {
    return new CaseBuilder()
        .when(auditLogEntity.eventType.eq(EventType.READ))
        .then(1)
        .otherwise(0)
        .sum();
  }

  private NumberExpression<Integer> cudCount() {
    return new CaseBuilder()
        .when(auditLogEntity.eventType.in(EventType.CREATE, EventType.UPDATE, EventType.DELETE))
        .then(1)
        .otherwise(0)
        .sum();
  }

  private NumberExpression<Integer> printCount() {
    return new CaseBuilder()
        .when(auditLogEntity.eventType.eq(EventType.PRINT))
        .then(1)
        .otherwise(0)
        .sum();
  }

  private NumberExpression<Integer> downloadCount() {
    return new CaseBuilder()
        .when(auditLogEntity.eventType.eq(EventType.DOWNLOAD))
        .then(1)
        .otherwise(0)
        .sum();
  }
}
