/*
 * This file is part of Nuts Framework.
 * Copyright(C) 2009-2012 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 *
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.exts.struts2.interceptor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.http.HttpServletRequest;

import nuts.core.collections.CollectionUtils;
import nuts.core.lang.ArrayUtils;
import nuts.core.lang.ClassLoaderUtils;
import nuts.core.lang.StringUtils;
import nuts.core.servlet.URLHelper;
import nuts.exts.exception.NoPermitException;
import nuts.exts.struts2.actions.NutsRC;
import nuts.exts.struts2.util.StrutsContextUtils;
import nuts.exts.xwork2.ActionSupport;
import nuts.exts.xwork2.util.ContextUtils;

import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;

/**
 */
@SuppressWarnings("serial")
public class AuthenticateInterceptor extends AbstractInterceptor {
	//--------------------------------------------------------
	// permission
	//--------------------------------------------------------
	/**
	 * PERMISSION_ALL = "*";
	 */
	public final static String PERMISSION_ALL = "*";
	
	/**
	 * PERMISSION_NONE = "-";
	 */
	public final static String PERMISSION_NONE = "-";

	/**
	 * "action-permits.properties"
	 */
	public static final String DEFAULT_PROPERTIES = "action-permits.properties";

	/**
	 * USER_ATTRIBUTE = "user";
	 */
	public static final String USER_ATTRIBUTE = "user";

	/**
	 * empty permits
	 */
	private static final List<String> EMPTY_PERMITS = new ArrayList<String>(0);

	/**
	 * action permit map
	 */
	private static Map<String, String[]> actionPermits = new HashMap<String, String[]>();

	/**
	 * action permit cache
	 */
	private static Map<String, String[]> actionPermitCache = new ConcurrentHashMap<String, String[]>();

	private static final int CHECK_NONE = 0;
	private static final int CHECK_LOGIN = 1;
	private static final int CHECK_SECURE = 2;

	/**
	 * check level: none, login(default), secure
	 */
	private int check = CHECK_LOGIN;
	
	/**
	 * loginResult
	 */
	private String loginResult = Action.LOGIN;
	
	/**
	 * secureResult
	 */
	private String secureResult = Action.LOGIN;

	/**
	 * @param check the check to set
	 */
	public void setCheck(String check) {
		if ("login".equalsIgnoreCase(check)) {
			this.check = CHECK_LOGIN;
		}
		else if ("secure".equals(check)) {
			this.check = CHECK_SECURE;
		}
		else {
			this.check = CHECK_NONE;
		}
	}

	/**
	 * @param loginResult the loginResult to set
	 */
	public void setLoginResult(String loginResult) {
		this.loginResult = loginResult;
	}

	/**
	 * @param secureResult the secureResult to set
	 */
	public void setSecureResult(String secureResult) {
		this.secureResult = secureResult;
	}

	/**
	 * loadActionPermits
	 * @throws Exception if an error occurs
	 */
	public synchronized static void loadActionPermits() throws Exception {
		actionPermits.clear();
		actionPermitCache.clear();

		Properties props = new Properties();
		props.load(ClassLoaderUtils.getResourceAsStream(DEFAULT_PROPERTIES));

		for (Entry<Object, Object> en : props.entrySet()) {
			actionPermits.put(en.getKey().toString(), StringUtils.split(en.getValue().toString()));
		}
	}

	/**
	 * hasPermission
	 * @param permits permits
	 * @param namespace namespace
	 * @param action action
	 * @return true if user has permit to access the action
	 */
	public static boolean hasPermission(String[] permits, String namespace, String action) {
		return hasPermission(Arrays.asList(permits), namespace, action);
	}
	
