package dev.awesomedev.mirim.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;

import java.nio.charset.StandardCharsets;

/**
 * 이 파일이 하는 일:
 * Spring Security 설정 — "어떤 URL에 어떤 권한이 필요한가"를 한곳에 선언한다.
 *
 * 인증 방식: 세션 쿠키.
 * 로그인 성공 시 서버가 세션을 만들고 브라우저에 JSESSIONID 쿠키를 준다.
 * 이후 요청마다 브라우저가 쿠키를 보내면 Spring Security가 세션에서 인증 정보를 복원한다.
 *
 * 학습 포인트: 세션 vs 토큰(JWT) —
 * 세션은 "로그인 상태"를 서버 메모리에 두고 브라우저엔 열쇠(세션 ID)만 준다.
 * 서버가 상태를 쥐고 있으므로 로그아웃 즉시 무효화가 쉽고 구현이 단순하다.
 * 대신 서버가 여러 대로 늘어나면 세션을 공유할 방법(Redis 등)이 필요해진다.
 * 서버 1대짜리 사내 학습 플랫폼에는 세션 방식이 가장 알맞다.
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /**
     * 학습 포인트: BCrypt가 뭔가? —
     * 1) 비밀번호 전용 단방향 해시 함수다. 해시에서 원문을 되돌리는 것은 불가능하다.
     * 2) 일부러 느리게 설계되어 있어(기본 2^10회 반복) 해커가 초당 수십억 번씩
     *    대입해 보는 무차별 대입 공격의 비용을 크게 올린다. SHA-256이 부적합한 이유다.
     * 3) 같은 비밀번호라도 매번 다른 해시를 만든다(해시 문자열 안에 무작위 salt가 포함).
     *    그래서 해시끼리 == 비교는 불가능하고 반드시 matches()로 검증해야 한다.
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 인증 정보를 HTTP 세션에 저장/복원하는 저장소.
     * AuthController가 로그인 성공 시 이 저장소로 SecurityContext를 세션에 넣는다.
     */
    @Bean
    public SecurityContextRepository securityContextRepository() {
        return new HttpSessionSecurityContextRepository();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http,
                                                   SecurityContextRepository securityContextRepository) throws Exception {
        http
                // 학습 포인트: CSRF 보호를 끈 이유 — CSRF 토큰을 프론트와 주고받는 절차가
                // 학습 초기엔 큰 진입장벽이라 이 프로젝트에선 단순화를 위해 비활성화했다.
                // 실서비스에서 세션 쿠키 인증을 쓴다면 CSRF 보호는 반드시 켜야 한다.
                .csrf(csrf -> csrf.disable())

                // URL별 접근 규칙. 위에서부터 순서대로 매칭되므로 구체적인 규칙을 먼저 쓴다.
                .authorizeHttpRequests(auth -> auth
                        // 학습 포인트: 이 네 API는 "아직 로그인하지 못한 사람"이 쓰는 기능이라
                        // permitAll이어야 한다. 로그인해야만 가입/비밀번호 찾기가 가능하다면 모순이다.
                        .requestMatchers("/api/auth/login").permitAll()          // 로그인
                        .requestMatchers("/api/auth/signup").permitAll()         // 회원가입
                        .requestMatchers("/api/auth/find-id").permitAll()        // 아이디 찾기(마스킹된 아이디만 반환)
                        // ⚠️ 여기 있던 /api/auth/reset-password(무인증 비밀번호 재설정)는 제거했다.
                        // 보안 검토에서 실제 계정 탈취 경로로 확인됐기 때문이다:
                        //   find-id로 아이디를 얻고 → 아이디+이름만으로 비밀번호를 바꾸면 → 남의 계정 로그인.
                        // 대신 두 개의 안전한 경로로 나눴다:
                        //   - 로그인한 본인: POST /api/auth/change-password (현재 비밀번호 확인 필요)
                        //   - 정말 잊은 학생: 멘토가 POST /api/mentor/users/{id}/reset-password 로 처리
                        // 학습 포인트: "무인증으로 열어도 되는 문"은 최소한이어야 한다.
                        .requestMatchers("/api/mentor/**").hasRole("MENTOR") // 멘토 전용
                        .requestMatchers("/api/**").authenticated()       // 나머지 API는 로그인 필요
                        .anyRequest().permitAll())                        // 그 외(정적 파일 등)는 공개

                // 로그인 안 한 요청이 보호된 API를 부르면: 로그인 페이지로 리다이렉트하는 대신
                // JSON 401을 돌려준다. (프론트가 SPA라 리다이렉트는 처리하기 어렵다)
                .exceptionHandling(handling -> handling
                        .authenticationEntryPoint((request, response, authException) -> {
                            response.setStatus(401);
                            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
                            response.getWriter().write("{\"error\":\"로그인이 필요합니다.\"}");
                        }))

                // AuthController가 저장한 세션의 인증 정보를 매 요청마다 읽어오게 한다.
                .securityContext(context -> context
                        .securityContextRepository(securityContextRepository));

        return http.build();
    }
}
