/**
 * 
 */
package com.dolby.pics.jboss.security;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.acl.Group;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;

import org.jboss.logging.Logger;
import org.jboss.security.auth.spi.LdapExtLoginModule;

/**
 * Superclass providing Multiple DC failover (only happens at initialisation).
 * 
 * TODO cache DC configuration, do ping check prior to login test, reset DC used
 * @author axb
 *
 */
public class DCFailoverLDAPLoginModule extends LdapExtLoginModule
{
	private static final Logger LOG = Logger.getLogger(DCFailoverLDAPLoginModule.class);
	
	protected final String LDAP_PREFIX="ldap://";
	protected final int DEFAULT_LDAP_PORT=389;
	protected final char COLON=':'; 

	/** lookup user attributes */
	private GlobalUserLookup fUserLookup=new GlobalUserLookup();

	private Map<String, String> fSharedState;
	private Map fOptions;
	
	/* (non-Javadoc)
	 * @see javax.security.auth.spi.LoginModule#initialize(javax.security.auth.Subject, javax.security.auth.callback.CallbackHandler, java.util.Map, java.util.Map)
	 */
	@SuppressWarnings("unchecked")
	public void initialize(Subject arg0, CallbackHandler arg1, Map sharedState, Map options)
	{
		LOG.info("initialize() - "+arg0.toString());
		
		//TODO ensure LIVE failover works, this will just happen at initialization
		try
		{
			fSharedState=sharedState;
			fOptions=options;
			checkForPrimaryAuthenticator();
		}
		catch (Exception e)
		{
			LOG.error("Unable to processess additional LDAP servers: "+e.getLocalizedMessage());
		}
		super.initialize(arg0, arg1, sharedState, options);
	}

	/**
	 * This Method is here to handle the situation of the Primary Domain Controller going offline,
	 * in which case the configured secondary controller will be used.
	 * Java doesnt have a ping command so we try to connect to an the ldap port
	 */
	private void checkForPrimaryAuthenticator()
	{
		LOG.info("Checking Ldap connectivity.");
		
		String primary=(String)fOptions.get(javax.naming.Context.PROVIDER_URL);
		String backups=(String)fOptions.get(javax.naming.Context.PROVIDER_URL+".backups");		
		String [] backupArray=backups.split(",");		

		ArrayList<String> l = new ArrayList<String>();
		l.add(primary);
		for (int i = 0; i < backupArray.length; i++)
		{
			l.add(backupArray[i]);
		}
		
		String validServer=null;
		for (Iterator iter = l.iterator(); iter.hasNext();)
		{		
			String s=(String)iter.next();
			try
			{
				String server = getServer(s);
				
				//remove any prefix present to detect presence of host:port
				int port = getPort(s.substring(s.indexOf(server)));
				
				LOG.info("Processed host '"+s+"' into server="+server+", port="+port);
				
				Socket socket=new Socket(server,port);
				if (socket.isConnected())
				{
					LOG.info("LDAP Server "+server+" is UP.");
					socket.close();
					
					//now reconstitute the verbose server string
					String reconstituted=LDAP_PREFIX+server+COLON+port;
					LOG.info("Reconstituted server string from '"+s+"' to '"+reconstituted+"'");
					try
					{
						fSharedState.put(javax.naming.Context.PROVIDER_URL, reconstituted );
						//now update shared state
						validServer=s;
						
						break; //no need to process further
					}
					catch (Throwable t)
					{
						LOG.error("Unable to update properties with LDAP server : "+t.getLocalizedMessage());
						validServer=null;
					}
				}
				else
				{
					LOG.info("LDAP server "+server+" is DOWN, trying another.");
				}
			}
			catch (UnknownHostException e)
			{				
				LOG.warn("Unknown Host "+e.getLocalizedMessage()+"), unable to lookup LDAP server to test connectivity : "+s);
			}
			catch (IOException e)
			{
				LOG.warn("IO Problem ("+e.getLocalizedMessage()+"), unable to lookup LDAP server to test connectivity : "+s);
			}	
		}
		
		if (validServer==null)
		{
			LOG.error("NO LDAP Servers available for authentication, somethings really broken, no authentication will be possible.  Tried: "+primary+" and backups: "+backups);
		}		
	}

	/**
	 * Assumes that server string has has ldap:// removed, hence the presences of the : or lack of can be detected and handled
	 * @param s
	 * @return
	 */
	private int getPort(String s)
	{
		int port=DEFAULT_LDAP_PORT;		
		int portIndex=s.lastIndexOf(COLON);
		if (portIndex==-1)
		{
			s=s+COLON+DEFAULT_LDAP_PORT;
			LOG.warn("URL doesnt shave a port indicator: , assuming (and ammending) as port 389 "+s);
		}
		else
		{
			port = Integer.parseInt(s.substring(portIndex+1));
		}		
		
		return port;
	}
	
	/**
	 * Method to extract the exact server name without the protocol prefix or port suffix
	 * @param s
	 * @return
	 */
	private String getServer(String s)
	{
		s = s.toLowerCase();
		
		String server=null;		
		int ldapIndex=s.indexOf(LDAP_PREFIX);

		if (ldapIndex!=-1)
		{			
			server=s.substring(ldapIndex+7);
		}
		
		//remove port if present
		int portIdx=server.indexOf(COLON);
		if (portIdx!=-1)
		{
			server=server.substring(0,portIdx);
		}
		
		return server;
	}
	
	/** Helper method to sum two role groups
	 * 
	 * @param group1
	 * @param group2
	 * @return
	 */
	protected Group[] mergeRoleSets(Group[] group1, Group[] group2)
	{
		Group[] roles = new Group[(group1 != null ? group1.length : 0) + (group2 != null ? group2.length : 0)];
		int i = 0;
		if (group1 != null)
		{
			for (int j = 0; j < group1.length; i++, j++)
			{
				roles[i] = group1[j];
				LOG.info(getUsername() + " has group1 role: " + roles[i].getName());
			}
		}

		if (group2 != null)
		{
			for (int j = 0; j < group2.length; i++, j++)
			{
				roles[i] = group2[j];
				LOG.info(getUsername() + " has group2 role: " + roles[i].getName());
			}
		}

		return roles;
	}
	
	
	public GlobalUserLookup getGlobalLookup()
	{
		return fUserLookup;
	}
	
	
}
