package kr.lunaticbum.back.lun.configs.security

import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.MalformedJwtException
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import kr.lunaticbum.back.lun.model.MongoPersistentTokenRepository
import kr.lunaticbum.back.lun.model.UserManager
import kr.lunaticbum.back.lun.utils.LogService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
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.configuration.WebSecurityCustomizer
import org.springframework.security.core.AuthenticationException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.access.AccessDeniedHandler
import org.springframework.security.web.authentication.RememberMeServices
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository
import org.springframework.web.ErrorResponse
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import jakarta.servlet.FilterChain
import kr.lunaticbum.back.lun.utils.JwtUtil
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.security.web.context.HttpRequestResponseHolder
import org.springframework.security.web.context.HttpSessionSecurityContextRepository
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository
import org.springframework.security.web.context.SecurityContextRepository
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
import org.springframework.security.web.util.matcher.NegatedRequestMatcher
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import java.security.SignatureException
import java.util.Date

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // @PreAuthorize 어노테이션을 사용하기 위해 추가
class SecurityConfig(
    private val jwtUtil: JwtUtil,
    private val userManager: UserManager,
    private val bCryptPasswordEncoder: BCryptPasswordEncoder,
    private val tokenRepository: MongoPersistentTokenRepository,
    private val customAccessDeniedHandler: CustomAccessDeniedHandler
) {
    @Autowired
    lateinit var logService: LogService

    @Bean
    fun securityContextRepository(): SecurityContextRepository {
        return ApiAndWebSecurityContextRepository()
    }

    @Bean
    fun webSecurityCustomizer(): WebSecurityCustomizer {
        // 이미지 경로는 Spring Security 필터 체인 자체를 무시하도록 설정합니다.
        return WebSecurityCustomizer { web ->
            web.ignoring().requestMatchers( "/images/**")
        }
    }
    val key = "your-remember-me-key"
    @Bean
    fun rememberMeServices(): RememberMeServices {

        return PersistentTokenBasedRememberMeServices(key, userManager, tokenRepository).apply {
            setParameter("rememberMe") // [핵심] JS에서 보내는 이름과 일치시킴 ('rememberMe')
            setTokenValiditySeconds(86400 * 14) // 2주 (14일) 유지
            setAlwaysRemember(false) // 사용자가 체크했을 때만 기억
        }
    }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        val configuration = CorsConfiguration()
        configuration.allowedOrigins = listOf("*")
        configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
        configuration.allowedHeaders = listOf("*")
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/**", configuration)
        return source
    }

    @Bean
    @Order(1) // API 보안 설정을 먼저 적용
    fun apiFilterChain(http: HttpSecurity): SecurityFilterChain {
        http.securityContext { context ->
            context.securityContextRepository(securityContextRepository())
        }
            .securityMatcher("/api/**") // 이 설정은 /api/ 경로에만 적용됨
            .csrf { it.disable() }
            .cors { it.configurationSource(corsConfigurationSource()) }
            .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } // API는 세션을 사용하지 않음
            .authorizeHttpRequests { auth ->
                auth
                    .requestMatchers(HttpMethod.GET,"/api/feed").permitAll()
                    .requestMatchers("/api/stock/**").permitAll()
                    .requestMatchers("/api/ranks/**").permitAll()
                    .requestMatchers("/api/stats/visitors").permitAll()
                    .requestMatchers(HttpMethod.GET,"/api/stock/**").permitAll()
                    .requestMatchers(HttpMethod.GET, "/api/images/**").permitAll()
                    .requestMatchers("/api/auth/login").permitAll() // 로그인 API는 모두 허용
                    .anyRequest().authenticated() // 나머지 API는 인증 필요
            }
            .exceptionHandling { handling ->
//                handling.authenticationEntryPoint(jwtAuthenticationEntryPoint())
                handling.accessDeniedHandler(accessDeniedHandler2)
            }
            // 모든 API 요청 전에 JWT 토큰을 검증하는 필터 추가
            .addFilterBefore(JwtAuthenticationFilter(jwtUtil, userManager), UsernamePasswordAuthenticationFilter::class.java)

        return http.build()
    }

    @Bean
    fun jwtAuthenticationEntryPoint(): AuthenticationEntryPoint {
        return AuthenticationEntryPoint { request, response, authException ->
            response.status = HttpServletResponse.SC_UNAUTHORIZED
            response.contentType = MediaType.APPLICATION_JSON_VALUE
            val body = mapOf(
                "status" to HttpServletResponse.SC_UNAUTHORIZED,
                "error" to "Unauthorized",
                "message" to (authException.message ?: "JWT Authentication Failed"),
                "path" to request.servletPath
            )
            ObjectMapper().writeValue(response.outputStream, body)
        }
    }

    @Bean
    @Order(2) // 웹 페이지 보안 설정
    fun webFilterChain(http: HttpSecurity): SecurityFilterChain {
        http.securityMatcher(NegatedRequestMatcher(AntPathRequestMatcher("/api/**")))

        http.cors { }
            .csrf { csrf ->
                csrf.ignoringRequestMatchers(
                    "/api/**", // <-- 이 줄을 추가하세요!
                    "/user/login.bjx", "/user/joinUser.bjx", "/tlg/repotToMe.bjx",
                    "/api/ranks/submit",
                    "/bums/save/loc.api",
                    "/puzzle/**",
                )
            }.authorizeHttpRequests { auth ->
                auth
                    .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                    // 1. 정적 리소스 = permitAll
                    .requestMatchers(
                        "/webfonts/**", "/css/**", "/js/**", "/assets/**", "/webjars/**"
                    ).permitAll()
                    .requestMatchers(HttpMethod.GET,"/stock/**").permitAll()
                    // 2. 공개 GET API 및 페이지 = permitAll
                    .requestMatchers(HttpMethod.GET,
                        "/api/images/**",
                        "/stock/**",
                        "/", "/home.bs", "/bums/where.bs",
                        "/user/login.bs", "/user/join.bs",
                        "/blog/viewer/**", "/blog/posts",
                        "/blog/rankOfViews.bjx", "/blog/recentOfPost.bjx",
                        "/blog/posts/{postId}/comments.bjx", "/blog/comments/{commentId}/replies.bjx",
                        "/blog/categories.bjx", "/blog/hashtags.bjx",
                        "/puzzle/**", "/api/ranks/list", "/licenses",
                        "/puzzle/images/**",
                        "/bums/face.bs",               // [추가] 사이트 소개 페이지
                        "/bookmarks/**",     // [추가] 북마크 목록 페이지
                        "/ads.txt"
                    ).permitAll()

                    // 3. 공개 POST API = permitAll
                    .requestMatchers(HttpMethod.POST,
                        "/user/login.bjx", "/user/joinUser.bjx",
                        "/api/ranks/submit",
                        "/bums/save/loc.api",
                        "/puzzle/**",
                        "/tlg/repotToMe.bjx",
                        "/blog/post/*/like.bjx",
                        "/blog/post/*/unlike.bjx",
                        "/bookmarks/*/like",           // [추가] 북마크 좋아요
                        "/bookmarks/*/unlike",
                        "/ads.txt"
                    // [추가] 북마크 싫어요
                    ).permitAll()

                    // 4. 'WRITE' 또는 'ADMIN' 권한이 필요한 요청
                    .requestMatchers(
                        "/blog/edit/**",
                        "/blog/post.bjx",
                        "/blog/post/imageUpload.bjx"
                    ).hasAnyRole("WRITE", "ADMIN")

                    // 5. 'ADMIN' 권한이 필요한 요청 (my_info.html의 관리자 기능)
                    .requestMatchers(
                        "/user/approve-writer/**", "/user/reject-writer/**",
                        "/blog/post/*/block", "/blog/post/*/unblock",
                        "/api/images/*/approve-banner",
                        "/api/images/*/revoke-banner"
                    ).hasRole("ADMIN")

                    // 6. 나머지 모든 요청 = authenticated (인증 필요)
                    .anyRequest().authenticated()
            }.formLogin { form ->
                form.loginPage("/home.bs?action=login")
                    .loginProcessingUrl("/user/login.bs")
                    .defaultSuccessUrl("/", true)
                    .permitAll()
            }.rememberMe { rememberMe ->
                rememberMe.rememberMeServices(rememberMeServices())
                    .key(key)
                    .tokenRepository(tokenRepository)
                    .tokenValiditySeconds(60 * 60 * 24 * 7)
                    .userDetailsService(userManager)
            }.logout { logout ->
                logout.logoutUrl("/user/logout.bs").logoutSuccessUrl("/").permitAll()
            }.exceptionHandling { handling ->
                handling.accessDeniedHandler(customAccessDeniedHandler)
//                    .authenticationEntryPoint(unauthorizedEntryPoint) // 인증되지 않은 사용자가 접근 시
//                    .accessDeniedHandler(accessDeniedHandler)         // 인증은 되었으나 권한이 없는 사용자가 접근 시
            }
        return http.build()
    }

    private val accessDeniedHandler2 =
        AccessDeniedHandler { request: HttpServletRequest?, response: HttpServletResponse, accessDeniedException: AccessDeniedException? ->
            println("${accessDeniedException?.message }\nSpring security forbidden...")
            val fail: ErrorResponse = ErrorResponse.create( Throwable("권한이 없습니다."),
                HttpStatus.FORBIDDEN, "${accessDeniedException?.message }\nSpring security forbidden..."
            )
            response.status = HttpStatus.FORBIDDEN.value()
            val json = ObjectMapper().writeValueAsString(fail)
            response.contentType = MediaType.APPLICATION_JSON_VALUE
            val writer = response.writer
            writer.write(json)
            writer.flush()
        }


    @Bean
    fun authenticationManager(http: HttpSecurity): AuthenticationManager {
        val authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder::class.java)
        authenticationManagerBuilder
            .userDetailsService(userManager)
            .passwordEncoder(bCryptPasswordEncoder)
        return authenticationManagerBuilder.build()
    }


    private val unauthorizedEntryPoint =
        AuthenticationEntryPoint { request: HttpServletRequest?, response: HttpServletResponse, authException: AuthenticationException? ->
            val fail: ErrorResponse = ErrorResponse.create( Throwable("아직 못들어와"),
                HttpStatus.UNAUTHORIZED, "Spring security unauthorized..."
            )
            response.status = HttpStatus.UNAUTHORIZED.value()
            val json = ObjectMapper().writeValueAsString(fail)
            response.contentType = MediaType.APPLICATION_JSON_VALUE
            val writer = response.writer
            writer.write(json)
            writer.flush()
        }

    private val accessDeniedHandler =
        AccessDeniedHandler { request: HttpServletRequest?, response: HttpServletResponse, accessDeniedException: AccessDeniedException? ->
            val fail: ErrorResponse = ErrorResponse.create( Throwable("아직 못들어와"),
                HttpStatus.FORBIDDEN, "Spring security forbidden..."
            )
            response.status = HttpStatus.FORBIDDEN.value()
            val json = ObjectMapper().writeValueAsString(fail)
            response.contentType = MediaType.APPLICATION_JSON_VALUE
            val writer = response.writer
            writer.write(json)
            writer.flush()
        }

    @Bean
    fun bCryptPasswordEncoder(): BCryptPasswordEncoder {
        return BCryptPasswordEncoder()
    }
}

