/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.core.authorization.internal;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;

import de.rcenvironment.core.authorization.api.AuthorizationAccessGroup;
import de.rcenvironment.core.authorization.api.AuthorizationAccessGroupKeyData;
import de.rcenvironment.core.authorization.api.AuthorizationAccessGroupListener;
import de.rcenvironment.core.authorization.api.AuthorizationIdRules;
import de.rcenvironment.core.authorization.api.AuthorizationPermissionSet;
import de.rcenvironment.core.authorization.api.AuthorizationService;
import de.rcenvironment.core.authorization.api.DefaultAuthorizationObjects;
import de.rcenvironment.core.authorization.cryptography.api.CryptographyOperationsProvider;
import de.rcenvironment.core.authorization.cryptography.api.SymmetricKey;
import de.rcenvironment.core.configuration.ConfigurationService;
import de.rcenvironment.core.configuration.SecureStorageImportService;
import de.rcenvironment.core.configuration.SecureStorageSection;
import de.rcenvironment.core.configuration.SecureStorageService;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.common.exception.OperationFailureException;
import de.rcenvironment.toolkit.modules.objectbindings.api.ObjectBindingsConsumer;
import de.rcenvironment.toolkit.modules.objectbindings.api.ObjectBindingsService;
import de.rcenvironment.toolkit.utils.common.IdGenerator;

/**
 * Default {@link AuthorizationService} implementation.
 *
 * @author Robert Mischke
 */
@Component
public class AuthorizationServiceImpl implements AuthorizationService {

    private static final String COLON = ":";

    private static final int PRE_RELEASE_KEY_DATA_LENGTH = 23;

    private static final String SECURE_STORAGE_NODE_ID = "authorization.accessGroups";

    private static final Pattern GROUP_EXPORT_FORMAT_PARSE_PATTERN = Pattern.compile("^(.+):([^:]+):(\\d:[^:]+)$");

    private final DefaultAuthorizationObjects defaultPermissionSets;

    private SortedMap<AuthorizationAccessGroup, AuthorizationAccessGroupKeyData> accessibleGroupKeyData = new TreeMap<>();

    private final List<AuthorizationAccessGroupListener> groupChangeListeners = new ArrayList<>();

    private CryptographyOperationsProvider cryptographyOperations;

    private Object storageLock = new Object(); // used to synchronize groupDataStorageNode, even if it is null

    private SecureStorageSection groupDataStorage; // may be null if the store could not be fetched

    private volatile boolean initializingFromPersistedData;

    @Reference
    private ConfigurationService configurationService;

    @Reference
    private SecureStorageImportService secureStorageImportService;

    private final Log log = LogFactory.getLog(getClass());

    public AuthorizationServiceImpl() {

        defaultPermissionSets = new DefaultAuthorizationObjects() {

            @Override
            public AuthorizationAccessGroup accessGroupPublicInLocalNetwork() {
                return AuthorizationConstants.GROUP_OBJECT_PUBLIC_IN_LOCAL_NETWORK;
            }

            @Override
            public AuthorizationPermissionSet permissionSetPublicInLocalNetwork() {
                return AuthorizationConstants.PERMISSION_SET_PUBLIC_IN_LOCAL_NETWORK;
            }

            @Override
            public AuthorizationPermissionSet permissionSetLocalOnly() {
                return AuthorizationConstants.PERMISSION_SET_LOCAL_ONLY;
            }

        };

    }

    /**
     * OSGi-DS activation method.
     */
    @Activate
    public void activate() {
        initializingFromPersistedData = true;
        processImportFiles();
        restorePersistedGroups();
        initializingFromPersistedData = false;
    }

    @Override
    public boolean isPersistentStorageAvailable() {
        synchronized (storageLock) {
            return groupDataStorage != null;
        }
    }

    @Override
    public DefaultAuthorizationObjects getDefaultAuthorizationObjects() {
        return defaultPermissionSets;
    }

    @Override
    public CryptographyOperationsProvider getCryptographyOperationsProvider() {
        return cryptographyOperations;
    }

    @Override
    public AuthorizationPermissionSet buildPermissionSet(AuthorizationAccessGroup... groups) {
        return new AuthorizationPermissionSetImpl(groups);
    }

