/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright (C) 2004 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

package org.jivesoftware.messenger.ldap;

import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.database.SequenceManager;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.LongList;
import org.jivesoftware.messenger.user.UserIDProvider;
import org.jivesoftware.messenger.user.UserNotFoundException;
import org.jivesoftware.messenger.user.spi.DbUserIDProvider;
import org.jivesoftware.database.DbConnectionManager;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

/**
 * <p>Ldap implementation of the UserIDProvider interface.</p>
 * <p>The LdapUserIDProvider can operate in two modes -- in the pure LDAP mode, all user data is stored in the LDAP
 * store. This mode generally requires modifications to the LDAP schema to accommodate data that Messenger needs.</p>
 * <p>In the mixed mode, data that Messenger needs is stored locally.</p>
 * * @author Jim Berrettini
 */


public class LdapUserIDProvider implements UserIDProvider {
    private LdapManager manager;
    /**
     * <p>Object type for users.</p>
     */
    public static final int USER_TYPE = 0;
    /**
     * <p>Object type for chatbots.</p>
     */
    public static final int CHATBOT_TYPE = 1;
    /**
     * <p>The default domain id - the messenger domain.</p>
     */
    public static final long DEFAULT_DOMAIN = 1;

    private static final String GET_USERID = "SELECT objectID FROM jiveUserID WHERE username=? AND domainID=? AND objectType=?";
    private static final String GET_USERNAME = "SELECT username FROM jiveUserID WHERE objectID=? AND domainID=? AND objectType=?";
    private static final String USER_COUNT = "SELECT count(*) FROM jiveUser";
    private static final String INSERT_USERID = "INSERT INTO jiveUserID (username,domainID,objectType,objectID) VALUES (?,?,?,?)";
    private static final String ALL_USERS = "SELECT userID from jiveUser";

    public LdapUserIDProvider() {
        manager = LdapManager.getInstance();
    }

    /**
     * <p>Obtain the user's username from their ID.</p>
     *
     * @param id the userID of the user
     * @return the name of the user with the given userID
     * @throws UserNotFoundException if no such user exists
     */
    public String getUsername(long id) throws UserNotFoundException {
        if (manager.getMode() == LdapManager.ALL_LDAP_MODE) {
            // Find userDN.
            DirContext ctx = null;
            try {
                ctx = manager.getContext();
                // Search for the dn based on the username.
                SearchControls constraints = new SearchControls();
                constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
                constraints.setReturningAttributes(new String[]{"jiveUserID"});

                StringBuffer filter = new StringBuffer();
                filter.append("(").append("jiveUserID").append("=");
                filter.append(id).append(")");

                NamingEnumeration answer = ctx.search("", filter.toString(), constraints);
                if (answer == null || !answer.hasMoreElements()) {
                    throw new UserNotFoundException("User not found: " + id);
                }
                String userDN = ((SearchResult)answer.next()).getName();
                // Make sure there are no more search results. If there are, then
                // the userID isn't unique on the LDAP server (a perfectly possible
                // scenario since only fully qualified dn's need to be unqiue).
                // There really isn't a way to handle this, so throw an exception.
                // The baseDN must be set correctly so that this doesn't happen.
                if (answer.hasMoreElements()) {
                    throw new UserNotFoundException("LDAP username lookup matched multiple entries.");
                }
            }
            catch (Exception e) {
                throw new UserNotFoundException(e);
            }
            finally {
                try {
                    ctx.close();
                }
                catch (Exception e) {
                }
            }
            return getUsernameFromLdap(id);
        }
        else {
            return getUsernameFromDb(id);
        }
    }


    /**
     * <p>Obtain the user's username from their ID.</p>
     *
     * @param username the username to look up
     * @return the userID corrresponding to the given username
     * @throws UserNotFoundException
     */
    public long getUserID(String username) throws UserNotFoundException {
        if (manager.getMode() == LdapManager.ALL_LDAP_MODE) {
            return getUserIDFromLdap(username);
        }

        long id = 0L;
        try {
            id = getUserIDLocally(username);
        }
        catch (UserNotFoundException e) {
            id = generateNewUserIDLocally(username);
        }
        return id;
    }

