package com.spring.infra.security.config;

import java.util.Arrays;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.infra.security.filter.AuthenticationProcessingFilter;
import com.spring.infra.security.filter.JwtAuthenticationFilter;
import com.spring.infra.security.filter.RedirectIfAuthenticatedFilter;
import com.spring.infra.security.handler.SecurityAccessDeniedHandler;
import com.spring.infra.security.handler.SecurityAuthenticationEntryPoint;
import com.spring.infra.security.handler.SignOutHandler;
import com.spring.infra.security.handler.SigninFailureHandler;
import com.spring.infra.security.handler.SigninSuccessHandler;
import com.spring.infra.security.jwt.JwtTokenService;
import com.spring.infra.security.provider.UserAuthenticationProvider;

/**
 * 애플리케이션의 보안 설정을 담당하는 구성 클래스입니다.
 * 
 * <p>이 클래스는 Spring Security를 구성하고 JWT 기반의 인증을 설정합니다.</p>
 * 
 * @author mindol
 * @version 1.0
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {
    
    /**
     * Spring Security의 필터 체인을 구성합니다.
     * 
     * <p>이 메서드는 HTTP 보안 설정을 정의하고, JWT 인증 및 접근 제어를 설정합니다.</p>
     * 
     * @param http HttpSecurity 객체, 보안 설정을 구성하는 데 사용됩니다.
     * @param tokenService JWT 토큰 서비스, 인증 및 토큰 관리를 담당합니다.
     * @param authenticationEntryPoint JWT 인증 진입점, 인증 실패 시 처리합니다.
     * @param accessDeniedHandler JWT 접근 거부 핸들러, 접근 권한이 없는 경우 처리합니다.
     * @param authenticationProcessingFilter 사용자 정의 인증 필터, 인증 프로세스를 처리합니다.
     * @return 구성된 SecurityFilterChain, 보안 필터 체인을 반환합니다.
     * @throws Exception 보안 설정 중 발생할 수 있는 예외를 처리합니다.
     */
    @Bean
    @Order(1)
    SecurityFilterChain securityFilterChain(
        HttpSecurity http,
        JwtTokenService tokenService,
        SecurityAuthenticationEntryPoint authenticationEntryPoint,
        SecurityAccessDeniedHandler accessDeniedHandler,
        AuthenticationProcessingFilter authenticationProcessingFilter
    ) throws Exception {
        http
            .headers(headers -> headers.frameOptions(FrameOptionsConfig::sameOrigin))
            .csrf(CsrfConfigurer::disable)
            .httpBasic(HttpBasicConfigurer::disable)
            .formLogin(FormLoginConfigurer::disable)
            .anonymous(AnonymousConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .antMatchers(Arrays.stream(PermittedURI.values()).map(PermittedURI::getUri).toArray(String[]::new)).permitAll()
                .anyRequest().authenticated()
            )
            .logout(logout -> logout
                .logoutUrl(PermittedURI.USER_SIGN_OUT.getUri())
                .addLogoutHandler(new SignOutHandler(tokenService))
                .logoutSuccessUrl(PermittedURI.ROOT_URI.getUri())
                .invalidateHttpSession(true))
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .addFilterBefore(
                authenticationProcessingFilter, 
                UsernamePasswordAuthenticationFilter.class
            )
            .addFilterAfter(
                new JwtAuthenticationFilter(tokenService), 
                AuthenticationProcessingFilter.class
            )
            .addFilterAfter(
                new RedirectIfAuthenticatedFilter(),
                JwtAuthenticationFilter.class
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler)
            );
        return http.build();
    }

    /**
     * 특정 요청에 대해 보안 검사를 무시하도록 설정합니다.
     * 
     * <p>이 메서드는 정적 리소스에 대한 접근을 허용하고, 보안 검사를 비활성화합니다.</p>
     * 
     * @param http HttpSecurity 객체, 보안 설정을 구성하는 데 사용됩니다.
     * @return 구성된 SecurityFilterChain, 보안 필터 체인을 반환합니다.
     * @throws Exception 보안 설정 중 발생할 수 있는 예외를 처리합니다.
     */
    @Bean
    @Order(0)
    SecurityFilterChain resources(HttpSecurity http) throws Exception {
        return http.requestMatchers(matchers -> matchers
                    .requestMatchers(PathRequest.toStaticResources().atCommonLocations()))
                    .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll())
                    .requestCache(RequestCacheConfigurer::disable)
                    .securityContext(AbstractHttpConfigurer::disable)
                    .sessionManagement(AbstractHttpConfigurer::disable)
                    .build();
    }

    /**
     * 비밀번호 인코더를 구성합니다.
     * 
     * <p>이 메서드는 비밀번호 인코딩 및 검증을 위한 PasswordEncoder 객체를 반환합니다.</p>
     * 
     * @return PasswordEncoder 객체, 비밀번호 인코딩 및 검증을 담당합니다.
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    /**
     * 사용자 인증 관리자를 구성합니다.
     * 
     * <p>이 메서드는 사용자 인증을 처리하는 AuthenticationManager 객체를 반환합니다.</p>
     * 
     * @param provider 사용자 인증 제공자, 인증 프로세스를 처리합니다.
     * @return AuthenticationManager 객체, 인증 요청을 처리합니다.
     */
    @Bean
	AuthenticationManager authenticationManager(UserAuthenticationProvider provider) {
		return new ProviderManager(provider);
	}

    /**
     * 사용자 정의 인증 필터를 구성합니다.
     * 
     * <p>이 메서드는 인증 요청을 처리하는 AuthenticationProcessingFilter 객체를 반환합니다.</p>
     * 
     * @param objectMapper JSON 변환을 위한 ObjectMapper 객체입니다.
     * @param authenticationManager 인증 요청을 처리하는 AuthenticationManager 객체입니다.
     * @param signinSuccessHandler 인증 성공 시 처리하는 핸들러입니다.
     * @param signinFailureHandler 인증 실패 시 처리하는 핸들러입니다.
     * @return AuthenticationProcessingFilter 객체, 인증 요청을 처리합니다.
     */
    @Bean
	AuthenticationProcessingFilter authenticationProcessingFilter(
        ObjectMapper objectMapper,
        AuthenticationManager authenticationManager,
        SigninSuccessHandler signinSuccessHandler,
        SigninFailureHandler signinFailureHandler
    ) {
        var filter = new AuthenticationProcessingFilter(objectMapper);
        filter.setAuthenticationManager(authenticationManager);
        filter.setAuthenticationSuccessHandler(signinSuccessHandler);
        filter.setAuthenticationFailureHandler(signinFailureHandler);
		return filter;
	}

}
