/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed 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.keycloak.storage.mongo;

import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.mongo.api.MongoStore;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.UserCredentialStore;
import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.mongo.keycloak.entities.CredentialEntity;
import org.keycloak.models.mongo.keycloak.entities.FederatedIdentityEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.federated.UserAttributeFederatedStorage;
import org.keycloak.storage.federated.UserBrokerLinkFederatedStorage;
import org.keycloak.storage.federated.UserConsentFederatedStorage;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
import org.keycloak.storage.federated.UserGroupMembershipFederatedStorage;
import org.keycloak.storage.federated.UserRequiredActionsFederatedStorage;
import org.keycloak.storage.federated.UserRoleMappingsFederatedStorage;
import org.keycloak.storage.mongo.entity.FederatedUser;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class MongoUserFederatedStorageProvider implements
        UserFederatedStorageProvider,
        UserAttributeFederatedStorage,
        UserBrokerLinkFederatedStorage,
        UserConsentFederatedStorage,
        UserGroupMembershipFederatedStorage,
        UserRequiredActionsFederatedStorage,
        UserRoleMappingsFederatedStorage,
        UserCredentialStore {

    private final MongoStoreInvocationContext invocationContext;
    private final KeycloakSession session;

    public MongoUserFederatedStorageProvider(KeycloakSession session, MongoStoreInvocationContext invocationContext) {
        this.session = session;
        this.invocationContext = invocationContext;
    }

    protected MongoStore getMongoStore() {
        return invocationContext.getMongoStore();
    }


    protected FederatedUser addUserEntity(RealmModel realm, String id) {
        FederatedUser userEntity = new FederatedUser();
        userEntity.setId(id);
        userEntity.setStorageId(StorageId.providerId(id));
        userEntity.setRealmId(realm.getId());

        getMongoStore().insertEntity(userEntity, invocationContext);
        return userEntity;
    }

    protected FederatedUser getUserById(String id) {
        return getMongoStore().loadEntity(FederatedUser.class, id, invocationContext);
    }

    protected FederatedUser findOrCreate(RealmModel realm, String id) {
        FederatedUser user = getUserById(id);
        if (user != null) return user;
        return addUserEntity(realm, id);
    }



    @Override
    public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null) return false;
        CredentialEntity ce = getCredentialEntity(id, userEntity);
        if (ce != null) return getMongoStore().pullItemFromList(userEntity, "credentials", ce, invocationContext);
        return false;
    }

    private CredentialEntity getCredentialEntity(String id, FederatedUser userEntity) {
        CredentialEntity ce = null;
        if (userEntity.getCredentials() != null) {
            for (CredentialEntity credentialEntity : userEntity.getCredentials()) {
                if (credentialEntity.getId().equals(id)) {
                    ce = credentialEntity;
                    break;

                }
            }
        }
        return ce;
    }

    protected CredentialModel toModel(CredentialEntity entity) {
        CredentialModel model = new CredentialModel();
        model.setId(entity.getId());
        model.setHashIterations(entity.getHashIterations());
        model.setType(entity.getType());
        model.setValue(entity.getValue());
        model.setAlgorithm(entity.getAlgorithm());
        model.setSalt(entity.getSalt());
        model.setPeriod(entity.getPeriod());
        model.setCounter(entity.getCounter());
        model.setCreatedDate(entity.getCreatedDate());
        model.setDevice(entity.getDevice());
        model.setDigits(entity.getDigits());
        MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
        model.setConfig(config);
        if (entity.getConfig() != null) {
            config.putAll(entity.getConfig());
        }

        return model;
    }

    @Override
    public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) {
        FederatedUser userEntity = getUserById(id);
        if (userEntity != null && userEntity.getCredentials() != null) {
            for (CredentialEntity credentialEntity : userEntity.getCredentials()) {
                if (credentialEntity.getId().equals(id)) {
                    return toModel(credentialEntity);

                }
            }
        }
        return null;
    }

    @Override
    public List<CredentialModel> getStoredCredentials(RealmModel realm, UserModel user) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity != null && userEntity.getCredentials() != null) {
            List<CredentialModel> list = new LinkedList<>();
            for (CredentialEntity credentialEntity : userEntity.getCredentials()) {
                list.add(toModel(credentialEntity));
            }
            return list;
        }
        return Collections.EMPTY_LIST;
    }

    @Override
    public List<CredentialModel> getStoredCredentialsByType(RealmModel realm, UserModel user, String type) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity != null && userEntity.getCredentials() != null) {
            List<CredentialModel> list = new LinkedList<>();
            for (CredentialEntity credentialEntity : userEntity.getCredentials()) {
                if (type.equals(credentialEntity.getType())) list.add(toModel(credentialEntity));
            }
            return list;
        }
        return Collections.EMPTY_LIST;
    }

    @Override
    public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity != null && userEntity.getCredentials() != null) {
            for (CredentialEntity credentialEntity : userEntity.getCredentials()) {
                if (credentialEntity.getDevice().equals(name) && type.equals(credentialEntity.getType())) {
                    return toModel(credentialEntity);

                }
            }
        }
        return null;
    }

    @Override
    public List<String> getStoredUsers(RealmModel realm, int first, int max) {
        QueryBuilder queryBuilder = new QueryBuilder()
                .and("realmId").is(realm.getId());

        DBObject query = queryBuilder.get();
        List<FederatedUser> users = getMongoStore().loadEntities(FederatedUser.class, query, null, first, max, invocationContext);
        List<String> ids = new LinkedList<>();
        for (FederatedUser user : users) ids.add(user.getId());
        return ids;
    }

    @Override
    public void preRemove(RealmModel realm) {
        DBObject query = new QueryBuilder()
                .and("realmId").is(realm.getId())
                .get();
        getMongoStore().removeEntities(FederatedUser.class, query, true, invocationContext);
    }

    @Override
    public void preRemove(RealmModel realm, UserFederationProviderModel link) {

    }

    @Override
    public void preRemove(RealmModel realm, GroupModel group) {
        DBObject query = new QueryBuilder()
                .and("groupIds").is(group.getId())
                .get();

        DBObject pull = new BasicDBObject("$pull", query);
        getMongoStore().updateEntities(FederatedUser.class, query, pull, invocationContext);

    }

    @Override
    public void preRemove(RealmModel realm, RoleModel role) {
        DBObject query = new QueryBuilder()
                .and("roleIds").is(role.getId())
                .get();

        DBObject pull = new BasicDBObject("$pull", query);
        getMongoStore().updateEntities(FederatedUser.class, query, pull, invocationContext);

    }

    @Override
    public void preRemove(RealmModel realm, ClientModel client) {

    }

    @Override
    public void preRemove(ProtocolMapperModel protocolMapper) {

    }

    @Override
    public void preRemove(RealmModel realm, UserModel user) {
        getMongoStore().removeEntity(FederatedUser.class, user.getId(), invocationContext);

    }

    @Override
    public void preRemove(RealmModel realm, ComponentModel model) {
        if (!model.getProviderType().equals(UserStorageProvider.class.getName())) return;
        DBObject query = new QueryBuilder()
                .and("storageId").is(model.getId())
                .get();
        getMongoStore().removeEntities(FederatedUser.class, query, true, invocationContext);

    }

    @Override
    public void close() {

    }

    @Override
    public void setSingleAttribute(RealmModel realm, UserModel user, String name, String value) {
        FederatedUser userEntity = findOrCreate(realm, user.getId());
        if (userEntity.getAttributes() == null) {
            userEntity.setAttributes(new HashMap<>());
        }

        List<String> attrValues = new LinkedList<>();
        attrValues.add(value);
        userEntity.getAttributes().put(name, attrValues);
        getMongoStore().updateEntity(userEntity, invocationContext);
    }

    @Override
    public void setAttribute(RealmModel realm, UserModel user, String name, List<String> values) {
        FederatedUser userEntity = findOrCreate(realm, user.getId());
        if (userEntity.getAttributes() == null) {
            userEntity.setAttributes(new HashMap<>());
        }

        userEntity.getAttributes().put(name, values);
        getMongoStore().updateEntity(userEntity, invocationContext);

    }

    @Override
    public void removeAttribute(RealmModel realm, UserModel user, String name) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null || userEntity.getAttributes() == null) return;

        userEntity.getAttributes().remove(name);
        getMongoStore().updateEntity(userEntity, invocationContext);
    }

    @Override
    public MultivaluedHashMap<String, String> getAttributes(RealmModel realm, UserModel user) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null || userEntity.getAttributes() == null) return new MultivaluedHashMap<>();
        MultivaluedHashMap<String, String> result = new MultivaluedHashMap<>();
        result.putAll(userEntity.getAttributes());
        return result;
    }

    @Override
    public List<String> getUsersByUserAttribute(RealmModel realm, String name, String value) {
        QueryBuilder queryBuilder = new QueryBuilder()
                .and("realmId").is(realm.getId());
        queryBuilder.and("attributes." + name).is(value);

        List<FederatedUser> users = getMongoStore().loadEntities(FederatedUser.class, queryBuilder.get(), invocationContext);
        List<String> ids = new LinkedList<>();
        for (FederatedUser user : users) ids.add(user.getId());
        return ids;
    }

    @Override
    public String getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm) {
        DBObject query = new QueryBuilder()
                .and("federatedIdentities.identityProvider").is(socialLink.getIdentityProvider())
                .and("federatedIdentities.userId").is(socialLink.getUserId())
                .and("realmId").is(realm.getId())
                .get();
        FederatedUser userEntity = getMongoStore().loadSingleEntity(FederatedUser.class, query, invocationContext);
        return userEntity != null ? userEntity.getId() : null;
    }

    @Override
    public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel socialLink) {
        FederatedUser userEntity = findOrCreate(realm, user.getId());
        FederatedIdentityEntity federatedIdentityEntity = new FederatedIdentityEntity();
        federatedIdentityEntity.setIdentityProvider(socialLink.getIdentityProvider());
        federatedIdentityEntity.setUserId(socialLink.getUserId());
        federatedIdentityEntity.setUserName(socialLink.getUserName().toLowerCase());
        federatedIdentityEntity.setToken(socialLink.getToken());

        getMongoStore().pushItemToList(userEntity, "federatedIdentities", federatedIdentityEntity, true, invocationContext);
    }

    @Override
    public boolean removeFederatedIdentity(RealmModel realm, UserModel user, String socialProvider) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null) return false;

        FederatedIdentityEntity federatedIdentityEntity = findFederatedIdentityLink(userEntity, socialProvider);
        if (federatedIdentityEntity == null) {
            return false;
        }
        return getMongoStore().pullItemFromList(userEntity, "federatedIdentities", federatedIdentityEntity, invocationContext);    }

    private FederatedIdentityEntity findFederatedIdentityLink(FederatedUser userEntity, String identityProvider) {
        List<FederatedIdentityEntity> linkEntities = userEntity.getFederatedIdentities();
        if (linkEntities == null) {
            return null;
        }

        for (FederatedIdentityEntity federatedIdentityEntity : linkEntities) {
            if (federatedIdentityEntity.getIdentityProvider().equals(identityProvider)) {
                return federatedIdentityEntity;
            }
        }
        return null;
    }

    @Override
    public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) {
        FederatedUser userEntity = getUserById(federatedUser.getId());
        if (userEntity == null) return;
        FederatedIdentityEntity federatedIdentityEntity = findFederatedIdentityLink(userEntity, federatedIdentityModel.getIdentityProvider());
        if (federatedIdentityEntity == null) return;
        //pushItemToList updates the whole federatedIdentities array in Mongo so we just need to remove this object from the Java
        //List and pushItemToList will handle the DB update.
        userEntity.getFederatedIdentities().remove(federatedIdentityEntity);
        federatedIdentityEntity.setToken(federatedIdentityModel.getToken());
        getMongoStore().pushItemToList(userEntity, "federatedIdentities", federatedIdentityEntity, true, invocationContext);
    }

    @Override
    public Set<FederatedIdentityModel> getFederatedIdentities(UserModel user, RealmModel realm) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null) return Collections.EMPTY_SET;
        List<FederatedIdentityEntity> linkEntities = userEntity.getFederatedIdentities();

        if (linkEntities == null) {
            return Collections.EMPTY_SET;
        }

        Set<FederatedIdentityModel> result = new HashSet<FederatedIdentityModel>();
        for (FederatedIdentityEntity federatedIdentityEntity : linkEntities) {
            FederatedIdentityModel model = new FederatedIdentityModel(federatedIdentityEntity.getIdentityProvider(),
                    federatedIdentityEntity.getUserId(), federatedIdentityEntity.getUserName(), federatedIdentityEntity.getToken());
            result.add(model);
        }
        return result;
    }

    @Override
    public FederatedIdentityModel getFederatedIdentity(UserModel user, String socialProvider, RealmModel realm) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null) return null;
        FederatedIdentityEntity federatedIdentityEntity = findFederatedIdentityLink(userEntity, socialProvider);

        return federatedIdentityEntity != null ? new FederatedIdentityModel(federatedIdentityEntity.getIdentityProvider(), federatedIdentityEntity.getUserId(),
                federatedIdentityEntity.getUserName(), federatedIdentityEntity.getToken()) : null;
    }

    @Override
    public void addConsent(RealmModel realm, UserModel user, UserConsentModel consent) {
        session.userLocalStorage().addConsent(realm, user, consent);

    }

    @Override
    public UserConsentModel getConsentByClient(RealmModel realm, UserModel user, String clientInternalId) {
        return session.userLocalStorage().getConsentByClient(realm, user, clientInternalId);
    }

    @Override
    public List<UserConsentModel> getConsents(RealmModel realm, UserModel user) {
        return session.userLocalStorage().getConsents(realm, user);
    }

    @Override
    public void updateConsent(RealmModel realm, UserModel user, UserConsentModel consent) {
        session.userLocalStorage().updateConsent(realm, user, consent);

    }

    @Override
    public boolean revokeConsentForClient(RealmModel realm, UserModel user, String clientInternalId) {
        return session.userLocalStorage().revokeConsentForClient(realm, user, clientInternalId);
    }

    @Override
    public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null) return;
        CredentialEntity entity = getCredentialEntity(cred.getId(), userEntity);
        if (entity == null) return;
        toEntity(cred, entity);
        userEntity.getCredentials().remove(entity);

        getMongoStore().pushItemToList(userEntity, "credentials", entity, true, invocationContext);
    }

    private void toEntity(CredentialModel cred, CredentialEntity entity) {
        entity.setAlgorithm(cred.getAlgorithm());
        entity.setCounter(cred.getCounter());
        entity.setCreatedDate(cred.getCreatedDate());
        entity.setDevice(cred.getDevice());
        entity.setDigits(cred.getDigits());
        entity.setHashIterations(cred.getHashIterations());
        entity.setPeriod(cred.getPeriod());
        entity.setSalt(cred.getSalt());
        entity.setType(cred.getType());
        entity.setValue(cred.getValue());

        if (cred.getConfig() == null) entity.setConfig(null);
        else {
            MultivaluedHashMap<String, String> newConfig = new MultivaluedHashMap<>();
            newConfig.putAll(cred.getConfig());
            entity.setConfig(newConfig);
        }
    }

    @Override
    public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) {
        FederatedUser userEntity = findOrCreate(realm, user.getId());
        CredentialEntity entity = new CredentialEntity();
        entity.setId(KeycloakModelUtils.generateId());
        toEntity(cred, entity);
        getMongoStore().pushItemToList(userEntity, "credentials", entity, true, invocationContext);
        cred.setId(entity.getId());
        return cred;
    }

    @Override
    public Set<GroupModel> getGroups(RealmModel realm, UserModel user) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null || userEntity.getGroupIds() == null || userEntity.getGroupIds().isEmpty()) return Collections.EMPTY_SET;
        Set<GroupModel> groups = new HashSet<>();
        for (String groupId : userEntity.getGroupIds()) {
            GroupModel group = session.realms().getGroupById(groupId, realm);
            if (group != null) groups.add(group);
        }

        return groups;
    }

    @Override
    public void joinGroup(RealmModel realm, UserModel user, GroupModel group) {
        FederatedUser userEntity = findOrCreate(realm, user.getId());
        getMongoStore().pushItemToList(userEntity, "groupIds", group.getId(), true, invocationContext);


    }

    @Override
    public void leaveGroup(RealmModel realm, UserModel user, GroupModel group) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null || group == null) return;
        getMongoStore().pullItemFromList(userEntity, "groupIds", group.getId(), invocationContext);


    }

    @Override
    public List<String> getMembership(RealmModel realm, GroupModel group, int firstResult, int max) {
        QueryBuilder queryBuilder = new QueryBuilder()
                .and("realmId").is(realm.getId());
        queryBuilder.and("groupIds").is(group.getId());

        List<FederatedUser> users = getMongoStore().loadEntities(FederatedUser.class, queryBuilder.get(), null, firstResult, max, invocationContext);
        List<String> ids = new LinkedList<>();
        for (FederatedUser user : users) ids.add(user.getId());
        return ids;
    }

    @Override
    public Set<String> getRequiredActions(RealmModel realm, UserModel user) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null || userEntity.getRequiredActions() == null || userEntity.getRequiredActions().isEmpty()) return Collections.EMPTY_SET;
        Set<String> set = new HashSet<>();
        set.addAll(userEntity.getRequiredActions());
        return set;
    }

    @Override
    public void addRequiredAction(RealmModel realm, UserModel user, String action) {
        FederatedUser userEntity = findOrCreate(realm, user.getId());
        getMongoStore().pushItemToList(userEntity, "requiredActions", action, true, invocationContext);

    }

    @Override
    public void removeRequiredAction(RealmModel realm, UserModel user, String action) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null || userEntity.getRequiredActions() == null || userEntity.getRequiredActions().isEmpty()) return;
        getMongoStore().pullItemFromList(userEntity, "requiredActions", action, invocationContext);

    }

    @Override
    public void grantRole(RealmModel realm, UserModel user, RoleModel role) {
        FederatedUser userEntity = findOrCreate(realm, user.getId());
        getMongoStore().pushItemToList(userEntity, "roleIds", role.getId(), true, invocationContext);

    }

    @Override
    public Set<RoleModel> getRoleMappings(RealmModel realm, UserModel user) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null || userEntity.getRoleIds() == null || userEntity.getRoleIds().isEmpty()) return Collections.EMPTY_SET;
        Set<RoleModel> roles = new HashSet<>();
        for (String roleId : userEntity.getRoleIds()) {
            RoleModel role = realm.getRoleById(roleId);
            if (role != null) roles.add(role);
        }
        return roles;
    }

    @Override
    public void deleteRoleMapping(RealmModel realm, UserModel user, RoleModel role) {
        FederatedUser userEntity = getUserById(user.getId());
        if (userEntity == null || userEntity.getRoleIds() == null || userEntity.getRoleIds().isEmpty()) return;
        getMongoStore().pullItemFromList(userEntity, "roleIds", role.getId(), invocationContext);

    }
}
