package org.inria.bmajwatcher.server;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashSet;
import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

import org.apache.log4j.Logger;
import org.inria.biomaj.sql.SQLConnection;
import org.inria.biomaj.sql.SQLConnectionFactory;
import org.inria.bmajwatcher.client.services.AuthenticationService;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * Service that handles authentication requests.
 * 
 * @author rsabas
 * 
 */
public class AuthenticationServiceImpl extends RemoteServiceServlet implements
		AuthenticationService {

	private static final long serialVersionUID = 1L;
	private static final String LOGGED = "logged";
	private static Logger log = Logger.getLogger(AuthenticationServiceImpl.class);

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.inria.bmajwatcher.client.services.AuthenticationService#authenticate
	 * (java.lang.String, java.lang.String)
	 */
	@Override
	public boolean authenticate(String login, String passwd) {
		
		/*
		 * Authentication principles :
		 * - LDAP is prioritary. If activated, no local authentication.
		 * - If admin tries to log in and LDAP fails, authenticate with local (only case where local is tried when ldap is activated).
		 * - If none succeeds, authentication fails.
		 */
		
		log.info("Logging in request");
		if (login == null || passwd == null)
			return false;

		String adminLogin = UserManagerServiceImpl.adminLogin();
		
		boolean auth = false;
		String useLDAP = getServletContext().getInitParameter("USELDAP");
		// If LDAP option is set in context, use ldap server method
		if (useLDAP != null && useLDAP.equals("1")) {
			log.debug("Authenticate with ldap");
			auth = authenticateWithLDAP(login, passwd);
			
			if (!auth && login.equals(adminLogin)) {
				log.debug("Trying to authenticate admin through local");
				auth = authenticateWithLocal(login, passwd);
			}
		}
		if (!auth) {
			String useLocal = getServletContext().getInitParameter("USELOCAL");			
			if (useLocal != null && useLocal.equals("1")) {
				log.debug("Authenticate with local");
				auth = authenticateWithLocal(login, passwd);
			}
		}
		
		if (auth) {
			String type = "user";
			if (login.equals(adminLogin))
				type = "admin";
			
			getThreadLocalRequest().getSession().setAttribute(LOGGED, login + ":" + type);
		}
		
		log.debug("Authentication success : " + auth);
		return auth;
		
	}
	
	private boolean authenticateWithLocal(String login, String passwd) {

//		if (checkLocalId(UserManagerServiceImpl.getSHA1Hash(login), UserManagerServiceImpl.getSHA1Hash(login))) {
//			getThreadLocalRequest().getSession().setAttribute(LOGGED, "true");
//			return true;
//		}
//		return false;
		
		return (UserManagerServiceImpl.getUser(login, passwd) != null);
	}

	/**
	 * Authenticates against a LDAP server
	 * 
	 * @param login
	 * @param passwd
	 * @return true if authentication succeeded
	 */
	private boolean authenticateWithLDAP(String login, String passwd) {
		// Set up the environment for creating the initial context
		Hashtable<String, String> env = new Hashtable<String, String>();
		env.put(Context.INITIAL_CONTEXT_FACTORY,
				"com.sun.jndi.ldap.LdapCtxFactory");
		String ldapHost = getServletContext().getInitParameter("LDAPHOST");
		String ldapDn = getServletContext().getInitParameter("LDAPDN");
		if (ldapHost == null || ldapDn == null)
			return false;
		env.put(Context.PROVIDER_URL, "ldap://" + ldapHost + ":389/" + ldapDn);
		// Authenticate as anonymous
		env.put(Context.SECURITY_AUTHENTICATION, "simple");

		// Create the initial context
		try {
			log.debug("connect to ldap");
			DirContext ctx = new InitialDirContext(env);
			String optFilter = "";
			String usercn = null;
			if (getServletContext().getInitParameter("OPTFILTER") != null)
				optFilter = getServletContext().getInitParameter("OPTFILTER");
			// Bind with filter

			String filter = "(&(uid=" + login + ")" + optFilter + ")";
			log.debug("Filter: " + filter);
			// limit returned attributes to those we care about
			String[] attrIDs = { "cn" };
			SearchControls ctls = new SearchControls();
			ctls.setReturningAttributes(attrIDs);
			// comment out next line to limit to one container otherwise it'll
			// walk down the tree
			ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
			ctls.setCountLimit(1);
			// Search for objects using filter and controls
			NamingEnumeration<SearchResult> answer = ctx.search("", filter, ctls);
			// cycle through result set
			while (answer.hasMore()) {
				SearchResult sr = (SearchResult) answer.next();
				usercn = sr.getName() + "," + ldapDn;
				ctx.close();
			}
			if (usercn == null)
				return false;
			env.put(Context.SECURITY_PRINCIPAL, usercn);
			env.put(Context.SECURITY_CREDENTIALS, passwd);
			log.debug("log with " + usercn);
			ctx = new InitialDirContext(env);
			ctx.close();

		} catch (NamingException e) {
			log.error("Ldap error: " + e.getMessage());
			return false;
		}
		
		SQLConnection connection = SQLConnectionFactory.getConnection();
		Statement stat = connection.getStatement();
		String query = "SELECT auth_type FROM bw_user WHERE login='" + login + "'";
		ResultSet rs = connection.executeQuery(query, stat);
		boolean ok = false;
		try {
			if (rs.next()) {
				if (rs.getString(1).equals("ldap"))
					ok = true; // If exists and not local
			} else {
				ok = true; // User doesn't exist, we can create it
				new UserManagerServiceImpl().addUser(login, new HashSet<String>(), "ldap", "");
			}
		} catch (SQLException ex) {
			log.error(ex);
		}
		SQLConnectionFactory.closeConnection(stat);
		
		return ok;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.inria.bmajwatcher.client.services.AuthenticationService#getUserAcces()
	 */
	@Override
	public String getUserAccess() {
		Object o = getThreadLocalRequest().getSession().getAttribute(LOGGED);
		if (o != null)
			return o.toString();
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.inria.bmajwatcher.client.services.AuthenticationService#logout()
	 */
	@Override
	public void logout() {
		getThreadLocalRequest().getSession().invalidate();
	}

}
