package com.spring.infra.security.filter;

import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;

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

import org.springframework.http.HttpHeaders;
import org.springframework.lang.NonNull;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import com.spring.infra.security.config.PermittedURI;
import com.spring.infra.security.jwt.JwtTokenRule;
import com.spring.infra.security.jwt.JwtTokenService;

import lombok.RequiredArgsConstructor;

/**
 * JWT 인증을 처리하는 필터 클래스입니다.
 * 
 * <p>이 필터는 요청마다 JWT 토큰을 검증하고, 필요한 경우 토큰을 갱신합니다.</p>
 * 
 * <p>인증된 사용자의 요청을 처리하기 위해 JWT 토큰을 사용하며,
 * 유효한 토큰이 없거나 만료된 경우에는 리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급합니다.</p>
 * 
 * @author mindol
 * @version 1.0
 */
@RequiredArgsConstructor
public final class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    /**
     * JWT 토큰 서비스를 제공하는 서비스 객체입니다.
     */
    private final JwtTokenService jwtTokenService;

    /**
     * 예외 속성 이름.
     */
    private static final String EXCEPTION_ATTRIBUTE = "exception";

    /**
     * 요청마다 실행되는 필터 메소드입니다.
     * 
     * <p>JWT 토큰을 검증하고, 필요한 경우 토큰을 갱신합니다.</p>
     * 
     * @param request HTTP 요청 객체
     * @param response HTTP 응답 객체
     * @param filterChain 필터 체인
     * @throws ServletException 서블릿 예외
     * @throws IOException 입출력 예외
     */
    @Override
    protected void doFilterInternal(
        @NonNull HttpServletRequest request,
        @NonNull HttpServletResponse response, 
        @NonNull FilterChain filterChain
    ) throws ServletException, IOException {

        // 허용된 URI에 대한 요청은 필터 체인을 계속 진행합니다.
        if (isPermittedURI(request.getRequestURI())) {
            filterChain.doFilter(request, response);
            return;
        }

        try {
            // 요청에서 액세스 토큰을 파싱합니다.
            String accessToken = parseBearerToken(request, HttpHeaders.AUTHORIZATION);
            if (jwtTokenService.validateAccessToken(accessToken)) {
                // 유효한 액세스 토큰인 경우 인증 정보를 설정합니다.
                setAuthenticationToContext(accessToken);
            } else {
                // 액세스 토큰이 유효하지 않은 경우 리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급합니다.
                String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
                jwtTokenService.validateRefreshToken(refreshToken);
                String reissuedAccessToken = jwtTokenService.getRefreshToken(refreshToken);
                Authentication authentication = jwtTokenService.getAuthentication(reissuedAccessToken);
                jwtTokenService.saveRefreshToken(authentication.getName(), jwtTokenService.generateRefreshToken(response, authentication));
                setAuthenticationToContext(jwtTokenService.generateAccessToken(response, authentication));
            }
        } catch (Exception e) {
            // 예외 발생 시 쿠키를 삭제하고 예외 정보를 요청 속성에 설정합니다.
            jwtTokenService.deleteCookie(response);
            request.setAttribute(EXCEPTION_ATTRIBUTE, e);
        }

        // 필터 체인을 계속 진행합니다.
        filterChain.doFilter(request, response);
    }

    /**
     * 인증 정보를 SecurityContext에 설정합니다.
     * 
     * @param token JWT 토큰
     */
    private void setAuthenticationToContext(final String token) {
        Authentication authentication = jwtTokenService.getAccessAuthentication(token);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    /**
     * Bearer 토큰을 파싱하여 반환합니다.
     * 
     * <p>HTTP 요청 헤더에서 Bearer 토큰을 추출하고, 쿠키에서 액세스 토큰을 가져옵니다.</p>
     * 
     * @param request HTTP 요청 객체
     * @param headerName 헤더 이름
     * @return 파싱된 Bearer 토큰
     */
    private String parseBearerToken(final HttpServletRequest request, final String headerName) {
        return Optional.ofNullable(request.getHeader(headerName))
                .filter(token -> token.substring(0, 7).equalsIgnoreCase(JwtTokenRule.BEARER_PREFIX.getValue()))
                .map(token -> token.substring(7))
                .orElseGet(() -> jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX));
    }

    /**
     * 허용된 URI인지 확인합니다.
     * 
     * <p>요청 URI가 허용된 URI 목록에 포함되어 있는지 확인합니다.</p>
     * 
     * @param requestURI 요청 URI
     * @return 허용된 URI이면 true, 그렇지 않으면 false
     */
    private boolean isPermittedURI(String requestURI) {
        return Arrays.stream(PermittedURI.values())
                    .map(PermittedURI::getUri)
                    .anyMatch(uri -> uri.equals(requestURI)) && 
                    !PermittedURI.ROOT_URI.getUri().equals(requestURI);
    }

}
