package com.spring.infra.security.filter;

import java.io.IOException;
import java.util.Objects;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.infra.security.config.PermittedURI;
import com.spring.infra.security.domain.UserPrincipal;
import com.spring.infra.security.dto.SignInRequest;
import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule;

/**
 * 사용자 인증 요청을 처리하는 필터 클래스입니다.
 * 
 * <p>이 클래스는 Spring Security의 AbstractAuthenticationProcessingFilter를 확장하여,
 * 사용자 로그인 요청을 처리하고 인증을 수행합니다.</p>
 * 
 * <p>HTTP POST 요청을 통해 사용자 이름과 비밀번호를 받아 인증을 시도하며,
 * 인증 결과에 따라 적절한 응답을 반환합니다.</p>
 * 
 * @author mindol
 * @version 1.0
 */
public class AuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * 기본 로그인 요청 URI.
     */
    private static final String DEFAULT_LOGIN_REQUEST_URI = PermittedURI.USER_SIGN_IN.getUri();

    /**
     * HTTP 메서드 (POST).
     */
    private static final String HTTP_METHOD = "POST"; 

    /**
     * 기본 로그인 경로 요청 매처.
     */
    private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER = 
        new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URI, HTTP_METHOD);

    /**
     * JSON 변환을 위한 ObjectMapper 객체.
     */
    private final ObjectMapper objectMapper;

    /**
     * AuthenticationProcessingFilter 생성자.
     * 
     * @param objectMapper JSON 변환을 위한 ObjectMapper 객체
     */
    public AuthenticationProcessingFilter(ObjectMapper objectMapper) {
        super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER);
        this.objectMapper = objectMapper;
    }

    /**
     * 사용자 인증을 시도합니다.
     * 
     * <p>HTTP 요청에서 사용자 이름과 비밀번호를 추출하고,
     * 인증 매니저를 사용하여 인증을 수행합니다.</p>
     * 
     * @param request HTTP 요청 객체
     * @param response HTTP 응답 객체
     * @return 인증된 Authentication 객체
     * @throws AuthenticationException 인증 실패 시 발생하는 예외
     * @throws IOException 입출력 예외
     * @throws ServletException 서블릿 예외
     */
    @Override
    public Authentication attemptAuthentication(
        HttpServletRequest request, 
        HttpServletResponse response
    ) throws AuthenticationException, IOException, ServletException {

        if (!isValidRequestType(request)) {
            throw new SecurityAuthException(SecurityExceptionRule.UNSUPPORTED_MEDIA_ERROR);
        }
        var signInRequest = objectMapper.readValue(request.getReader(), SignInRequest.class);
        if (!isValidRequest(signInRequest)) {
            throw new SecurityAuthException(SecurityExceptionRule.USER_BAD_REQUEST);
        }
        var token = new UsernamePasswordAuthenticationToken(signInRequest.getUsername(), signInRequest.getPassword());
        var authentication = this.getAuthenticationManager().authenticate(token);

        if (authentication.getPrincipal() instanceof UserPrincipal) {
            UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
            if (!user.getAgentUser().isApproved()) {
                throw new SecurityAuthException(SecurityExceptionRule.USER_NOT_APPROVED);
            }
        }

        return authentication;
    }

    /**
     * 요청의 유효성 유형을 검사합니다.
     * 
     * <p>요청 메서드가 POST이고, 콘텐츠 타입이 JSON인지 확인합니다.</p>
     * 
     * @param request HTTP 요청 객체
     * @return 요청이 유효하면 true, 그렇지 않으면 false
     */
    private boolean isValidRequestType(HttpServletRequest request) {
        return Objects.equals(request.getMethod(), HttpMethod.POST.name()) &&
                Objects.equals(request.getContentType(), MediaType.APPLICATION_JSON_VALUE);
    }

    /**
     * 사용자 로그인 요청의 유효성을 검사합니다.
     * 
     * <p>사용자 이름과 비밀번호가 모두 존재하는지 확인합니다.</p>
     * 
     * @param signInRequest 로그인 요청 객체
     * @return 요청이 유효하면 true, 그렇지 않으면 false
     */
    private boolean isValidRequest(SignInRequest signInRequest) {
        return StringUtils.hasText(signInRequest.getUsername()) || 
                StringUtils.hasText(signInRequest.getPassword());
    }
    
}
