package com.github.jrcodeza.schema.v2.generator;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.jrcodeza.OpenApiIgnore;
import com.github.jrcodeza.schema.v2.generator.config.OpenApiV2GeneratorConfig;
import com.github.jrcodeza.schema.v2.generator.config.builder.OpenApiV2GeneratorConfigBuilder;
import com.github.jrcodeza.schema.v2.generator.interceptors.OperationInterceptor;
import com.github.jrcodeza.schema.v2.generator.interceptors.OperationParameterInterceptor;
import com.github.jrcodeza.schema.v2.generator.interceptors.RequestBodyInterceptor;
import com.github.jrcodeza.schema.v2.generator.interceptors.SchemaFieldInterceptor;
import com.github.jrcodeza.schema.v2.generator.interceptors.SchemaInterceptor;
import com.github.jrcodeza.schema.v2.generator.model.GenerationContext;
import com.github.jrcodeza.schema.v2.generator.model.Header;
import com.github.jrcodeza.schema.v2.generator.model.InheritanceInfo;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.env.Environment;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.models.Info;
import io.swagger.models.Model;
import io.swagger.models.Path;
import io.swagger.models.Swagger;

import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;

public class OpenAPIV2Generator {

	private static final String DEFAULT_DISCRIMINATOR_NAME = "type";
	private static Logger logger = LoggerFactory.getLogger(OpenAPIV2Generator.class);
	private final ComponentSchemaTransformer componentSchemaTransformer;
	private final OperationsTransformer operationsTransformer;
	private final Info info;
	private final List<SchemaInterceptor> schemaInterceptors;
	private final List<SchemaFieldInterceptor> schemaFieldInterceptors;
	private final List<OperationParameterInterceptor> operationParameterInterceptors;
	private final List<OperationInterceptor> operationInterceptors;
	private final List<RequestBodyInterceptor> requestBodyInterceptors;
	private final List<Header> globalHeaders;
	private List<String> modelPackages;
	private List<String> controllerBasePackages;
	private Environment environment;

	public OpenAPIV2Generator(List<String> modelPackages, List<String> controllerBasePackages, Info info) {
		this(modelPackages, controllerBasePackages, info, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
	}

	public OpenAPIV2Generator(List<String> modelPackages, List<String> controllerBasePackages, Info info,
							  List<SchemaInterceptor> schemaInterceptors,
							  List<SchemaFieldInterceptor> schemaFieldInterceptors,
							  List<OperationParameterInterceptor> operationParameterInterceptors,
							  List<OperationInterceptor> operationInterceptors,
							  List<RequestBodyInterceptor> requestBodyInterceptors) {
		this.modelPackages = modelPackages;
		this.controllerBasePackages = controllerBasePackages;
		componentSchemaTransformer = new ComponentSchemaTransformer(schemaFieldInterceptors);
		globalHeaders = new ArrayList<>();

		GenerationContext operationsGenerationContext = new GenerationContext(null, removeRegexFormatFromPackages(modelPackages));
		operationsTransformer = new OperationsTransformer(
				operationsGenerationContext, operationParameterInterceptors, operationInterceptors, requestBodyInterceptors, globalHeaders
		);

		this.info = info;
		this.schemaInterceptors = schemaInterceptors;
		this.schemaFieldInterceptors = schemaFieldInterceptors;
		this.operationParameterInterceptors = operationParameterInterceptors;
		this.operationInterceptors = operationInterceptors;
		this.requestBodyInterceptors = requestBodyInterceptors;
	}

	public String generateJson() throws JsonProcessingException {
		return generateJson(OpenApiV2GeneratorConfigBuilder.empty().build());
	}

	public String generateJson(OpenApiV2GeneratorConfig config) throws JsonProcessingException {
		Swagger openAPI = generate(config);
		ObjectMapper objectMapper = new ObjectMapper();
		objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
		DocumentContext doc = JsonPath.parse(objectMapper.writeValueAsString(openAPI));
		doc.delete("$..responseSchema");
		doc.delete("$..originalRef");
		return doc.jsonString();
	}

	public Swagger generate(OpenApiV2GeneratorConfig config) {
		logger.info("Starting OpenAPI v2 generation");
		environment = config.getEnvironment();
		Swagger openAPI = new Swagger();
		openAPI.setDefinitions(createDefinitions());
		openAPI.setPaths(createPaths(config));
		openAPI.setInfo(info);
		openAPI.setBasePath(config.getBasePath());
		openAPI.setHost(config.getHost());
		logger.info("OpenAPI v2 generation done!");
		return openAPI;
	}

	public Swagger generate() {
		return generate(OpenApiV2GeneratorConfigBuilder.empty().build());
	}

	public void addSchemaInterceptor(SchemaInterceptor schemaInterceptor) {
		schemaInterceptors.add(schemaInterceptor);
	}

	public void addSchemaFieldInterceptor(SchemaFieldInterceptor schemaFieldInterceptor) {
		schemaFieldInterceptors.add(schemaFieldInterceptor);
	}

	public void addOperationParameterInterceptor(OperationParameterInterceptor operationParameterInterceptor) {
		operationParameterInterceptors.add(operationParameterInterceptor);
	}

	public void addOperationInterceptor(OperationInterceptor operationInterceptor) {
		operationInterceptors.add(operationInterceptor);
	}

	public void addRequestBodyInterceptor(RequestBodyInterceptor requestBodyInterceptor) {
		requestBodyInterceptors.add(requestBodyInterceptor);
	}

	public void addGlobalHeader(String name, String description, boolean required) {
		globalHeaders.add(new Header(name, description, required));
	}

	private Map<String, Path> createPaths(OpenApiV2GeneratorConfig config) {
		ClassPathScanningCandidateComponentProvider scanner = createClassPathScanningCandidateComponentProvider();
		scanner.addIncludeFilter(new AnnotationTypeFilter(RestController.class));

		List<Class<?>> controllerClasses = new ArrayList<>();
		List<String> packagesWithoutRegex = removeRegexFormatFromPackages(controllerBasePackages);
		for (String controllerPackage : packagesWithoutRegex) {
			logger.debug("Scanning controller package=[{}]", controllerPackage);
			for (BeanDefinition beanDefinition : scanner.findCandidateComponents(controllerPackage)) {
				logger.debug("Scanning controller class=[{}]", beanDefinition.getBeanClassName());
				controllerClasses.add(getClass(beanDefinition));
			}
		}
		return operationsTransformer.transformOperations(controllerClasses, config);
	}

	private ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider() {
		if (environment == null) {
			return new ClassPathScanningCandidateComponentProvider(false);
		}
		return new ClassPathScanningCandidateComponentProvider(false, environment);
	}

	private Map<String, Model> createDefinitions() {
		Map<String, Model> schemaMap = new HashMap<>();
		ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
		modelPackages.forEach(modelPackage -> scanner.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(modelPackage))));

