package eu.dnetlib.organizations;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.access.AccessDeniedHandler;

import eu.dnetlib.organizations.controller.UserInfo;
import eu.dnetlib.organizations.controller.UserRole;
import eu.dnetlib.organizations.model.User;
import eu.dnetlib.organizations.utils.DatabaseUtils;
import eu.dnetlib.organizations.utils.OpenOrgsConstants;

@Profile("!dev")
@Configuration
@EnableWebSecurity
public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private DatabaseUtils databaseUtils;

	@Autowired
	private ClientRegistrationRepository clientRegistrationRepository;

	@Value("${openaire.api.valid.subnet}")
	private String openaireApiValidSubnet;

	@Value("${openaire.override.logout.url}")
	private String openaireLogoutUrl;

	private static Logger log = LoggerFactory.getLogger(OAuth2WebSecurityConfig.class);

	@Override
	protected void configure(final HttpSecurity http) throws Exception {
		http.headers().frameOptions().sameOrigin();

		http.csrf()
			.disable()
			.authorizeRequests()
			.antMatchers("/main", "/api/**")
			.hasAnyRole(OpenOrgsConstants.VALID_ROLES)
			.antMatchers("/registration_api/**")
			.hasRole(OpenOrgsConstants.NOT_AUTORIZED_ROLE)
			.antMatchers("/", "/common/**", "/resources/**", "/webjars/**", "/metrics", "/health", "/kpis", "/dbmodel/**")
			.permitAll()
			.antMatchers("/oa_api/**")
			.hasIpAddress(openaireApiValidSubnet)
			.anyRequest()
			.authenticated()
			.and()
			.exceptionHandling()
			.accessDeniedHandler(accessDeniedHandler())
			.and()
			.logout()
			.logoutSuccessHandler(oidcLogoutSuccessHandler())
			.invalidateHttpSession(true)
			.clearAuthentication(true)
			.deleteCookies("JSESSIONID")
			.and()
			.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo.oidcUserService(this.oidcUserService())));
	}

	private AccessDeniedHandler accessDeniedHandler() {
		return (req, res, e) -> {
			final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			if (authentication != null) {
				log.warn(String
					.format("User '%s' (%s) attempted to access the protected URL: %s", UserInfo.getEmail(authentication), req
						.getRemoteAddr(), req.getRequestURI()));
			}

			if (UserInfo.isNotAuthorized(authentication)) {
				res.sendRedirect(req.getContextPath() + "/authorizationRequest");
			} else {
				res.sendRedirect(req.getContextPath() + "/alreadyRegistered");
			}
		};
	}

	private OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler() {
		final OidcClientInitiatedLogoutSuccessHandler handler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
		// NB:
		// The same URL must be configured server side:
		// Manage Clients > Edit Client > Other > Post-Logout Redirect

		handler.setPostLogoutRedirectUri("{baseUrl}");
		handler.setRedirectStrategy((req, res, url) -> {
			if (StringUtils.isNotBlank(openaireLogoutUrl)) {
				log.info("Performing remote logout: " + openaireLogoutUrl);
				res.sendRedirect(openaireLogoutUrl);
			} else {
				log.info("Performing remote logout: " + url);
				res.sendRedirect(url);
			}
		});
		return handler;

	}

	private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
		final OidcUserService delegate = new OidcUserService();

		return (userRequest) -> {
			final OidcUser oidcUser = delegate.loadUser(userRequest);

			log.debug("User attributes:");
			oidcUser.getAttributes().forEach((k, v) -> {
				log.debug("   - " + k + ": " + v);
			});

			final Optional<User> user = databaseUtils.findUser(UserInfo.getEmail(oidcUser));

			if (user.isPresent()) {
				databaseUtils.updateUserDetails(UserInfo.getEmail(oidcUser), UserInfo.getFullname(oidcUser), UserInfo.getOrganization(oidcUser));
			}

			final String role = "ROLE_" + OpenOrgsConstants.OPENORGS_ROLE_PREFIX + user
				.map(User::getRole)
				.filter(StringUtils::isNotBlank)
				.orElse(UserRole.NOT_AUTHORIZED.toString());

			final Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
			mappedAuthorities.add(new SimpleGrantedAuthority(role));

			return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
		};
	}

}
