package study.securitybrowser;

import java.util.UUID;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
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.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.context.NullSecurityContextRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.header.writers.StaticHeadersWriter;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
public class SecurityConfig {
    private static final String CSP = "default-src 'self'; frame-ancestors 'none'; base-uri 'self'; "
            + "form-action 'self'; style-src 'self' 'unsafe-inline'";

    @Bean
    WebSecurityCustomizer firewall() {
        return web -> web.httpFirewall(new StrictHttpFirewall())
                .requestRejectedHandler((request, response, failure) -> {
                    // 仅标明真实 firewall 的拒绝位置；不回显恶意 URL 或异常内容。
                    response.setHeader("X-Lab-Rejected-By", "StrictHttpFirewall");
                    response.setStatus(400);
                });
    }

    @Bean
    CorsConfigurationSource corsPolicy() {
        var policy = new CorsConfiguration();
        policy.setAllowedOrigins(List.of("https://classroom.example"));
        policy.setAllowedMethods(List.of("GET", "POST", "OPTIONS"));
        policy.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-XSRF-TOKEN", "X-CSRF-TOKEN"));
        policy.setAllowCredentials(true);
        policy.setMaxAge(600L);
        var source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", policy);
        return source;
    }

    @Bean
    PersistentTokenRepository rememberTokens() {
        return new InMemoryTokenRepositoryImpl();
    }

    @Bean
    SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    HttpSessionEventPublisher sessionEvents() {
        return new HttpSessionEventPublisher();
    }

    @Bean
    PasswordEncoder passwords() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    SyntheticCredentials credentials(PasswordEncoder passwords) {
        return new SyntheticCredentials(passwords);
    }

    @Bean
    DaoAuthenticationProvider provider(SyntheticCredentials credentials, PasswordEncoder passwords) {
        var provider = new DaoAuthenticationProvider(credentials);
        provider.setPasswordEncoder(passwords);
        provider.setUserDetailsPasswordService(credentials);
        return provider;
    }

    @Bean
    @Order(1)
    SecurityFilterChain api(HttpSecurity http, DaoAuthenticationProvider provider, CorsConfigurationSource corsPolicy) throws Exception {
        var csrfCookies = new CookieCsrfTokenRepository();
        csrfCookies.setCookiePath("/api");
        return http.securityMatcher("/api/**")
                .authenticationProvider(provider)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/open", "/api/csrf").permitAll()
                        .requestMatchers("/api/admin/**").hasRole("ADMIN")
                        .anyRequest().authenticated())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .securityContext(context -> context.securityContextRepository(new NullSecurityContextRepository()))
                .requestCache(cache -> cache.requestCache(new NullRequestCache()))
                .csrf(csrf -> csrf.csrfTokenRepository(csrfCookies))
                .cors(cors -> cors.configurationSource(corsPolicy))
                .httpBasic(basic -> basic.authenticationEntryPoint((request, response, failure) -> {
                    response.setHeader("WWW-Authenticate", "Basic realm=\"u29-api\"");
                    response.setStatus(401);
                }))
                .exceptionHandling(errors -> errors
                        .authenticationEntryPoint((request, response, failure) -> {
                            response.setHeader("WWW-Authenticate", "Basic realm=\"u29-api\"");
                            response.setStatus(401);
                        })
                        .accessDeniedHandler((request, response, failure) -> response.setStatus(403)))
                .headers(headers -> headers
                        .contentSecurityPolicy(csp -> csp.policyDirectives(CSP))
                        .addHeaderWriter(new StaticHeadersWriter("X-Lab-Chain", "api")))
                .build();
    }

    @Bean
    @Order(2)
    SecurityFilterChain browser(HttpSecurity http, DaoAuthenticationProvider provider, SessionRegistry registry,
            SyntheticCredentials credentials, PersistentTokenRepository tokens, CorsConfigurationSource corsPolicy,
            @Value("${lab.remember-seconds:300}") int rememberSeconds,
            @Value("${lab.prevent-new-login:true}") boolean preventNewLogin) throws Exception {
        // 没有 securityMatcher：最后一条链覆盖全部剩余请求，避免新端点落到安全链之外。
        return http.authenticationProvider(provider)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/browser/prelogin", "/browser/csrf", "/browser/credentials/legacy").permitAll()
                        .requestMatchers("/browser/sensitive").fullyAuthenticated()
                        .requestMatchers("/browser/admin/**").hasRole("ADMIN")
                        .anyRequest().authenticated())
                .formLogin(form -> form.defaultSuccessUrl("/browser/whoami", true)
                        .failureHandler((request, response, failure) -> {
                            boolean capped = failure instanceof SessionAuthenticationException;
                            response.setStatus(capped ? 409 : 401);
                            response.setHeader("X-Lab-Login-Failure", capped ? "session-limit" : "bad-credentials");
                        }))
                .sessionManagement(session -> session
                        .sessionFixation(fixation -> fixation.changeSessionId())
                        .sessionConcurrency(concurrency -> concurrency
                                // 丢 cookie 不等于服务端会话消失：remember-me 演示账号预留第二个名额。
                                .maximumSessions(authentication -> authentication.getName().equals("remember-user") ? 2 : 1)
                                .maxSessionsPreventsLogin(preventNewLogin).sessionRegistry(registry)
                                .expiredSessionStrategy(event -> {
                                    event.getResponse().setStatus(401);
                                    event.getResponse().setHeader("X-Lab-Session-State", "expired");
                                })))
                .logout(logout -> logout.deleteCookies("JSESSIONID"))
                .cors(cors -> cors.configurationSource(corsPolicy))
                .rememberMe(remember -> remember
                        .key(UUID.randomUUID().toString())
                        .userDetailsService(credentials).tokenRepository(tokens).tokenValiditySeconds(rememberSeconds))
                .exceptionHandling(errors -> errors.accessDeniedHandler((request, response, failure) -> response.setStatus(403)))
                .headers(headers -> headers
                        .contentSecurityPolicy(csp -> csp.policyDirectives(CSP))
                        .addHeaderWriter(new StaticHeadersWriter("X-Lab-Chain", "browser")))
                .build();
    }
}
