package com.palmnest.service;

import com.palmnest.application.Constants;
import com.palmnest.model.Role;
import com.palmnest.model.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
 * This advice is responsible for enforcing security and only allowing administrators
 * to modify users. Users are allowed to modify themselves.
 *
 * @author <a href="mailto:chasel.java@gmail.com">Chasel Li</a>
 */
@Slf4j
public class UserSecurityAdvice implements MethodBeforeAdvice, AfterReturningAdvice {
	/**
	 * Default "Access Denied" error message (not i18n-ized).
	 */
	public static final String ACCESS_DENIED = "Access Denied: Only administrators are allowed to modify other users.";


	/**
	 * Method to enforce security and only allow administrators to modify users. Regular
	 * users are allowed to modify themselves.
	 *
	 * @param method the name of the method executed
	 * @param args   the arguments to the method
	 * @param target the target class
	 * @throws Throwable thrown when args[0] is null or not a User object
	 */
	public void before(Method method, Object[] args, Object target) throws Throwable {
		SecurityContext ctx = SecurityContextHolder.getContext();

		if (ctx.getAuthentication() != null) {
			Authentication                         auth          = ctx.getAuthentication();
			boolean                                administrator = false;
			Collection<? extends GrantedAuthority> roles         = auth.getAuthorities();
			for (GrantedAuthority role : roles) {
				if (role.getAuthority().equals(Constants.ADMIN_ROLE)) {
					administrator = true;
					break;
				}
			}

			User user = (User) args[0];

			AuthenticationTrustResolver resolver = new AuthenticationTrustResolverImpl();
			// allow new users to signup - this is OK b/c Signup doesn't allow setting of roles
			boolean signupUser = resolver.isAnonymous(auth);

			if (!signupUser) {
				User currentUser = getCurrentUser(auth);

				if (user.getId() != null && !user.getId().equals(currentUser.getId()) && !administrator) {
					log.warn("Access Denied: '" + currentUser.getUsername() + "' tried to modify '" + user.getUsername() + "'!");
					throw new AccessDeniedException(ACCESS_DENIED);
				} else if (user.getId() != null && user.getId().equals(currentUser.getId()) && !administrator) {
					// get the list of roles the user is trying add
					Set<String> userRoles = new HashSet<String>();
					if (user.getRoles() != null) {
						for (Object o : user.getRoles()) {
							Role role = (Role) o;
							userRoles.add(role.getName());
						}
					}

					// get the list of roles the user currently has
					Set<String> authorizedRoles = new HashSet<String>();
					for (GrantedAuthority role : roles) {
						authorizedRoles.add(role.getAuthority());
					}

					// if they don't match - access denied
					// regular users aren't allowed to change their roles
					if (!CollectionUtils.isEqualCollection(userRoles, authorizedRoles)) {
						log.warn("Access Denied: '" + currentUser.getUsername() + "' tried to change their role(s)!");
						throw new AccessDeniedException(ACCESS_DENIED);
					}
				}
			} else {
				if (log.isDebugEnabled()) {
					log.debug("Registering new user '" + user.getUsername() + "'");
				}
			}
		}
	}

	/**
	 * After returning, grab the user, check if they've been modified and reset the SecurityContext if they have.
	 *
	 * @param returnValue the user object
	 * @param method      the name of the method executed
	 * @param args        the arguments to the method
	 * @param target      the target class
	 * @throws Throwable thrown when args[0] is null or not a User object
	 */
	public void afterReturning(Object returnValue, Method method, Object[] args, Object target)
			throws Throwable {
		User user = (User) args[0];

		if (user.getVersion() != null) {
			// reset the authentication object if current user
			Authentication              auth     = SecurityContextHolder.getContext().getAuthentication();
			AuthenticationTrustResolver resolver = new AuthenticationTrustResolverImpl();
			// allow new users to signup - this is OK b/c Signup doesn't allow setting of roles
			boolean signupUser = resolver.isAnonymous(auth);
			if (auth != null && !signupUser) {
				User currentUser = getCurrentUser(auth);
				if (currentUser.getId().equals(user.getId())) {
					auth = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
					SecurityContextHolder.getContext().setAuthentication(auth);
				}
			}
		}
	}

	private User getCurrentUser(Authentication auth) {
		User currentUser;
		if (auth.getPrincipal() instanceof UserDetails) {
			currentUser = (User) auth.getPrincipal();
		} else if (auth.getDetails() instanceof UserDetails) {
			currentUser = (User) auth.getDetails();
		} else {
			throw new AccessDeniedException("User not properly authenticated.");
		}
		return currentUser;
	}
}
