package com.kamco.cd.kamcoback.config;

import com.kamco.cd.kamcoback.auth.CustomUserDetails;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.common.exception.DuplicateFileException;
import com.kamco.cd.kamcoback.common.exception.ValidationException;
import com.kamco.cd.kamcoback.config.api.ApiLogFunction;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ApiResponseCode;
import com.kamco.cd.kamcoback.log.dto.ErrorLogDto;
import com.kamco.cd.kamcoback.members.exception.MemberException;
import com.kamco.cd.kamcoback.postgres.entity.ErrorLogEntity;
import com.kamco.cd.kamcoback.postgres.repository.log.ErrorLogRepository;
import jakarta.persistence.EntityNotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import java.nio.file.AccessDeniedException;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

@Slf4j
@Order(value = 1)
@RestControllerAdvice
public class GlobalExceptionHandler {

  private final ErrorLogRepository errorLogRepository;

  public GlobalExceptionHandler(ErrorLogRepository errorLogRepository) {
    this.errorLogRepository = errorLogRepository;
  }

  @ResponseStatus(HttpStatus.CONFLICT)
  @ExceptionHandler(DuplicateFileException.class)
  public ApiResponseDto<String> handleDuplicateFileException(
      DuplicateFileException e, HttpServletRequest request) {
    log.warn("[DuplicateFileException] resource :{} ", e.getMessage());
    ApiResponseCode code = ApiResponseCode.CONFLICT;
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            code,
            HttpStatus.CONFLICT,
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        code, e.getMessage(), HttpStatus.CONFLICT, errorLog.getId());
  }

  @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
  @ExceptionHandler(ValidationException.class)
  public ApiResponseDto<String> handleValidationException(
      ValidationException e, HttpServletRequest request) {
    log.warn("[ValidationException] resource :{} ", e.getMessage());
    ApiResponseCode code = ApiResponseCode.UNPROCESSABLE_ENTITY;
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            code,
            HttpStatus.UNPROCESSABLE_ENTITY,
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        code, e.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY, errorLog.getId());
  }

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(IllegalArgumentException.class)
  public ApiResponseDto<String> handleIllegalArgumentException(
      IllegalArgumentException e, HttpServletRequest request) {
    log.warn("[IllegalArgumentException] resource :{} ", e.getMessage());
    ApiResponseCode code = ApiResponseCode.BAD_REQUEST;
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            code,
            HttpStatus.BAD_REQUEST,
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        code, e.getMessage(), HttpStatus.BAD_REQUEST, errorLog.getId());
  }

  @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
  @ExceptionHandler(EntityNotFoundException.class)
  public ApiResponseDto<String> handlerEntityNotFoundException(
      EntityNotFoundException e, HttpServletRequest request) {
    log.warn("[EntityNotFoundException] resource :{} ", e.getMessage());
    String codeName = "NOT_FOUND_DATA";
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf("UNPROCESSABLE_ENTITY"),
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf("UNPROCESSABLE_ENTITY"),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(HttpMessageNotReadableException.class)
  public ApiResponseDto<String> handleUnreadable(
      HttpMessageNotReadableException e, HttpServletRequest request) {
    log.warn("[HttpMessageNotReadableException] resource :{} ", e.getMessage());
    String codeName = "BAD_REQUEST";
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf(codeName),
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf(codeName),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.NOT_FOUND)
  @ExceptionHandler(NoSuchElementException.class)
  public ApiResponseDto<String> handlerNoSuchElementException(
      NoSuchElementException e, HttpServletRequest request) {
    log.warn("[NoSuchElementException] resource :{} ", e.getMessage());
    String codeName = "NOT_FOUND_DATA";
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf(codeName),
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf("NOT_FOUND"),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
  @ExceptionHandler(DataIntegrityViolationException.class)
  public ApiResponseDto<String> handlerDataIntegrityViolationException(
      DataIntegrityViolationException e, HttpServletRequest request) {
    log.warn("[DataIntegrityViolationException] resource :{} ", e.getMessage());
    String codeName = "DATA_INTEGRITY_ERROR";
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf("UNPROCESSABLE_ENTITY"),
            ErrorLogDto.LogErrorLevel.CRITICAL,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf("UNPROCESSABLE_ENTITY"),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ApiResponseDto<String> handlerMethodArgumentNotValidException(
      MethodArgumentNotValidException e, HttpServletRequest request) {
    log.warn("[MethodArgumentNotValidException] resource :{} ", e.getMessage());
    String codeName = "BAD_REQUEST";
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf(codeName),
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf(codeName),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.FORBIDDEN)
  @ExceptionHandler(AccessDeniedException.class)
  public ApiResponseDto<String> handlerAccessDeniedException(
      AccessDeniedException e, HttpServletRequest request) {
    log.warn("[AccessDeniedException] resource :{} ", e.getMessage());
    String codeName = "FORBIDDEN";
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf(codeName),
            ErrorLogDto.LogErrorLevel.ERROR,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf(codeName),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.BAD_GATEWAY)
  @ExceptionHandler(HttpServerErrorException.BadGateway.class)
  public ApiResponseDto<String> handlerHttpServerErrorException(
      HttpServerErrorException e, HttpServletRequest request) {
    log.warn("[HttpServerErrorException] resource :{} ", e.getMessage());
    String codeName = "BAD_GATEWAY";
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf(codeName),
            ErrorLogDto.LogErrorLevel.CRITICAL,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf(codeName),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
  @ExceptionHandler(IllegalStateException.class)
  public ApiResponseDto<String> handlerIllegalStateException(
      IllegalStateException e, HttpServletRequest request) {
    log.warn("[IllegalStateException] resource :{} ", e.getMessage());

    String codeName = "UNPROCESSABLE_ENTITY";
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf(codeName),
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf(codeName),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(MemberException.DuplicateMemberException.class)
  public ApiResponseDto<String> handlerDuplicateMemberException(
      MemberException.DuplicateMemberException e, HttpServletRequest request) {
    log.warn("[DuplicateMemberException] resource :{} ", e.getMessage());

    String codeName = "";

    switch (e.getField()) {
      case EMPLOYEE_NO -> {
        codeName = "DUPLICATE_EMPLOYEEID";
      }
      default -> {
        codeName = "DUPLICATE_DATA";
      }
    }

    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf("BAD_REQUEST"),
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf("BAD_REQUEST"),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(MemberException.MemberNotFoundException.class)
  public ApiResponseDto<String> handlerMemberNotFoundException(
      MemberException.MemberNotFoundException e, HttpServletRequest request) {
    log.warn("[MemberNotFoundException] resource :{} ", e.getMessage());

    String codeName = "NOT_FOUND_USER";

    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf("BAD_REQUEST"),
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf("BAD_REQUEST"),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.CONFLICT)
  @ExceptionHandler(DuplicateKeyException.class)
  public ApiResponseDto<String> handlerDuplicateKeyException(
      DuplicateKeyException e, HttpServletRequest request) {
    log.warn("[DuplicateKeyException] resource :{} ", e.getMessage());

    String codeName = "DUPLICATE_DATA";

    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf("CONFLICT"),
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf("CONFLICT"),
        errorLog.getId());
  }

  @ExceptionHandler(BadCredentialsException.class)
  public ResponseEntity<ApiResponseDto<String>> handleBadCredentials(
      BadCredentialsException e, HttpServletRequest request) {
    log.warn("[BadCredentialsException] resource : {} ", e.getMessage());

    String codeName = "UNAUTHORIZED";

    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf(codeName),
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    ApiResponseDto<String> body =
        ApiResponseDto.createException(
            ApiResponseCode.getCode(codeName),
            ApiResponseCode.getMessage(codeName),
            HttpStatus.valueOf(codeName),
            errorLog.getId());

    return ResponseEntity.status(HttpStatus.UNAUTHORIZED) // 🔥 여기서 401 지정
        .body(body);
  }

  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler(RuntimeException.class)
  public ApiResponseDto<String> handlerRuntimeException(
      RuntimeException e, HttpServletRequest request) {
    log.warn("[RuntimeException] resource :{} ", e.getMessage());

    String codeName = "INTERNAL_SERVER_ERROR";
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf(codeName),
            ErrorLogDto.LogErrorLevel.CRITICAL,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf(codeName),
        errorLog.getId());
  }

  @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  @ExceptionHandler(Exception.class)
  public ApiResponseDto<String> handlerException(Exception e, HttpServletRequest request) {
    log.error("[Exception] resource: {}, message: {}", request.getRequestURI(), e.getMessage());
    log.error("Exception stacktrace: ", e);

    String codeName = "INTERNAL_SERVER_ERROR";
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf(codeName),
            ErrorLogDto.LogErrorLevel.CRITICAL,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf(codeName),
        errorLog.getId());
  }

  /**
   * 에러 로그 테이블 저장 로직
   *
   * @param request : request
   * @param errorCode : 정의된 enum errorCode
   * @param httpStatus : HttpStatus 값
   * @param logErrorLevel : WARNING, ERROR, CRITICAL
   * @param stackTrace : 에러 내용
   * @return : insert하고 결과로 받은 Entity
   */
  private ErrorLogEntity saveErrorLogData(
      HttpServletRequest request,
      ApiResponseCode errorCode,
      HttpStatus httpStatus,
      ErrorLogDto.LogErrorLevel logErrorLevel,
      StackTraceElement[] stackTrace) {

    Long userid = null;

    /**
     * servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth 이 요청이
     * JWT 인증을 통과한 요청인가? 그리고 Spring Security Authentication 객체가 UsernamePasswordAuthenticationToken
     * 타입인가? 체크
     */
    /**
     * auth.getPrincipal() instanceof CustomUserDetails customUserDetails principal 안에 들어있는 객체가 내가
     * 만든 CustomUserDetails 타입인가? 체크
     */
    if (request.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
        && auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {

      // audit 에는 long 타입 user_id가 들어가지만 토큰 sub은 uuid여서 user_id 가져오기
      userid = customUserDetails.getMember().getId();
    }

    String stackTraceStr =
        Arrays.stream(stackTrace)
            .map(StackTraceElement::toString)
            .collect(Collectors.joining("\n"))
            .substring(0, Math.min(stackTrace.length, 255));

    ErrorLogEntity errorLogEntity =
        new ErrorLogEntity(
            request.getRequestURI(),
            ApiLogFunction.getEventType(request),
            logErrorLevel,
            String.valueOf(httpStatus.value()),
            errorCode.getText(),
            stackTraceStr,
            userid,
            ZonedDateTime.now());

    return errorLogRepository.save(errorLogEntity);
  }

  @ExceptionHandler(CustomApiException.class)
  public ApiResponseDto<String> handleCustomApiException(
      CustomApiException e, HttpServletRequest request) {
    log.warn("[CustomApiException] resource : {}", e.getMessage());

    String codeName = e.getCodeName();
    HttpStatus status = e.getStatus();
    //    String message = e.getMessage() == null ? ApiResponseCode.getMessage(codeName) :
    // e.getMessage();
    //
    //    ApiResponseCode apiCode = ApiResponseCode.getCode(codeName);
    //
    //    ErrorLogEntity errorLog =
    //        saveErrorLogData(
    //            request, apiCode, status, ErrorLogDto.LogErrorLevel.WARNING, e.getStackTrace());
    //
    //    ApiResponseDto<String> body =
    //        ApiResponseDto.createException(apiCode, message, status, errorLog.getId());

    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            ApiResponseCode.getCode(codeName),
            HttpStatus.valueOf(status.value()),
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        ApiResponseCode.getCode(codeName),
        ApiResponseCode.getMessage(codeName),
        HttpStatus.valueOf(status.value()),
        errorLog.getId());

    //    return new ResponseEntity<>(body, status);
  }

  @ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
  @ExceptionHandler(MaxUploadSizeExceededException.class)
  public ApiResponseDto<String> handleMaxUploadSizeExceeded(
      MaxUploadSizeExceededException e, HttpServletRequest request) {
    log.warn("[MaxUploadSizeExceededException] resource :{} ", e.getMessage());
    ApiResponseCode code = ApiResponseCode.PAYLOAD_TOO_LARGE;
    ErrorLogEntity errorLog =
        saveErrorLogData(
            request,
            code,
            HttpStatus.PAYLOAD_TOO_LARGE,
            ErrorLogDto.LogErrorLevel.WARNING,
            e.getStackTrace());

    return ApiResponseDto.createException(
        code, code.getText(), HttpStatus.PAYLOAD_TOO_LARGE, errorLog.getId());
  }
}