    /**
     * <p>Obtain the total number of users on the system.</p>
     *
     * @return total number of users on the system.
     */
    public int getUserCount() {
        int count = 0;
        // If using the pure LDAP mode.
        if (manager.getMode() == LdapManager.ALL_LDAP_MODE) {
            // Note: the performance of this check may suffer badly for very large
            // numbers of users since we manually iterate through results to get
            // a count.
            DirContext ctx = null;
            try {
                ctx = manager.getContext();
                // Search for the dn based on the username.
                SearchControls constraints = new SearchControls();
                constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
                constraints.setReturningAttributes(new String[]{"jiveUserID"});
                String filter = "(jiveUserID=*)";
                NamingEnumeration answer = ctx.search("", filter, constraints);
                while (answer.hasMoreElements()) {
                    count++;
                    answer.nextElement();
                }
            }
            catch (Exception e) {
                Log.error(e);
            }
            finally {
                try {
                    ctx.close();
                }
                catch (Exception e) {
                }
            }
        }
        // Otherwise, we're using the mixed LDAP mode.
        else {
            Connection con = null;
            PreparedStatement pstmt = null;

            try {
                con = DbConnectionManager.getConnection();
                pstmt = con.prepareStatement(USER_COUNT);
                ResultSet rs = pstmt.executeQuery();
                if (rs.next()) {
                    count = rs.getInt(1);
                }
            }
            catch (SQLException e) {
                Log.error(e);
            }
            finally {
                try {
                    if (pstmt != null) {
                        pstmt.close();
                    }
                }
                catch (Exception e) {
                    Log.error(e);
                }
                try {
                    if (con != null) {
                        con.close();
                    }
                }
                catch (Exception e) {
                    Log.error(e);
                }
            }
        }
        return count;
    }

    /**
     * <p>Obtain a list all user IDs on the system.</p>
     *
     * @return LongList of user ID's
     */
    public LongList getUserIDs() {
        LongList users = new LongList(500);

        if (manager.getMode() == LdapManager.LDAP_DB_MODE) { // if in mixed mode, get id's from DB.
            Connection con = null;
            PreparedStatement pstmt = null;
            try {
                con = DbConnectionManager.getConnection();
                pstmt = con.prepareStatement(ALL_USERS);
                ResultSet rs = pstmt.executeQuery();
                // Set the fetch size. This will prevent some JDBC drivers from trying
                // to load the entire result set into memory.
                DbConnectionManager.setFetchSize(rs, 500);
                while (rs.next()) {
                    users.add(rs.getLong(1));
                }
            }
            catch (SQLException e) {
                Log.error(e);
            }
            finally {
                try {
                    if (pstmt != null) {
                        pstmt.close();
                    }
                }
                catch (Exception e) {
                    Log.error(e);
                }
                try {
                    if (con != null) {
                        con.close();
                    }
                }
                catch (Exception e) {
                    Log.error(e);
                }
            }
            return users;
        }
        // else, in LDAP-only mode
        DirContext ctx = null;
        try {
            ctx = manager.getContext();
            // Search for the dn based on the username.
            SearchControls constraints = new SearchControls();
            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
            constraints.setReturningAttributes(new String[]{"jiveUserID"});
            String filter = "(jiveUserID=*)";
            NamingEnumeration answer = ctx.search("", filter, constraints);

            while (answer.hasMoreElements()) {
                // Get the next userID.
                users.add(Long.parseLong((String)(((SearchResult)answer.next()).getAttributes().get("jiveUserID")).get()));
            }
        }
        catch (Exception e) {
            Log.error(e);
        }
        finally {
            try {
                ctx.close();
            }
            catch (Exception e) {
            }
        }
        return users;
    }

