// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.ldap;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.inject.Inject;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

public class OpenLdapUserManagerImpl implements LdapUserManager {
    private static final Logger s_logger = Logger.getLogger(OpenLdapUserManagerImpl.class.getName());

    @Inject
    protected LdapConfiguration _ldapConfiguration;

    public OpenLdapUserManagerImpl() {
    }

    public OpenLdapUserManagerImpl(final LdapConfiguration ldapConfiguration) {
        _ldapConfiguration = ldapConfiguration;
    }

    protected LdapUser createUser(final SearchResult result) throws NamingException {
        final Attributes attributes = result.getAttributes();

        final String username = LdapUtils.getAttributeValue(attributes, _ldapConfiguration.getUsernameAttribute());
        final String email = LdapUtils.getAttributeValue(attributes, _ldapConfiguration.getEmailAttribute());
        final String firstname = LdapUtils.getAttributeValue(attributes, _ldapConfiguration.getFirstnameAttribute());
        final String lastname = LdapUtils.getAttributeValue(attributes, _ldapConfiguration.getLastnameAttribute());
        final String principal = result.getNameInNamespace();

        String domain = principal.replace("cn=" + LdapUtils.getAttributeValue(attributes, _ldapConfiguration.getCommonNameAttribute()) + ",", "");
        domain = domain.replace("," + _ldapConfiguration.getBaseDn(), "");
        domain = domain.replace("ou=", "");

        boolean disabled = isUserDisabled(result);

        return new LdapUser(username, email, firstname, lastname, principal, domain, disabled);
    }

    private String generateSearchFilter(final String username) {
        final StringBuilder userObjectFilter = new StringBuilder();
        userObjectFilter.append("(objectClass=");
        userObjectFilter.append(_ldapConfiguration.getUserObject());
        userObjectFilter.append(")");

        final StringBuilder usernameFilter = new StringBuilder();
        usernameFilter.append("(");
        usernameFilter.append(_ldapConfiguration.getUsernameAttribute());
        usernameFilter.append("=");
        usernameFilter.append((username == null ? "*" : username));
        usernameFilter.append(")");

        final StringBuilder memberOfFilter = new StringBuilder();
        if (_ldapConfiguration.getSearchGroupPrinciple() != null) {
            memberOfFilter.append("(memberof=");
            memberOfFilter.append(_ldapConfiguration.getSearchGroupPrinciple());
            memberOfFilter.append(")");
        }

        final StringBuilder result = new StringBuilder();
        result.append("(&");
        result.append(userObjectFilter);
        result.append(usernameFilter);
        result.append(memberOfFilter);
        result.append(")");

        return result.toString();
    }

    private String generateGroupSearchFilter(final String groupName) {
        final StringBuilder groupObjectFilter = new StringBuilder();
        groupObjectFilter.append("(objectClass=");
        groupObjectFilter.append(_ldapConfiguration.getGroupObject());
        groupObjectFilter.append(")");

        final StringBuilder groupNameFilter = new StringBuilder();
        groupNameFilter.append("(");
        groupNameFilter.append(_ldapConfiguration.getCommonNameAttribute());
        groupNameFilter.append("=");
        groupNameFilter.append((groupName == null ? "*" : groupName));
        groupNameFilter.append(")");

        final StringBuilder result = new StringBuilder();
        result.append("(&");
        result.append(groupObjectFilter);
        result.append(groupNameFilter);
        result.append(")");

        return result.toString();
    }

    @Override
    public LdapUser getUser(final String username, final LdapContext context) throws NamingException, IOException {
        List<LdapUser> result = searchUsers(username, context);
        if (result!= null && result.size() == 1) {
            return result.get(0);
        } else {
            throw new NamingException("No user found for username " + username);
        }
    }

    @Override
    public LdapUser getUser(final String username, final String type, final String name, final LdapContext context) throws NamingException, IOException {
        String basedn;
        if("OU".equals(type)) {
            basedn = name;
        } else {
            basedn = _ldapConfiguration.getBaseDn();
        }

        final StringBuilder userObjectFilter = new StringBuilder();
        userObjectFilter.append("(objectClass=");
        userObjectFilter.append(_ldapConfiguration.getUserObject());
        userObjectFilter.append(")");

        final StringBuilder usernameFilter = new StringBuilder();
        usernameFilter.append("(");
        usernameFilter.append(_ldapConfiguration.getUsernameAttribute());
        usernameFilter.append("=");
        usernameFilter.append((username == null ? "*" : username));
        usernameFilter.append(")");

        final StringBuilder memberOfFilter = new StringBuilder();
        if ("GROUP".equals(type)) {
            memberOfFilter.append("(").append(getMemberOfAttribute()).append("=");
            memberOfFilter.append(name);
            memberOfFilter.append(")");
        }

        final StringBuilder searchQuery = new StringBuilder();
        searchQuery.append("(&");
        searchQuery.append(userObjectFilter);
        searchQuery.append(usernameFilter);
        searchQuery.append(memberOfFilter);
        searchQuery.append(")");

        return searchUser(basedn, searchQuery.toString(), context);
    }

