package com.takensoft.common.config;

import com.takensoft.cms.accesCtrl.service.AccesCtrlService;
import com.takensoft.cms.cntxtPth.service.CntxtPthService;
import com.takensoft.cms.loginPolicy.service.LoginModeService;
import com.takensoft.cms.loginPolicy.service.LoginPolicyService;
import com.takensoft.common.filter.*;
import com.takensoft.common.util.HttpRequestUtil;
import com.takensoft.common.exception.CustomAccessDenieHandler;
import com.takensoft.common.exception.CustomAuthenticationEntryPoint;
import com.takensoft.common.util.JWTUtil;
import com.takensoft.common.util.LoginUtil;
import com.takensoft.common.verify.service.Impl.EmailServiceImpl;
import com.takensoft.common.oauth.service.Impl.CustomOAuth2UserServiceImpl;
import com.takensoft.common.oauth.handler.OAuth2AuthenticationSuccessHandler;
import com.takensoft.common.oauth.handler.OAuth2AuthenticationFailureHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;

/**
 * @author takensoft
 * @since 2025.01.22
 * @modification
 *     since    |    author    | description
 *  2025.01.22  |  takensoft   | 최초 등록
 *  2025.05.22  |  takensoft   | OAuth2 로그인 추가
 *  2025.05.26  |    하석형     | 로그인 유틸 추가
 *
 * Spring Security 설정을 위한 Config
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // AuthenticationManager가 인자로 받을 AuthenticationConfiguration 객체 생성자 주입
    private final AuthenticationConfiguration authenticationConfiguration;
    private final JWTUtil jwtUtil;
    private final CntxtPthService cntxtPthService;
    private final AccesCtrlService accesCtrlService;
    private final CustomAuthenticationEntryPoint authenticationEntryPoint;
    private final CustomAccessDenieHandler accessDenieHandler;
    private final HttpRequestUtil httpRequestUtil;
    private final AppConfig appConfig;
    private final LoginModeService loginModeService;
    private final LoginPolicyService loginPolicyService;
    private final EmailServiceImpl emailServiceImpl;
    private final LoginUtil loginUtil;

    @Autowired
    private CustomOAuth2UserServiceImpl customOAuth2UserServiceImpl;

    @Autowired
    private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;

    @Autowired
    private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;

    private static String FRONT_URL;    // 프론트 접근 허용 URL

    private final RedisTemplate<String, String> redisTemplate;

    /**
     * SecurityConfig 생성자
     */
    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, CntxtPthService cntxtPthService, AccesCtrlService accesCtrlService, AppConfig appConfig, CustomAuthenticationEntryPoint authenticationEntryPoint, CustomAccessDenieHandler accessDenieHandler,
                          HttpRequestUtil httpRequestUtil, LoginModeService loginModeService, LoginPolicyService loginPolicyService, EmailServiceImpl emailServiceImpl, @Value("${front.url}") String fUrl, RedisTemplate<String, String> redisTemplate, LoginUtil loginUtil) {
        this.authenticationConfiguration = authenticationConfiguration;
        this.cntxtPthService = cntxtPthService;
        this.accesCtrlService = accesCtrlService;
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.accessDenieHandler = accessDenieHandler;
        this.jwtUtil = jwtUtil;
        this.httpRequestUtil = httpRequestUtil;
        this.appConfig = appConfig;
        this.loginModeService = loginModeService;
        this.loginPolicyService = loginPolicyService;
        this.emailServiceImpl = emailServiceImpl;
        this.FRONT_URL = fUrl;
        this.redisTemplate = redisTemplate;
        this.loginUtil = loginUtil;
    }

    /**
     * @param configuration - 인증 구성 객체
     * @return AuthenticationManager 객체
     * @throws Exception - 인증 관리자 초기화 실패 시
     *
     * AuthenticationManager Bean 등록
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    /**
     * @return BCryptPasswordEncoder 객체
     *
     * 비밀번호 해시 암호화를 위한 Bean 등록
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * @param http - HTTP 보안 설정 객체
     * @return SecurityFilterChain 객체
     * @throws Exception - 보안 필터 체인 초기화 실패 시
     *
     * Spring Security 필터 체인 설정
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.cors((cors) -> cors
                .configurationSource(new CorsConfigurationSource() {
                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                        CorsConfiguration configuration = new CorsConfiguration();
                        configuration.setAllowedOrigins(Collections.singletonList(FRONT_URL)); // 허용할 프론트 포트 포함 경로 입력
                        configuration.setAllowedMethods(Collections.singletonList("*")); // 허용할 메소드(GET, POST, PUT 등)
                        configuration.setAllowedHeaders(Collections.singletonList("*")); // 허용할 헤더
                        configuration.setAllowCredentials(true); // 프론트에서 credentials 설정하면 true
                        configuration.setMaxAge(3600L); // 허용을 물고 있을 시간
                        configuration.setExposedHeaders(Collections.singletonList("Authorization")); // 서버에서 JWT를 Authorization에 담아 보내기 위해 허용을 함
                        return configuration;
                    }
                })
        );

        // csrf disable
        http.csrf((auth) -> auth.disable());
        // formLogin disable
        http.formLogin((auth) -> auth.disable());
        // http basic 인증 방식 disable
        http.httpBasic((auth) -> auth.disable());
        // 세션 설정
        http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        http.exceptionHandling((exception) -> exception
                .authenticationEntryPoint(authenticationEntryPoint) // 접근 권한이 없는 경우에 호출
                .accessDeniedHandler(accessDenieHandler) // 인증되지 않은 상태로 접근 했을 때 호출
        );

        http.authorizeHttpRequests((auth) -> auth
                .requestMatchers("/", "/mbr/**", "/refresh/**", "/sys/**", "/editFileUpload/**", "/fileUpload/**", "/oauth2/**", "/login/oauth2/**", "/.well-known/**").permitAll() // 회원, 토큰, 시스템 제공, 파일, OAuth2 접근 모두 허용
                .requestMatchers("/admin/**").hasRole("ADMIN") // 관리자 페이지는 ADMIN 권한을 가진 사용자만 접근 가능
                .anyRequest().authenticated() // 그 외에는 로그인한 사용자만 접근 가능
        );

        // OAuth2 로그인 설정
        http.oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(authorization -> authorization
                        .baseUri("/oauth2/authorization")
                )
                .redirectionEndpoint(redirection -> redirection
                        .baseUri("/login/oauth2/code/*")
                )
                .userInfoEndpoint(userInfo -> userInfo
                        .userService(customOAuth2UserServiceImpl)
                )
                .successHandler(oAuth2AuthenticationSuccessHandler)
                .failureHandler(oAuth2AuthenticationFailureHandler)
        );

        // Context Path 검증 필터
        http.addFilterBefore(new ContextPathFilter(cntxtPthService), SecurityContextPersistenceFilter.class);

        // JWT 토큰 검증 필터
        http.addFilterBefore(new JWTFilter(jwtUtil, appConfig, loginModeService, loginPolicyService, redisTemplate), LoginFilter.class);

        // 접근(아이피) 검증 필터
        http.addFilterBefore(new AccesFilter(accesCtrlService, httpRequestUtil, appConfig), JWTFilter.class);

        // 로그인 필터
        http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), emailServiceImpl, loginUtil), UsernamePasswordAuthenticationFilter.class);


        return http.build();
    }
}