package kr.itn.itnhub.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.function.Supplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.http.HttpStatus;

@Configuration
public class SecurityConfig {

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 관리자 1계정. 값은 환경변수로만 들어온다.
     * 운영에서는 APP_ADMIN_PASSWORD에 충분히 긴 무작위 문자열을 넣는다.
     */
    @Bean
    UserDetailsService userDetailsService(AdminProperties admin, PasswordEncoder encoder) {
        return new InMemoryUserDetailsManager(
                User.withUsername(admin.username())
                        .password(encoder.encode(admin.password()))
                        .roles("ADMIN")
                        .build());
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // SPA가 읽어서 X-XSRF-TOKEN 헤더로 되돌려 보낸다
                .csrf(csrf -> csrf
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                        // 기본 XorCsrfTokenRequestAttributeHandler는 BREACH 방지를 위해
                        // .getToken()이 돌려주는 값을 매 요청 XOR 마스킹한다. 그러면 쿠키에
                        // 저장된 원문 토큰과 브라우저가 그대로 되돌려 보내는 X-XSRF-TOKEN
                        // 헤더 값이 서로 달라 검증에 실패한다(SpaCsrfTokenRequestHandler로
                        // 실측: 마스킹 없이 헤더 값을 그대로 신뢰해야 쿠키 왕복이 성립함).
                        .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()))
                // XorCsrfTokenRequestAttributeHandler는 토큰을 지연 계산 Supplier로 _csrf
                // 요청 속성에 넣기만 하고 실제로 resolve하지 않는다. 뷰 레이어가 없는 이
                // 프로젝트에서는 아무도 .getToken()을 호출하지 않아 CookieCsrfTokenRepository의
                // saveToken()이 끝내 실행되지 않고, 브라우저는 XSRF-TOKEN 쿠키를 영영 받지
                // 못한다. Spring Security 레퍼런스가 SPA용으로 제시하는 방식대로,
                // BasicAuthenticationFilter 뒤에 필터를 두어 토큰을 강제로 resolve시킨다.
                .addFilterAfter(new OncePerRequestFilter() {
                    @Override
                    protected void doFilterInternal(HttpServletRequest request,
                                                     HttpServletResponse response,
                                                     FilterChain filterChain)
                            throws ServletException, IOException {
                        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
                        if (csrfToken != null) {
                            csrfToken.getToken();
                        }
                        filterChain.doFilter(request, response);
                    }
                }, BasicAuthenticationFilter.class)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/auth/login").permitAll()
                        .requestMatchers("/api/**").authenticated()
                        .anyRequest().permitAll())
                .formLogin(form -> form
                        .loginProcessingUrl("/api/auth/login")
                        // 커스텀 로그인 페이지를 지정하면(실제 뷰가 없어도) 스프링 시큐리티가
                        // DefaultLoginPageGeneratingFilter를 아예 등록하지 않는다. Task 10의
                        // React SPA가 "/login"을 클라이언트 라우트로 쓸 수 있어야 하므로,
                        // 스프링이 만든 기본 로그인 폼이 그 경로를 가로채면 안 된다.
                        .loginPage("/login")
                        .successHandler((req, res, a) -> res.setStatus(HttpServletResponse.SC_OK))
                        .failureHandler((req, res, e) ->
                                res.setStatus(HttpServletResponse.SC_UNAUTHORIZED)))
                .logout(logout -> logout
                        .logoutUrl("/api/auth/logout")
                        .logoutSuccessHandler((req, res, a) ->
                                res.setStatus(HttpServletResponse.SC_NO_CONTENT)))
                // API는 로그인 페이지로 리다이렉트하지 않고 401을 준다
                .exceptionHandling(ex -> ex.authenticationEntryPoint(
                        new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)));

        return http.build();
    }

    /**
     * 렌더링(handle)은 기존 XorCsrfTokenRequestAttributeHandler 그대로 위임해 BREACH
     * 방지 마스킹을 유지한다. 검증(resolveCsrfTokenValue)만 다르다: X-XSRF-TOKEN
     * 헤더가 있으면(브라우저가 쿠키 원문을 그대로 담아 보내는 SPA 관례) 마스킹 해제
     * 없이 그 값을 그대로 신뢰하고, 헤더가 없으면(예: 폼 파라미터 `_csrf`, 테스트의
     * {@code with(csrf())} 등) 기존 Xor 위임자의 마스킹 해제 로직을 그대로 쓴다.
     * 이렇게 두 경로를 다 살려야 쿠키→헤더 왕복과 기존 파라미터 기반 검증이 동시에
     * 성립한다. Spring Security 레퍼런스가 SPA 연동용으로 제시하는 표준 패턴이다.
     */
    private static final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
        private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();

        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
            this.delegate.handle(request, response, csrfToken);
        }

        @Override
        public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
            String headerValue = request.getHeader(csrfToken.getHeaderName());
            return StringUtils.hasText(headerValue) ? headerValue : this.delegate.resolveCsrfTokenValue(request, csrfToken);
        }
    }
}