    protected String getMemberOfAttribute() {
        return "memberof";
    }

    @Override
    public List<LdapUser> getUsers(final LdapContext context) throws NamingException, IOException {
        return getUsers(null, context);
    }

    @Override
    public List<LdapUser> getUsers(final String username, final LdapContext context) throws NamingException, IOException {
        List<LdapUser> users = searchUsers(username, context);

        if (CollectionUtils.isNotEmpty(users)) {
            Collections.sort(users);
        }
        return users;
    }

    @Override
    public List<LdapUser> getUsersInGroup(String groupName, LdapContext context) throws NamingException {
        String attributeName = _ldapConfiguration.getGroupUniqueMemeberAttribute();
        final SearchControls controls = new SearchControls();
        controls.setSearchScope(_ldapConfiguration.getScope());
        controls.setReturningAttributes(new String[] {attributeName});

        NamingEnumeration<SearchResult> result = context.search(_ldapConfiguration.getBaseDn(), generateGroupSearchFilter(groupName), controls);

        final List<LdapUser> users = new ArrayList<LdapUser>();
        //Expecting only one result which has all the users
        if (result.hasMoreElements()) {
            Attribute attribute = result.nextElement().getAttributes().get(attributeName);
            NamingEnumeration<?> values = attribute.getAll();

            while (values.hasMoreElements()) {
                String userdn = String.valueOf(values.nextElement());
                try{
                    users.add(getUserForDn(userdn, context));
                } catch (NamingException e){
                    s_logger.info("Userdn: " + userdn + " Not Found:: Exception message: " + e.getMessage());
                }
            }
        }

        Collections.sort(users);

        return users;
    }

    private LdapUser getUserForDn(String userdn, LdapContext context) throws NamingException {
        final SearchControls controls = new SearchControls();
        controls.setSearchScope(_ldapConfiguration.getScope());
        controls.setReturningAttributes(_ldapConfiguration.getReturnAttributes());

        NamingEnumeration<SearchResult> result = context.search(userdn, "(objectClass=" + _ldapConfiguration.getUserObject() + ")", controls);
        if (result.hasMoreElements()) {
            return createUser(result.nextElement());
        } else {
            throw new NamingException("No user found for dn " + userdn);
        }
    }

    @Override
    public List<LdapUser> searchUsers(final LdapContext context) throws NamingException, IOException {
        return searchUsers(null, context);
    }

    protected boolean isUserDisabled(SearchResult result) throws NamingException {
        return false;
    }

    public LdapUser searchUser(final String basedn, final String searchString, final LdapContext context) throws NamingException, IOException {
        final SearchControls searchControls = new SearchControls();

        searchControls.setSearchScope(_ldapConfiguration.getScope());
        searchControls.setReturningAttributes(_ldapConfiguration.getReturnAttributes());

        NamingEnumeration<SearchResult> results = context.search(basedn, searchString, searchControls);
        final List<LdapUser> users = new ArrayList<LdapUser>();
        while (results.hasMoreElements()) {
            final SearchResult result = results.nextElement();
                users.add(createUser(result));
        }

        if (users.size() == 1) {
            return users.get(0);
        } else {
            throw new NamingException("No user found for basedn " + basedn + " and searchString " + searchString);
        }
    }

    @Override
    public List<LdapUser> searchUsers(final String username, final LdapContext context) throws NamingException, IOException {

        final SearchControls searchControls = new SearchControls();

        searchControls.setSearchScope(_ldapConfiguration.getScope());
        searchControls.setReturningAttributes(_ldapConfiguration.getReturnAttributes());

        String basedn = _ldapConfiguration.getBaseDn();
        if (StringUtils.isBlank(basedn)) {
            throw new IllegalArgumentException("ldap basedn is not configured");
        }
        byte[] cookie = null;
        int pageSize = _ldapConfiguration.getLdapPageSize();
        context.setRequestControls(new Control[]{new PagedResultsControl(pageSize, Control.NONCRITICAL)});
        final List<LdapUser> users = new ArrayList<LdapUser>();
        NamingEnumeration<SearchResult> results;
        do {
            results = context.search(basedn, generateSearchFilter(username), searchControls);
            while (results.hasMoreElements()) {
                final SearchResult result = results.nextElement();
                if (!isUserDisabled(result)) {
                    users.add(createUser(result));
                }
            }
            Control[] contextControls = context.getResponseControls();
            if (contextControls != null) {
                for (Control control : contextControls) {
                    if (control instanceof PagedResultsResponseControl) {
                        PagedResultsResponseControl prrc = (PagedResultsResponseControl) control;
                        cookie = prrc.getCookie();
                    }
                }
            } else {
                s_logger.info("No controls were sent from the ldap server");
            }
            context.setRequestControls(new Control[] {new PagedResultsControl(pageSize, cookie, Control.CRITICAL)});
        } while (cookie != null);

        return users;
    }
}