/********************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ********************************************************************/
package gov.va.med.fw.security;

import javax.security.auth.login.LoginException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.AuthenticationManager;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.context.HttpSessionContextIntegrationFilter;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.acegisecurity.ui.WebAuthenticationDetails;
import org.acegisecurity.userdetails.memory.UserAttribute;
import org.apache.commons.lang.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;

import gov.va.med.fw.model.RolePrincipalImpl;
import gov.va.med.fw.model.UserPrincipalImpl;


/**
 * LoginManager implementation, centralized point to perform the authentication
 * and clean up at the end. UI use this through LogonAction to perform login and
 * SignoutAction to perform logout at the end. If the AnonymousKey and
 * AnonymousAttribute are defined and no username or password is specified, then
 * anonymous authentication will be attempted, otherwise normal authentication
 * will be attempted. If the HttpServletRequest is specified, it will set the
 * request details such as session id, remote ip address in a authentication
 * token. To do login and logout from the UI application, use the methods that
 * has HttpServletRequest paramter, do add/remove attributes required by the
 * Acegi Security.
 *
 * @author DNS   MANSOG
 * @date May 5, 2005 6:41:43 PM
 */
public class LoginManagerImpl implements InitializingBean, LoginManager {
    protected transient Log log = LogFactory.getLog(getClass());

    private AuthenticationManager authenticationManager;

    //Acegi's anonymous user information.
    private String anonymousKey;
    private UserAttribute anonymousUserAttribute;

    //Acegi's secure context
    private Class context;

    /**
     * Return the AuthenticationManager
     */
    public AuthenticationManager getAuthenticationManager() {
        return authenticationManager;
    }

