// 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.api.command;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

import javax.inject.Inject;

import com.cloud.domain.Domain;
import com.cloud.user.User;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.log4j.Logger;

import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.LdapUserResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.UserResponse;
import org.apache.cloudstack.ldap.LdapManager;
import org.apache.cloudstack.ldap.LdapUser;
import org.apache.cloudstack.ldap.NoLdapUserMatchingQueryException;
import org.apache.cloudstack.query.QueryService;

import com.cloud.user.Account;

/**
 * a short flow, use plantuml to view (see http://plantuml.com)
 * @startuml
 * start
 * :list ldap users request;
 * :get ldap binding;
 * if (domain == null) then (true)
 *   :get global trust domain;
 * else (false)
 *   :get trustdomain for domain;
 * endif
 * :get ldap users\n using trust domain;
 * if (filter == 'NoFilter') then (pass as is)
 * elseif (filter == 'AnyDomain') then (anydomain)
 *   :filterList = all\n\t\tcloudstack\n\t\tusers;
 * elseif (filter == 'LocalDomain')
 *   :filterList = local users\n\t\tfor domain;
 * elseif (filter == 'PotentialImport') then (address account\nsynchronisation\nconfigurations)
 *   :query\n the account\n bindings;
 *   :check and markup\n ldap users\n for bound OUs\n with usersource;
 * else ( unknown value for filter )
 *   :throw invalid parameter;
 *   stop
 * endif
 *   :remove users in filterList\nfrom ldap users list;
 * :return remaining;
 * stop
 * @enduml
 */
@APICommand(name = "listLdapUsers", responseObject = LdapUserResponse.class, description = "Lists LDAP Users according to the specifications from the user request.", since = "4.2.0",
        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin,RoleType.DomainAdmin})
public class LdapListUsersCmd extends BaseListCmd {

    public static final Logger s_logger = Logger.getLogger(LdapListUsersCmd.class.getName());
    private static final String s_name = "ldapuserresponse";
    @Inject
    private LdapManager _ldapManager;

    @Parameter(name = "listtype",
            type = CommandType.STRING,
            required = false,
            description = "Determines whether all ldap users are returned or just non-cloudstack users. This option is deprecated in favour for the more option rich 'userfilter' parameter")
    @Deprecated
    private String listType;

    @Parameter(name = ApiConstants.USER_FILTER,
            type = CommandType.STRING,
            required = false,
            since = "4.13",
            description = "Determines what type of filter is applied on the list of users returned from LDAP.\n"
                    + "\tvalid values are\n"
                    + "\t'NoFilter'\t no filtering is done,\n"
                    + "\t'LocalDomain'\tusers already in the current or requested domain will be filtered out of the result list,\n"
                    + "\t'AnyDomain'\tusers that already exist anywhere in cloudstack will be filtered out, and\n"
                    + "\t'PotentialImport'\tall users that would be automatically imported from the listing will be shown,"
                    + " including those that are already in cloudstack, the later will be annotated with their userSource")
    private String userFilter;

