package com.zioinfo.mall.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
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.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.sql.SQLException;

/**
 * 전역 예외 처리.
 *
 * <p>보안 불변 규칙: 스택트레이스 · SQL문 · 테이블/컬럼명 · DB 드라이버 메시지를
 * 응답에 절대 노출하지 않는다. 클라이언트에는 에러 코드 + 일반 요약 메시지만 반환하고,
 * 실제 원인은 서버 로그(logger.error)에만 기록한다.
 *
 * <p>업무/검증 예외는 {@code ERR-XXX-NNN:} 형태의 비민감 메시지만 통과시킨다.
 * 그 외(미분류 RuntimeException, DataAccessException, SQLException, 일반 Exception)는
 * 메시지를 마스킹하고 일반 코드로만 응답한다.
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 안전한 업무/검증 에러 코드 패턴 (예: ERR-MALL-413, ERR-ORD-404, ERR-LOY-400 ...).
     * 메시지가 이 코드로 시작하면 사전 정의된 비민감 메시지로 간주하여 그대로 노출한다.
     * 그렇지 않은 메시지(SQL/드라이버/내부 원인 가능성)는 응답에서 마스킹한다.
     */
    private static final java.util.regex.Pattern SAFE_CODE =
            java.util.regex.Pattern.compile("^ERR-[A-Z0-9]+-[A-Z0-9]+\\b.*");

    /** 사전 정의된 안전 코드 메시지인지 검사. */
    private static boolean isSafeBusinessMessage(String message) {
        return message != null && SAFE_CODE.matcher(message).matches();
    }

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    @ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
    public ApiResponse<Void> handleMaxSize(MaxUploadSizeExceededException e) {
        log.warn("업로드 크기 초과: {}", e.getMessage());
        return ApiResponse.fail("ERR-MALL-413: 파일 크기 초과 (최대 20MB)");
    }

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleIllegalArg(IllegalArgumentException e) {
        // 사전 정의된 비민감 검증 메시지(ERR-XXX-NNN)만 노출, 그 외는 마스킹
        if (isSafeBusinessMessage(e.getMessage())) {
            log.warn("잘못된 요청: {}", e.getMessage());
            return ApiResponse.fail(e.getMessage());
        }
        log.warn("잘못된 요청(메시지 마스킹)", e);
        return ApiResponse.fail("ERR-MALL-400: 잘못된 요청입니다");
    }

    /**
     * DB 접근 오류 — SQL문/테이블·컬럼명/드라이버 메시지가 e.getMessage()에 포함되므로
     * 절대 응답에 노출하지 않는다. 원인은 서버 로그에만 기록.
     */
    @ExceptionHandler({DataAccessException.class, SQLException.class})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<Void> handleDataAccess(Exception e) {
        log.error("데이터 접근 오류", e);
        return ApiResponse.fail("ERR-MALL-DB: 요청 처리 중 오류가 발생했습니다");
    }

    /** 권한 부족(메서드 보안 @PreAuthorize 등) — 403 유지, 내부 메시지 미노출. */
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ApiResponse<Void> handleAccessDenied(AccessDeniedException e) {
        log.warn("권한 부족: {}", e.getMessage());
        return ApiResponse.fail("ERR-MALL-403: 접근 권한이 없습니다");
    }

    /** 인증 실패 — 401 유지, 내부 메시지 미노출. */
    @ExceptionHandler(AuthenticationException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ApiResponse<Void> handleAuthentication(AuthenticationException e) {
        log.warn("인증 실패: {}", e.getMessage());
        return ApiResponse.fail("ERR-MALL-401: 인증이 필요합니다");
    }

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleRuntime(RuntimeException e) {
        // 사전 정의된 비민감 업무 메시지(ERR-XXX-NNN)만 노출.
        // 미분류 런타임 예외는 SQL/드라이버/내부 원인이 섞일 수 있어 마스킹하고 원인은 로그에만 기록.
        if (isSafeBusinessMessage(e.getMessage())) {
            log.warn("업무 오류: {}", e.getMessage());
            return ApiResponse.fail(e.getMessage());
        }
        log.error("미분류 런타임 오류(메시지 마스킹)", e);
        return ApiResponse.fail("ERR-MALL-500: 요청 처리 중 오류가 발생했습니다");
    }

    /** 정적 리소스 미존재 — 500 으로 감싸지 않고 깔끔히 404 반환 (SPA 폴백 이후의 실제 미스). */
    @ExceptionHandler(NoResourceFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResponse<Void> handleNoResource(NoResourceFoundException e) {
        log.warn("리소스 없음: {}", e.getResourcePath());
        return ApiResponse.fail("ERR-MALL-404: 리소스를 찾을 수 없습니다");
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<Void> handleGeneral(Exception e) {
        log.error("시스템 오류", e);
        return ApiResponse.fail("ERR-SYS-001");
    }
}
