/*-
 * ---license-start
 * keycloak-config-cli
 * ---
 * Copyright (C) 2017 - 2020 adorsys GmbH & Co. KG @ https://adorsys.de
 * ---
 * 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.
 * ---license-end
 */

package de.adorsys.keycloak.config.service;

import de.adorsys.keycloak.config.model.RealmImport;
import de.adorsys.keycloak.config.properties.ImportConfigProperties;
import de.adorsys.keycloak.config.repository.GroupRepository;
import de.adorsys.keycloak.config.repository.RoleRepository;
import de.adorsys.keycloak.config.repository.UserRepository;
import de.adorsys.keycloak.config.util.CloneUtil;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;

@Service
public class UserImportService {
    private static final Logger logger = LoggerFactory.getLogger(UserImportService.class);

    private static final String[] IGNORED_PROPERTIES_FOR_UPDATE = {"realmRoles", "clientRoles"};

    private final UserRepository userRepository;
    private final RoleRepository roleRepository;
    private final GroupRepository groupRepository;

    private final ImportConfigProperties importConfigProperties;

    @Autowired
    public UserImportService(
            UserRepository userRepository,
            RoleRepository roleRepository,
            GroupRepository groupRepository, ImportConfigProperties importConfigProperties) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
        this.groupRepository = groupRepository;
        this.importConfigProperties = importConfigProperties;
    }

    public void doImport(RealmImport realmImport) {
        List<UserRepresentation> users = realmImport.getUsers();

        if (users == null) {
            return;
        }

        if (users.isEmpty()) {
            logger.warn("Purging users isn't supported in keycloak-config-cli!");
            return;
        }

        Consumer<UserRepresentation> loop = user -> importUser(realmImport.getRealm(), user);
        if (importConfigProperties.isParallel()) {
            users.parallelStream().forEach(loop);
        } else {
            users.forEach(loop);
        }
    }

    private void importUser(String realm, UserRepresentation user) {
        UserImport userImport = new UserImport(realm, user);
        userImport.importUser();
    }

    private class UserImport {
        private final String realm;
        private final UserRepresentation userToImport;
        private final String username;

        private UserImport(String realm, UserRepresentation userToImport) {
            this.realm = realm;
            this.userToImport = userToImport;
            this.username = userToImport.getUsername();
        }

        public void importUser() {
            Optional<UserRepresentation> maybeUser = userRepository.tryToFindUser(realm, username);

            if (maybeUser.isPresent()) {
                updateUser(maybeUser.get());
            } else {
                logger.debug("Create user '{}' in realm '{}'", username, realm);
                userRepository.create(realm, userToImport);
            }

            handleRealmRoles();
            handleClientRoles();
            handleGroups();
        }

        private void updateUser(UserRepresentation existingUser) {
            UserRepresentation patchedUser = CloneUtil.deepPatch(existingUser, userToImport, IGNORED_PROPERTIES_FOR_UPDATE);
            if (userToImport.getAttributes() != null) {
                patchedUser.setAttributes(userToImport.getAttributes());
            }

            if (!CloneUtil.deepEquals(existingUser, patchedUser, "access")) {
                logger.debug("Update user '{}' in realm '{}'", username, realm);
                userRepository.updateUser(realm, patchedUser);
            } else {
                logger.debug("No need to update user '{}' in realm '{}'", username, realm);
            }
        }

        private void handleGroups() {
            List<String> userGroupsToUpdate = userToImport.getGroups();
            if (userGroupsToUpdate == null) {
                userGroupsToUpdate = Collections.emptyList();
            }

            List<String> existingUserGroups = userRepository.getGroups(realm, userToImport)
                    .stream().map(GroupRepresentation::getName).collect(Collectors.toList());

            handleGroupsToBeAdded(userGroupsToUpdate, existingUserGroups);
            handleGroupsToBeRemoved(userGroupsToUpdate, existingUserGroups);
        }

        private void handleGroupsToBeAdded(List<String> userGroupsToUpdate, List<String> existingUserGroupsToUpdate) {
            List<String> groupsToAdd = searchForMissing(userGroupsToUpdate, existingUserGroupsToUpdate);
            if (groupsToAdd.isEmpty()) return;

            List<GroupRepresentation> groups = groupRepository.searchGroups(realm, groupsToAdd);

            logger.debug("Add groups {} to user '{}' in realm '{}'", groupsToAdd, username, realm);

            groupRepository.addGroupsToUser(realm, username, groups);
        }

        private void handleGroupsToBeRemoved(List<String> userGroupsToUpdate, List<String> existingUserGroupsToUpdate) {
            List<String> groupsToDelete = searchForMissing(existingUserGroupsToUpdate, userGroupsToUpdate);
            if (groupsToDelete.isEmpty()) return;

            List<GroupRepresentation> groups = groupRepository.searchGroups(realm, groupsToDelete);

            logger.debug("Remove groups {} from user '{}' in realm '{}'", groupsToDelete, username, realm);

            groupRepository.removeGroupsFromUser(realm, username, groups);
        }

        private void handleRealmRoles() {
            List<String> usersRealmLevelRolesToUpdate = userToImport.getRealmRoles();
            if (usersRealmLevelRolesToUpdate == null) {
                usersRealmLevelRolesToUpdate = Collections.emptyList();
            }

            List<String> existingUsersRealmLevelRoles = roleRepository.getUserRealmLevelRoles(realm, username);

            handleRolesToBeAdded(usersRealmLevelRolesToUpdate, existingUsersRealmLevelRoles);
            handleRolesToBeRemoved(usersRealmLevelRolesToUpdate, existingUsersRealmLevelRoles);
        }

        private void handleRolesToBeAdded(List<String> usersRealmLevelRolesToUpdate, List<String> existingUsersRealmLevelRoles) {
            List<String> rolesToAdd = searchForMissing(usersRealmLevelRolesToUpdate, existingUsersRealmLevelRoles);
            if (rolesToAdd.isEmpty()) return;

            List<RoleRepresentation> realmRoles = roleRepository.searchRealmRoles(realm, rolesToAdd);

            logger.debug("Add realm-level roles {} to user '{}' in realm '{}'", rolesToAdd, username, realm);

            roleRepository.addRealmRolesToUser(realm, username, realmRoles);
        }

        private void handleRolesToBeRemoved(List<String> usersRealmLevelRolesToUpdate, List<String> existingUsersRealmLevelRoles) {
            List<String> rolesToDelete = searchForMissing(existingUsersRealmLevelRoles, usersRealmLevelRolesToUpdate);
            if (rolesToDelete.isEmpty()) return;

            List<RoleRepresentation> realmRoles = roleRepository.searchRealmRoles(realm, rolesToDelete);

            logger.debug("Remove realm-level roles {} from user '{}' in realm '{}'", rolesToDelete, username, realm);

            roleRepository.removeRealmRolesForUser(realm, username, realmRoles);
        }

        private void handleClientRoles() {
            Map<String, List<String>> clientRolesToImport = userToImport.getClientRoles();
            if (clientRolesToImport == null) return;

            for (Map.Entry<String, List<String>> clientRoles : clientRolesToImport.entrySet()) {
                setupClientRoles(clientRoles);
            }
        }

        private void setupClientRoles(Map.Entry<String, List<String>> clientRoles) {
            String clientId = clientRoles.getKey();

            ClientRoleImport clientRoleImport = new ClientRoleImport(clientId);
            clientRoleImport.importClientRoles();
        }

        private List<String> searchForMissing(List<String> searchedFor, List<String> trawled) {
            return searchedFor.stream().filter(role -> !trawled.contains(role)).collect(Collectors.toList());
        }

        private class ClientRoleImport {
            private final String clientId;
            private final List<String> existingClientLevelRoles;
            private final List<String> clientRolesToImport;

            private ClientRoleImport(String clientId) {
                this.clientId = clientId;
                this.existingClientLevelRoles = roleRepository.getUserClientLevelRoles(realm, username, clientId);

                Map<String, List<String>> clientsRolesToImport = userToImport.getClientRoles();
                this.clientRolesToImport = clientsRolesToImport.get(clientId);
            }

            public void importClientRoles() {
                handleClientRolesToBeAdded();
                handleClientRolesToBeRemoved();
            }

            private void handleClientRolesToBeAdded() {
                List<String> clientRolesToAdd = searchForMissing(clientRolesToImport, existingClientLevelRoles);
                if (clientRolesToAdd.isEmpty()) return;

                List<RoleRepresentation> foundClientRoles = roleRepository.searchClientRoles(realm, clientId, clientRolesToAdd);

                logger.debug("Add client-level roles {} for client '{}' to user '{}' in realm '{}'", clientRolesToAdd, clientId, username, realm);

                roleRepository.addClientRolesToUser(realm, username, clientId, foundClientRoles);
            }

            private void handleClientRolesToBeRemoved() {
                List<String> clientRolesToRemove = searchForMissing(existingClientLevelRoles, clientRolesToImport);
                if (clientRolesToRemove.isEmpty()) return;

                List<RoleRepresentation> foundClientRoles = roleRepository.searchClientRoles(realm, clientId, clientRolesToRemove);

                logger.debug("Remove client-level roles {} for client '{}' from user '{}' in realm '{}'", clientRolesToRemove, clientId, username, realm);

                roleRepository.removeClientRolesForUser(realm, username, clientId, foundClientRoles);
            }
        }
    }
}