/*
 * 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.nifi.minifi.properties;

import static java.util.Arrays.asList;
import static org.apache.nifi.minifi.commons.api.MiNiFiProperties.ADDITIONAL_SENSITIVE_PROPERTIES_KEY;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.nifi.minifi.commons.api.MiNiFiProperties;
import org.apache.nifi.properties.ApplicationPropertiesProtector;
import org.apache.nifi.properties.ProtectedProperties;
import org.apache.nifi.properties.SensitivePropertyProtectionException;
import org.apache.nifi.properties.SensitivePropertyProtector;
import org.apache.nifi.properties.SensitivePropertyProvider;
import org.apache.nifi.util.NiFiProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Decorator class for intermediate phase when {@link MiNiFiPropertiesLoader} loads the
 * raw properties file and performs unprotection activities before returning a clean
 * implementation of {@link NiFiProperties}.
 * This encapsulates the sensitive property access logic from external consumers
 * of {@code NiFiProperties}.
 */
public class ProtectedMiNiFiProperties extends NiFiProperties implements ProtectedProperties<NiFiProperties>,
    SensitivePropertyProtector<ProtectedMiNiFiProperties, NiFiProperties> {
    private static final Logger logger = LoggerFactory.getLogger(ProtectedMiNiFiProperties.class);
    public static final List<String> DEFAULT_SENSITIVE_PROPERTIES = new ArrayList<>(asList(
        SECURITY_KEY_PASSWD,
        SECURITY_KEYSTORE_PASSWD,
        SECURITY_TRUSTSTORE_PASSWD,
        SENSITIVE_PROPS_KEY
    ));

    private final SensitivePropertyProtector<ProtectedMiNiFiProperties, NiFiProperties> propertyProtectionDelegate;

    private NiFiProperties applicationProperties;

    public ProtectedMiNiFiProperties() {
        this(new NiFiProperties());
    }

    /**
     * Creates an instance containing the provided {@link NiFiProperties}.
     *
     * @param props the NiFiProperties to contain
     */
    public ProtectedMiNiFiProperties(NiFiProperties props) {
        this.applicationProperties = props;
        this.propertyProtectionDelegate = new ApplicationPropertiesProtector<>(this);
        logger.debug("Loaded {} properties (including {} protection schemes) into ProtectedNiFiProperties", getApplicationProperties()
                .getPropertyKeys().size(), getProtectedPropertyKeys().size());
    }

    /**
     * Creates an instance containing the provided raw {@link Properties}.
     *
     * @param rawProps the Properties to contain
     */
    public ProtectedMiNiFiProperties(Properties rawProps) {
        this(new NiFiProperties(rawProps));
    }

    @Override
    public String getAdditionalSensitivePropertiesKeys() {
        return getProperty(getAdditionalSensitivePropertiesKeysName());
    }

    @Override
    public String getAdditionalSensitivePropertiesKeysName() {
        return ADDITIONAL_SENSITIVE_PROPERTIES_KEY;
    }

    @Override
    public List<String> getDefaultSensitiveProperties() {
        return Stream.of(DEFAULT_SENSITIVE_PROPERTIES,
            Arrays.stream(MiNiFiProperties.values()).filter(MiNiFiProperties::isSensitive).map(MiNiFiProperties::getKey).toList()).flatMap(List::stream).distinct().toList();
    }

    /**
     * Returns the internal representation of the {@link NiFiProperties} -- protected
     * or not as determined by the current state. No guarantee is made to the
     * protection state of these properties. If the internal reference is null, a new
     * {@link NiFiProperties} instance is created.
     *
     * @return the internal properties
     */
    public NiFiProperties getApplicationProperties() {
        if (this.applicationProperties == null) {
            this.applicationProperties = new NiFiProperties();
        }

        return this.applicationProperties;
    }

    @Override
    public NiFiProperties createApplicationProperties(final Properties rawProperties) {
        return new NiFiProperties(rawProperties);
    }

    /**
     * Retrieves the property value for the given property key.
     *
     * @param key the key of property value to lookup
     * @return value of property at given key or null if not found
     */
    @Override
    public String getProperty(String key) {
        return getApplicationProperties().getProperty(key);
    }

    /**
     * Retrieves all known property keys.
     *
     * @return all known property keys
     */
    @Override
    public Set<String> getPropertyKeys() {
        return propertyProtectionDelegate.getPropertyKeys();
    }

    /**
     * Returns the number of properties, excluding protection scheme properties.
     * <p>
     * Example:
     * <p>
     * key: E(value, key)
     * key.protected: aes/gcm/256
     * key2: value2
     * <p>
     * would return size 2
     *
     * @return the count of real properties
     */
    @Override
    public int size() {
        return propertyProtectionDelegate.size();
    }

    @Override
    public Set<String> getPropertyKeysIncludingProtectionSchemes() {
        return propertyProtectionDelegate.getPropertyKeysIncludingProtectionSchemes();
    }

    @Override
    public List<String> getSensitivePropertyKeys() {
        return propertyProtectionDelegate.getSensitivePropertyKeys();
    }

    @Override
    public List<String> getPopulatedSensitivePropertyKeys() {
        return propertyProtectionDelegate.getPopulatedSensitivePropertyKeys();
    }

    @Override
    public boolean hasProtectedKeys() {
        return propertyProtectionDelegate.hasProtectedKeys();
    }

    @Override
    public Map<String, String> getProtectedPropertyKeys() {
        return propertyProtectionDelegate.getProtectedPropertyKeys();
    }

    @Override
    public boolean isPropertySensitive(final String key) {
        return propertyProtectionDelegate.isPropertySensitive(key);
    }

    @Override
    public boolean isPropertyProtected(final String key) {
        return propertyProtectionDelegate.isPropertyProtected(key);
    }

    @Override
    public NiFiProperties getUnprotectedProperties() throws SensitivePropertyProtectionException {
        return propertyProtectionDelegate.getUnprotectedProperties();
    }

    @Override
    public void addSensitivePropertyProvider(final SensitivePropertyProvider sensitivePropertyProvider) {
        propertyProtectionDelegate.addSensitivePropertyProvider(sensitivePropertyProvider);
    }

    public Map<String, String> getUnprotectedPropertiesAsMap() {
        NiFiProperties niFiProperties = propertyProtectionDelegate.getUnprotectedProperties();
        return niFiProperties.getPropertyKeys().stream().collect(Collectors.toMap(Function.identity(), niFiProperties::getProperty));
    }

    /**
     * Returns the number of properties that are marked as protected in the provided {@link NiFiProperties} instance without requiring external creation of a
     * {@link ProtectedMiNiFiProperties} instance.
     *
     * @param plainProperties the instance to count protected properties
     * @return the number of protected properties
     */
    public static int countProtectedProperties(final NiFiProperties plainProperties) {
        return new ProtectedMiNiFiProperties(plainProperties).getProtectedPropertyKeys().size();
    }

    /**
     * Returns the number of properties that are marked as sensitive in the provided {@link NiFiProperties} instance without requiring external creation of a
     * {@link ProtectedMiNiFiProperties} instance.
     *
     * @param plainProperties the instance to count sensitive properties
     * @return the number of sensitive properties
     */
    public static int countSensitiveProperties(final NiFiProperties plainProperties) {
        return new ProtectedMiNiFiProperties(plainProperties).getSensitivePropertyKeys().size();
    }

    @Override
    public String toString() {
        return String.format("%s Size [%d]", getClass().getSimpleName(), size());
    }
}
