// 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.cloudstack.ldap.dao.LdapTrustMapDao;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

public class OpenLdapUserManagerImpl implements LdapUserManager {
    protected Logger logger = LogManager.getLogger(getClass());

    @Inject
    protected LdapConfiguration _ldapConfiguration;

    @Inject
    LdapTrustMapDao _ldapTrustMapDao;

    public OpenLdapUserManagerImpl() {
    }

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

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

        final String username = LdapUtils.getAttributeValue(attributes, _ldapConfiguration.getUsernameAttribute(domainId));
        final String email = LdapUtils.getAttributeValue(attributes, _ldapConfiguration.getEmailAttribute(domainId));
        final String firstname = LdapUtils.getAttributeValue(attributes, _ldapConfiguration.getFirstnameAttribute(domainId));
        final String lastname = LdapUtils.getAttributeValue(attributes, _ldapConfiguration.getLastnameAttribute(domainId));
        final String principal = result.getNameInNamespace();
        final List<String> memberships = LdapUtils.getAttributeValues(attributes, getMemberOfAttribute(domainId));

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

        boolean disabled = isUserDisabled(result);

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

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

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

        String memberOfAttribute = getMemberOfAttribute(domainId);
        StringBuilder ldapGroupsFilter = new StringBuilder();
        // this should get the trustmaps for this domain
        List<String> ldapGroups = getMappedLdapGroups(domainId);
        if (null != ldapGroups && ldapGroups.size() > 0) {
            ldapGroupsFilter.append("(|");
            for (String ldapGroup : ldapGroups) {
                ldapGroupsFilter.append(getMemberOfGroupString(ldapGroup, memberOfAttribute));
            }
            ldapGroupsFilter.append(')');
        }
        // make sure only users in the principle group are retrieved
        String pricipleGroup = _ldapConfiguration.getSearchGroupPrinciple(domainId);
        final StringBuilder principleGroupFilter = new StringBuilder();
        if (null != pricipleGroup) {
            principleGroupFilter.append(getMemberOfGroupString(pricipleGroup, memberOfAttribute));
        }
        final StringBuilder result = new StringBuilder();
        result.append("(&");
        result.append(userObjectFilter);
        result.append(usernameFilter);
        result.append(ldapGroupsFilter);
        result.append(principleGroupFilter);
        result.append(")");

        String returnString = result.toString();
        if (logger.isTraceEnabled()) {
            logger.trace("constructed ldap query: " + returnString);
        }
        return returnString;
    }

    private List<String> getMappedLdapGroups(Long domainId) {
        List <String> ldapGroups = new ArrayList<>();
        // first get the trustmaps
        if (null != domainId) {
            for (LdapTrustMapVO trustMap : _ldapTrustMapDao.searchByDomainId(domainId)) {
                // then retrieve the string from it
                ldapGroups.add(trustMap.getName());
            }
        }
        return ldapGroups;
    }

    private String getMemberOfGroupString(String group, String memberOfAttribute) {
        final StringBuilder memberOfFilter = new StringBuilder();
        if (null != group) {
            if(logger.isDebugEnabled()) {
                logger.debug("adding search filter for '" + group +
                "', using '" + memberOfAttribute + "'");
            }
            memberOfFilter.append("(" + memberOfAttribute + "=");
            memberOfFilter.append(group);
            memberOfFilter.append(")");
        }
        return memberOfFilter.toString();
    }

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

        final StringBuilder groupNameFilter = new StringBuilder();
        groupNameFilter.append("(");
        groupNameFilter.append(_ldapConfiguration.getCommonNameAttribute());
        groupNameFilter.append("=");
        groupNameFilter.append((groupName == null ? "*" : LdapUtils.escapeLDAPSearchFilter(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, Long domainId) throws NamingException, IOException {
        List<LdapUser> result = searchUsers(username, context, domainId);
        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, Long domainId) throws NamingException, IOException {
        String basedn;
        if("OU".equals(type)) {
            basedn = name;
        } else {
            basedn = _ldapConfiguration.getBaseDn(domainId);
        }

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

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

        final StringBuilder memberOfFilter = new StringBuilder();
        if ("GROUP".equals(type)) {
            memberOfFilter.append("(").append(getMemberOfAttribute(domainId)).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, domainId);
    }

    protected String getMemberOfAttribute(final Long domainId) {
        return _ldapConfiguration.getUserMemberOfAttribute(domainId);
    }

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

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

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

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

        NamingEnumeration<SearchResult> result = context.search(_ldapConfiguration.getBaseDn(domainId), generateGroupSearchFilter(groupName, domainId), 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, domainId));
                } catch (NamingException e){
                    logger.info("Userdn: " + userdn + " Not Found:: Exception message: " + e.getMessage());
                }
            }
        }

        Collections.sort(users);

        return users;
    }

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

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

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

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

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

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

        NamingEnumeration<SearchResult> results = context.search(basedn, searchString, searchControls);
        if(logger.isDebugEnabled()) {
            logger.debug("searching user(s) with filter: \"" + searchString + "\"");
        }
        final List<LdapUser> users = new ArrayList<LdapUser>();
        while (results.hasMoreElements()) {
            final SearchResult result = results.nextElement();
                users.add(createUser(result, domainId));
        }

        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, Long domainId) throws NamingException, IOException {

        final SearchControls searchControls = new SearchControls();

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

        String basedn = _ldapConfiguration.getBaseDn(domainId);
        if (StringUtils.isBlank(basedn)) {
            throw new IllegalArgumentException(String.format("ldap basedn is not configured (for domain: %s)", domainId));
        }
        byte[] cookie = null;
        int pageSize = _ldapConfiguration.getLdapPageSize(domainId);
        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, domainId), searchControls);
            while (results.hasMoreElements()) {
                final SearchResult result = results.nextElement();
                if (!isUserDisabled(result)) {
                    users.add(createUser(result, domainId));
                }
            }
            Control[] contextControls = context.getResponseControls();
            if (contextControls != null) {
                for (Control control : contextControls) {
                    if (control instanceof PagedResultsResponseControl) {
                        PagedResultsResponseControl prrc = (PagedResultsResponseControl) control;
                        cookie = prrc.getCookie();
                    }
                }
            } else {
                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;
    }
}
