/**
 * 
 */
package com.digitalsanctuary.jforum;

import java.rmi.RemoteException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.http.Cookie;

import net.jforum.ControllerUtils;
import net.jforum.JForumExecutionContext;
import net.jforum.context.RequestContext;
import net.jforum.context.SessionContext;
import net.jforum.dao.DataAccessDriver;
import net.jforum.dao.GroupDAO;
import net.jforum.dao.UserDAO;
import net.jforum.entities.Group;
import net.jforum.entities.User;
import net.jforum.entities.UserSession;
import net.jforum.sso.SSO;
import net.jforum.sso.SSOUtils;
import net.jforum.util.preferences.ConfigKeys;
import net.jforum.util.preferences.SystemGlobals;

import com.atlassian.crowd.integration.exception.InvalidAuthorizationTokenException;
import com.atlassian.crowd.integration.exception.InvalidTokenException;
import com.atlassian.crowd.integration.exception.ObjectNotFoundException;
import com.atlassian.crowd.integration.service.soap.client.SecurityServerClientFactory;
import com.atlassian.crowd.integration.soap.SOAPAttribute;
import com.atlassian.crowd.integration.soap.SOAPPrincipal;

/**
 * The CrowdSSO class implements the JForum SSO interface and hooks into Crowd to enable single sign-on and
 * auto-registration.
 * 
 * @author Devon Hillard
 */
public class CrowdSSO implements SSO {

    /**
     * 
     */
    private static final String SSO_CROWD_SYNC_GROUPS_FLAG = "sso.crowd.syncGroups";

    /** The Constant CROWD_TOKEN_ATTR. */
    private static final String CROWD_TOKEN_ATTR = "CrowdToken";

    /** The Constant CROWD_TOKEN_KEY_COOKIE_NAME. */
    private static final String CROWD_TOKEN_KEY_COOKIE_NAME = "crowd.token_key";

    /** The Constant CROWD_SOAP_ATTR_EMAIL. */
    private static final String CROWD_SOAP_ATTR_EMAIL = "mail";

    /** The logger. */
    static final Logger log = Logger.getLogger(CrowdSSO.class.getName());

    /**
     * Authenticate user.
     * 
     * @param pRequestContext
     *                the request context
     * 
     * @return the string
     * 
     * @see net.jforum.sso.SSO#authenticateUser(net.jforum.context.RequestContext)
     */
    public String authenticateUser(final RequestContext pRequestContext) {
	String jforumUsername = null;
	log.log(Level.INFO, "Entering authenticateUser method");
	User user = null;
	try {
	    // Lookup the user from Crowd based on the token key in the cookie.
	    SOAPPrincipal principal = SecurityServerClientFactory.getSecurityServerClient().findPrincipalByToken(
		    getCrowdTokenKey(pRequestContext));
	    final SOAPAttribute[] attributes = principal.getAttributes();
	    String crowdUsername = principal.getName();
	    log.log(Level.INFO, "principal username: " + crowdUsername);
	    SSOUtils ssoUtils = new SSOUtils();
	    if (!ssoUtils.userExists(crowdUsername)) {
		log.log(Level.INFO, "user wasn't found in jforum. Creating.");
		String email = getAttribute(CROWD_SOAP_ATTR_EMAIL, attributes);
		ssoUtils.register(crowdUsername, email);
	    }
	    user = ssoUtils.getUser();

	    // If we managed to load up a user, cache the crowd token in a cookie to check the session later.
	    if (user != null) {
		jforumUsername = user.getUsername();
		SessionContext session = JForumExecutionContext.getRequest().getSessionContext();
		session.setAttribute(CROWD_TOKEN_ATTR, getCrowdTokenKey(pRequestContext));
		if (SystemGlobals.getBoolValue(SSO_CROWD_SYNC_GROUPS_FLAG)) {
		    syncGroups(user.getId());
		}
	    }
	} catch (RemoteException e) {
	    log.log(Level.WARNING, e.getMessage());
	} catch (InvalidAuthorizationTokenException e) {
	    log.log(Level.WARNING, e.getMessage());
	} catch (InvalidTokenException e) {
	    log.log(Level.WARNING, e.getMessage());
	}
	log.log(Level.INFO, "Returning username: " + jforumUsername);
	return jforumUsername;
    }

    /**
     * Checks if is session valid.
     * 
     * @param pUserSession
     *                the user session
     * @param pRequestContext
     *                the request context
     * 
     * @return true, if checks if is session valid
     * 
     * @see net.jforum.sso.SSO#isSessionValid(net.jforum.entities.UserSession, net.jforum.context.RequestContext)
     */
    public boolean isSessionValid(final UserSession pUserSession, final RequestContext pRequestContext) {
	log.log(Level.INFO, "isSessionValid entered.");
	boolean validSession = false;
	SessionContext session = JForumExecutionContext.getRequest().getSessionContext();

	// if the current user is anonymous, blow out a potential old session token
	if (pUserSession.getUserId() == SystemGlobals.getIntValue(ConfigKeys.ANONYMOUS_USER_ID)) {
	    session.setAttribute(CROWD_TOKEN_ATTR, null);
	}

	String crowdSessionToken = (String) session.getAttribute(CROWD_TOKEN_ATTR);
	String crowdToken = getCrowdTokenKey(pRequestContext);
	log.log(Level.INFO, "isSessionValid: crowdSessionToken: " + crowdSessionToken);
	log.log(Level.INFO, "isSessionValid: crowdToken: " + crowdToken);

	// If we've already loaded in a crowd token, check to see if it's the same as the current one
	if (crowdSessionToken != null && crowdSessionToken.length() > 0) {
	    if (crowdSessionToken.equals(crowdToken)) {
		validSession = true;
	    }
	}
	log.log(Level.INFO, "isSessionValid: returning: " + validSession);
	return validSession;
    }