		List<String> packagesWithoutRegex = removeRegexFormatFromPackages(modelPackages);
		Map<String, InheritanceInfo> inheritanceMap = new HashMap<>();
		for (String modelPackage : packagesWithoutRegex) {
			logger.debug("Scanning model package=[{}]", modelPackage);
			for (BeanDefinition beanDefinition : scanner.findCandidateComponents(modelPackage)) {
				logger.debug("Scanning model class=[{}]", beanDefinition.getBeanClassName());
				// populating inheritance info
				Class<?> clazz = getClass(beanDefinition);
				if (inheritanceMap.containsKey(clazz.getName()) || clazz.getAnnotation(OpenApiIgnore.class) != null) {
					continue;
				}
				getInheritanceInfo(clazz).ifPresent(inheritanceInfo -> {
					logger.debug("Adding entry [{}] to inheritance map", clazz.getName());
					inheritanceMap.put(clazz.getName(), inheritanceInfo);
				});
			}
			for (BeanDefinition beanDefinition : scanner.findCandidateComponents(modelPackage)) {
				Class<?> clazz = getClass(beanDefinition);
				if (schemaMap.containsKey(clazz.getSimpleName()) || clazz.getAnnotation(OpenApiIgnore.class) != null) {
					continue;
				}
				GenerationContext generationContext = new GenerationContext(inheritanceMap, packagesWithoutRegex);
				Model transformedComponentSchema = componentSchemaTransformer.transformSimpleSchema(clazz, generationContext);
				schemaInterceptors.forEach(schemaInterceptor -> schemaInterceptor.intercept(clazz, transformedComponentSchema));
				schemaMap.put(clazz.getSimpleName(), transformedComponentSchema);
			}
		}
		return schemaMap;
	}

	private Class<?> getClass(BeanDefinition beanDefinition) {
		try {
			return Class.forName(beanDefinition.getBeanClassName());
		} catch (ClassNotFoundException e) {
			throw new IllegalArgumentException(e);
		}
	}

	private Optional<InheritanceInfo> getInheritanceInfo(Class<?> clazz) {
		if (clazz.getAnnotation(JsonSubTypes.class) != null) {
			List<Annotation> annotations = unmodifiableList(asList(clazz.getAnnotations()));
			JsonTypeInfo jsonTypeInfo = annotations.stream()
												   .filter(annotation -> annotation instanceof JsonTypeInfo)
												   .map(annotation -> (JsonTypeInfo) annotation)
												   .findFirst()
												   .orElse(null);

			InheritanceInfo inheritanceInfo = new InheritanceInfo();
			inheritanceInfo.setDiscriminatorFieldName(getDiscriminatorName(jsonTypeInfo));
			inheritanceInfo.setDiscriminatorClassMap(scanJacksonInheritance(annotations));
			return Optional.of(inheritanceInfo);
		}
		return Optional.empty();
	}

	private String getDiscriminatorName(JsonTypeInfo jsonTypeInfo) {
		if (jsonTypeInfo == null) {
			return DEFAULT_DISCRIMINATOR_NAME;
		}
		return jsonTypeInfo.property();
	}

	private List<String> removeRegexFormatFromPackages(List<String> modelPackages) {
		return modelPackages.stream()
							.map(modelPackage -> modelPackage.replace(".*", ""))
							.collect(Collectors.toList());
	}

	private Map<String, String> scanJacksonInheritance(List<Annotation> annotations) {
		return annotations.stream()
						  .filter(annotation -> annotation instanceof JsonSubTypes)
						  .map(annotation -> (JsonSubTypes) annotation)
						  .flatMap(jsonSubTypesMapped -> Arrays.stream(jsonSubTypesMapped.value()))
						  .collect(Collectors.toMap(o -> o.value().getCanonicalName(), JsonSubTypes.Type::name));
	}

}