    @Override
    public AuthorizationPermissionSet buildPermissionSet(Collection<AuthorizationAccessGroup> groups) {
        return new AuthorizationPermissionSetImpl(groups);
    }

    @Override
    public List<AuthorizationAccessGroup> listAccessibleGroups(boolean includeHardcodedGroups) {
        synchronized (accessibleGroupKeyData) {
            int size = accessibleGroupKeyData.size();
            if (includeHardcodedGroups) {
                size++;
            }
            final ArrayList<AuthorizationAccessGroup> result = new ArrayList<>(size);
            result.addAll(accessibleGroupKeyData.keySet()); // sorted
            if (includeHardcodedGroups) {
                result.add(AuthorizationConstants.GROUP_OBJECT_PUBLIC_IN_LOCAL_NETWORK);
            }
            return result;
        }
    }

    @Override
    public boolean isValidGroupName(String displayName) {
        // simply delegate
        return !AuthorizationIdRules.validateAuthorizationGroupId(displayName).isPresent();
    }

    @Override
    public AuthorizationAccessGroup createLocalGroup(String displayName) throws OperationFailureException {
        Optional<String> validationError = AuthorizationIdRules.validateAuthorizationGroupId(displayName);
        if (validationError.isPresent()) {
            throw new OperationFailureException(validationError.get());
        }
        final String idPart = IdGenerator.secureRandomHexString(GROUP_ID_SUFFIX_LENGTH); // note: secure id may not be necessary
        final AuthorizationAccessGroupKeyData keyMaterial = generateNewKeyData();
        return createAndRegisterGroup(displayName, idPart, keyMaterial);
    }

    @Override
    public AuthorizationAccessGroup representRemoteGroupId(String fullGroupId) throws OperationFailureException {
        fullGroupId = fullGroupId.trim();
        Optional<String> validationError = AuthorizationIdRules.validateAuthorizationGroupFullId(fullGroupId);
        if (validationError.isPresent()) {
            throw new OperationFailureException(validationError.get());
        }
        String[] parts = fullGroupId.split(COLON);
        final String groupName = parts[0];
        final String idPart = parts[1];
        return new AuthorizationAccessGroupImpl(groupName, idPart, fullGroupId, defaultGroupDisplayName(groupName, idPart));
    }

    @Override
    public Set<AuthorizationAccessGroup> representRemoteGroupIds(Collection<String> fullGroupIds) throws OperationFailureException {
        Set<AuthorizationAccessGroup> result = new HashSet<>(fullGroupIds.size());
        for (String fullGroupId : fullGroupIds) {
            result.add(representRemoteGroupId(fullGroupId));
        }
        return result;
    }

    @Override
    public Set<AuthorizationAccessGroup> intersectWithAccessibleGroups(Set<AuthorizationAccessGroup> inputGroups) {
        Set<AuthorizationAccessGroup> result = new HashSet<>(inputGroups.size());

        for (AuthorizationAccessGroup group : inputGroups) {
            if (isGroupAccessible(group)) {
                result.add(group);
            }
        }
        return result;
    }

    @Override
    public AuthorizationAccessGroup findLocalGroupById(String searchString) throws OperationFailureException {
        // local group lists are typically short, so just iterate over them instead of maintaining maps
        synchronized (accessibleGroupKeyData) {
            // look for full id match first
            for (AuthorizationAccessGroup group : accessibleGroupKeyData.keySet()) {
                if (group.getFullId().equals(searchString)) {
                    return group;
                }
            }
            AuthorizationAccessGroup match = null;
            // look for unique match for user-given id or generated suffix
            for (AuthorizationAccessGroup group : accessibleGroupKeyData.keySet()) {
                if (group.getName().equals(searchString) || group.getIdPart().equals(searchString)) {
                    if (match != null) {
                        throw new OperationFailureException(
                            StringUtils.format(" The group id %s is ambiguous - it is matched by local groups %s and %s",
                                searchString, match.getFullId(), group.getFullId()));
                    }
                    match = group;
                }
            }
            return match;
        }
    }

