package test;

import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.Arrays;
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", "id");
		PROP_REMAPPER.setOpenSSOtoJDBC("userpassword", "password");
		PROP_REMAPPER.setOpenSSOtoJDBC("givenname", "firstName");
		PROP_REMAPPER.setOpenSSOtoJDBC("mail", "emailAddress");
	}

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

	private static final Map SUPPORTED_OPS;
	static {
		SUPPORTED_OPS = new HashMap();
		Set userOps= new HashSet();
		userOps.add(IdOperation.READ);
		userOps.add(IdOperation.CREATE);
		userOps.add(IdOperation.EDIT);
		userOps.add(IdOperation.SERVICE);
		userOps.add(IdOperation.DELETE);
		SUPPORTED_OPS.put(IdType.USER, userOps);
	}


	// -- capability declarations ----


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

	public Set getSupportedTypes() {
		return SUPPORTED_TYPES;
	}

	public boolean supportsAuthentication() {
		return true;
	}


	// -- plugin lifecycle ----


	public void initialize(Map configParams) {
		super.initialize(configParams);
		dao = IdDAO.getInstance();
	}

	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.loadUserById(Long.parseLong(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
	{
System.out.println("isActive("+token+", "+type+", "+name+") => true");
		return true;
	}

	public Map getAttributes(SSOToken token, IdType type, String name)
			throws IdRepoException, SSOException
	{
		return getAttributes(token, type, name, null);
	}

	public Map getAttributes(SSOToken token, IdType type, String name, Set attrNames)
		throws IdRepoException, SSOException
	{
System.out.println("getAttributes("+token+", "+type+", "+name+", "+attrNames+")");
		assertUserType(type);
		User user;
		try {
			user = dao.loadUserById(Long.parseLong(name));
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		Map attrs = toAttrs(user);
		if (attrNames != null) {
			// restrict to just requested attributes of the user,
			filter(attrs, attrNames);

			if (containsAddressProperties(attrNames)) {
				addAddressProperties(user, attrs, attrNames);
			}
		}
System.out.println("    getAttributes() => "+attrs);
		return attrs;
	}

	private boolean containsAddressProperties(Set attrNames) {
		for (Iterator i=attrNames.iterator(); i.hasNext(); ) {
			String name = (String)i.next();
			if (name.startsWith("contactAddress")) {
				return true;
			}
		}
		return false;
	}

	private void addAddressProperties(User user, Map attrs, Set attrNames) throws IdRepoException {
		Address address;
		try {
			address = dao.loadAddress(user);
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
		Map addrProps = describe(address);
		filter(addrProps, attrNames);
		attrs.putAll(toAttrs(address));
	}

	/**
	 * Removes entries from the given map who's keys aren't present in the
	 * given Set.
	 */
	private void filter(Map map, Set allowedKeySet) {
		for (Iterator i=map.keySet().iterator(); i.hasNext(); ) {
			if (!allowedKeySet.contains(i.next())) {
				i.remove();
			}
		}
	}

	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 : remapSSOPropsToDBProps(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);
		Map resultsMap = toEntryAttributeSetMap(users);
		return new RepoSearchResults(searchResults,
		                             RepoSearchResults.SUCCESS,
		                             resultsMap,
		                             searchType);
	}


	// -- CREATE identity operations ----


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

	private Address addressFromAttributes(Map attrMap) {
		Map props = remapSSOPropsToDBProps(attrMap);
		props = prefixedProperties("contactAddress.", props);
		Address address = new Address();
		copyProperties(address, props);
		return null;
	}

	private Map prefixedProperties(String prefix, Map props) {
		Iterator i = props.entrySet().iterator();
		Map result = new HashMap();
		while (i.hasNext()) {
			Map.Entry entry = (Map.Entry)i.next();
			String key = (String)entry.getKey();
			if (key.startsWith(prefix)) {
				key = key.substring(prefix.length());
				result.put(key, entry.getValue());
			}
		}
		return result;
	}


	// -- EDIT identity operations ----

	public void setAttributes(SSOToken token, IdType type, String name, Map attributes, boolean isAdd)
		throws IdRepoException, SSOException
	{
System.err.println("setAttributes("+token+", "+type+", "+name+", "+attributes+", "+isAdd+")");
		// TODO: not sure what isAdd indicates
		assertUserType(type);
		try {
			// load user and copy changed properties
			User user = dao.loadUserById(Long.parseLong(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
	{
System.out.println("delete("+token+", "+type+", "+name+")");
		try {
			dao.deleteUserById(Long.parseLong(name));
		} catch (SQLException e) {
			throw new IdRepoException(e.toString());
		}
	}


	// -- authentication operations ----


	public boolean authenticate(Callback[] credentials) throws IdRepoException, AuthLoginException {
System.out.println("authenticate("+Arrays.asList(credentials)+")");
		// 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.loadUserById(Long.parseLong(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
	{
System.out.println("unassignService("+token+", "+type+", "+name+", "+serviceName+", "+stype+", "+attrMap+")");
		throw new IdRepoUnsupportedOpException("SERVICE operations not supported");
	}

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

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

	public Map getServiceAttributes(SSOToken token, IdType type, String name, String serviceName, Set attrNames)
		throws IdRepoException, SSOException
	{
System.out.println("getServiceAttributes("+token+", "+type+", "+name+", "+serviceName+", "+attrNames+")");
//		throw new IdRepoUnsupportedOpException("SERVICE operations not supported");
		return Collections.EMPTY_MAP;
	}

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


	// -- private helper methods ----


	private Map attrMapToPropMap(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);
			Object val = ((Set)attr.getValue()).toArray()[0];
			result.put(key, val);
		}
		return result;
	}

	private Map remapSSOPropsToDBProps(Map props) {
		Map result = new HashMap();
		Iterator i = props.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 Map remapDBPropsToSSOProps(Map props) {
		Map result = new HashMap();
		Iterator i = props.entrySet().iterator();
		while (i.hasNext()) {
			Map.Entry attr = (Map.Entry)i.next();
			String key = (String)attr.getKey();
			key = PROP_REMAPPER.getSSOPropForDBProp(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(principalNameFor(user));
		}
		return result;
	}

	private String principalNameFor(User user) {
		return String.valueOf(user.getId());
	}

	private Map toEntryAttributeSetMap(List users) {
		Map result = new HashMap();
		for (Iterator i=users.iterator(); i.hasNext(); ) {
			User user = (User)i.next();
			String name = principalNameFor(user);
			result.put(name, 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);
		assignPrincipalName(newUser, name);
		return newUser;
	}

	private void assignPrincipalName(User newUser, String name) {
		// TODO actually add a UUID property
	}

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

	private Map describe(Object obj) {
		try {
			Map result = BeanUtils.describe(obj);
			result.remove("class");  // because everything has getClass()
			return result;
		} 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 props = describe(user);
		Map attrs = toAttrs(props);
		// falsify an 'active' status, or login will not be possible,
		attrs.put("inetuserstatus", Collections.singleton("Active"));
		return remapDBPropsToSSOProps(attrs);
	}

	private Map toAttrs(Map props) {
		Map attrs = new HashMap();
		for (Iterator i=props.entrySet().iterator(); i.hasNext(); ) {
			Map.Entry prop = (Map.Entry)i.next();
			String attrName = (String)prop.getKey();
			Set attrValue = Collections.singleton(prop.getValue());
			attrs.put(attrName, attrValue);
		}
		return attrs;
	}

	private Map toAttrs(Address address) {
		Map props = describe(address);
		props = prefixAllKeysWith(props, "contactAddress.");
		Map attrs = toAttrs(props);
		return remapDBPropsToSSOProps(attrs);
	}

	public Map prefixAllKeysWith(Map map, String prefix) {
		Map result = new HashMap();
		for (Iterator i=map.entrySet().iterator(); i.hasNext(); ) {
			Map.Entry prop = (Map.Entry)i.next();
			String attrName = prefix+prop.getKey();
			result.put(attrName, prop.getValue());
		}
		return result;
	}


	private void assertUserType(IdType type) throws IdRepoUnsupportedOpException {
		if (!IdType.USER.equals(type)) {
			throw new IdRepoUnsupportedOpException("only type USER is supported");
//			System.err.println("only type USER should be supported");
		}
	}
}