package com.ffii.fpsms.modules.common;

import java.util.Optional;

import org.springframework.dao.DataAccessException;
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 org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import com.ffii.fpsms.modules.user.entity.User;

/**
 * Security Utils - for Spring Security
 *
 * @author Patrick
 */
public class SecurityUtils {

  /**
   * Obtains the current {@code SecurityContext}.
   *
   * @return the security context (never {@code null})
   */
  public static final SecurityContext getSecurityContext() {
    return SecurityContextHolder.getContext();
  }

  /**
   * @return the authenticated {@code Principal})
   * @see Authentication#getPrincipal()
   */
  public static final Optional<User> getUser() {
    try {
      return Optional.of((User) getSecurityContext().getAuthentication().getPrincipal());
    } catch (ClassCastException e) {
      // no authenticated principal
      return Optional.empty();
    } catch (NullPointerException e) {
      // no authentication information is available
      return Optional.empty();
    }
  }

  /**
   * Updates the Authentication Token with the user (e.g. user changed the password)
   *
   * @see SecurityContext#setAuthentication(Authentication)
   */
  public static final void updateUserAuthentication(final UserDetails user) {
    getSecurityContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()));
  }

  /**
   * Checks if the current user is GRANTED the {@code role}
   *
   * @param role
   *          the {@code role} to check for
   * @return {@code true} if the current user is GRANTED the {@code role}, else {@code false}
   */
  public static final boolean isGranted(String role) {
    Authentication authentication = getSecurityContext().getAuthentication();
    if (authentication == null) return false;
    for (GrantedAuthority auth : authentication.getAuthorities()) {
      if (role.equals(auth.getAuthority())) return true;
    }
    return false;
  }

  /**
   * Checks if the current user is NOT GRANTED the {@code role}
   *
   * @param role
   *          the {@code role} to check for
   * @return {@code true} if the current user is NOT GRANTED the {@code role}, else {@code false}
   */
  public static final boolean isNotGranted(String role) {
    return !isGranted(role);
  }

  /**
   * Checks if the current user is GRANTED ANY of the {@code role}s
   *
   * @param roles
   *          the {@code role}s to check for
   * @return {@code true} if the current user is GRANTED ANY of the {@code role}s, else {@code false}
   */
  public static final boolean isGrantedAny(String... roles) {
    for (int i = 0; i < roles.length; i++) {
      if (isGranted(roles[i])) return true;
    }
    return false;
  }

  /**
   * Checks if the current user is NOT GRANTED ANY of the {@code role}s
   *
   * @param roles
   *          the {@code role}s to check for
   * @return {@code true} if the current user is NOT GRANTED ANY of the {@code role}s, else {@code false}
   */
  public static final boolean isNotGrantedAny(String... roles) {
    return !isGrantedAny(roles);
  }

  /**
   * Checks if the current user is GRANTED ALL of the {@code role}s
   *
   * @param roles
   *          the {@code role}s to check for
   * @return {@code true} if the current user is GRANTED ALL of the {@code role}s, else {@code false}
   */
  public static final boolean isGrantedAll(String... roles) {
    for (int i = 0; i < roles.length; i++) {
      if (isNotGranted(roles[i])) return false;
    }
    return true;
  }

  /**
   * Login a user non-interactively
   *
   * @param userService
   *          any implementation of {@link UserDetailsService}
   * @param username
   *          the username
   *
   * @throws UsernameNotFoundException
   *           if the user could not be found or the user has no GrantedAuthority
   * @throws DataAccessException
   *           if user could not be found for a repository-specific reason
   */
  public static final void loginUser(UserDetailsService userService, String username) {
    /* load the user, throw exception if user not found */
    UserDetails userDetails = userService.loadUserByUsername(username);

    /* create authentication token for the specified user */
    Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
    getSecurityContext().setAuthentication(authentication);
  }

}