    @Override
    public AuthorizationAccessGroupKeyData getKeyDataForGroup(AuthorizationAccessGroup group) {
        synchronized (accessibleGroupKeyData) {
            return accessibleGroupKeyData.get(group);
        }
    }

    @Override
    public void deleteLocalGroupData(AuthorizationAccessGroup group) {
        List<AuthorizationAccessGroup> immutableGroupList;
        synchronized (accessibleGroupKeyData) {
            accessibleGroupKeyData.remove(group);
            immutableGroupList = createImmutableCopyOfGroupList();
        }

        if (!initializingFromPersistedData) {
            deleteGroupFromPersistence(group);
        } else {
            log.warn("Unexpected state - group deleted during initialization?");
        }

        notifyChangeListeners(immutableGroupList);
    }

    @Override
    public boolean isGroupAccessible(AuthorizationAccessGroup group) {
        synchronized (accessibleGroupKeyData) {
            return accessibleGroupKeyData.containsKey(group);
        }
    }

    @Override
    public String exportToString(AuthorizationAccessGroup group) {
        synchronized (accessibleGroupKeyData) {
            if (!isGroupAccessible(group)) {
                return null;
            }
            final String encodedKeyData = accessibleGroupKeyData.get(group).getEncodedStringForm();
            return group.getFullId() + ID_SEPARATOR + encodedKeyData;
        }
    }

    @Override
    public AuthorizationAccessGroup importFromString(String externalFormat) throws OperationFailureException {
        // TODO (p1) 9.0.0: check for collision with existing groups
        final Matcher matcher = GROUP_EXPORT_FORMAT_PARSE_PATTERN.matcher(externalFormat);
        if (!matcher.matches()) {
            throw new OperationFailureException("Invalid exported group data: " + externalFormat);
        }
        final String keyString = matcher.group(3);
        SymmetricKey groupKey;
        try {
            groupKey = cryptographyOperations.decodeSymmetricKey(keyString);
        } catch (RuntimeException e) {
            throw new OperationFailureException("Invalid exported access key: " + e.toString());
        }
        return createAndRegisterGroup(matcher.group(1), matcher.group(2), new AuthorizationAccessGroupKeyData(groupKey));
    }

    @Reference
    protected void bindSecureStorageService(SecureStorageService secureStorageService) {
        try {
            synchronized (storageLock) {
                groupDataStorage = secureStorageService.getSecureStorageSection(SECURE_STORAGE_NODE_ID);
            }
        } catch (IOException e) {
            log.error("Failed to acquire Secure Storage, authorization information will not be persisted: " + e.toString());
            groupDataStorage = null;
        }
    }

    @Reference
    protected void bindObjectBindingsService(ObjectBindingsService objectBindingsService) {
        objectBindingsService.setConsumer(AuthorizationAccessGroupListener.class,
            new ObjectBindingsConsumer<AuthorizationAccessGroupListener>() {

                @Override
                public void addInstance(AuthorizationAccessGroupListener instance) {
                    addAuthorizationAccessGroupListener(instance);
                }

                @Override
                public void removeInstance(AuthorizationAccessGroupListener instance) {
                    removeAuthorizationAccessGroupListener(instance);
                }
            });
    }

    /**
     * OSGi-DS injection method; public for access by test utilities.
     * 
     * @param newInstance the instance to set
     */
    @Reference
    public void bindCryptographyOperationsProvider(CryptographyOperationsProvider newInstance) {
        this.cryptographyOperations = newInstance;
    }

    @Override
    public boolean isPublicAccessGroup(AuthorizationAccessGroup accessGroup) {
        return accessGroup.getFullId().equals(AuthorizationConstants.GROUP_ID_PUBLIC_IN_LOCAL_NETWORK);
    }

    private boolean persistGroupData(final AuthorizationAccessGroupImpl group, final AuthorizationAccessGroupKeyData keyData) {
        boolean success = true;
        synchronized (storageLock) {
            if (groupDataStorage != null) {
                try {
                    groupDataStorage.store(group.getFullId(), keyData.getEncodedStringForm());
                } catch (OperationFailureException e) {
                    log.error(
                        StringUtils.format("Error while saving new key data for access group %s: %s", group.getFullId(), e.toString()));
                    success = false;
                }
            } else {
                log.warn("Authorization storage is disabled - the new data for group " + group.getFullId()
                    + " will not be saved! Export it and save the exported data manually to prevent losing group access");
            }
        }
        return success;
    }