    private void syncGroups(final int pUserId) {
	log.log(Level.INFO, "Entering syncGroups method");
	UserDAO userDAO = DataAccessDriver.getInstance().newUserDAO();
	User user = userDAO.selectById(pUserId);
	// Get the current groups from Crowd
	String[] crowdGroups = getCrowdGroups(user.getUsername());
	// Get the current groups from JForum
	List<Group> forumGroups = user.getGroupsList();
	// Loop through all the crowd groups
	for (String crowdGroup : crowdGroups) {
	    boolean crowdGroupInForumGroups = false;
	    // Check if the crowd group is already in the JForum list
	    for (Group group : forumGroups) {
		if (group.getName().equals(crowdGroup)) {
		    crowdGroupInForumGroups = true;
		}
	    }
	    // if the crowd group isn't in JForum, add it
	    if (!crowdGroupInForumGroups) {
		GroupDAO groupDAO = DataAccessDriver.getInstance().newGroupDAO();
		// See if the group exists in JForum
		Group newGroup = getGroupByName(crowdGroup);
		// If not, create it
		if (newGroup == null) {
		    newGroup = new Group();
		    newGroup.setName(crowdGroup);
		    newGroup.setDescription("Crowd Group " + crowdGroup);
		    newGroup.setParentId(0);
		    groupDAO.addNew(newGroup);
		}
		Group loadedGroup = getGroupByName(newGroup.getName());
		userDAO.addToGroup(user.getId(), new int[] { loadedGroup.getId() });
		forumGroups.add(newGroup);
	    }
	}

	// Loop through all the JForum groups
	for (Group jforumGroup : forumGroups) {
	    boolean jForumGroupInCrowdGroups = false;
	    // Check if the JForum group is still live in the Crowd list
	    for (String crowdGroup : crowdGroups) {
		if (crowdGroup.equals(jforumGroup.getName())) {
		    jForumGroupInCrowdGroups = true;
		}
	    }
	    // If the JForum group isn't in Crowd, remove it from the user's list
	    if (!jForumGroupInCrowdGroups) {
		userDAO.removeFromGroup(user.getId(), new int[] { jforumGroup.getId() });
	    }
	}
	userDAO.update(user);
    }

    private String[] getCrowdGroups(final String pCrowdUsername) {
	String[] groups = null;
	try {
	    groups = SecurityServerClientFactory.getSecurityServerClient().findGroupMemberships(pCrowdUsername);
	} catch (RemoteException e) {
	    log.log(Level.WARNING, e.getMessage());
	} catch (InvalidAuthorizationTokenException e) {
	    log.log(Level.WARNING, e.getMessage());
	} catch (ObjectNotFoundException e) {
	    log.log(Level.WARNING, e.getMessage());
	}
	return groups;
    }

    private Group getGroupByName(final String pGroupName) {
	List<Group> groups = DataAccessDriver.getInstance().newGroupDAO().selectAll();
	for (Group group : groups) {
	    if (group.getName() != null && group.getName().equals(pGroupName)) {
		return group;
	    }
	}
	return null;
    }

    /**
     * Gets the crowd token key from the cookie.
     * 
     * @param pRequestContext
     *                the request context.
     * 
     * @return the crowd token key.
     */
    private String getCrowdTokenKey(final RequestContext pRequestContext) {
	Cookie crowdTokenCookie = ControllerUtils.getCookie(CROWD_TOKEN_KEY_COOKIE_NAME);
	if (crowdTokenCookie == null) {
	    log.log(Level.WARNING, "crowdTokenCookie is null.");
	}
	String crowdTokenKey = null;
	if (crowdTokenCookie != null) {
	    crowdTokenKey = crowdTokenCookie.getValue();
	    log.log(Level.WARNING, "crowdTokenKey: " + crowdTokenKey);
	}
	return crowdTokenKey;
    }

    /**
     * Look up the value of the given attribute name within the SOAP attributes provided from Crowd.
     * 
     * @param pAttributeName
     *                The attribute to lookup
     * @param pAttributesArray
     *                SOAPAttributes provided by crowd for a user.
     * 
     * @return String value of the attribute
     */
    private String getAttribute(final String pAttributeName, final SOAPAttribute[] pAttributesArray) {
	for (int i = 0; i < pAttributesArray.length; i++) {
	    final SOAPAttribute attribute = pAttributesArray[i];
	    final String name = attribute.getName();
	    if (name != null && pAttributeName.equals(name)) {
		final String[] values = attribute.getValues();
		if (values != null) {
		    return values[0];
		}
	    }
	}
	return null;
    }
}
