package gov.va.vinci.security.providers.ad;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com4j.COM4J;
import com4j.Com4jObject;
import com4j.ComException;
import com4j.Variant;
import com4j.typelibs.activeDirectory.IADs;
import com4j.typelibs.activeDirectory.IADsGroup;
import com4j.typelibs.activeDirectory.IADsOpenDSObject;
import com4j.typelibs.activeDirectory.IADsUser;
import com4j.typelibs.ado20.Fields;
import com4j.typelibs.ado20._Command;
import com4j.typelibs.ado20._Connection;
import com4j.typelibs.ado20._Recordset;

import gov.va.vinci.security.AuthenticationException;
import gov.va.vinci.security.BadCredentialsException;
import gov.va.vinci.security.GrantedAuthority;
import gov.va.vinci.security.GrantedAuthorityImpl;
import gov.va.vinci.security.UsernameNotFoundException;
import gov.va.vinci.security.providers.UsernamePasswordAuthenticationToken;
import gov.va.vinci.security.userdetails.UserDetails;

public class ActiveDirectoryProvider {
	private _Connection con;
	private String defaultNamingContext;
	private String rootDomainNamingContext;
	private static Log log = LogFactory.getLog(ActiveDirectoryProvider.class);
	 
	public ActiveDirectoryProvider() { 
		// TODO: is there any memory leak by creating the rootDSE and the con object and leaving them lying around?
		 IADs rootDSE = COM4J.getObject(IADs.class, "LDAP://RootDSE", null);

		 // Active Directory domain is DC=vha,DC=med,DC=va,DC=gov
		 defaultNamingContext = (String)rootDSE.get("defaultNamingContext");
		 rootDomainNamingContext = (String)rootDSE.get("rootDomainNamingContext");
		 
		 log.debug("Active Directory domain is "+defaultNamingContext);
		 log.debug("Active Directory root naming context is "+rootDomainNamingContext);

	     con = com4j.typelibs.ado20.ClassFactory.createConnection();
	     con.provider("ADsDSOObject");
	     con.open("Active Directory Provider",""/*default*/,""/*default*/,-1/*default*/);
	}
	
	protected String getDnOfUserOrGroup(String userOrGroupname) throws UsernameNotFoundException {
		_Command cmd = com4j.typelibs.ado20.ClassFactory.createCommand();
        cmd.activeConnection(con); 

        cmd.commandText("<GC://"+rootDomainNamingContext+">;(sAMAccountName="+userOrGroupname+");distinguishedName;subTree");
        _Recordset rs = cmd.execute(null, Variant.getMissing(), -1/*default*/);
        if(rs.eof())
            throw new UsernameNotFoundException("No such user or group: "+userOrGroupname);

        return rs.fields().item("distinguishedName").value().toString();
	}
	