    private void processImportFiles() {
        synchronized (storageLock) {
            if (groupDataStorage != null) {
                // perform any file-based password imports
                final File importFilesDir =
                    configurationService.getStandardImportDirectory(GROUP_KEY_FILE_IMPORT_SUBDIRECTORY);
                // define mappings of file contents to secure storage entries
                BiFunction<String, String, String> keyMapping = (filename, content) -> {
                    boolean isDeletion = content.startsWith(GROUP_KEY_FILE_IMPORT_DELETION_PREFIX);
                    if (isDeletion) {
                        content = content.substring(GROUP_KEY_FILE_IMPORT_DELETION_PREFIX.length()).trim();
                    }
                    String[] parts = content.split("\\:");
                    if (!isDeletion) {
                        // normal import
                        if (parts.length != 4) {
                            log.warn("Ignoring import file " + filename + " as its content is not a complete group import string");
                            return null;
                        }
                    } else {
                        // deletion
                        if (parts.length < 2) {
                            log.warn("Ignoring import file " + filename
                                + " as it contains a deletion statement, but the following data is not at least a two-part group key");
                            return null;
                        }
                    }
                    // in both cases (import+deletion), return the full group key
                    return parts[0] + COLON + parts[1];
                };
                BiFunction<String, String, String> valueMapping = (filename, content) -> {
                    boolean isDeletion = content.startsWith(GROUP_KEY_FILE_IMPORT_DELETION_PREFIX);
                    if (isDeletion) {
                        // if the content passed the key mapping function above, no more checking is required;
                        // return "null" to delete that key from secure storage
                        return null;
                    }
                    String[] parts = content.split(COLON);
                    // at this point, we are in import (not deletion) mode, so the content should already be validated
                    if (parts.length != 4) {
                        // should never happen
                        throw new IllegalStateException(
                            "Import file content passed key mapping, but had unexpected format in value mapping");
                    }
                    if (!parts[2].equals("1")) {
                        log.warn("Invalid group key data in import file " + filename + " as it uses unknown version " + parts[2]
                            + "; deleting any pre-existing key");
                        // a better behavior may be to simply ignore this import file, but the API does not support this yet;
                        // this case should not happen at this time at all, anyway, so when we add more key versions, this should be adapted
                        return null;
                    }
                    return parts[2] + COLON + parts[3];
                };
                try {
                    secureStorageImportService.processImportDirectory(importFilesDir, SECURE_STORAGE_NODE_ID, keyMapping, valueMapping,
                        true, true);
                } catch (OperationFailureException e) {
                    log.warn(
                        "Error while attempting to import SSH Uplink connection passwords from " + importFilesDir + ": " + e.getMessage());
                }

            } else {
                log.warn("Not processing group key import files as the secure storage is not available");
            }
        }
    }

    private void restorePersistedGroups() {
        synchronized (storageLock) {
            if (groupDataStorage != null) {
                final String[] ids = groupDataStorage.listKeys();
                if (ids != null && ids.length != 0) {
                    log.debug("Restoring " + ids.length + " persisted access group(s) with access keys");
                    List<AuthorizationAccessGroup> immutableGroupList;
                    synchronized (accessibleGroupKeyData) {
                        for (String id : ids) {
                            restorePersistedGroup(id);
                        }
                        immutableGroupList = createImmutableCopyOfGroupList();
                    }
                    notifyChangeListeners(immutableGroupList);
                }
            }
        }
    }

