/*
 * 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.cassandra.db.virtual;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;

import org.yaml.snakeyaml.introspector.Property;

import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.Loader;
import org.apache.cassandra.config.Properties;
import org.apache.cassandra.config.Redacted;
import org.apache.cassandra.config.Replacement;
import org.apache.cassandra.config.Replacements;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.dht.LocalPartitioner;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.utils.JsonUtils;

@VisibleForTesting
public final class SettingsTable extends AbstractVirtualTable
{
    private static final String NAME = "name";
    private static final String VALUE = "value";

    public static final Map<String, String> BACKWARDS_COMPATIBLE_NAMES = ImmutableMap.copyOf(getBackwardsCompatibleNames());
    protected static final Map<String, Property> PROPERTIES = ImmutableMap.copyOf(getProperties());

    private final Config config;
    private final boolean useJsonFormat;

    SettingsTable(String keyspace)
    {
        this(keyspace, DatabaseDescriptor.getRawConfig());
    }

    SettingsTable(String keyspace, Config config)
    {
        super(TableMetadata.builder(keyspace, "settings")
                           .comment("current settings")
                           .kind(TableMetadata.Kind.VIRTUAL)
                           .partitioner(new LocalPartitioner(UTF8Type.instance))
                           .addPartitionKeyColumn(NAME, UTF8Type.instance)
                           .addRegularColumn(VALUE, UTF8Type.instance)
                           .build());
        this.config = config;
        this.useJsonFormat = CassandraRelevantProperties.VIRTUAL_TABLE_COMPLEX_SETTINGS_FORMAT_JSON.getBoolean();
    }

    @Override
    public DataSet data(DecoratedKey partitionKey)
    {
        SimpleDataSet result = new SimpleDataSet(metadata());
        String name = UTF8Type.instance.compose(partitionKey.getKey());
        if (BACKWARDS_COMPATIBLE_NAMES.containsKey(name))
            ClientWarn.instance.warn("key '" + name + "' is deprecated; should switch to '" + BACKWARDS_COMPATIBLE_NAMES.get(name) + "'");
        if (PROPERTIES.containsKey(name))
            result.row(name).column(VALUE, getValue(PROPERTIES.get(name)));
        return result;
    }

    @Override
    public DataSet data()
    {
        SimpleDataSet result = new SimpleDataSet(metadata());
        for (Map.Entry<String, Property> e : PROPERTIES.entrySet())
            result.row(e.getKey()).column(VALUE, getValue(e.getValue()));
        return result;
    }

    @VisibleForTesting
    String getValue(Property prop)
    {
        Redacted maybeCredential = prop.getAnnotation(Redacted.class);
        if (maybeCredential != null)
            return maybeCredential.redactedValue();

        Object value = prop.get(config);
        if (value == null)
            return null;

        if (value.getClass().isArray())
            return tryConstructJson(Arrays.asList((Object[]) value));
        else if (value instanceof Collection)
            return tryConstructJson(value);
        else if (value instanceof Map)
        {
            Map<String, Object> map = new HashMap<>();
            for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
            {
                String key = String.valueOf(entry.getKey());
                // this is done on best-effort basis as we do not have names in parameters
                // inherently under control as this is what a user is responsible for
                // when dealing with custom implementations
                if (key.endsWith("_password") || key.equals("password"))
                    map.put(key, Redacted.REDACTED_STRING);
                else
                    map.put(key, entry.getValue());
            }

            return tryConstructJson(map);
        }

        return value.toString();
    }

    private String tryConstructJson(Object o)
    {
        if (useJsonFormat)
        {
            try
            {
                return JsonUtils.JSON_OBJECT_MAPPER.writeValueAsString(o);
            }
            catch (Exception e)
            {
                return o.toString();
            }
        }
        else
        {
            return o.toString();
        }
    }

    private static Map<String, Property> getProperties()
    {
        Loader loader = Properties.defaultLoader();
        Map<String, Property> properties = loader.flatten(Config.class);
        // only handling top-level replacements for now, previous logic was only top level so not a regression
        Map<String, Replacement> replacements = Replacements.getNameReplacements(Config.class).get(Config.class);
        if (replacements != null)
        {
            for (Replacement r : replacements.values())
            {
                Property latest = properties.get(r.newName);
                assert latest != null : "Unable to find replacement new name: " + r.newName;
                Property conflict = properties.put(r.oldName, r.toProperty(latest));
                // some configs kept the same name, but changed the type, if this is detected then rely on the replaced property
                assert conflict == null || r.oldName.equals(r.newName) : String.format("New property %s attempted to replace %s, but this property already exists", latest.getName(), conflict.getName());
            }
        }
        for (Map.Entry<String, String> e : BACKWARDS_COMPATIBLE_NAMES.entrySet())
        {
            String oldName = e.getKey();
            if (properties.containsKey(oldName))
                throw new AssertionError("Name " + oldName + " is present in Config, this adds a conflict as this name had a different meaning in " + SettingsTable.class.getSimpleName());
            String newName = e.getValue();
            Property prop = Objects.requireNonNull(properties.get(newName), newName + " cant be found for " + oldName);
            properties.put(oldName, Properties.rename(oldName, prop));
        }
        return properties;
    }

    /**
     * settings table was released in 4.0 and attempted to support nested properties for a few hand selected properties.
     * The issue is that 4.0 used '_' to seperate the names, which makes it hard to map back to the yaml names; to solve
     * this 4.1+ uses '.' to avoid possible conflicts, this class provides mappings from old names to the '.' names.
     *
     * There were a handle full of properties which had custom names, names not present in the yaml, this map also
     * fixes this and returns the proper (what is accessable via yaml) names.
     */
    private static Map<String, String> getBackwardsCompatibleNames()
    {
        Map<String, String> names = new HashMap<>();
        // Names that dont match yaml
        names.put("audit_logging_options_logger", "audit_logging_options.logger.class_name");
        names.put("server_encryption_options_client_auth", "server_encryption_options.require_client_auth");
        names.put("server_encryption_options_endpoint_verification", "server_encryption_options.require_endpoint_verification");
        names.put("server_encryption_options_legacy_ssl_storage_port", "server_encryption_options.legacy_ssl_storage_port_enabled");
        names.put("server_encryption_options_protocol", "server_encryption_options.accepted_protocols");
        names.put("authenticator", "authenticator.class_name");
        names.put("authorizer", "authorizer.class_name");
        names.put("network_authorizer", "network_authorizer.class_name");
        names.put("role_manager", "role_manager.class_name");
        names.put("internode_authenticator", "internode_authenticator.class_name");


        // matching names
        names.put("audit_logging_options_audit_logs_dir", "audit_logging_options.audit_logs_dir");
        names.put("audit_logging_options_enabled", "audit_logging_options.enabled");
        names.put("audit_logging_options_excluded_categories", "audit_logging_options.excluded_categories");
        names.put("audit_logging_options_excluded_keyspaces", "audit_logging_options.excluded_keyspaces");
        names.put("audit_logging_options_excluded_users", "audit_logging_options.excluded_users");
        names.put("audit_logging_options_included_categories", "audit_logging_options.included_categories");
        names.put("audit_logging_options_included_keyspaces", "audit_logging_options.included_keyspaces");
        names.put("audit_logging_options_included_users", "audit_logging_options.included_users");
        names.put("server_encryption_options_algorithm", "server_encryption_options.algorithm");
        names.put("server_encryption_options_cipher_suites", "server_encryption_options.cipher_suites");
        names.put("server_encryption_options_enabled", "server_encryption_options.enabled");
        names.put("server_encryption_options_internode_encryption", "server_encryption_options.internode_encryption");
        names.put("server_encryption_options_optional", "server_encryption_options.optional");
        names.put("transparent_data_encryption_options_chunk_length_kb", "transparent_data_encryption_options.chunk_length_kb");
        names.put("transparent_data_encryption_options_cipher", "transparent_data_encryption_options.cipher");
        names.put("transparent_data_encryption_options_enabled", "transparent_data_encryption_options.enabled");
        names.put("transparent_data_encryption_options_iv_length", "transparent_data_encryption_options.iv_length");

        return names;
    }
}