class JwtAuthenticationFilter(
    private val jwtUtil: JwtUtil,
    private val userManager: UserManager
) : OncePerRequestFilter() {
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val authHeader = request.getHeader("Authorization")

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response)
            return
        }
        try {
        val jwt = authHeader.substring(7)
        val username = jwtUtil.extractUsername(jwt)

        if (SecurityContextHolder.getContext().authentication == null) {
            val userDetails = this.userManager.loadUserByUsername(username)
            if (jwtUtil.isTokenValid(jwt, userDetails)) {
                println("jwtUtil.isTokenValid($jwt, $userDetails)")
                val authToken = UsernamePasswordAuthenticationToken(
                    userDetails,
                    null,
                    userDetails.authorities
                )
                authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
                println("authToken.details >>> ${authToken.details}")
                SecurityContextHolder.getContext().authentication = authToken
            }
        }

        } catch (e: ExpiredJwtException) {
            println("JWT Error: Token has expired -  ${e.message}")
        } catch (e: SignatureException) {
            println("JWT Error: Signature validation failed - ${e.message}")
        } catch (e: MalformedJwtException) {
            println("JWT Error: Malformed token - ${e.message}")
        } catch (e: Exception) {
            println("JWT Error: Could not set user authentication in security context - ${e.message}")
        }
        filterChain.doFilter(request, response)
        println("JWT Token validated")
    }
}