    private void restorePersistedGroup(String id) {
        try {
            String[] idParts = id.split(ID_SEPARATOR);
            if (idParts.length != 2) {
                log.error("Ignoring invalid access group id in secure storage: " + id);
                return;
            }
            final String keyData = groupDataStorage.read(id, null);
            if (keyData == null || keyData.isEmpty()) {
                log.error("Ignoring invalid stored key data for access group " + id);
                return;
            }
            if (keyData.length() == PRE_RELEASE_KEY_DATA_LENGTH) {
                log.warn("Ignoring pre-release 128 bit key for access group " + id
                    + "; delete the related line in the secure storage file "
                    + "<profile>/internal/settings.secure.dat to get rid of this message");
                return;
            }
            SymmetricKey key = cryptographyOperations.decodeSymmetricKey(keyData);
            createAndRegisterGroup(idParts[0], idParts[1], new AuthorizationAccessGroupKeyData(key));
            log.debug("Restored persisted access group " + id);
        } catch (OperationFailureException e) {
            log.error("Error while restoring access group " + id + ": " + e.toString());
            // continue with other entries; no harm in trying, even if all entries are invalid
        }
    }

    private void deleteGroupFromPersistence(AuthorizationAccessGroup group) {
        synchronized (storageLock) {
            if (groupDataStorage != null) {
                try {
                    groupDataStorage.delete(group.getFullId());
                } catch (OperationFailureException e) {
                    log.error("Failed to delete the local access group " + group.getFullId() + " from authorization storage - "
                        + " it may return after a restart if it was present before startup");
                }
            } else {
                log.warn("Authorization storage is disabled - group " + group.getFullId()
                    + " was not deleted from storage and may return after restart if it was present before startup");
            }
        }
    }

    private AuthorizationAccessGroupKeyData generateNewKeyData() throws OperationFailureException {
        return new AuthorizationAccessGroupKeyData(cryptographyOperations.generateSymmetricKey());
    }

    private AuthorizationAccessGroupImpl createAndRegisterGroup(String name, final String idPart,
        final AuthorizationAccessGroupKeyData keyData) {
        final AuthorizationAccessGroupImpl group =
            new AuthorizationAccessGroupImpl(name, idPart, name + ID_SEPARATOR + idPart,
                defaultGroupDisplayName(name, idPart));

        if (!initializingFromPersistedData) {
            // attempt to save
            if (!persistGroupData(group, keyData)) {
                // if saving failed, do not apply this group locally to avoid inconsistencies
                return null;
            }
        }

        // apply
        final List<AuthorizationAccessGroup> immutableGroupList;
        synchronized (accessibleGroupKeyData) {
            accessibleGroupKeyData.put(group, keyData);
            immutableGroupList = createImmutableCopyOfGroupList();
        }

        notifyChangeListeners(immutableGroupList);
        return group;
    }

    @Override
    @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MULTIPLE, // forced line break
        unbind = "removeAuthorizationAccessGroupListener")
    public void addAuthorizationAccessGroupListener(AuthorizationAccessGroupListener listener) {
        synchronized (groupChangeListeners) {
            groupChangeListeners.add(listener);
        }
    }

    @Override
    public void removeAuthorizationAccessGroupListener(AuthorizationAccessGroupListener listener) {
        synchronized (groupChangeListeners) {
            if (!groupChangeListeners.remove(listener)) {
                LogFactory.getLog(getClass()).error("Removed a listener that was not previously registered: " + listener.getClass());
            }
        }
    }

    private List<AuthorizationAccessGroup> createImmutableCopyOfGroupList() {
        return Collections.unmodifiableList(listAccessibleGroups(true));
    }

    private String defaultGroupDisplayName(String name, String idPart) {
        if (idPart != null) {
            return StringUtils.format("%s [%s]", name, idPart);
        } else {
            // should usually not be used in this service, but left in for safe fallback
            return name;
        }
    }

    private void notifyChangeListeners(final List<AuthorizationAccessGroup> immutableGroupList) {
        if (initializingFromPersistedData) {
            return;
        }
        // simply using blocking callbacks as these changes are always triggered locally
        synchronized (groupChangeListeners) {
            log.debug("Notifying " + groupChangeListeners.size()
                + " listener(s) of an authorization group change; new group list entry count: " + immutableGroupList.size());
            for (AuthorizationAccessGroupListener listener : groupChangeListeners) {
                listener.onAvailableAuthorizationAccessGroupsChanged(immutableGroupList);
            }
        }
    }
}