    /**
     * Get paginated sublist of userID's
     *
     * @param startIndex index to begin sublist with.
     * @param numResults maximum number of results to return.
     * @return sublist of userID's.
     */
    public LongList getUserIDs(int startIndex, int numResults) {
        LongList users = new LongList();
        if (manager.getMode() == LdapManager.LDAP_DB_MODE) { // if in mixed mode, get id's from DB.
            Connection con = null;
            PreparedStatement pstmt = null;

            try {
                con = DbConnectionManager.getConnection();
                pstmt = con.prepareStatement(ALL_USERS);
                ResultSet rs = pstmt.executeQuery();
                DbConnectionManager.setFetchSize(rs, startIndex + numResults);
                // Move to start of index
                for (int i = 0; i < startIndex; i++) {
                    rs.next();
                }
                // Now read in desired number of results (or stop if we run out of results).
                for (int i = 0; i < numResults; i++) {
                    if (rs.next()) {
                        users.add(rs.getLong(1));
                    }
                    else {
                        break;
                    }
                }
            }
            catch (SQLException e) {
                Log.error(e);
            }
            finally {
                try {
                    if (pstmt != null) {
                        pstmt.close();
                    }
                }
                catch (Exception e) {
                    Log.error(e);
                }
                try {
                    if (con != null) {
                        con.close();
                    }
                }
                catch (Exception e) {
                    Log.error(e);
                }
            }
            return users;
        }
        // else, in LDAP-only mode
        DirContext ctx = null;
        try {
            ctx = manager.getContext();
            // Search for the dn based on the username.
            SearchControls constraints = new SearchControls();
            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
            constraints.setReturningAttributes(new String[]{"jiveUserID"});
            String filter = "(jiveUserID=*)";
            NamingEnumeration answer = ctx.search("", filter, constraints);
            for (int i = 0; i < startIndex; i++) {
                answer.next();
            }
            // Now read in desired number of results (or stop if we run out of results).
            for (int i = 0; i < numResults; i++) {
                if (answer.hasMoreElements()) {
                    // Get the next userID.
                    users.add(Long.parseLong((String)(((SearchResult)answer.next()).getAttributes().get("jiveUserID")).get()));
                }
                else {
                    break;
                }
            }
        }
        catch (Exception e) {
            Log.error(e);
        }
        finally {
            try {
                ctx.close();
            }
            catch (Exception e) {
            }
        }
        return users;
    }

