package au.com.agic.apptesting.utils.impl;

import au.com.agic.apptesting.constants.Constants;
import au.com.agic.apptesting.utils.FeatureFileImporter;
import au.com.agic.apptesting.utils.StringBuilderUtils;
import io.vavr.control.Try;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;

import javax.validation.constraints.NotNull;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

/**
 * An implementation of the FeatureFileImporter interface
 */

public class FeatureFileImporterImpl implements FeatureFileImporter {

	private static final Logger LOGGER = LoggerFactory.getLogger(FeatureFileImporterImpl.class);
	private static final Pattern IMPORT_COMMENT_RE = Pattern.compile("^\\s*#\\s*IMPORT\\s*:\\s*(?<filename>.*?)$");
	/**
	 * This is a regex to match Feature lines
	 */
	private static final Pattern FEATURE_STANZA_RE =
		Pattern.compile("^\\s*Feature\\s*:.*?$", Pattern.CASE_INSENSITIVE);
	/**
	 * This is a regex to match Scenario, Background, Scenario Outline etc lines
	 */
	private static final Pattern START_STANZA_RE =
		Pattern.compile("^\\s*(Scenario|Background|Scenario Outline|Scenario Template)\\s*:.*?$", Pattern.CASE_INSENSITIVE);
	/**
	 * This is a regex to match a tag
	 */
	private static final Pattern TAG_ANNOTATION_RE = Pattern.compile("^\\s*@.*?$");
	private static final StringBuilderUtils STRING_BUILDER_UTILS = new StringBuilderUtilsImpl();

	@Override
	public File processFeatureImportComments(@NotNull final FileDetails file, final String baseUrl) {
		checkNotNull(file);

		try {
			/*
				Assume a null base url means we are looking for files relative
				to the base feature file
			 */
			final String fixedBaseUrl = getFixedBaseUrl(file, baseUrl);

			/*
				The contents of the new, processed file
			 */
			final StringBuilder output = new StringBuilder();
			/*
				Read the original file
			 */
			final String[] fileContents = FileUtils.readFileToString(file.getFile(), Charset.defaultCharset())
				.split(Constants.LINE_END_REGEX);
			/*
				Loop over each line looking for an import comment
			 */
			for (final String line : fileContents) {
				/*
					Test the line against the import comment regex
				 */
				final Matcher matcher = IMPORT_COMMENT_RE.matcher(line);

				if (matcher.find()) {
					/*
						Import comment found, so replace it with the contents of the file to be
						imported
					 */
					final String filename = matcher.group("filename");
					final String completeFileName = fixedBaseUrl + filename;

					Try.of(() -> FileUtils.readFileToString(new File(completeFileName), Charset.defaultCharset()))
						.orElse(Try.of(() -> processRemoteUrl(completeFileName)))
						.map(this::clearContentToFirstScenario)
						.peek(s -> STRING_BUILDER_UTILS.appendWithDelimiter(
							output, s, Constants.LINE_END_OUTPUT)
						);
				} else {
					/*
						This is not an import comment, so copy the input line directly to the
						output
					 */
					STRING_BUILDER_UTILS.appendWithDelimiter(output, line, Constants.LINE_END_OUTPUT);
				}
			}

			/*
				Save the new file
			 */
			final File newFile = Files.createTempFile("", file.getFile().getName()).toFile();
			FileUtils.write(newFile, output.toString(), Charset.defaultCharset(), false);
			return newFile;

		} catch (final IOException ex) {
			LOGGER.error("Could not process file {}", file.getFile().getAbsolutePath(), ex);
		}

		/*
			All else fails, return the file we were passed in
		 */
		return file.getFile();
	}

	/**
	 * https://github.com/AutoGeneral/IridiumApplicationTesting/issues/66
	 * @param contents The raw contents
	 * @return The contents of the supplied string from the first Scenario to the end of the file
	 */
	public String clearContentToFirstScenario(@NotNull final String contents) {
		checkNotNull(contents);
		/*
			http://stackoverflow.com/questions/25569836/equivalent-of-scala-dropwhile
			Make up for the last of a dropWhile
		 */
		//CHECKSTYLE.OFF: VisibilityModifier
		class MutableBoolean {
			boolean foundFeature;
			boolean foundScenarioOrTag;
		}
		//CHECKSTYLE.ON: VisibilityModifier

		final MutableBoolean inTail = new MutableBoolean();

		final String processedFeature = Stream.of(contents.split(Constants.LINE_END_REGEX))
			.filter(i -> {
				inTail.foundFeature = inTail.foundFeature || FEATURE_STANZA_RE.matcher(i).matches();
				inTail.foundScenarioOrTag = inTail.foundFeature
					&& (inTail.foundScenarioOrTag
					|| TAG_ANNOTATION_RE.matcher(i).matches()
					|| START_STANZA_RE.matcher(i).matches());

				return inTail.foundFeature && inTail.foundScenarioOrTag;
			})
			.collect(Collectors.joining(Constants.LINE_END_OUTPUT));

		/*
			If the result is empty, then the file being processed is not a complete
			feature file, and we simply return the original input, which we assume
			is a fragment.
		 */
		return StringUtils.isBlank(processedFeature)
			? contents
			: processedFeature;
	}

	private String processRemoteUrl(@NotNull final String path) throws IOException {
		final File copy = File.createTempFile("webapptester", ".feature");

		try {
			final RetryTemplate template = new RetryTemplate();
			final SimpleRetryPolicy policy = new SimpleRetryPolicy();
			policy.setMaxAttempts(Constants.URL_COPY_RETRIES);
			template.setRetryPolicy(policy);
			return template.execute(context -> {
				FileUtils.copyURLToFile(new URL(path), copy);
				return FileUtils.readFileToString(copy, Charset.defaultCharset());
			});
		} finally {
			FileUtils.deleteQuietly(copy);
		}
	}

	private String getFixedBaseUrl(@NotNull final FileDetails file, final String baseUrl) {
		checkNotNull(file);

		if (file.isLocalSource() || StringUtils.isBlank(baseUrl)) {
			checkState(file.getFile() != null);
			checkState(file.getFile().getAbsoluteFile().getParentFile() != null);
			return file.getFile().getAbsoluteFile().getParentFile().getAbsolutePath() + "/";
		}

		return baseUrl;
	}
}