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>
 * 
 * @author mindol
 * @version 1.0
 */
@RequiredArgsConstructor
public final class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    private final JwtTokenService jwtTokenService;
    private static final String EXCEPTION_ATTRIBUTE = "exception";

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

        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.validateToken(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 토큰
     */
    private void setAuthenticationToContext(final String token) {
        Authentication authentication = jwtTokenService.getAccessAuthentication(token);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    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))
                .orElse(jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX));
    }

    private boolean isPermittedURI(String requestURI) {
        return Arrays.stream(PermittedURI.values())
                    .map(PermittedURI::getUri)
                    .anyMatch(uri -> uri.equals(requestURI)) && 
                    !PermittedURI.ROOT_URI.getUri().equals(requestURI);
    }

}