	/**
	 * hasPermission
	 * @param permits permits
	 * @param namespace namespace
	 * @param action action
	 * @return true if user has permit to access the action
	 */
	public static boolean hasPermission(Collection<String> permits, String namespace, String action) {
		if (permits.contains(PERMISSION_ALL)) {
			return true;
		}

		String uri = ("/".equals(namespace) ? "" : namespace) + "/" + action;
		
		String[] ps = actionPermitCache.get(uri);
		if (ps == null) {
			ps = new String[0];
			for (Entry<String, String[]> e : actionPermits.entrySet()) {
				if (StringUtils.wildcardMatch(uri, e.getKey())) {
					ps = e.getValue();
					break;
				}
			}
			actionPermitCache.put(uri, ps);
		}

		if (ArrayUtils.isEmpty(ps)) {
			// uri is not in the check list, pass
			return true;
		}

		if (CollectionUtils.isEmpty(permits)) {
			return false;
		}
		
		if (permits.contains(PERMISSION_NONE)) {
			return false;
		}
		
		for (String p : ps) {
			if (permits.contains(p)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * add error to action
	 * @param invocation the action invocation
	 * @param id msgId
	 */
	protected void addActionError(ActionInvocation invocation, String id) {
		HttpServletRequest request = StrutsContextUtils.getServletRequest();
		String uri = request.getRequestURI();

		String msg = "";

		Object action = invocation.getAction();
		if (action instanceof ActionSupport) {
			ActionSupport a = (ActionSupport)action;
			msg = a.getText(id, new String[] { uri });
			a.addActionError(msg);
		}
		else {
			Map<String, String> m = new HashMap<String, String>();
			m.put("id", id);
			m.put("msg", msg);
			request.setAttribute("actionErrors", m);
		}
	}
	
	/**
	 * remove session error
	 * @param invocation the action invocation
	 * @param id msgId
	 */
	protected void removeSessionError(ActionInvocation invocation, String id) {
		Object action = invocation.getAction();
		if (action instanceof ActionSupport) {
			ActionSupport a = (ActionSupport)action;
			a.removeSessionError(id);
		}
	}
	
	/**
	 * clear session errors
	 * @param invocation the action invocation
	 * @param id msgId
	 */
	protected void clearSessionErrors(ActionInvocation invocation) {
		Object action = invocation.getAction();
		if (action instanceof ActionSupport) {
			ActionSupport a = (ActionSupport)action;
			a.clearSessionErrors();
		}
	}
	
	/**
	 * add error to session
	 * @param invocation the action invocation
	 * @param id msgId
	 */
	protected void addSessionError(ActionInvocation invocation, String id) {
		HttpServletRequest request = StrutsContextUtils.getServletRequest();
		String uri = request.getRequestURI();

		String msg = "";

		Object action = invocation.getAction();
		if (action instanceof ActionSupport) {
			ActionSupport a = (ActionSupport)action;
			msg = a.getText(id, new String[] { uri });
			a.addSessionError(id, msg);
		}
		else {
			Map<String, String> m = new HashMap<String, String>();
			m.put("id", id);
			m.put("msg", msg);
			request.setAttribute("sessionErrors", m);
		}
	}

	protected Object getSessionUser() {
		Object u = ContextUtils.getRequest().get(USER_ATTRIBUTE);
		if (u == null) {
			u = ContextUtils.getSession().get(USER_ATTRIBUTE);
		}
		return u;
	}
	
	protected void saveRedirectUrl(ActionInvocation actionInvocation) {
		HttpServletRequest request = StrutsContextUtils.getServletRequest();
		String requestUrl = URLHelper.buildURL(request);
		actionInvocation.getStack().getContext().put("redirect", requestUrl);
	}
	
	protected boolean isSecureSessionUser(Object su) {
		return false;
	}
	
	protected List<String> getUserPermits(Object su) {
		return EMPTY_PERMITS;
	}

	/**
	 * Allows the Interceptor to do some processing on the request before and/or after the rest of the processing of the
	 * request by the {@link ActionInvocation} or to short-circuit the processing and just return a String return code.
	 *
	 * @param actionInvocation  the action invocation
	 * @return the return code, either returned from {@link ActionInvocation#invoke()}, or from the interceptor itself.
	 * @throws Exception any system-level error, as defined in {@link com.opensymphony.xwork2.Action#execute()}.
	 */
	public String intercept(ActionInvocation actionInvocation) throws Exception {
		Object su = getSessionUser();
		if (check < CHECK_LOGIN) {
			return actionInvocation.invoke();
		}

		if (su == null) {
			saveRedirectUrl(actionInvocation);
			addActionError(actionInvocation, NutsRC.ERROR_UNLOGIN);
			return loginResult;
		}
		
		if (check == CHECK_SECURE && !isSecureSessionUser(su)) {
			saveRedirectUrl(actionInvocation);
			addActionError(actionInvocation, NutsRC.ERROR_UNSECURE);
			return secureResult;
		}

		String n = actionInvocation.getProxy().getNamespace();
		String a = actionInvocation.getInvocationContext().getName();
		if (hasPermission(getUserPermits(su), n, a)) {
			return actionInvocation.invoke();
		}
		else {
			throw new NoPermitException();
		}
	}
}