    /**
     * This is used when operating in mixed mode -- generate a new user ID for a user stored in our database.
     *
     * @param username
     * @return id corresponding to that username
     * @throws UserNotFoundException
     */
    private long generateNewUserIDLocally(String username) throws UserNotFoundException {
        long id = -1;
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            id = SequenceManager.nextID(JiveConstants.USER);
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(INSERT_USERID);
            pstmt.setString(1, username);
            pstmt.setLong(2, DbUserIDProvider.DEFAULT_DOMAIN);
            pstmt.setLong(3, DbUserIDProvider.USER_TYPE);
            pstmt.setLong(4, id);
            pstmt.executeUpdate();
        }
        catch (SQLException e) {
            Log.error(e);
            throw new UserNotFoundException(e);
        }
        finally {
            try {
                if (pstmt != null) {
                    pstmt.close();
                }
            }
            catch (Exception e) {
                Log.error(e);
            }
            try {
                if (con != null) {
                    con.close();
                }
            }
            catch (Exception e) {
                Log.error(e);
            }
        }
        return id;
    }

    /**
     * This is used when operating in mixed mode -- get user ID for a user stored locally.
     *
     * @param username
     * @return user id corresponding to that username.
     * @throws UserNotFoundException
     */
    private long getUserIDLocally(String username) throws UserNotFoundException {
        long id = -1;
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(GET_USERID);
            pstmt.setString(1, username);
            pstmt.setLong(2, DEFAULT_DOMAIN);
            pstmt.setLong(3, USER_TYPE);
            ResultSet rs = pstmt.executeQuery();
            if (rs.next()) {
                id = rs.getLong(1);
            }
        }
        catch (Exception e) {
            Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
        }
        finally {
            try {
                if (pstmt != null) {
                    pstmt.close();
                }
            }
            catch (Exception e) {
                Log.error(e);
            }
            try {
                if (con != null) {
                    con.close();
                }
            }
            catch (Exception e) {
                Log.error(e);
            }
        }
        if (id == -1) {
            throw new UserNotFoundException();
        }
        return id;
    }

    /**
     * This method is used when operating in pure LDAP mode. Get user ID from the LDAP store.
     *
     * @param username
     * @return user id corresponding to that username.
     * @throws UserNotFoundException
     */
    private long getUserIDFromLdap(String username) throws UserNotFoundException {
        DirContext ctx = null;
        try {
            String userDN = manager.findUserDN(username);
            ctx = manager.getContext();
            String[] attributes = new String[]{"jiveUserID"};
            Attributes attrs = ctx.getAttributes(userDN, attributes);
            return Long.parseLong((String)attrs.get("jiveUserID").get());
        }
        catch (Exception e) {
            Log.error(e);
            throw new UserNotFoundException(e);
        }
        finally {
            try {
                ctx.close();
            }
            catch (Exception e) {
            }
        }

    }

    /**
     * This method is used in mixed mode. Get the username that corresponds to a given ID.
     *
     * @param id
     * @return username for that user id.
     * @throws UserNotFoundException
     */
    private String getUsernameFromDb(long id) throws UserNotFoundException {
        String name = null;
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(GET_USERNAME);
            pstmt.setLong(1, id);
            pstmt.setLong(2, DEFAULT_DOMAIN);
            pstmt.setLong(3, USER_TYPE);
            ResultSet rs = pstmt.executeQuery();
            if (rs.next()) {
                name = rs.getString(1);
            }
        }
        catch (Exception e) {
            Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
        }
        finally {
            try {
                if (pstmt != null) {
                    pstmt.close();
                }
            }
            catch (Exception e) {
                Log.error(e);
            }
            try {
                if (con != null) {
                    con.close();
                }
            }
            catch (Exception e) {
                Log.error(e);
            }
        }
        if (name == null) {
            throw new UserNotFoundException();
        }
        return name;
    }

    /**
     * This method is used when operating in pure LDAP mode. Get the username that corresponds to a given ID.
     *
     * @param id
     * @return
     * @throws UserNotFoundException
     */
    private String getUsernameFromLdap(long id) throws UserNotFoundException {
        DirContext ctx = null;
        try {
            ctx = manager.getContext();
            SearchControls constraints = new SearchControls();
            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
            constraints.setReturningAttributes(new String[]{"jiveUserID"});
            StringBuffer filter = new StringBuffer();
            filter.append("(").append("jiveUserID").append("=");
            filter.append(id).append(")");
            NamingEnumeration answer = ctx.search("", filter.toString(), constraints);
            if (answer == null || !answer.hasMoreElements()) {
                throw new UserNotFoundException("User not found: " + id);
            }
            String userDN = ((SearchResult)answer.next()).getName();
            return getUsernameFromUserDN(userDN);

        }
        catch (NamingException e) {
            Log.error(e);
            throw new UserNotFoundException(e);
        }
        catch (UserNotFoundException e) {
            Log.error(e);
            throw new UserNotFoundException(e);
        }
        finally {
            try {
                ctx.close();
            }
            catch (Exception e) {
            }
        }

    }

    /**
     * Helper function to retrieve username from userDN.
     *
     * @param userDN
     * @return username
     * @throws NamingException
     */
    private String getUsernameFromUserDN(String userDN) throws NamingException {
        DirContext ctx = null;
        try {
            ctx = manager.getContext();
            // Load record.
            String[] attributes = new String[]{manager.getUsernameField()};
            Attributes attrs = ctx.getAttributes(userDN, attributes);
            return (String)attrs.get(manager.getUsernameField()).get();
        }
        finally {
            try {
                ctx.close();
            }
            catch (Exception e) {
            }
        }

    }
}
