package org.keycloak.federation.ldap.idm.query.internal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.naming.directory.SearchControls;

import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.idm.model.LDAPObject;
import org.keycloak.federation.ldap.idm.query.Condition;
import org.keycloak.federation.ldap.idm.query.Sort;
import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.UserFederationMapperModel;

import static java.util.Collections.unmodifiableSet;

/**
 * Default IdentityQuery implementation.
 *
 *
 * @author Shane Bryzak
 */
public class LDAPIdentityQuery {

    private final LDAPFederationProvider ldapFedProvider;

    private int offset;
    private int limit;
    private byte[] paginationContext;
    private final Set<Condition> conditions = new LinkedHashSet<Condition>();
    private final Set<Sort> ordering = new LinkedHashSet<Sort>();

    private final Set<String> searchDns = new LinkedHashSet<String>();
    private final Set<String> returningLdapAttributes = new LinkedHashSet<String>();

    // Contains just those returningLdapAttributes, which are read-only. They will be marked as read-only in returned LDAPObject instances as well
    private final Set<String> returningReadOnlyLdapAttributes = new LinkedHashSet<String>();
    private final Set<String> objectClasses = new LinkedHashSet<String>();

    private final List<UserFederationMapperModel> mappers = new ArrayList<UserFederationMapperModel>();

    private int searchScope = SearchControls.SUBTREE_SCOPE;

    public LDAPIdentityQuery(LDAPFederationProvider ldapProvider) {
        this.ldapFedProvider = ldapProvider;
    }

    public LDAPIdentityQuery where(Condition... condition) {
        this.conditions.addAll(Arrays.asList(condition));
        return this;
    }

    public LDAPIdentityQuery sortBy(Sort... sorts) {
        this.ordering.addAll(Arrays.asList(sorts));
        return this;
    }

    public LDAPIdentityQuery addSearchDns(Collection<String> searchDns) {
        this.searchDns.addAll(searchDns);
        return this;
    }

    public LDAPIdentityQuery addObjectClasses(Collection<String> objectClasses) {
        this.objectClasses.addAll(objectClasses);
        return this;
    }

    public LDAPIdentityQuery addReturningLdapAttribute(String ldapAttributeName) {
        this.returningLdapAttributes.add(ldapAttributeName);
        return this;
    }

    public LDAPIdentityQuery addReturningReadOnlyLdapAttribute(String ldapAttributeName) {
        this.returningReadOnlyLdapAttributes.add(ldapAttributeName);
        return this;
    }

    public LDAPIdentityQuery addMappers(Collection<UserFederationMapperModel> mappers) {
        this.mappers.addAll(mappers);
        return this;
    }

    public LDAPIdentityQuery setSearchScope(int searchScope) {
        this.searchScope = searchScope;
        return this;
    }

    public Set<Sort> getSorting() {
        return unmodifiableSet(this.ordering);
    }

    public Set<String> getSearchDns() {
        return unmodifiableSet(this.searchDns);
    }

    public Set<String> getObjectClasses() {
        return unmodifiableSet(this.objectClasses);
    }

    public Set<String> getReturningLdapAttributes() {
        return unmodifiableSet(this.returningLdapAttributes);
    }

    public Set<String> getReturningReadOnlyLdapAttributes() {
        return unmodifiableSet(this.returningReadOnlyLdapAttributes);
    }

    public List<UserFederationMapperModel> getMappers() {
        return mappers;
    }

    public int getSearchScope() {
        return searchScope;
    }

    public int getLimit() {
        return limit;
    }

    public int getOffset() {
        return offset;
    }

    public byte[] getPaginationContext() {
        return paginationContext;
    }


    public List<LDAPObject> getResultList() {

        // Apply mappers now
        for (UserFederationMapperModel mapperModel : mappers) {
            LDAPFederationMapper fedMapper = ldapFedProvider.getMapper(mapperModel);
            fedMapper.beforeLDAPQuery(mapperModel, this);
        }

        List<LDAPObject> result = new ArrayList<LDAPObject>();

        try {
            for (LDAPObject ldapObject : ldapFedProvider.getLdapIdentityStore().fetchQueryResults(this)) {
                result.add(ldapObject);
            }
        } catch (Exception e) {
            throw new ModelException("LDAP Query failed", e);
        }

        return result;
    }

    public LDAPObject getFirstResult() {
        List<LDAPObject> results = getResultList();

        if (results.isEmpty()) {
            return null;
        } else if (results.size() == 1) {
            return results.get(0);
        } else {
            throw new ModelDuplicateException("Error - multiple LDAP objects found but expected just one");
        }
    }

    public int getResultCount() {
        return ldapFedProvider.getLdapIdentityStore().countQueryResults(this);
    }

    public LDAPIdentityQuery setOffset(int offset) {
        this.offset = offset;
        return this;
    }

    public LDAPIdentityQuery setLimit(int limit) {
        this.limit = limit;
        return this;
    }

    public LDAPIdentityQuery setPaginationContext(byte[] paginationContext) {
        this.paginationContext = paginationContext;
        return this;
    }

    public Set<Condition> getConditions() {
        return this.conditions;
    }

}