    /**
     * Set the AuthenticationManager.
     *
     * @param authenticationManager
     */
    public void setAuthenticationManager(
            AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    /**
     * Return the Context class.
     */
    public Class getContext() {
        return context;
    }

    /**
     * Set the Context class.
     */
    public void setContext(Class context) {
        this.context = context;
    }

    /**
     * @return String anonymous key
     */
    public String getAnonymousKey() {
        return anonymousKey;
    }

    /**
     * Set the anonymous authentication key, only required for
     * AnonymousAuthentication
     *
     * @param anonymousKey
     */
    public void setAnonymousKey(String anonymousKey) {
        this.anonymousKey = anonymousKey;
    }

    /**
     * Return the anonymous UserAttribute
     */
    public UserAttribute getAnonymousUserAttribute() {
        return anonymousUserAttribute;
    }

    /**
     * Set the UserAttribute for anonymous user.
     *
     * @param anonymousUserAttribute
     */
    public void setAnonymousUserAttribute(UserAttribute anonymousUserAttribute) {
        this.anonymousUserAttribute = anonymousUserAttribute;
    }

    public void afterPropertiesSet() throws Exception {
        if (authenticationManager == null) {
            throw new IllegalArgumentException(
                    "Missing required property - AuthenticationManager: Must be specified.");
        }
        if ((this.context == null)
                || (!org.acegisecurity.context.SecurityContext.class.isAssignableFrom(this.context))) {
            throw new IllegalArgumentException(
                    "context must be defined and implement Context (typically use net.sf.acegisecurity.context.security.SecureContextImpl)");
        }
    }

    /**
     * Perform the login given the HttpServletRequest, username and password.
     *
     * @param request
     * @param username
     * @param password
     *
     * @throws IllegalArgumentException
     *             if username or password is null.
     * @throws LoginExceptio
     *             if authentication fails
     */
    public void login(HttpServletRequest request, UserCredentials userCredentails)
    throws LoginException
	{
        Validate.notNull(userCredentails, "Null UserCredentails not allowed.");
        Validate.notNull(userCredentails.getUserID(), "Null username not allowed.");
        //CCR13343 SSOi integration
        //Validate.notNull(userCredentails.getPassword(), "Null password not allowed.");
        doAuthentication(request,userCredentails);
    }

    /**
     * Login using username and password. This call is same as calling
     * login(null, username, password).
     *
     * @param userCredentails - user credentails to be authenticated
     * @throws IllegalArgumentException
     *             if username or password is null.
     * @throws LoginExceptio
     *             if authentication fails.
     */
    public void login(UserCredentials userCredentails)
            throws LoginException {
    	if(userCredentails.isVerified()) {
    		initializeUser(userCredentails);
    	} else {
    		login(null,userCredentails);
    	}
    }

    private void initializeUser(UserCredentials userCredentials) {
		/* TODO: this is the ideal way to go as it will correctly/fully populate the SecurityContext for the caller
	 	however, must wait until there is a mechanism that does not update the database (uses a new flag in UserCredentials).
	 	This would not "pollute" the database login table for last_logged_in_time, etc.
	 	*/

    	// for now, just do this
    	String id = userCredentials.getLogicalID() != null ? userCredentials.getLogicalID() : userCredentials.getUserID();
		loginAnonymous(id);
    }


    /**
     * Anonymously login the user. Throws
     * <code>java.lang.IllegalStateException</code> if anonymous login is
     * not enabled/allowed.
     *
     * @throws IllegalStateException
     *             if anonymous login is not allowed.
     */
    public void loginAnonymous() throws IllegalStateException {
    	loginAnonymous(null);
    }

    /**
     * Anonymously login the user. Throws
     * <code>java.lang.IllegalStateException</code> if anonymous login is
     * not enabled/allowed.
     *
     * 	@param logicalID Logical id to use for this anonymous user
     * @throws IllegalStateException
     *             if anonymous login is not allowed.
     */
    public void loginAnonymous(String logicalID) throws IllegalStateException {
        if (allowAnonymous()) {
            doAnonymousAuthentication(logicalID);
        } else {
            throw new IllegalArgumentException("Anonymous login not allowed.");
        }
    }

    /**
     * Check whether the anonymous authentication is allowed or not. If
     * anonymous authentication key and UserAttribute are specified, then this
     * method will return true, false otherwise.
     *
     * @return true if anonymous authentication is allowed, false otherwise.
     */
    private boolean allowAnonymous() {
        return anonymousKey != null && anonymousUserAttribute != null;
    }

    /**
     * Perform the actual task of authentication. On successful authentication,
     * sets the authentication object in a context, as required by Acegi
     * Security, On failed authentication, it will clear the authentication
     * object in the context, as required by Acegi Security.
     *
     * @param userCredentails
     * @return
     */
    private AuthenticationToken doAuthentication(HttpServletRequest request,UserCredentials userCredentails) throws LoginException
    {
        AuthenticationToken authToken = new AuthenticationToken(userCredentails.getUserID(),userCredentails.getPassword());
        try {
            AuthenticationToken result = (AuthenticationToken) this
                    .getAuthenticationManager().authenticate(authToken);
            onSuccessfulAuthentication(request, result);
            return result;
        } catch (AuthenticationException e) {
            String errMsg = "Error while authenticating user "; // + userCredentails.getUserID();
            if (log.isErrorEnabled()) {
                String exClassName = e.getCause().getClass().getName();
                String aEx = LoginException.class.getName();
                if (aEx.equals(exClassName)) {
                    log.error(errMsg, e);
                }
            } else if (log.isInfoEnabled()) {
                log.info(errMsg, e);
            }
            onUnsuccessfulAuthentication(request, e);
            if (e.getCause() instanceof LoginException) {
                throw (LoginException)e.getCause();
            }
            else {
                throw new LoginException(e.toString());
            }
        }
    }

    /**
     * Perform post authentication task in case of authentication failure.
     * Subclasses can override this, but must make a call to this method using
     * super to perform tasks required by Ace
     *
     * @param request
     * @param ae
     */
    protected void onUnsuccessfulAuthentication(HttpServletRequest request,
            AuthenticationException ae) {
        setAcegiSecureContext(null);
    }

    /**
     * Perform post authentication task. Subclasses can override this, but must
     * make a call to this method using super to perform tasks required by Ace
     *
     * @param request
     * @param result
     */
    protected void onSuccessfulAuthentication(HttpServletRequest request,
            AuthenticationToken result) {
        //set the request details.
        if (request != null) {
            setDetails(request, result);
        }
        setAcegiSecureContext(result);
    }

    /**
     * Performs anonymous authentication
     */
    private Authentication doAnonymousAuthentication(String logicalID) {
        AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(
                anonymousKey, anonymousUserAttribute.getPassword(),
                anonymousUserAttribute.getAuthorities());
        Authentication result = authenticationManager.authenticate(token);
        // make sure there is a valid UserPrincipal set on the result
        UserPrincipalImpl anonPrin = new UserPrincipalImpl((String) result.getName(), (String) result.getCredentials());
        anonPrin.setAnonymous(true);
        anonPrin.setLogicalName(logicalID);
        GrantedAuthority[] authorities = result.getAuthorities();
        for (int a = 0; a < authorities.length; a++) {
            GrantedAuthority authority = authorities[a];
            anonPrin.addUserRole(new RolePrincipalImpl(authority.getAuthority()));
        }
        result = new AnonymousAuthenticationToken(anonymousKey, anonPrin,
        		result.getAuthorities());

        setAcegiSecureContext(result);
        return result;
    }

    /**
     * Logout user by removing the Authentication information from Context.
     */
    public void logout() {
        //set the authentication Object to null.
    	org.acegisecurity.context.SecurityContext ctxt = SecurityContextHolder.getContext();
		if (ctxt != null && ctxt instanceof org.acegisecurity.context.SecurityContext) {
            ((org.acegisecurity.context.SecurityContext) ctxt).setAuthentication(null);
    	}
    }

    /**
     * Logout user by remove the Authentication information from Context as well
     * as remove the Acegi specific key from the Session.
     *
     * @param request
     */
    public void logout(HttpServletRequest request) {
        //set the authentication Object to null.
        logout();
        HttpSession httpSession = request.getSession(false);
        if (httpSession != null) {
            httpSession
                    .removeAttribute(HttpSessionContextIntegrationFilter.ACEGI_SECURITY_CONTEXT_KEY);
        }
    }

    /**
     * Set the Secure Context.
     *
     * @param auth
     */
    private void setAcegiSecureContext(Authentication auth) {
        if (SecurityContextHolder.getContext() == null) {
        	try {
        		SecurityContextHolder.setContext(generateNewContext());
        	} catch(Exception e) {
        		RuntimeException re = new IllegalStateException("Unable to generateNewContext");
        		re.initCause(e);
        		throw re;
        	}
        }
        org.acegisecurity.context.SecurityContext sc = (org.acegisecurity.context.SecurityContext) SecurityContextHolder.getContext();
        sc.setAuthentication(auth);
        SecurityContextHolder.setContext(sc);
    }

    /**
     * Provided so that subclasses may configure what is put into the
     * authentication request's details property. The default implementation
     * simply constructs {@link WebAuthenticationDetails}.
     *
     * @param request
     *            that an authentication request is being created for
     * @param authRequest
     *            the authentication request object that should have its details
     *            set
     */
    protected void setDetails(HttpServletRequest request,
            AuthenticationToken authRequest) {
        authRequest.setDetails(new WebAuthenticationDetails(request));
    }

    private org.acegisecurity.context.SecurityContext generateNewContext() throws Exception {
        return (org.acegisecurity.context.SecurityContext) this.context.newInstance();
    }
}