/*
 * Copyright 2013-2021 the original author or authors.
 *
 * 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
 *
 *      https://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.springframework.cloud.sleuth.docs;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.springframework.util.StringUtils;

/**
 * In order to turn on the assertions you need to either turn on the
 * {@code spring.cloud.sleuth.assertions.enabled} system property or
 * {@code SPRING_CLOUD_SLEUTH_ASSERTIONS_ENABLED} environment variable.
 */
final class DocumentedSpanAssertions {

	static boolean SLEUTH_SPAN_ASSERTIONS_ON = Boolean.parseBoolean(System.getProperty(
			"spring.cloud.sleuth.assertions.enabled", System.getenv("SPRING_CLOUD_SLEUTH_ASSERTIONS_ENABLED") != null
					? System.getenv("SPRING_CLOUD_SLEUTH_ASSERTIONS_ENABLED") : "false"));

	private static final Map<String, Pattern> PATTERN_CACHE = new ConcurrentHashMap<>();

	private static final Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\].+*?^$\\\\|]");

	private DocumentedSpanAssertions() {
		throw new IllegalStateException("Can't instantiate utility class");
	}

	static void assertThatKeyIsValid(String key, DocumentedSpan documentedSpan) {
		if (SLEUTH_SPAN_ASSERTIONS_ON) {
			TagKey[] allowedKeys = documentedSpan.getTagKeys();
			if (allowedKeys.length == 0) {
				return;
			}
			boolean validTagKey = Arrays.stream(allowedKeys)
					.anyMatch(tagKey -> patternOrValueMatches(key, tagKey.getKey())
							&& hasRequiredPrefix(key, documentedSpan.prefix()));
			if (!validTagKey) {
				throw new AssertionError("The key [" + key + "] is invalid. You can use only one matching "
						+ Arrays.stream(allowedKeys).map(TagKey::getKey).collect(Collectors.toList())
						+ prefixWarningIfPresent(documentedSpan));
			}
		}
	}

	private static String prefixWarningIfPresent(DocumentedSpan documentedSpan) {
		return StringUtils.hasText(documentedSpan.prefix())
				? ". Also it has start with [" + documentedSpan.prefix() + "] prefix" : "";
	}

	static void assertThatKeyIsValid(TagKey key, DocumentedSpan documentedSpan) {
		if (SLEUTH_SPAN_ASSERTIONS_ON) {
			TagKey[] allowedKeys = documentedSpan.getTagKeys();
			if (allowedKeys.length == 0) {
				return;
			}
			if (Arrays.stream(allowedKeys).noneMatch(tagKey -> patternOrValueMatches(key.getKey(), tagKey.getKey())
					&& hasRequiredPrefix(key.getKey(), documentedSpan.prefix()))) {
				throw new AssertionError("The key [" + key.getKey() + "] is invalid. You can use only one matching "
						+ Arrays.stream(allowedKeys).map(TagKey::getKey).collect(Collectors.toList())
						+ prefixWarningIfPresent(documentedSpan));
			}
		}
	}

	static void assertThatNameIsValid(String name, DocumentedSpan documentedSpan) {
		String allowedName = documentedSpan.getName();
		if (SLEUTH_SPAN_ASSERTIONS_ON && !patternOrValueMatches(name, allowedName)) {
			throw new AssertionError(
					"The name [" + name + "] is invalid. You can use only one matching [" + allowedName + "]");
		}
	}

	static void assertThatEventIsValid(String eventValue, DocumentedSpan documentedSpan) {
		if (SLEUTH_SPAN_ASSERTIONS_ON) {
			EventValue[] allowed = documentedSpan.getEvents();
			if (allowed.length == 0) {
				return;
			}
			boolean valid = Arrays.stream(allowed).anyMatch(value -> patternOrValueMatches(eventValue, value.getValue())
					&& hasRequiredPrefix(eventValue, documentedSpan.prefix()));
			if (!valid) {
				throw new AssertionError("The event [" + eventValue + "] is invalid. You can use only one matching "
						+ Arrays.stream(allowed).map(EventValue::getValue).collect(Collectors.toList())
						+ prefixWarningIfPresent(documentedSpan));
			}
		}
	}

	static void assertThatEventIsValid(EventValue eventValue, DocumentedSpan documentedSpan) {
		if (SLEUTH_SPAN_ASSERTIONS_ON) {
			EventValue[] allowed = documentedSpan.getEvents();
			if (allowed.length == 0) {
				return;
			}
			if (Arrays.stream(allowed).noneMatch(value -> patternOrValueMatches(eventValue.getValue(), value.getValue())
					&& hasRequiredPrefix(eventValue.getValue(), documentedSpan.prefix()))) {
				throw new AssertionError(
						"The event [" + eventValue.getValue() + "] is invalid. You can use only one matching "
								+ Arrays.stream(allowed).map(EventValue::getValue).collect(Collectors.toList())
								+ prefixWarningIfPresent(documentedSpan));
			}
		}
	}

	static void assertThatSpanStartedBeforeEnd(AssertingSpan span) {
		if (SLEUTH_SPAN_ASSERTIONS_ON && !span.isStarted()) {
			throw new AssertionError("The span was not started, however you're trying to end it");
		}
	}

	private static boolean patternOrValueMatches(String pickedValue, String allowedValue) {
		if (allowedValue.contains("%s")) {
			String stringPattern = escapeSpecialRegexWithSingleEscape(allowedValue).replaceAll("%s", ".*?");
			Pattern pattern = PATTERN_CACHE.computeIfAbsent(stringPattern, Pattern::compile);
			return pattern.matcher(pickedValue).matches();
		}
		return allowedValue.equals(pickedValue);
	}

	private static boolean hasRequiredPrefix(String value, String prefix) {
		if (StringUtils.hasText(prefix)) {
			return value.startsWith(prefix);
		}
		return true;
	}

	private static String escapeSpecialRegexWithSingleEscape(String str) {
		return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0");
	}

}
