/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.common.settings;

import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.apache.lucene.search.spell.LevensteinDistance;
import org.apache.lucene.util.CollectionUtil;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.regex.Regex;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * A basic setting service that can be used for per-index and per-cluster settings.
 * This service offers transactional application of updates settings.
 */
public abstract class AbstractScopedSettings extends AbstractComponent {
    public static final String ARCHIVED_SETTINGS_PREFIX = "archived.";
    private Settings lastSettingsApplied = Settings.EMPTY;
    private final List<SettingUpdater<?>> settingUpdaters = new CopyOnWriteArrayList<>();
    private final Map<String, Setting<?>> complexMatchers;
    private final Map<String, Setting<?>> keySettings;
    private final Setting.Property scope;
    private static final Pattern KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])*[-\\w]+$");
    private static final Pattern GROUP_KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])+$");
    private static final Pattern AFFIX_KEY_PATTERN = Pattern.compile("^(?:[-\\w]+[.])+[*](?:[.][-\\w]+)+$");

    protected AbstractScopedSettings(Settings settings, Set<Setting<?>> settingsSet, Setting.Property scope) {
        super(settings);
        this.lastSettingsApplied = Settings.EMPTY;
        this.scope = scope;
        Map<String, Setting<?>> complexMatchers = new HashMap<>();
        Map<String, Setting<?>> keySettings = new HashMap<>();
        for (Setting<?> setting : settingsSet) {
            if (setting.getProperties().contains(scope) == false) {
                throw new IllegalArgumentException("Setting must be a " + scope + " setting but has: " + setting.getProperties());
            }
            validateSettingKey(setting);

            if (setting.hasComplexMatcher()) {
                Setting<?> overlappingSetting = findOverlappingSetting(setting, complexMatchers);
                if (overlappingSetting != null) {
                    throw new IllegalArgumentException("complex setting key: [" + setting.getKey() + "] overlaps existing setting key: [" +
                        overlappingSetting.getKey() + "]");
                }
                complexMatchers.putIfAbsent(setting.getKey(), setting);
            } else {
                keySettings.putIfAbsent(setting.getKey(), setting);
            }
        }
        this.complexMatchers = Collections.unmodifiableMap(complexMatchers);
        this.keySettings = Collections.unmodifiableMap(keySettings);
    }

    protected void validateSettingKey(Setting setting) {
        if (isValidKey(setting.getKey()) == false && (setting.isGroupSetting() && isValidGroupKey(setting.getKey())
            || isValidAffixKey(setting.getKey())) == false) {
            throw new IllegalArgumentException("illegal settings key: [" + setting.getKey() + "]");
        }
    }

    protected AbstractScopedSettings(Settings nodeSettings, Settings scopeSettings, AbstractScopedSettings other) {
        super(nodeSettings);
        this.lastSettingsApplied = scopeSettings;
        this.scope = other.scope;
        complexMatchers = other.complexMatchers;
        keySettings = other.keySettings;
        settingUpdaters.addAll(other.settingUpdaters);
    }

    /**
     * Returns <code>true</code> iff the given key is a valid settings key otherwise <code>false</code>
     */
    public static boolean isValidKey(String key) {
        return KEY_PATTERN.matcher(key).matches();
    }

    private static boolean isValidGroupKey(String key) {
        return GROUP_KEY_PATTERN.matcher(key).matches();
    }

    // pkg private for tests
    static boolean isValidAffixKey(String key) {
        return AFFIX_KEY_PATTERN.matcher(key).matches();
    }

    public Setting.Property getScope() {
        return this.scope;
    }

    /**
     * Validates the given settings by running it through all update listeners without applying it. This
     * method will not change any settings but will fail if any of the settings can't be applied.
     */
    public synchronized Settings validateUpdate(Settings settings) {
        final Settings current = Settings.builder().put(this.settings).put(settings).build();
        final Settings previous = Settings.builder().put(this.settings).put(this.lastSettingsApplied).build();
        List<RuntimeException> exceptions = new ArrayList<>();
        for (SettingUpdater<?> settingUpdater : settingUpdaters) {
            try {
                // ensure running this through the updater / dynamic validator
                // don't check if the value has changed we wanna test this anyways
                settingUpdater.getValue(current, previous);
            } catch (RuntimeException ex) {
                exceptions.add(ex);
                logger.debug((Supplier<?>) () -> new ParameterizedMessage("failed to prepareCommit settings for [{}]", settingUpdater), ex);
            }
        }
        // here we are exhaustive and record all settings that failed.
        ExceptionsHelper.rethrowAndSuppress(exceptions);
        return current;
    }

    /**
     * Applies the given settings to all the settings consumers or to none of them. The settings
     * will be merged with the node settings before they are applied while given settings override existing node
     * settings.
     * @param newSettings the settings to apply
     * @return the unmerged applied settings
    */
    public synchronized Settings applySettings(Settings newSettings) {
        if (lastSettingsApplied != null && newSettings.equals(lastSettingsApplied)) {
            // nothing changed in the settings, ignore
            return newSettings;
        }
        final Settings current = Settings.builder().put(this.settings).put(newSettings).build();
        final Settings previous = Settings.builder().put(this.settings).put(this.lastSettingsApplied).build();
        try {
            List<Runnable> applyRunnables = new ArrayList<>();
            for (SettingUpdater<?> settingUpdater : settingUpdaters) {
                try {
                    applyRunnables.add(settingUpdater.updater(current, previous));
                } catch (Exception ex) {
                    logger.warn(
                        (Supplier<?>) () -> new ParameterizedMessage("failed to prepareCommit settings for [{}]", settingUpdater), ex);
                    throw ex;
                }
            }
            for (Runnable settingUpdater : applyRunnables) {
                settingUpdater.run();
            }
        } catch (Exception ex) {
            logger.warn("failed to apply settings", ex);
            throw ex;
        } finally {
        }
        return lastSettingsApplied = newSettings;
    }

    /**
     * Adds a settings consumer with a predicate that is only evaluated at update time.
     * <p>
     * Note: Only settings registered in {@link SettingsModule} can be changed dynamically.
     * </p>
     * @param validator an additional validator that is only applied to updates of this setting.
     *                  This is useful to add additional validation to settings at runtime compared to at startup time.
     */
    public synchronized <T> void addSettingsUpdateConsumer(Setting<T> setting, Consumer<T> consumer, Consumer<T> validator) {
        if (setting != get(setting.getKey())) {
            throw new IllegalArgumentException("Setting is not registered for key [" + setting.getKey() + "]");
        }
        addSettingsUpdater(setting.newUpdater(consumer, logger, validator));
    }

    /**
     * Adds a settings consumer for affix settings. Affix settings have a namespace associated to it that needs to be available to the
     * consumer in order to be processed correctly.
     */
    public synchronized <T> void addAffixUpdateConsumer(Setting.AffixSetting<T> setting,  BiConsumer<String, T> consumer,
                                                        BiConsumer<String, T> validator) {
        final Setting<?> registeredSetting = this.complexMatchers.get(setting.getKey());
        if (setting != registeredSetting) {
            throw new IllegalArgumentException("Setting is not registered for key [" + setting.getKey() + "]");
        }
        addSettingsUpdater(setting.newAffixUpdater(consumer, logger, validator));
    }

    synchronized void addSettingsUpdater(SettingUpdater<?> updater) {
        this.settingUpdaters.add(updater);
    }

    /**
     * Adds a settings consumer that accepts the values for two settings. The consumer if only notified if one or both settings change.
     * <p>
     * Note: Only settings registered in {@link SettingsModule} can be changed dynamically.
     * </p>
     * This method registers a compound updater that is useful if two settings are depending on each other. The consumer is always provided
     * with both values even if only one of the two changes.
     */
    public synchronized <A, B> void addSettingsUpdateConsumer(Setting<A> a, Setting<B> b, BiConsumer<A, B> consumer) {
        if (a != get(a.getKey())) {
            throw new IllegalArgumentException("Setting is not registered for key [" + a.getKey() + "]");
        }
        if (b != get(b.getKey())) {
            throw new IllegalArgumentException("Setting is not registered for key [" + b.getKey() + "]");
        }
        addSettingsUpdater(Setting.compoundUpdater(consumer, a, b, logger));
    }

    /**
     * Adds a settings consumer.
     * <p>
     * Note: Only settings registered in {@link org.elasticsearch.cluster.ClusterModule} can be changed dynamically.
     * </p>
     */
    public synchronized <T> void addSettingsUpdateConsumer(Setting<T> setting, Consumer<T> consumer) {
       addSettingsUpdateConsumer(setting, consumer, (s) -> {});
    }

    /**
     * Validates that all settings in the builder are registered and valid
     */
    public final void validate(Settings.Builder settingsBuilder) {
        validate(settingsBuilder.build());
    }

    /**
     * * Validates that all given settings are registered and valid
     */
    public final void validate(Settings settings) {
        List<RuntimeException> exceptions = new ArrayList<>();
        for (String key : settings.keySet()) { // settings iterate in deterministic fashion
            try {
                validate(key, settings);
            } catch (RuntimeException ex) {
                exceptions.add(ex);
            }
        }
        ExceptionsHelper.rethrowAndSuppress(exceptions);
    }


    /**
     * Validates that the setting is valid
     */
    public final void validate(String key, Settings settings) {
        Setting setting = get(key);
        if (setting == null) {
            LevensteinDistance ld = new LevensteinDistance();
            List<Tuple<Float, String>> scoredKeys = new ArrayList<>();
            for (String k : this.keySettings.keySet()) {
                float distance = ld.getDistance(key, k);
                if (distance > 0.7f) {
                    scoredKeys.add(new Tuple<>(distance, k));
                }
            }
            CollectionUtil.timSort(scoredKeys, (a,b) -> b.v1().compareTo(a.v1()));
            String msgPrefix = "unknown setting";
            SecureSettings secureSettings = settings.getSecureSettings();
            if (secureSettings != null && settings.getSecureSettings().getSettingNames().contains(key)) {
                msgPrefix = "unknown secure setting";
            }
            String msg = msgPrefix + " [" + key + "]";
            List<String> keys = scoredKeys.stream().map((a) -> a.v2()).collect(Collectors.toList());
            if (keys.isEmpty() == false) {
                msg += " did you mean " + (keys.size() == 1 ? "[" + keys.get(0) + "]": "any of " + keys.toString()) + "?";
            } else {
                msg += " please check that any required plugins are installed, or check the breaking changes documentation for removed " +
                    "settings";
            }
            throw new IllegalArgumentException(msg);
        }
        setting.get(settings);
    }

    /**
     * Transactional interface to update settings.
     * @see Setting
     * @param <T> the type of the value of the setting
     */
    public interface SettingUpdater<T> {

        /**
         * Returns true if this updaters setting has changed with the current update
         * @param current the current settings
         * @param previous the previous setting
         * @return true if this updaters setting has changed with the current update
         */
        boolean hasChanged(Settings current, Settings previous);

        /**
         * Returns the instance value for the current settings. This method is stateless and idempotent.
         * This method will throw an exception if the source of this value is invalid.
         */
        T getValue(Settings current, Settings previous);

        /**
         * Applies the given value to the updater. This methods will actually run the update.
         */
        void apply(T value, Settings current, Settings previous);

        /**
         * Updates this updaters value if it has changed.
         * @return <code>true</code> iff the value has been updated.
         */
        default boolean apply(Settings current, Settings previous) {
            if (hasChanged(current, previous)) {
                T value = getValue(current, previous);
                apply(value, current, previous);
                return true;
            }
            return false;
        }

        /**
         * Returns a callable runnable that calls {@link #apply(Object, Settings, Settings)} if the settings
         * actually changed. This allows to defer the update to a later point in time while keeping type safety.
         * If the value didn't change the returned runnable is a noop.
         */
        default Runnable updater(Settings current, Settings previous) {
            if (hasChanged(current, previous)) {
                T value = getValue(current, previous);
                return () -> { apply(value, current, previous);};
            }
            return () -> {};
        }
    }

    /**
     * Returns the {@link Setting} for the given key or <code>null</code> if the setting can not be found.
     */
    public Setting<?> get(String key) {
        Setting<?> setting = keySettings.get(key);
        if (setting != null) {
            return setting;
        }
        for (Map.Entry<String, Setting<?>> entry : complexMatchers.entrySet()) {
            if (entry.getValue().match(key)) {
                assert assertMatcher(key, 1);
                return entry.getValue().getConcreteSetting(key);
            }
        }
        return null;
    }

    private boolean assertMatcher(String key, int numComplexMatchers) {
        List<Setting<?>> list = new ArrayList<>();
        for (Map.Entry<String, Setting<?>> entry : complexMatchers.entrySet()) {
            if (entry.getValue().match(key)) {
                list.add(entry.getValue().getConcreteSetting(key));
            }
        }
        assert list.size() == numComplexMatchers : "Expected " + numComplexMatchers + " complex matchers to match key [" +
            key + "] but got: "  + list.toString();
        return true;
    }

    /**
     * Returns <code>true</code> if the setting for the given key is dynamically updateable. Otherwise <code>false</code>.
     */
    public boolean isDynamicSetting(String key) {
        final Setting<?> setting = get(key);
        return setting != null && setting.isDynamic();
    }

    /**
     * Returns <code>true</code> if the setting for the given key is final. Otherwise <code>false</code>.
     */
    public boolean isFinalSetting(String key) {
        final Setting<?> setting = get(key);
        return setting != null && setting.isFinal();
    }

    /**
     * Returns a settings object that contains all settings that are not
     * already set in the given source. The diff contains either the default value for each
     * setting or the settings value in the given default settings.
     */
    public Settings diff(Settings source, Settings defaultSettings) {
        Settings.Builder builder = Settings.builder();
        for (Setting<?> setting : keySettings.values()) {
            setting.diff(builder, source, defaultSettings);
        }
        for (Setting<?> setting : complexMatchers.values()) {
            setting.diff(builder, source, defaultSettings);
        }
        return builder.build();
    }

    /**
     * Returns the value for the given setting.
     */
    public <T> T get(Setting<T> setting) {
        if (setting.getProperties().contains(scope) == false) {
            throw new IllegalArgumentException("settings scope doesn't match the setting scope [" + this.scope + "] not in [" +
                setting.getProperties() + "]");
        }
        if (get(setting.getKey()) == null) {
            throw new IllegalArgumentException("setting " + setting.getKey() + " has not been registered");
        }
        return setting.get(this.lastSettingsApplied, settings);
    }

    /**
     * Updates a target settings builder with new, updated or deleted settings from a given settings builder.
     * <p>
     * Note: This method will only allow updates to dynamic settings. if a non-dynamic setting is updated an
     * {@link IllegalArgumentException} is thrown instead.
     * </p>
     *
     * @param toApply the new settings to apply
     * @param target the target settings builder that the updates are applied to. All keys that have explicit null value in toApply will be
     *        removed from this builder
     * @param updates a settings builder that holds all updates applied to target
     * @param type a free text string to allow better exceptions messages
     * @return <code>true</code> if the target has changed otherwise <code>false</code>
     */
    public boolean updateDynamicSettings(Settings toApply, Settings.Builder target, Settings.Builder updates, String type) {
        return updateSettings(toApply, target, updates, type, true);
    }

    /**
     * Updates a target settings builder with new, updated or deleted settings from a given settings builder.
     *
     * @param toApply the new settings to apply
     * @param target the target settings builder that the updates are applied to. All keys that have explicit null value in toApply will be
     *        removed from this builder
     * @param updates a settings builder that holds all updates applied to target
     * @param type a free text string to allow better exceptions messages
     * @return <code>true</code> if the target has changed otherwise <code>false</code>
     */
    public boolean updateSettings(Settings toApply, Settings.Builder target, Settings.Builder updates, String type) {
        return updateSettings(toApply, target, updates, type, false);
    }

    /**
     * Updates a target settings builder with new, updated or deleted settings from a given settings builder.
     *
     * @param toApply the new settings to apply
     * @param target the target settings builder that the updates are applied to. All keys that have explicit null value in toApply will be
     *        removed from this builder
     * @param updates a settings builder that holds all updates applied to target
     * @param type a free text string to allow better exceptions messages
     * @param onlyDynamic if <code>false</code> all settings are updated otherwise only dynamic settings are updated. if set to
     *        <code>true</code> and a non-dynamic setting is updated an exception is thrown.
     * @return <code>true</code> if the target has changed otherwise <code>false</code>
     */
    private boolean updateSettings(Settings toApply, Settings.Builder target, Settings.Builder updates, String type, boolean onlyDynamic) {
        boolean changed = false;
        final Set<String> toRemove = new HashSet<>();
        Settings.Builder settingsBuilder = Settings.builder();
        final Predicate<String> canUpdate = (key) -> (
            isFinalSetting(key) == false && // it's not a final setting
                ((onlyDynamic == false && get(key) != null) || isDynamicSetting(key)));
        final Predicate<String> canRemove = (key) ->(// we can delete if
            isFinalSetting(key) == false && // it's not a final setting
                (onlyDynamic && isDynamicSetting(key)  // it's a dynamicSetting and we only do dynamic settings
                || get(key) == null && key.startsWith(ARCHIVED_SETTINGS_PREFIX) // the setting is not registered AND it's been archived
                || (onlyDynamic == false && get(key) != null))); // if it's not dynamic AND we have a key
        for (Map.Entry<String, String> entry : toApply.getAsMap().entrySet()) {
            if (entry.getValue() == null && (canRemove.test(entry.getKey()) || entry.getKey().endsWith("*"))) {
                // this either accepts null values that suffice the canUpdate test OR wildcard expressions (key ends with *)
                // we don't validate if there is any dynamic setting with that prefix yet we could do in the future
                toRemove.add(entry.getKey());
                // we don't set changed here it's set after we apply deletes below if something actually changed
            } else if (entry.getValue() != null && canUpdate.test(entry.getKey())) {
                validate(entry.getKey(), toApply);
                settingsBuilder.put(entry.getKey(), entry.getValue());
                updates.put(entry.getKey(), entry.getValue());
                changed = true;
            } else {
                if (isFinalSetting(entry.getKey())) {
                    throw new IllegalArgumentException("final " + type + " setting [" + entry.getKey() + "], not updateable");
                } else {
                    throw new IllegalArgumentException(type + " setting [" + entry.getKey() + "], not dynamically updateable");
                }
            }
        }
        changed |= applyDeletes(toRemove, target, canRemove);
        target.put(settingsBuilder.build());
        return changed;
    }

    private static boolean applyDeletes(Set<String> deletes, Settings.Builder builder, Predicate<String> canRemove) {
        boolean changed = false;
        for (String entry : deletes) {
            Set<String> keysToRemove = new HashSet<>();
            Set<String> keySet = builder.internalMap().keySet();
            for (String key : keySet) {
                if (Regex.simpleMatch(entry, key) && canRemove.test(key)) {
                    // we have to re-check with canRemove here since we might have a wildcard expression foo.* that matches
                    // dynamic as well as static settings if that is the case we might remove static settings since we resolve the
                    // wildcards late
                    keysToRemove.add(key);
                }
            }
            for (String key : keysToRemove) {
                builder.remove(key);
                changed = true;
            }
        }
        return changed;
    }

    private static Setting<?> findOverlappingSetting(Setting<?> newSetting, Map<String, Setting<?>> complexMatchers) {
        assert newSetting.hasComplexMatcher();
        if (complexMatchers.containsKey(newSetting.getKey())) {
            // we return null here because we use a putIfAbsent call when inserting into the map, so if it exists then we already checked
            // the setting to make sure there are no overlapping settings.
            return null;
        }

        for (Setting<?> existingSetting : complexMatchers.values()) {
            if (newSetting.match(existingSetting.getKey()) || existingSetting.match(newSetting.getKey())) {
                return existingSetting;
            }
        }
        return null;
    }

    /**
     * Archives invalid or unknown settings. Any setting that is not recognized or fails validation
     * will be archived. This means the setting is prefixed with {@value ARCHIVED_SETTINGS_PREFIX}
     * and remains in the settings object. This can be used to detect invalid settings via APIs.
     *
     * @param settings        the {@link Settings} instance to scan for unknown or invalid settings
     * @param unknownConsumer callback on unknown settings (consumer receives unknown key and its
     *                        associated value)
     * @param invalidConsumer callback on invalid settings (consumer receives invalid key, its
     *                        associated value and an exception)
     * @return a {@link Settings} instance with the unknown or invalid settings archived
     */
    public Settings archiveUnknownOrInvalidSettings(
        final Settings settings,
        final Consumer<Map.Entry<String, String>> unknownConsumer,
        final BiConsumer<Map.Entry<String, String>, IllegalArgumentException> invalidConsumer) {
        Settings.Builder builder = Settings.builder();
        boolean changed = false;
        for (Map.Entry<String, String> entry : settings.getAsMap().entrySet()) {
            try {
                Setting<?> setting = get(entry.getKey());
                if (setting != null) {
                    setting.get(settings);
                    builder.put(entry.getKey(), entry.getValue());
                } else {
                    if (entry.getKey().startsWith(ARCHIVED_SETTINGS_PREFIX) || isPrivateSetting(entry.getKey())) {
                        builder.put(entry.getKey(), entry.getValue());
                    } else {
                        changed = true;
                        unknownConsumer.accept(entry);
                        /*
                         * We put them back in here such that tools can check from the outside if there are any indices with invalid
                         * settings. The setting can remain there but we want users to be aware that some of their setting are invalid and
                         * they can research why and what they need to do to replace them.
                         */
                        builder.put(ARCHIVED_SETTINGS_PREFIX + entry.getKey(), entry.getValue());
                    }
                }
            } catch (IllegalArgumentException ex) {
                changed = true;
                invalidConsumer.accept(entry, ex);
                /*
                 * We put them back in here such that tools can check from the outside if there are any indices with invalid settings. The
                 * setting can remain there but we want users to be aware that some of their setting are invalid and they can research why
                 * and what they need to do to replace them.
                 */
                builder.put(ARCHIVED_SETTINGS_PREFIX + entry.getKey(), entry.getValue());
            }
        }
        if (changed) {
            return builder.build();
        } else {
            return settings;
        }
    }

    /**
     * Returns <code>true</code> iff the setting is a private setting ie. it should be treated as valid even though it has no internal
     * representation. Otherwise <code>false</code>
     */
    // TODO this should be replaced by Setting.Property.HIDDEN or something like this.
    protected boolean isPrivateSetting(String key) {
        return false;
    }
}