@Component // 이 클래스를 Spring Bean으로 등록
class CustomAccessDeniedHandler : AccessDeniedHandler {

    override fun handle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        accessDeniedException: AccessDeniedException
    ) {
        // 1. 요청(Request) 객체에 오류 정보를 속성(Attribute)으로 담습니다.
        request.setAttribute("timestamp", Date())
        request.setAttribute("exception", accessDeniedException)
        request.setAttribute("path", request.requestURI)

        // 2. 응답 상태 코드를 403 (Forbidden)으로 설정합니다.
        response.status = HttpServletResponse.SC_FORBIDDEN

        // 3. /access-denied 경로로 요청을 전달(Forward)합니다.
        //    Redirect가 아닌 Forward를 사용해야 request에 담은 정보가 유지됩니다.
        val dispatcher = request.getRequestDispatcher("/access-denied")
        dispatcher.forward(request, response)
    }
}

class ApiAndWebSecurityContextRepository : SecurityContextRepository {

    // API 요청은 /api/** 패턴에 매칭됩니다.
    private val apiRequestMatcher = AntPathRequestMatcher("/api/**")

    // API 요청에 대해서는 세션을 전혀 사용하지 않고, 오직 요청 기간 동안만 SecurityContext를 저장합니다. (완벽한 STATELESS)
    private val apiContextRepository = RequestAttributeSecurityContextRepository()

    // 그 외 모든 웹 요청에 대해서는 기본 HttpSession 리포지토리를 사용합니다 (STATEFUL).
    private val webContextRepository = HttpSessionSecurityContextRepository()

    override fun loadContext(requestResponseHolder: HttpRequestResponseHolder): SecurityContext {
        val request = requestResponseHolder.request
        return if (apiRequestMatcher.matches(request)) {
            apiContextRepository.loadContext(requestResponseHolder)
        } else {
            webContextRepository.loadContext(requestResponseHolder)
        }
    }

    override fun saveContext(context: SecurityContext, request: HttpServletRequest, response: HttpServletResponse) {
        if (apiRequestMatcher.matches(request)) {
            apiContextRepository.saveContext(context, request, response)
        } else {
            webContextRepository.saveContext(context, request, response)
        }
    }

    override fun containsContext(request: HttpServletRequest): Boolean {
        return if (apiRequestMatcher.matches(request)) {
            apiContextRepository.containsContext(request)
        } else {
            webContextRepository.containsContext(request)
        }
    }
}
