/*
 * Copyright 2008-present MongoDB, Inc.
 *
 * Licensed 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.
 *
 * Original Work: Apache License, Version 2.0, Copyright 2017 Hans-Peter Grahsl.
 * Original Work: Apache License, Version 2.0, Copyright 2018 Confluent Inc. EnumValidatorAndRecommender
 */

package com.mongodb.kafka.connect.util;

import static com.mongodb.kafka.connect.sink.MongoSinkConfig.TOPIC_OVERRIDE_DOC;
import static com.mongodb.kafka.connect.sink.MongoSinkTopicConfig.FULLY_QUALIFIED_CLASS_NAME;
import static java.lang.String.format;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;

import org.apache.kafka.common.config.ConfigDef;
import org.apache.kafka.common.config.ConfigException;
import org.apache.kafka.common.config.types.Password;
import org.slf4j.Logger;

import com.mongodb.kafka.connect.util.config.BsonTimestampParser;

public final class Validators {

  public interface ValidatorWithOperators extends ConfigDef.Validator {
    default ValidatorWithOperators or(final ValidatorWithOperators other) {
      return withStringDef(
          format("%s OR %s", this.toString(), other.toString()),
          (name, value) -> {
            try {
              this.ensureValid(name, value);
            } catch (ConfigException e) {
              other.ensureValid(name, value);
            }
          });
    }
  }

  public static ValidatorWithOperators emptyString() {
    return withStringDef(
        "An empty string",
        (name, value) -> {
          // value type already validated when parsed as String, hence ignoring ClassCastException
          if (!((String) value).isEmpty()) {
            throw new ConfigException(name, value, "Not empty");
          }
        });
  }

  public static ValidatorWithOperators matching(final Pattern pattern) {
    return withStringDef(
        format("A string matching `%s`", pattern),
        (name, value) -> matchPattern(pattern, name, (String) value));
  }

  @SuppressWarnings("unchecked")
  public static ValidatorWithOperators listMatchingPattern(final Pattern pattern) {
    return withStringDef(
        format("A list matching: `%s`", pattern),
        (name, value) -> {
          try {
            ((List) value).forEach(v -> matchPattern(pattern, name, (String) v));
          } catch (ConnectConfigException e) {
            throw new ConfigException(name, value, e.getOriginalMessage());
          }
        });
  }

  private static void matchPattern(final Pattern pattern, final String name, final String value) {
    if (!pattern.matcher(value).matches()) {
      String message = "Does not match: " + pattern.pattern();
      if (pattern.equals(FULLY_QUALIFIED_CLASS_NAME)) {
        message = "Does not match expected class pattern.";
      }
      throw new ConnectConfigException(name, value, message);
    }
  }

  public static ValidatorWithOperators isAValidRegex() {
    return withStringDef(
        "A valid regex",
        ((name, value) -> {
          try {
            Pattern.compile((String) value);
          } catch (Exception e) {
            throw new ConfigException(name, value, "Invalid regex: " + e.getMessage());
          }
        }));
  }

  public static ValidatorWithOperators topicOverrideValidator() {
    return withStringDef(
        "Topic override",
        (name, value) -> {
          if (!((String) value).isEmpty()) {
            throw new ConfigException(
                name,
                value,
                "This configuration shouldn't be set directly. See the documentation about how to "
                    + "configure topic based overrides.\n"
                    + TOPIC_OVERRIDE_DOC);
          }
        });
  }

  public static ValidatorWithOperators errorCheckingValueValidator(
      final String validValuesString, final Consumer<String> consumer) {
    return withStringDef(
        validValuesString,
        ((name, value) -> {
          try {
            consumer.accept((String) value);
          } catch (Exception e) {
            throw new ConfigException(name, value, e.getMessage());
          }
        }));
  }

  public static ValidatorWithOperators withStringDef(
      final String validatorString, final ConfigDef.Validator validator) {
    return new ValidatorWithOperators() {
      @Override
      public void ensureValid(final String name, final Object value) {
        validator.ensureValid(name, value);
      }

      @Override
      public String toString() {
        return validatorString;
      }
    };
  }

  public static ValidatorWithOperators startAtOperationTimeValidator(final Logger logger) {
    return (propertyName, propertyValue) ->
        BsonTimestampParser.parse(propertyName, (String) propertyValue, logger);
  }

  public static ValidatorWithOperators errorCheckingPasswordValueValidator(
      final String validValuesString, final Consumer<String> consumer) {
    return withPasswordDef(
        validValuesString,
        ((name, value) -> {
          try {
            consumer.accept((String) value);
          } catch (Exception e) {
            throw new ConfigException(name, value, e.getMessage());
          }
        }));
  }

  public static ValidatorWithOperators withPasswordDef(
      final String validatorString, final ConfigDef.Validator validator) {
    return new ValidatorWithOperators() {
      @Override
      public void ensureValid(final String name, final Object value) {
        validator.ensureValid(name, ((Password) value).value());
      }

      @Override
      public String toString() {
        return validatorString;
      }
    };
  }

  public static final class EnumValidatorAndRecommender
      implements ValidatorWithOperators, ConfigDef.Recommender {
    private final List<String> values;
    private final boolean caseSensitive;

    private EnumValidatorAndRecommender(final List<String> values, final boolean caseSensitive) {
      this.values = values;
      this.caseSensitive = caseSensitive;
    }

    /**
     * Return a case-insensitive enum validator and recommender
     *
     * @param enumerators the enum values
     * @param <E> the enum type
     * @return the validator and recommender
     */
    public static <E> EnumValidatorAndRecommender in(final E[] enumerators) {
      return in(enumerators, e -> e.toString().toLowerCase(Locale.ROOT), false);
    }

    /**
     * Return a case-sensitive enum validator and recommender
     *
     * @param enumerators the enum values
     * @param mapper the enum values to case sensitive string mapper
     * @param <E> the enum type
     * @return the validator and recommender
     */
    public static <E> EnumValidatorAndRecommender in(
        final E[] enumerators, final Function<E, String> mapper) {
      return in(enumerators, mapper, true);
    }

    private static <E> EnumValidatorAndRecommender in(
        final E[] enumerators, final Function<E, String> mapper, final boolean caseSensitive) {
      final List<String> values = new ArrayList<>(enumerators.length);
      for (E e : enumerators) {
        values.add(mapper.apply(e));
      }
      return new EnumValidatorAndRecommender(values, caseSensitive);
    }

    @Override
    public void ensureValid(final String key, final Object value) {
      String enumValue = (String) value;
      boolean invalid =
          caseSensitive
              ? !values.contains(enumValue)
              : !values.contains(enumValue.toLowerCase(Locale.ROOT));
      if (invalid) {
        throw new ConfigException(
            key, value, format("Invalid enumerator value. Should be one of: %s", values));
      }
    }

    @Override
    public String toString() {
      return values.toString();
    }

    @Override
    public List<Object> validValues(final String name, final Map<String, Object> parsedConfig) {
      return new ArrayList<>(values);
    }

    @Override
    public boolean visible(final String name, final Map<String, Object> parsedConfig) {
      return true;
    }
  }

  private Validators() {}
}