	/** Fetch details of user from Active Directory.  If authentication is not null, perform an authentication of user credentials as well.
	 * 
	 * @param username  User name to search for.
	 * @param authentication  Credentials for authentication.
	 * @return
	 * @throws AuthenticationException
	 */
	public UserDetails retrieveUser(final String username, final UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {

		UserPrincipalName upn = new UserPrincipalName(username);
		
		String password = Xull;
	   if(authentication != null) {
		   password = XXXXXXXX authentication.getCredentials();
	   }
	   
	   String dn = getDnOfUserOrGroup(upn.getUserName());
		
	   log.debug("user dn = " + dn);
	   
	   // TODO: we could also use the domain controller name from the upn if present
	   
	   String dc = parseUserDC(dn);
	   String ldapHost = getDefaultLdapHost(dc);
	   
	   if (ldapHost == null) {
		   ldapHost = "";
	   }
	   else {
		   ldapHost += "/";
	   }
	   
	   String ldapQuery = "LDAP://" + ldapHost + dn;
	   log.debug("LDAP query = " + ldapQuery);
	   
		  // now we got the DN of the user
        IADsOpenDSObject dso = COM4J.getObject(IADsOpenDSObject.class,"LDAP:",null);

        // turns out we don't need DN for authentication
        // we can bind with the user name
        // dso.openDSObject("LDAP://"+context,args[0],args[1],1);

        // to do bind with DN as the user name, the flag must be 0
        IADsUser usr;
        try {
            usr = (authentication==null
                ? dso.openDSObject(ldapQuery, null, null, 0)
                : dso.openDSObject(ldapQuery, dn, password, 0))
                    .queryInterface(IADsUser.class);
        } catch (ComException e) {
            throw new BadCredentialsException("Incorrect password for "+username);
        }
        
        log.debug("user credentials successfully validated.");
        
        if (usr == null) {   // the user name was in fact a group
            log.debug("user name refered to a group.");
        	throw new UsernameNotFoundException("User not found: "+username);
        }

        boolean accountDisabled = false;
        boolean accountLocked = false;
        boolean passwordRequired = true;
        String userFullName = null;
        String userEmailAddress = null;
//        Object officeLocations = null;
        
        try {
        	accountDisabled = usr.accountDisabled();
        	log.debug("account disabled = " + accountDisabled);
        } catch (Throwable th) { 
        	log.error("Error fetching AD account disabled attribute for user " + username);
        }
        
        try {
        	accountLocked = usr.isAccountLocked();
        	log.debug("account locked = " + accountLocked);
        } catch (Throwable th) { 
        	log.error("Error fetching AD account locked attribute for user " + username);
        }

        try {
        	userFullName = usr.fullName();
        	log.debug("full name = " + userFullName);
		} catch (Throwable th) { 
			log.error("Error fetching AD full name attribute for user " + username);
		}

        try {
        	passwordRequired = usr.passwordRequired();
        	log.debug("password required = " + passwordRequired);
		} catch (Throwable th) { 
			log.error("Error fetching AD password required attribute for user " + username);
		}

        try {
        	userEmailAddress = usr.emailAddress();
	        log.debug("user email address = "  + userEmailAddress);
		} catch (Throwable th) { 
			log.error("Error fetching AD email address attribute for user " + username);
		}

        List<GrantedAuthority> groups = new ArrayList<GrantedAuthority>();

        try {
	        for( Com4jObject g : usr.groups() ) {
	            IADsGroup grp = g.queryInterface(IADsGroup.class);
	            // cut "CN=" and make that the role name
	            log.debug("grp.name = " + grp.name());
	            groups.add(new GrantedAuthorityImpl(grp.name().substring(3)));
	        }
		} catch (Throwable th) { 
			log.error("Error fetching AD groups attribute for user " + username);
		}
        
        groups.add(SecurityRealm.AUTHENTICATED_AUTHORITY);
        
        ActiveDirectoryUserDetail result = 
	        new ActiveDirectoryUserDetail(
	            username, password,
	            !accountDisabled,
	            true, true, true,
	            groups.toArray(new GrantedAuthority[groups.size()])
	        );
        
        // TODO: any additional attributes you'd like out of AD?
        result.setFullName(userFullName);
        result.setEmailAddress(userEmailAddress);
        
        return result;
	}
	
	
	public List<String[]> searchDnOfUser(final String username) throws UsernameNotFoundException {
		List<String[]> result = new ArrayList<String[]>();
		
		log.debug("searchDnOfUser = " + username);
		  
		UserPrincipalName upn = new UserPrincipalName(username);
		
		_Command cmd = com4j.typelibs.ado20.ClassFactory.createCommand();
        cmd.activeConnection(con); 

        String namePattern = upn.getUserName();
        if (namePattern.endsWith("*") == false) {
        	namePattern += "*";
        }
        
        cmd.commandText("<GC://"+rootDomainNamingContext+">;(&(objectCategory=person)(name="+namePattern+"));sAMAccountName,name;subTree");
        _Recordset rs = cmd.execute(null, Variant.getMissing(), -1/*default*/);
        
        while(rs.eof() == false) {

        	Fields fields = rs.fields();
        	String[] names = new String[2];

        	if (fields.item("sAMAccountName") != null ) {
        		Object obj = fields.item("sAMAccountName").value();
        		if (obj != null) {
        			names[0] = obj.toString();
        		}
            	else { 
            		rs.moveNext();
            		continue;
            	}
        	}
        	else { 
        		rs.moveNext();
        		continue;
        	}
        	
        	if (fields.item("name") != null ) {
        		Object obj = fields.item("name").value();
        		if (obj != null) {
        			names[1] = obj.toString();
        		}
        	}

        	result.add(names);
        	
        	rs.moveNext();
        }
        
        return result;
	}
	
	private String parseUserDC(final String userDN) {
		StringBuffer buf = new StringBuffer();
	
		// first split on comma
		String[] components = userDN.split(",");
		
		for (String component : components) {
			System.err.println("DC = " + component);
			
			if (component.startsWith("DC=")) {
				if (buf.length() > 0) {
					buf.append('.');
				}
				buf.append(component.substring(3));
			}
		}
		
		if (buf.length() < 1) {
			return "";
		}
		
		String result = buf.toString();
		
		return result;
	}
	
	private String getDefaultLdapHost(final String hostDomain) {
	    try {
	        Hashtable<String, String> env = new Hashtable<String,String>();
	        env.put( "java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory" );
	        DirContext dns = new InitialDirContext( env );

	        Attributes attrs = dns.getAttributes( "_ldap._tcp." + hostDomain, new String[] { "SRV" } );

	        // TODO: at some point the indicated host might not be available.  Can we look for other hosts in the set of responses?
	        Attribute attr = attrs.getAll().nextElement();
	        String srv = attr.get().toString();

	        log.debug("retrieved LDAP server srv from DNS for hostDomain " + hostDomain + " : " + srv);
	        
	        String[] parts = srv.split( " " );
	        return parts[3] + ":" + parts[2];
	        
	    } catch( Exception ex ) {
	        ex.printStackTrace();
	        return null;
	    }
	}

}
