package test;

import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import org.apache.commons.beanutils.BeanUtils;
import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.sun.identity.authentication.spi.AuthLoginException;
import com.sun.identity.idm.IdOperation;
import com.sun.identity.idm.IdRepo;
import com.sun.identity.idm.IdRepoBundle;
import com.sun.identity.idm.IdRepoException;
import com.sun.identity.idm.IdRepoFatalException;
import com.sun.identity.idm.IdRepoListener;
import com.sun.identity.idm.IdRepoUnsupportedOpException;
import com.sun.identity.idm.IdType;
import com.sun.identity.idm.RepoSearchResults;
import com.sun.identity.sm.SchemaType;


public class JDBCIdRepo extends IdRepo {
	
	IdDAO dao = null;

	private static final PropertyNameRemapper PROP_REMAPPER = new PropertyNameRemapper();
	static {
		PROP_REMAPPER.setOpenSSOtoJDBC("sn", "lastName");
		PROP_REMAPPER.setOpenSSOtoJDBC("uid", "emailAddress");
		PROP_REMAPPER.setOpenSSOtoJDBC("userpassword", "password");
		PROP_REMAPPER.setOpenSSOtoJDBC("givenname", "firstName");
	}

	private static final Set SUPPORTED_TYPES;
	static {
		SUPPORTED_TYPES = new HashSet();
		SUPPORTED_TYPES.add(IdType.USER);
	}

	private static final Set SUPPORTED_OPS;
	static {
		SUPPORTED_OPS = new HashSet();
		// TODO: DELETE?  &  SERVICE?
		SUPPORTED_OPS.add(IdOperation.READ);
		SUPPORTED_OPS.add(IdOperation.CREATE);
		SUPPORTED_OPS.add(IdOperation.EDIT);
	}


	// -- capability declarations ----


	public Set getSupportedOperations(IdType type) {
		return SUPPORTED_OPS;
	}

	public Set getSupportedTypes() {
		return SUPPORTED_TYPES;
	}

	public boolean supportsAuthentication() {
		return true;
	}


	// -- plugin lifecycle ----


