/*******************************************************************************
 * Copyright 2Fi Business Solutions Ltd.
 *
 * This code is copyrighted. Under no circumstances should any party, people,
 * or organization should redistribute any portions of this code in any form,
 * either verbatim or through electronic media, to any third parties, unless
 * under explicit written permission by 2Fi Business Solutions Ltd.
 ******************************************************************************/
package com.ffii.core.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.regex.Pattern;

import com.ffii.core.User;
import com.ffii.tbms.user.service.UserService;

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;

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

	private static final Pattern PATTERN_DIGITS = Pattern.compile("[0-9]");
	private static final Pattern PATTERN_A2Z_LOWER = Pattern.compile("[a-z]");
	private static final Pattern PATTERN_A2Z_UPPER = Pattern.compile("[A-Z]");

	private static final String A2Z_LOWER = "abcdefghijklmnopqrstuvwxyz";
	private static final String A2Z_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
	private static final String DIGITS = "0123456789";

	/*
	 * Ref: https://www.owasp.org/index.php/Password_special_characters
	 * without space character
	 */
	private static final String SPECIAL_CHARS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
	private static Pattern PATTERN_SPECIAL_CHARS = Pattern.compile("[!\"#$%&'()*+,-./:;<=>?@\\[\\\\\\]^_`{|}~]");

	/**
	 * 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} ({@code User})
	 * @see Authentication#getPrincipal()
	 */
	public static final User getUser() {
		try {
			return (User) getSecurityContext().getAuthentication().getPrincipal();
		} catch (ClassCastException e) {
			// no authenticated principal
			return null;
		} catch (NullPointerException e) {
			// no authentication information is available
			return null;
		}
	}

	/**
	 * Updates the Authentication Token with the user (e.g. user changed the password)
	 *
	 * @see SecurityContext#setAuthentication(Authentication)
	 */
	public static final void updateUserAuthentication(final User 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 (auth.getAuthority().equals("SUPERUSER")) return true;
			if (role.equals(auth.getAuthority())) return true;
		}
		return false;
	}

	public static final boolean isGrantedOnly(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);
	}

	public static final boolean checkPwd(String pwd, int min, int max, boolean num, boolean upperEng, boolean lowerEng, boolean special) {
		if (pwd == null) return false;
		if (pwd.length() < min) return false;
		if (pwd.length() > max) return false;

		if (num && !PATTERN_DIGITS.matcher(pwd).find()) return false;
		if (upperEng && !PATTERN_A2Z_UPPER.matcher(pwd).find()) return false;
		if (lowerEng && !PATTERN_A2Z_LOWER.matcher(pwd).find()) return false;
		if (special && !PATTERN_SPECIAL_CHARS.matcher(pwd).find()) return false;

		return true;
	}

	public static String genPwd(int length, boolean num, boolean upperEng, boolean lowerEng, boolean special) {
		if (length <= 0) return "";

		StringBuilder password = new StringBuilder(length);
		Random random = new Random(System.nanoTime());

		List<String> charCategories = new ArrayList<>(4);
		if (lowerEng) charCategories.add(A2Z_LOWER);
		if (upperEng) charCategories.add(A2Z_UPPER);
		if (num) charCategories.add(DIGITS);
		if (special) charCategories.add(SPECIAL_CHARS);

		for (int i = 0; i < length; i++) {
			String charCategory = charCategories.get(i % charCategories.size());
			char randomChar = charCategory.charAt(random.nextInt(charCategory.length()));
			if (password.length() > 0)
				password.insert(random.nextInt(password.length()), randomChar);
			else
				password.append(randomChar);
		}

		return password.toString();
	}

	public static void authArgs(Map<String, Object> args){
		args.put("sysGroupId", getUser().getSysGroupId());
	}

}
