/*
 * 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.guardrails;

import java.util.Map;
import java.util.Set;

import javax.annotation.Nonnull;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.utils.FBUtilities;

import static java.lang.String.format;

/**
 * Generates a value which respective {@link ValueValidator} successfuly validates.
 */
public abstract class ValueGenerator<VALUE>
{
    private static final Logger logger = LoggerFactory.getLogger(ValueGenerator.class);

    public static final String GENERATOR_CLASS_NAME_KEY = "generator_class_name";

    private static final String DEFAULT_VALIDATOR_IMPLEMENTATION_PACKAGE = ValueGenerator.class.getPackage().getName();

    protected final CustomGuardrailConfig config;

    public ValueGenerator(CustomGuardrailConfig config)
    {
        this.config = config;
    }

    /**
     * Generates a value. Generated value will not be validated before returning in any way.
     *
     * @return generated value.
     */
    public VALUE generate()
    {
        return generate((NoOpValidator<VALUE>) NoOpValidator.INSTANCE);
    }

    /**
     * Generate a value.
     *
     * @param validator validator possible to use when value is internally generated, to be sure that
     *                  what was generated is valid.
     * @return generated value
     */
    public VALUE generate(ValueValidator<VALUE> validator)
    {
        return generate(validator, Map.of());
    }

    /**
     * Generate a value with given runtime options from OPTIONS map.
     * Generated value will not be validated before returning in any way.
     *
     * @return generated value
     */
    public VALUE generate(Map<String, Object> options)
    {
        return generate((NoOpValidator<VALUE>) NoOpValidator.INSTANCE, options);
    }

    /**
     * Generate a value.
     *
     * @param validator validator to validate generated value with
     * @param options   runtime options to be used upon generation, from CQL OPTIONS, if any.
     * @return generated value of given size
     */
    public abstract VALUE generate(ValueValidator<VALUE> validator, Map<String, Object> options);

    /**
     * @return parameters for this generator
     */
    @Nonnull
    public abstract CustomGuardrailConfig getParameters();

    /**
     * Validates parameters for this generator.
     *
     * @throws ConfigurationException in case configuration for this generator is invalid
     */
    public abstract void validateParameters() throws ConfigurationException;

    /**
     * Tells what keys this generator supports when they are specified in CQL in OPTIONS.
     * The default implementation returns empty set.
     *
     * @return set of keys for OPTIONS in CQL
     */
    public Set<String> getSupportedOptionsKeys()
    {
        return Set.of();
    }

    /**
     * Returns an instance of a validator according to the parameters in {@code config}.
     *
     * @param name   name of a guardrail a generator is created for
     * @param config configuration to instantiate a generator with. After a generator is instantiated, it will
     *               validate this configuration internally and throw an exception if such configuration is invalid.
     * @return instance of a generator of class as specified under key {@code class_name} in {@code config}. If not set,
     * {@link NoOpGenerator} will be used.
     * @throws ConfigurationException thrown in case {@code config} for the constructed generator is invalid, or it was
     *                                not possible to construct a generator.
     */
    public static <VALUE> ValueGenerator<VALUE> getGenerator(String name, @Nonnull CustomGuardrailConfig config)
    {
        String className = config.resolveString(GENERATOR_CLASS_NAME_KEY);

        if (className == null || className.isEmpty())
        {
            logger.debug("Configuration for generator for guardrail '{}' does not contain key " +
                         "'generator_class_name' or its value is null or empty string. No-op generator will be used.",
                         name);
            return (ValueGenerator<VALUE>) NoOpGenerator.INSTANCE;
        }

        if (!className.contains("."))
            className = DEFAULT_VALIDATOR_IMPLEMENTATION_PACKAGE + '.' + className;

        try
        {
            Class<? extends ValueGenerator> rawGeneratorClass =
                FBUtilities.classForNameWithoutInitialization(className, "generator", ValueGenerator.class);
            @SuppressWarnings("unchecked")
            Class<? extends ValueGenerator<VALUE>> generatorClass =
                (Class<? extends ValueGenerator<VALUE>>) rawGeneratorClass;

            @SuppressWarnings("unchecked")
            ValueGenerator<VALUE> generator = generatorClass.getConstructor(CustomGuardrailConfig.class)
                                                            .newInstance(config);
            logger.debug("Using {} generator for guardrail '{}' with parameters {}",
                         generator.getClass(), name, generator.getParameters());
            return generator;
        }
        catch (Exception ex)
        {
            String message;
            if (ex.getCause() instanceof ConfigurationException)
                message = ex.getCause().getMessage();
            else
                message = ex.getMessage();

            throw new ConfigurationException(format("Unable to create instance of generator of class %s: %s",
                                                    className, message), ex);
        }
    }
}