	public void initialize(Map configParams) {
		super.initialize(configParams);
		dao = new IdDAO();
		try {
			dao.init();
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	public void shutdown() {
		super.shutdown();
	}

	public int addListener(SSOToken token, IdRepoListener listener)
		throws IdRepoException, SSOException
	{
		// TODO: no idea what this needs to do
		return 0;
	}

	public void removeListener() {
		// TODO: no idea what this needs to do
	}


	// -- READ identity operations ----


	public boolean isExists(SSOToken token, IdType type, String name)
		throws IdRepoException, SSOException
	{
System.out.println("isExists("+token+", "+type+", "+name+")");
		assertUserType(type);
		try {
			boolean result = dao.loadUser(name) != null;
System.out.println("    isExists() => "+result);
			return result;
		} catch (SQLException e) {
			throw new IdRepoFatalException(e.toString());
		}
	}
	
	public boolean isActive(SSOToken token, IdType type, String name)
		throws IdRepoException, SSOException
	{
		// We could load the entry from the database to check a status
		// flag, but our simple schema doesn't define one; all user
		// accounts are always enabled. 
System.out.println("isActive("+token+", "+type+", "+name+") => true");
//		assertUserType(type);
		return true;
	}

	public Map getAttributes(SSOToken token, IdType type, String name)
			throws IdRepoException, SSOException
	{
System.out.println("getAttributes("+token+", "+type+", "+name+")");
		assertUserType(type);
		User user;
		try {
			user = dao.loadUser(name);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
System.out.println("    getAttributes() => "+toAttrs(user));
		return toAttrs(user);
	}

	public Map getAttributes(SSOToken token, IdType type, String name, Set attrNames)
		throws IdRepoException, SSOException
	{
		// TODO: should filter the results to reflect requested attrs,
		// but for the moment, lets just return then all,
		return getAttributes(token, type, name);
	}

	public Map getBinaryAttributes(SSOToken token, IdType type, String name, Set attrNames)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		// we don't have any binary attributes,
		return Collections.EMPTY_MAP;
	}

	public RepoSearchResults search(SSOToken token, IdType searchType,
			String pattern, int maxTime, int maxResults,
			Set returnAttrs, boolean returnAllAttrs, int filterOp,
			Map avPairs, boolean recursive)
		throws IdRepoException, SSOException
	{
System.out.println("search("+token+", "+searchType+", "+pattern+", "+maxResults+", "+maxResults+", "+returnAttrs+", "+returnAllAttrs+", "+filterOp+", "+avPairs+", "+recursive+")");
//		assertUserType(searchType);
		if ("*".equals(pattern)) {
			// TODO: other 'glob'-like matches allowed?
			pattern = null;
		}
		List users;
		Map queryParams = avPairs==null ? null : remap(avPairs);
		try {
			users = dao.search(pattern, maxResults, queryParams, filterOp);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
System.out.println("    search() => "+users);
		Set searchResults = toDNSet(users);
		int errorCode = RepoSearchResults.SUCCESS;
		Map resultsMap = toEntryAttributeSetMap(users);
		return new RepoSearchResults(searchResults,
		                             errorCode,
		                             resultsMap,
		                             searchType);
	}


	// -- CREATE identity operations ----


	public String create(SSOToken token, IdType type, String name,
	                     Map attrMap)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		User newUser = userFromAttributes(name, attrMap);
		try {
			dao.insertUser(newUser);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		// TODO: "string representation of created value" wha?
		return "["+name+"]";
	}


	// -- EDIT identity operations ----

	public void setAttributes(SSOToken token, IdType type, String name, Map attributes, boolean isAdd)
		throws IdRepoException, SSOException
	{
System.err.println("setAttributes(isAdd="+isAdd+")");
		// TODO: not sure what isAdd indicates
		assertUserType(type);
		try {
			// load user and copy changed properties
			User user = dao.loadUser(name);
			Map props = attrMapToPropMap(attributes);
			copyProperties(user, props);
			dao.updateUser(user);
		} catch (SQLException e) {
			e.printStackTrace();
			throw new IdRepoException(e.toString());
		}
	}

	public void setBinaryAttributes(SSOToken token, IdType type, String name, Map attributes, boolean isAdd)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		// don't support any binary attributes, so nothing to do
	}

	public void removeAttributes(SSOToken token, IdType type, String name, Set attrNames)
		throws IdRepoException, SSOException
	{
		assertUserType(type);
		// at the moment, there are no attributes that can resonably be
		// removed.  We could in theory set the columns to 'null' in
		// the database, except we've deliberately defined the columns
		// to dissallow this.
	}


	// -- DELETE operations ----


	public void delete(SSOToken token, IdType type, String name)
		throws IdRepoException, SSOException
	{
		throw new IdRepoUnsupportedOpException("DEETE not supported");
	}


	// -- authentication operations ----


	public boolean authenticate(Callback[] credentials) throws IdRepoException, AuthLoginException {
		// Obtain user name and password from credentials and authenticate
		String username = null;
		String password = null;
		for (int i = 0; i < credentials.length; i++) {
			Callback cred = credentials[i];
			if (cred instanceof NameCallback) {
				username = ((NameCallback)cred).getName();
			} else if (cred instanceof PasswordCallback) {
				char[] passwd = ((PasswordCallback)cred).getPassword();
				if (passwd != null) {
					password = new String(passwd);
				}
			}
		}
		if (username == null || password == null) {
			Object args[] = { getClass().getName() };
			throw new IdRepoException(IdRepoBundle.BUNDLE_NAME, "221", args);
		}
		User user;
		try {
			user = dao.loadUser(username);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		return user != null && password.equals(user.getPassword());
	}


	// -- membership operations ----


	public void modifyMemberShip(SSOToken token, IdType type, String name, Set members, IdType membersType, int operation)
		throws IdRepoException, SSOException
	{
		throw new IdRepoUnsupportedOpException("only USER type supported; users can't have members");
	}

	public Set getMembers(SSOToken token, IdType type, String name, IdType membersType)
		throws IdRepoException, SSOException
	{
		throw new IdRepoUnsupportedOpException("only USER type supported; users can't have members");
	}

	public Set getMemberships(SSOToken token, IdType type, String name, IdType membershipType)
		throws IdRepoException, SSOException
	{
		// our users are never members of groups, etc.
		return Collections.EMPTY_SET;
	}


	// -- SERVICE operations ----


	public void assignService(SSOToken token, IdType type, String name, String serviceName, SchemaType stype, Map attrMap)
		throws IdRepoException, SSOException
	{
		throw new IdRepoUnsupportedOpException("SERVICE operations not supported");
	}

	public Set getAssignedServices(SSOToken token, IdType type, String name, Map mapOfServicesAndOCs)
		throws IdRepoException, SSOException
	{
		throw new IdRepoUnsupportedOpException("SERVICE operations not supported");
	}

	public void unassignService(SSOToken token, IdType type, String name, String serviceName, Map attrMap)
		throws IdRepoException, SSOException
	{
		throw new IdRepoUnsupportedOpException("SERVICE operations not supported");
	}

	public Map getServiceAttributes(SSOToken token, IdType type, String name, String serviceName, Set attrNames)
		throws IdRepoException, SSOException
	{
		throw new IdRepoUnsupportedOpException("SERVICE operations not supported");
	}

	public void modifyService(SSOToken token, IdType type, String name, String serviceName, SchemaType sType, Map attrMap)
		throws IdRepoException, SSOException
	{
		throw new IdRepoUnsupportedOpException("SERVICE operations not supported");
	}


	// -- private helper methods ----


	private Map attrMapToPropMap(Map attrMap) {
		// flatten the set of values from the given Map, which are
		// Sets, so that the values in the result Map are the first
		// (usually only) entry from each Set,
		Map result = new HashMap();
		Iterator i = attrMap.entrySet().iterator();
		while (i.hasNext()) {
			Map.Entry attr = (Map.Entry)i.next();
			String key = (String)attr.getKey();
			key = PROP_REMAPPER.getDbPropForSSOProp(key);
			Object val = ((Set)attr.getValue()).toArray()[0];
			result.put(key, val);
		}
		return result;
	}

	private Map remap(Map attrMap) {
		Map result = new HashMap();
		Iterator i = attrMap.entrySet().iterator();
		while (i.hasNext()) {
			Map.Entry attr = (Map.Entry)i.next();
			String key = (String)attr.getKey();
			key = PROP_REMAPPER.getDbPropForSSOProp(key);
			result.put(key, attr.getValue());
		}
		return result;
	}

	private Set toDNSet(List users) {
		Set result = new HashSet();
		for (Iterator i=users.iterator(); i.hasNext(); ) {
			User user = (User)i.next();
			result.add(user.getEmailAddress());
		}
		return result;
	}

	private Map toEntryAttributeSetMap(List users) {
		Map result = new HashMap();
		for (Iterator i=users.iterator(); i.hasNext(); ) {
			User user = (User)i.next();
			String email = user.getEmailAddress();
			result.put(email, toAttrs(user));
		}
		return result;
	}

	private User userFromAttributes(String name, Map attrMap) {
		User newUser = new User();
		Map props = attrMapToPropMap(attrMap);
System.err.println("userFromAttributes("+name+", "+attrMap+") : props="+props);
		copyProperties(newUser, props);
		newUser.setEmailAddress(name);
		return newUser;
	}

	private void copyProperties(User destUser, Map sourceProps) {
		try {
			BeanUtils.copyProperties(destUser, sourceProps);
		} catch (IllegalAccessException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		} catch (InvocationTargetException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	private Map describe(User user) {
		try {
			return BeanUtils.describe(user);
		} catch (IllegalAccessException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		} catch (InvocationTargetException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	private Map toAttrs(User user) {
		Map attrs = new HashMap();
		Map props = describe(user);
		for (Iterator i=props.entrySet().iterator(); i.hasNext(); ) {
			Map.Entry prop = (Map.Entry)i.next();
			String attrName = PROP_REMAPPER.getSSOPropForDBProp((String)prop.getKey());
			attrs.put(attrName,
			          Collections.singleton(prop.getValue()));
		}
//		attrs.put(PROP_REMAPPER.getSSOPropForDBProp("firstName"),
//		          Collections.singleton(user.getFirstName()));
//		attrs.put(PROP_REMAPPER.getSSOPropForDBProp("lastName"),
//		          Collections.singleton(user.getLastName()));
//		attrs.put(PROP_REMAPPER.getSSOPropForDBProp("id"),
//		          Collections.singleton(String.valueOf(user.getId())));
//		attrs.put(PROP_REMAPPER.getSSOPropForDBProp("password"),
//		          Collections.singleton(user.getPassword()));
		// falsify an 'active' status, or login will not be possible,
		attrs.put("inetuserstatus", Collections.singleton("Active"));
		return attrs;
	}

	private void assertUserType(IdType type) throws IdRepoUnsupportedOpException {
		if (!IdType.USER.equals(type)) {
			throw new IdRepoUnsupportedOpException("only type USER is supported");
		}
	}
}