    @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = false, entityType = DomainResponse.class, description = "linked domain")
    private Long domainId;

    public LdapListUsersCmd() {
        super();
    }

    public LdapListUsersCmd(final LdapManager ldapManager, final QueryService queryService) {
        super();
        _ldapManager = ldapManager;
        _queryService = queryService;
    }

    /**
     * (as a check for isACloudstackUser is done) only non cloudstack users should be shown
     * @param users a list of {@code LdapUser}s
     * @return a (filtered?) list of user response objects
     */
    private List<LdapUserResponse> createLdapUserResponse(final List<LdapUser> users) {
        final List<LdapUserResponse> ldapResponses = new ArrayList<LdapUserResponse>();
        for (final LdapUser user : users) {
            final LdapUserResponse ldapResponse = _ldapManager.createLdapUserResponse(user);
            ldapResponse.setObjectName("LdapUser");
            ldapResponses.add(ldapResponse);
        }
        return ldapResponses;
    }

    private List<UserResponse> cloudstackUsers = null;

    @Override
    public void execute() throws ServerApiException {
        cloudstackUsers = null;
        List<LdapUserResponse> ldapResponses = new ArrayList<LdapUserResponse>();
        final ListResponse<LdapUserResponse> response = new ListResponse<LdapUserResponse>();
        try {
            final List<LdapUser> users = _ldapManager.getUsers(domainId);
            ldapResponses = createLdapUserResponse(users);
//            now filter and annotate
            ldapResponses = applyUserFilter(ldapResponses);
        } catch (final NoLdapUserMatchingQueryException ex) {
            // ok, we'll make do with the empty list ldapResponses = new ArrayList<LdapUserResponse>();
        } finally {
            response.setResponses(ldapResponses);
            response.setResponseName(getCommandName());
            setResponseObject(response);
        }
    }

    /**
     * get a list of relevant cloudstack users, depending on the userFilter
     */
    private List<UserResponse> getCloudstackUsers() {
        if (cloudstackUsers == null) {
            try {
                cloudstackUsers = getUserFilter().getCloudstackUserList(this).getResponses();
            } catch (IllegalArgumentException e) {
                throw new CloudRuntimeException("error in program login; we are not filtering but still querying users to filter???", e);
            }
            traceUserList();
        }
        return cloudstackUsers;
    }

    private void traceUserList() {
        if(s_logger.isTraceEnabled()) {
            StringBuilder users = new StringBuilder();
            for (UserResponse user : cloudstackUsers) {
                if (users.length()> 0) {
                    users.append(", ");
                }
                users.append(user.getUsername());
            }

            s_logger.trace(String.format("checking against %d cloudstackusers: %s.", this.cloudstackUsers.size(), users.toString()));
        }
    }

    private List<LdapUserResponse> applyUserFilter(List<LdapUserResponse> ldapResponses) {
        if(s_logger.isTraceEnabled()) {
            s_logger.trace(String.format("applying filter: %s or %s.", this.getListTypeString(), this.getUserFilter()));
        }
        List<LdapUserResponse> responseList = getUserFilter().filter(this,ldapResponses);
        return responseList;
    }

    @Override
    public String getCommandName() {
        return s_name;
    }

    @Override
    public long getEntityOwnerId() {
        return Account.ACCOUNT_ID_SYSTEM;
    }

    String getListTypeString() {
        return listType == null ? "all" : listType;
    }

    String getUserFilterString() {
        return userFilter == null ? getListTypeString() == null ? "NoFilter" : getListTypeString().equals("all") ? "NoFilter" : "AnyDomain" : userFilter;
    }

    UserFilter getUserFilter() {
        return UserFilter.fromString(getUserFilterString());
    }

    boolean isACloudstackUser(final LdapUser ldapUser) {
        boolean rc = false;
        final List<UserResponse> cloudstackUsers = getCloudstackUsers();
        if (cloudstackUsers != null) {
            for (final UserResponse cloudstackUser : cloudstackUsers) {
                if (ldapUser.getUsername().equals(cloudstackUser.getUsername())) {
                    if(s_logger.isTraceEnabled()) {
                        s_logger.trace(String.format("found user %s in cloudstack", ldapUser.getUsername()));
                    }

                    rc = true;
                } else {
                    if(s_logger.isTraceEnabled()) {
                        s_logger.trace(String.format("ldap user %s does not match cloudstack user", ldapUser.getUsername(), cloudstackUser.getUsername()));
                    }
                }
            }
        }
        return rc;
    }

    boolean isACloudstackUser(final LdapUserResponse ldapUser) {
        if(s_logger.isTraceEnabled()) {
            s_logger.trace("checking response : " + ldapUser.toString());
        }
        final List<UserResponse> cloudstackUsers = getCloudstackUsers();
        if (cloudstackUsers != null && cloudstackUsers.size() != 0) {
            for (final UserResponse cloudstackUser : cloudstackUsers) {
                if (ldapUser.getUsername().equals(cloudstackUser.getUsername())) {
                    if(s_logger.isTraceEnabled()) {
                        s_logger.trace(String.format("found user %s in cloudstack", ldapUser.getUsername()));
                    }
                    return true;
                } else {
                    if(s_logger.isTraceEnabled()) {
                        s_logger.trace(String.format("ldap user %s does not match cloudstack user", ldapUser.getUsername(), cloudstackUser.getUsername()));
                    }
                }
            }
        }
        return false;
    }
    /**
     * typecheck for userfilter values and filter type dependend functionalities.
     * This could have been in two switch statements elsewhere in the code.
     * Arguably this is a cleaner solution.
     */
    enum UserFilter {
        NO_FILTER("NoFilter"){
            @Override public List<LdapUserResponse> filter(LdapListUsersCmd cmd, List<LdapUserResponse> input) {
                return cmd.filterNoFilter(input);
            }

            /**
             * in case of no filter we should find all users in the current domain for annotation.
             */
            @Override public ListResponse<UserResponse> getCloudstackUserList(LdapListUsersCmd cmd) {
                return cmd._queryService.searchForUsers(cmd.domainId,true);

            }
        },
        LOCAL_DOMAIN("LocalDomain"){
            @Override public List<LdapUserResponse> filter(LdapListUsersCmd cmd, List<LdapUserResponse> input) {
                return cmd.filterLocalDomain(input);
            }

            /**
             * if we are filtering for local domain, only get users for the current domain
             */
            @Override public ListResponse<UserResponse> getCloudstackUserList(LdapListUsersCmd cmd) {
                return cmd._queryService.searchForUsers(cmd.domainId,false);
            }
        },
        ANY_DOMAIN("AnyDomain"){
            @Override public List<LdapUserResponse> filter(LdapListUsersCmd cmd, List<LdapUserResponse> input) {
                return cmd.filterAnyDomain(input);
            }

            /*
             * if we are filtering for any domain, get recursive all users for the root domain
             */
            @Override public ListResponse<UserResponse> getCloudstackUserList(LdapListUsersCmd cmd) {
                return cmd._queryService.searchForUsers(CallContext.current().getCallingAccount().getDomainId(), true);
            }
        },
        POTENTIAL_IMPORT("PotentialImport"){
            @Override public List<LdapUserResponse> filter(LdapListUsersCmd cmd, List<LdapUserResponse> input) {
                return cmd.filterPotentialImport(input);
            }

            /**
             * if we are filtering for potential imports,
             *    we are only looking for users in the linked domains/accounts,
             *    which is only relevant if we ask ldap users for this domain.
             *    So we are asking for all users in the current domain as well
             */
            @Override public ListResponse<UserResponse> getCloudstackUserList(LdapListUsersCmd cmd) {
                return cmd._queryService.searchForUsers(cmd.domainId,false);
            }
        };

        private final String value;

        UserFilter(String val) {
            this.value = val;
        }

        public abstract List<LdapUserResponse> filter(LdapListUsersCmd cmd, List<LdapUserResponse> input);

        public abstract ListResponse<UserResponse> getCloudstackUserList(LdapListUsersCmd cmd);

        static UserFilter fromString(String val) {
            if(NO_FILTER.toString().equalsIgnoreCase(val)) {
                return NO_FILTER;
            } else if (LOCAL_DOMAIN.toString().equalsIgnoreCase(val)) {
                return LOCAL_DOMAIN;
            } else if(ANY_DOMAIN.toString().equalsIgnoreCase(val)) {
                return ANY_DOMAIN;
            } else if(POTENTIAL_IMPORT.toString().equalsIgnoreCase(val)) {
                return POTENTIAL_IMPORT;
            } else {
                throw new IllegalArgumentException(String.format("%s is not a legal 'UserFilter' value", val));
            }
        }

        @Override public String toString() {
            return value;
        }
    }

    /**
     * no filtering but improve with annotation of source for existing ACS users
     * @param input ldap response list of users
     * @return unfiltered list of the input list of ldap users
     */
    public List<LdapUserResponse> filterNoFilter(List<LdapUserResponse> input) {
        if(s_logger.isTraceEnabled()) {
            s_logger.trace("returning unfiltered list of ldap users");
        }
        annotateUserListWithSources(input);
        return input;
    }

    /**
     * filter the list of ldap users. no users visible to the caller should be in the returned list
     * @param input ldap response list of users
     * @return a list of ldap users not already in ACS
     */
    public List<LdapUserResponse> filterAnyDomain(List<LdapUserResponse> input) {
        if(s_logger.isTraceEnabled()) {
            s_logger.trace("filtering existing users");
        }
        final List<LdapUserResponse> ldapResponses = new ArrayList<LdapUserResponse>();
        for (final LdapUserResponse user : input) {
            if (isNotAlreadyImportedInTheCurrentDomain(user)) {
                ldapResponses.add(user);
            }
        }
        annotateUserListWithSources(ldapResponses);

        return ldapResponses;
    }

    /**
     * @return true unless the the user is imported in the specified cloudstack domain from LDAP
     */
    private boolean isNotAlreadyImportedInTheCurrentDomain(LdapUserResponse user) {
        UserResponse cloudstackUser = getCloudstackUser(user);
        String domainId = getCurrentDomainId();

        return cloudstackUser == null /*doesn't exist in cloudstack*/
                || ! (
                        cloudstackUser.getUserSource().equalsIgnoreCase(User.Source.LDAP.toString())
                                && domainId.equals(cloudstackUser.getDomainId())); /* is from another source */
    }

    /**
     * filter the list of ldap users. no users visible to the caller already in the domain specified should be in the returned list
     * @param input ldap response list of users
     * @return a list of ldap users not already in ACS
     */
    public List<LdapUserResponse> filterLocalDomain(List<LdapUserResponse> input) {
        if(s_logger.isTraceEnabled()) {
            s_logger.trace("filtering local domain users");
        }
        final List<LdapUserResponse> ldapResponses = new ArrayList<LdapUserResponse>();
        String domainId = getCurrentDomainId();
        for (final LdapUserResponse user : input) {
            UserResponse cloudstackUser = getCloudstackUser(user);
            if (cloudstackUser == null /*doesn't exist in cloudstack*/
                    || !domainId.equals(cloudstackUser.getDomainId()) /* doesn't exist in this domain */
                    || !cloudstackUser.getUserSource().equalsIgnoreCase(User.Source.LDAP.toString()) /* is from another source */
            ) {
                ldapResponses.add(user);
            }
        }
        annotateUserListWithSources(ldapResponses);
        return ldapResponses;
    }

    private String getCurrentDomainId() {
        String domainId = null;
        if (this.domainId != null) {
            Domain domain = _domainService.getDomain(this.domainId);
            domainId = domain.getUuid();
        } else {
            final CallContext callContext = CallContext.current();
            domainId = _domainService.getDomain(callContext.getCallingAccount().getDomainId()).getUuid();
        }
        return domainId;
    }

    /**
     *
     * @param input a list of ldap users
     * @return annotated list of the users of the input list, that will be automatically imported or synchronised
     */
    public List<LdapUserResponse> filterPotentialImport(List<LdapUserResponse> input) {
        if(s_logger.isTraceEnabled()) {
            s_logger.trace("should be filtering potential imports!!!");
        }
        // functional possibility do not add only users not yet in cloudstack but include users that would be moved if they are so in ldap?
        // this means if they are part of a account linked to an ldap group/ou
        input.removeIf(ldapUser ->
                (
                        (isACloudstackUser(ldapUser))
                        && (getCloudstackUser(ldapUser).getUserSource().equalsIgnoreCase(User.Source.LDAP.toString()))
                )
        );
        annotateUserListWithSources(input);
        return input;
    }

    private void annotateUserListWithSources(List<LdapUserResponse> input) {
        for (final LdapUserResponse user : input) {
            annotateCloudstackSource(user);
        }
    }

    private void annotateCloudstackSource(LdapUserResponse user) {
        final UserResponse cloudstackUser = getCloudstackUser(user);
        if (cloudstackUser != null) {
            user.setUserSource(cloudstackUser.getUserSource());
        } else {
            user.setUserSource("");
        }
    }

    private UserResponse getCloudstackUser(LdapUserResponse user) {
        UserResponse returnObject = null;
        final List<UserResponse> cloudstackUsers = getCloudstackUsers();
        if (cloudstackUsers != null) {
            for (final UserResponse cloudstackUser : cloudstackUsers) {
                if (user.getUsername().equals(cloudstackUser.getUsername())) {
                    returnObject = cloudstackUser;
                    if (returnObject.getDomainId() == this.getCurrentDomainId()) {
                        break;
                    }
                }
            }
        }
        return returnObject;
    }

    private void checkFilterMethodType(Type returnType) {
        String msg = null;
        if (returnType instanceof ParameterizedType) {
            ParameterizedType type = (ParameterizedType) returnType;
            if(type.getRawType().equals(List.class)) {
                Type[] typeArguments = type.getActualTypeArguments();
                if (typeArguments.length == 1) {
                    if (typeArguments[0].equals(LdapUserResponse.class)) {
                        // we're good'
                    } else {
                        msg = new String("list of return type contains " + typeArguments[0].getTypeName());
                    }
                } else {
                    msg = String.format("type %s has to the wrong number of arguments", type.getRawType());
                }
            } else {
                msg = String.format("type %s is not a List<>", type.getTypeName());
            }
        } else {
            msg = new String("can't even begin to explain; review your method signature");
        }
        if(msg != null) {
            throw new IllegalArgumentException(msg);
        }
    }

}