package com.its.op.security;

import com.its.op.security.interceptor.UserLogoutHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.session.HttpSessionEventPublisher;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
//@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final WebLoginService loginService;
    private final WebLoginSuccessHandler webLoginSuccessHandler;
    private final WebLoginFailureHandler webLoginFailureHandler;

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/favicon.ico");
        // static 디렉터리의 하위 파일 목록은 인증 무시 ( = 항상통과 )
        web.ignoring().antMatchers("/js/**", "/images/**", "/libs/**", "/css/**", "/application/fonts/**");
        //web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());    // 정적 리소스 접근 가능하게
        web.ignoring().antMatchers(HttpMethod.GET, "/api/**");  // GET Method 는 모두 통과

        // cs-api
        web.ignoring().antMatchers(HttpMethod.GET, "/cs-api/**");  // GET Method 는 모두 통과
        web.ignoring().antMatchers(HttpMethod.POST, "/cs-api/**");  // GET Method 는 모두 통과
        web.ignoring().antMatchers(HttpMethod.PUT, "/cs-api/**");  // GET Method 는 모두 통과
        web.ignoring().antMatchers(HttpMethod.DELETE, "/cs-api/**");  // GET Method 는 모두 통과
    }

    @Override
    protected void configure(HttpSecurity http) {

        // URL 권한 설정
        //setAntMatchers(http, "ROLE_");

        try {
            http.csrf()
                    .disable()
            ;  // REST API 호출 유효하게(POST...)
            http
                    .authorizeRequests()
                    // SWAGGER 권한 설정
                    .antMatchers("/swagger-ui.html", "/swagger/**", "/swagger-resources/**", "/webjars/**", "/v2/api-docs").permitAll()
                    // 웹소켓 권한 설정하지
                    .antMatchers("/ws/**").permitAll()
                    .antMatchers("/api/**").permitAll()
                    // API 권한 설정하지
                    //.antMatchers("/api/**").permitAll()
                    // 지도 URI 권한 설정하지
                    .antMatchers("/MAPDATA/**").permitAll()
                    .antMatchers("/download/**").permitAll()
                    // 페이지 권한 설정
//                    .antMatchers("/application/facility/**", "/facility/**").permitAll()
                    .antMatchers("/application/**", "/facility/**").permitAll()
                    .antMatchers("/application/wall/**", "/wall/**").permitAll()
                    .antMatchers("/application/login/**").permitAll()
                    .antMatchers("/api/auth/**").permitAll()
//                .antMatchers("/api/**").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
//                    .loginPage("/application/login/login.html")
                    .loginPage("/application/op/00.main/main.html")
                    .loginProcessingUrl("/api/auth/login.do")
                    .defaultSuccessUrl("/application/op/00.main/main.html", true)
                    .usernameParameter("username")
                    .passwordParameter("password")
                    .successHandler(this.webLoginSuccessHandler)
                    .failureHandler(this.webLoginFailureHandler)
                    .permitAll()
                    .and()
                    .logout()
                    //.logoutUrl("/api/auth/logout.do")
                    //.logoutRequestMatcher(new AntPathRequestMatcher("/api/auth/logout.do"))
                    .addLogoutHandler(new UserLogoutHandler()).permitAll()
                    .logoutSuccessUrl("/application/login/login.html").permitAll()
                    //.logoutSuccessUrl("/api/auth/login.do").permitAll()
                    .invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID")
                    .deleteCookies(WebMvcConfig.USER_UUID)
                    .deleteCookies(WebMvcConfig.USER_TIME)
                    .and()
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)   // 스프링 시큐리티가 필요 시 생성 (default)
                    // 인증에 성공할 때 마다 세션 ID나 세션을 변경해서 발급해줌으로써
                    // 세션을 중간에서 가로채더라도 해당 세션이 유효하지 않게 하는 기능
                    .invalidSessionUrl("/application/login/login.html")    // 세션이 유효하지 않을 경우 이동 할 페이지
                    //.invalidSessionUrl("/api/auth/login.do")    // 세션이 유효하지 않을 경우 이동 할 페이지
                    .sessionFixation().changeSessionId()        // changeSessionId : 새로운 세션 ID를 발급해서 전달(default)
                    // none            : 아무 동작 안함
                    // migrateSession  : 새로운 세션을 생성해서 전달 (속성값 유지)
                    // newSession      : 새로운 세션 전달 (속성값 유지 안됨)
                    .maximumSessions(20)                        // 최대 허용 가능 세션 수, -1인 경우 무제한 세션 허용
                    .maxSessionsPreventsLogin(true)             // 동시 로그인 차단, false 인 경우 기존 세션 만료(default)
                    .expiredUrl("/application/login/login.html")          // 세션이 만료된 경우 이동 할 페이지
                    //.expiredUrl("/api/auth/login.do")          // 세션이 만료된 경우 이동 할 페이지
                    .sessionRegistry(sessionRegistry())
//                .and()
//            .exceptionHandling()
//                .accessDeniedPage("/login.do")
//                .and()
//            .headers()
//                .defaultsDisabled()
//                .frameOptions()
//                .sameOrigin()
//                .cacheControl();

//                .and() // 로그아웃 설정
//                .logout()
//                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
//                .logoutSuccessUrl("/login")
//                .invalidateHttpSession(true)
//                .deleteCookies("JSESSIONID")
//                .and()
//                // 403 예외처리 핸들링
//                .exceptionHandling().accessDeniedPage("/login");
            ;
        } catch (IOException e) {
            // FOR KISA Secure Coding pass
            log.error("{configure: IOException}");
        } catch (Exception e) {
            log.error("{configure: Exception}");
        }
    }

    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(this.loginService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        // loadUserByUsername 의 UsernameNotFoundException 이 BadCredentialsException 로 발생함.
        // Exception 을 catch 하기 위해서는 아래를 false 로 설정하면 됨.
        authenticationProvider.setHideUserNotFoundExceptions(true);
        return authenticationProvider;
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) {
        // loadUserByUsername 의 UsernameNotFoundException 를 처리하기 위해
        // AuthenticationProvider 를 빈으로 등록해서 사용자 로그인 처리를 수행한다.
        //auth.userDetailsService(this.loginService).passwordEncoder(passwordEncoder());
        auth.authenticationProvider(daoAuthenticationProvider());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new WebPasswordEncoder();
    }

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

    /**
     * 로그아웃을 했기 때문에 세션의 개수가 0이라 생각했는데 로그인이 안되었다.
     * 이에 대한 해결책으로 SessionRegistry 빈을 생성 후 sessionManagement 에 DI 시킨다.
     * @return
     */
    @Bean
    public static ServletListenerRegistrationBean httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
    }

    protected List<Map<String, Object>> getAuthReq() {
        Map<String, Object> roll = new HashMap<>();
//        roll.put("id", "id");
//        roll.put("url", "url");
//        roll.put("hasAuthority", "auth");
//        roll.put("date", "date");
        return (List<Map<String, Object>>) roll;
    }

    protected void setAntMatchers(HttpSecurity http, String rolePrefix) {
        List<Map<String, Object>> list = getAuthReq();
        for(Map<String, Object> m : list) {
            // 쉼표(,)로 구분된 권한 정보를 분리 후 배열로 저장
            String[] roles = m.get("hasAuthority").toString().split(",");
            // 권한 앞에 접두사(rolePrefix) 붙임
            for(int ii = 0; ii < roles.length; ii++) {
                roles[ii] = rolePrefix + roles[ii].toUpperCase();
            }

            String url = m.get("url").toString();
            if(url.charAt(0) != '/') {
                url = "/" + url;
            }
            // url, 권한 정보를 넣는다.
            try {
                http.authorizeRequests()
                       .antMatchers(url)
                       .hasAnyAuthority(roles);
            } catch (IOException ie) {
                // FOR KISA Secure Coding pass
                log.error("setAntMatchers: IOException");
            } catch (Exception e) {
                log.error("setAntMatchers: Exception");
            }
        }
        try {
            http.authorizeRequests()
                    .antMatchers("/**").permitAll()
                    .anyRequest().authenticated();
        } catch (IOException ie) {
            // FOR KISA Secure Coding pass
            log.error("setAntMatchers: IOException, permitAll");
        } catch (Exception e) {
            log.error("setAntMatchers: Exception, permitAll");
        }
    }
}
