package io.leangen.graphql.spqr.spring.autoconfigure;

import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import io.leangen.geantyref.GenericTypeReflector;
import io.leangen.graphql.ExtendedGeneratorConfiguration;
import io.leangen.graphql.ExtensionProvider;
import io.leangen.graphql.GeneratorConfiguration;
import io.leangen.graphql.GraphQLSchemaGenerator;
import io.leangen.graphql.execution.ResolverInterceptorFactory;
import io.leangen.graphql.generator.mapping.ArgumentInjector;
import io.leangen.graphql.generator.mapping.InputConverter;
import io.leangen.graphql.generator.mapping.OutputConverter;
import io.leangen.graphql.generator.mapping.SchemaTransformer;
import io.leangen.graphql.generator.mapping.TypeMapper;
import io.leangen.graphql.generator.mapping.strategy.AbstractInputHandler;
import io.leangen.graphql.generator.mapping.strategy.InterfaceMappingStrategy;
import io.leangen.graphql.metadata.messages.MessageBundle;
import io.leangen.graphql.metadata.strategy.InclusionStrategy;
import io.leangen.graphql.metadata.strategy.query.AnnotatedResolverBuilder;
import io.leangen.graphql.metadata.strategy.query.BeanResolverBuilder;
import io.leangen.graphql.metadata.strategy.query.PublicResolverBuilder;
import io.leangen.graphql.metadata.strategy.query.ResolverBuilder;
import io.leangen.graphql.metadata.strategy.type.TypeInfoGenerator;
import io.leangen.graphql.metadata.strategy.value.InputFieldBuilder;
import io.leangen.graphql.metadata.strategy.value.ValueMapperFactory;
import io.leangen.graphql.module.Module;
import io.leangen.graphql.spqr.spring.annotations.GraphQLApi;
import io.leangen.graphql.spqr.spring.annotations.WithResolverBuilder;
import io.leangen.graphql.spqr.spring.annotations.WithResolverBuilders;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.StandardMethodMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

@Configuration
@ConditionalOnClass(GraphQLSchemaGenerator.class)
@EnableConfigurationProperties(SpqrProperties.class)
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
public class BaseAutoConfiguration {

    private final ConfigurableApplicationContext context;

    @Autowired(required = false)
    private ExtensionProvider<GeneratorConfiguration, ResolverBuilder> globalResolverBuilderExtensionProvider;

    @Autowired(required = false)
    private ExtensionProvider<GeneratorConfiguration, TypeMapper> typeMapperExtensionProvider;

    @Autowired(required = false)
    private ExtensionProvider<GeneratorConfiguration, InputConverter> inputConverterExtensionProvider;

    @Autowired(required = false)
    private ExtensionProvider<GeneratorConfiguration, OutputConverter> outputConverterExtensionProvider;

    @Autowired(required = false)
    private ExtensionProvider<GeneratorConfiguration, ArgumentInjector> argumentInjectorExtensionProvider;

    @Autowired(required = false)
    private ExtensionProvider<GeneratorConfiguration, SchemaTransformer> schemaTransformerExtensionProvider;

    @Autowired(required = false)
    private ExtensionProvider<GeneratorConfiguration, ResolverInterceptorFactory> resolverInterceptorFactoryExtensionProvider;

    @Autowired(required = false)
    private ValueMapperFactory valueMapperFactory;

    @Autowired(required = false)
    private ExtensionProvider<ExtendedGeneratorConfiguration, InputFieldBuilder> inputFieldBuilderProvider;

    @Autowired(required = false)
    private TypeInfoGenerator typeInfoGenerator;

    @Autowired(required = false)
    private AbstractInputHandler abstractInputHandler;

    @Autowired(required = false)
    private InclusionStrategy inclusionStrategy;

    @Autowired(required = false)
    private InterfaceMappingStrategy interfaceMappingStrategy;

    @Autowired(required = false)
    private Set<MessageBundle> messageBundles;

    @Autowired(required = false)
    private ExtensionProvider<GeneratorConfiguration, Module> moduleExtensionProvider;

    @Autowired(required = false)
    private List<Internal<Module>> internalModules;

    @Autowired
    public BaseAutoConfiguration(ConfigurableApplicationContext context) {
        this.context = context;
    }

    @Bean
    @ConditionalOnMissingBean
    public AnnotatedResolverBuilder defaultAnnotatedResolverBuilder() {
        return new AnnotatedResolverBuilder();
    }

    @Bean
    @ConditionalOnMissingBean
    public BeanResolverBuilder defaultBeanResolverBuilder() {
        return new BeanResolverBuilder();
    }

    @Bean
    @ConditionalOnMissingBean
    public PublicResolverBuilder defaultPublicResolverBuilder() {
        return new PublicResolverBuilder();
    }

    @Bean
    @ConditionalOnMissingBean
    public GraphQLSchemaGenerator graphQLSchemaGenerator(SpqrProperties spqrProperties) {
        GraphQLSchemaGenerator schemaGenerator = new GraphQLSchemaGenerator();

        schemaGenerator.withBasePackages(spqrProperties.getBasePackages());

        if (spqrProperties.getRelay().isEnabled()) {
            if (!StringUtils.isEmpty(spqrProperties.getRelay().getMutationWrapper())) {
                schemaGenerator.withRelayCompliantMutations(
                        spqrProperties.getRelay().getMutationWrapper(), spqrProperties.getRelay().getMutationWrapperDescription()
                );
            } else {
                schemaGenerator.withRelayCompliantMutations();
            }
        }

        Map<String, SpqrBean> beansWiredAsComponent = findGraphQLApiComponents();
        addOperationSources(schemaGenerator, beansWiredAsComponent.values());

        List<SpqrBean> beansWiredWithAsBeans = findGraphQLApiBeans();
        addOperationSources(schemaGenerator, beansWiredWithAsBeans);

        // Modules should be registered first, so that extension providers have a chance to override what they need
        // Built-in modules must go before the user-provided ones for similar reasons
        if (internalModules != null) {
            internalModules.forEach(module -> schemaGenerator.withModules(module.get()));
        }

        if (moduleExtensionProvider != null) {
            schemaGenerator.withModules(moduleExtensionProvider);
        }

        if (globalResolverBuilderExtensionProvider != null) {
            schemaGenerator.withResolverBuilders(globalResolverBuilderExtensionProvider);
        }

        if (typeMapperExtensionProvider != null) {
            schemaGenerator.withTypeMappers(typeMapperExtensionProvider);
        }

        if (inputConverterExtensionProvider != null) {
            schemaGenerator.withInputConverters(inputConverterExtensionProvider);
        }

        if (outputConverterExtensionProvider != null) {
            schemaGenerator.withOutputConverters(outputConverterExtensionProvider);
        }

        if (argumentInjectorExtensionProvider != null) {
            schemaGenerator.withArgumentInjectors(argumentInjectorExtensionProvider);
        }

        if (schemaTransformerExtensionProvider != null) {
            schemaGenerator.withSchemaTransformers(schemaTransformerExtensionProvider);
        }

        if (resolverInterceptorFactoryExtensionProvider != null) {
            schemaGenerator.withResolverInterceptorFactories(resolverInterceptorFactoryExtensionProvider);
        }

        if (valueMapperFactory != null) {
            schemaGenerator.withValueMapperFactory(valueMapperFactory);
        }

        if (inputFieldBuilderProvider != null) {
            schemaGenerator.withInputFieldBuilders(inputFieldBuilderProvider);
        }

        if (typeInfoGenerator != null) {
            schemaGenerator.withTypeInfoGenerator(typeInfoGenerator);
        }

        if (spqrProperties.isAbstractInputTypeResolution()) {
            schemaGenerator.withAbstractInputTypeResolution();
        }

        if (abstractInputHandler != null) {
            schemaGenerator.withAbstractInputHandler(abstractInputHandler);
        }

        if (messageBundles != null && !messageBundles.isEmpty()) {
            schemaGenerator.withStringInterpolation(messageBundles.toArray(new MessageBundle[0]));
        }

        if (inclusionStrategy != null) {
            schemaGenerator.withInclusionStrategy(inclusionStrategy);
        }

        if (interfaceMappingStrategy != null) {
            schemaGenerator.withInterfaceMappingStrategy(interfaceMappingStrategy);
        }

        if (spqrProperties.getRelay().isConnectionCheckRelaxed()) {
            schemaGenerator.withRelayConnectionCheckRelaxed();
        }

        return schemaGenerator;
    }

    private void addOperationSources(GraphQLSchemaGenerator schemaGenerator, Collection<SpqrBean> spqrBeans) {
        spqrBeans.stream()
                .filter(spqrBean -> spqrBean.getScope().equals(BeanScope.SINGLETON))
                .forEach(
                        spqrBean ->
                                schemaGenerator.withOperationsFromSingleton(
                                        spqrBean.getSpringBean(),
                                        spqrBean.getType(),
                                        spqrBean.resolverBuilders.stream()
                                                .map(resolverBuilderBeanIdentity -> findQualifiedBeanByType(resolverBuilderBeanIdentity.getResolverType(),
                                                        resolverBuilderBeanIdentity.getValue(),
                                                        resolverBuilderBeanIdentity.getQualifierType()))
                                                .toArray(ResolverBuilder[]::new)
                                )
                );
    }

    @Bean
    @ConditionalOnMissingBean
    public GraphQLSchema graphQLSchema(GraphQLSchemaGenerator schemaGenerator) {
        return schemaGenerator.generate();
    }

    @Bean
    @ConditionalOnMissingBean
    public GraphQL graphQL(GraphQLSchema schema) {
        GraphQL.Builder builder = GraphQL.newGraphQL(schema);
        return builder.build();
    }

    private <T> T findQualifiedBeanByType(Class<? extends T> type, String qualifierValue, Class<? extends Annotation> qualifierType) {
        final NoSuchBeanDefinitionException noSuchBeanDefinitionException = new NoSuchBeanDefinitionException(qualifierValue, "No matching " + type.getSimpleName() +
                " bean found for qualifier " + qualifierValue + " of type " + qualifierType.getSimpleName() + " !");
        try {

            if (StringUtils.isEmpty(qualifierValue)) {
                if (qualifierType.equals(Qualifier.class)) {
                    return Optional.of(
                            context.getBean(type))
                            .orElseThrow(() -> noSuchBeanDefinitionException);
                }
                return context.getBean(
                        Arrays.stream(context.getBeanNamesForAnnotation(qualifierType))
                                .filter(beanName -> type.isInstance(context.getBean(beanName)))
                                .findFirst()
                                .orElseThrow(() -> noSuchBeanDefinitionException),
                        type);
            }

            return BeanFactoryAnnotationUtils.qualifiedBeanOfType(context.getBeanFactory(), type, qualifierValue);
        } catch (NoSuchBeanDefinitionException noBeanException) {
            ConfigurableListableBeanFactory factory = context.getBeanFactory();

            for (String name : factory.getBeanDefinitionNames()) {
                BeanDefinition bd = factory.getBeanDefinition(name);

                if (bd.getSource() instanceof StandardMethodMetadata) {
                    StandardMethodMetadata metadata = (StandardMethodMetadata) bd.getSource();

                    if (metadata.getReturnTypeName().equals(type.getName())) {
                        Map<String, Object> attributes = metadata.getAnnotationAttributes(qualifierType.getName());
                        if (null != attributes) {
                            if (qualifierType.equals(Qualifier.class)) {
                                if (qualifierValue.equals(attributes.get("value"))) {
                                    return context.getBean(name, type);
                                }
                            }
                            return context.getBean(name, type);
                        }
                    }
                }
            }

            throw noSuchBeanDefinitionException;
        }
    }

    @SuppressWarnings({"unchecked"})
    private List<SpqrBean> findGraphQLApiBeans() {
        ConfigurableListableBeanFactory factory = context.getBeanFactory();

        List<SpqrBean> spqrBeans = new ArrayList<>();

        for (String beanName : factory.getBeanDefinitionNames()) {
            BeanDefinition bd = factory.getBeanDefinition(beanName);

            if (bd.getSource() instanceof StandardMethodMetadata) {
                StandardMethodMetadata metadata = (StandardMethodMetadata) bd.getSource();

                Map<String, Object> attributes = metadata.getAnnotationAttributes(GraphQLApi.class.getName());
                if (null == attributes) {
                    continue;
                }

                SpqrBean spqrBean = new SpqrBean(context, beanName, metadata.getIntrospectedMethod().getAnnotatedReturnType());

                Map<String, Object> withResolverBuildersAttributes = metadata.getAnnotationAttributes(WithResolverBuilders.class.getTypeName());
                if (withResolverBuildersAttributes != null) {
                    AnnotationAttributes[] annotationAttributesArray = (AnnotationAttributes[]) withResolverBuildersAttributes.get("value");
                    Arrays.stream(annotationAttributesArray)
                            .forEach(annotationAttributes ->
                                    spqrBean.getResolverBuilders().add(
                                            new ResolverBuilderBeanIdentity(
                                                    (Class<? extends ResolverBuilder>) annotationAttributes.get("value"),
                                                    (String) annotationAttributes.get("qualifierValue"),
                                                    (Class<? extends Annotation>) annotationAttributes.get("qualifierType"))
                                    )
                            );
                } else {
                    Map<String, Object> withResolverBuilderAttributes = metadata.getAnnotationAttributes(WithResolverBuilder.class.getTypeName());
                    if (withResolverBuilderAttributes != null) {
                        spqrBean.getResolverBuilders().add(
                                new ResolverBuilderBeanIdentity(
                                        (Class<? extends ResolverBuilder>) withResolverBuilderAttributes.get("value"),
                                        (String) withResolverBuilderAttributes.get("qualifierValue"),
                                        (Class<? extends Annotation>) withResolverBuilderAttributes.get("qualifierType"))
                        );
                    }
                }

                spqrBeans.add(spqrBean);
            }
        }

        return spqrBeans;
    }

    private Map<String, SpqrBean> findGraphQLApiComponents() {
        final Map<String, Object> operationSourcesBeans = context.getBeansWithAnnotation(GraphQLApi.class);

        Map<String, SpqrBean> result = new HashMap<>();
        for (String beanName : operationSourcesBeans.keySet()) {
            Class<?> operationSourceBeanClass = operationSourcesBeans.get(beanName).getClass();
            result.put(beanName, new SpqrBean(context, beanName, GenericTypeReflector.annotate(ClassUtils.getUserClass(operationSourceBeanClass))));

            if (operationSourceBeanClass.isAnnotationPresent(WithResolverBuilder.class)) {
                WithResolverBuilder withResolverBuilder = operationSourceBeanClass.getAnnotation(WithResolverBuilder.class);
                result.get(beanName).resolverBuilders.add(new ResolverBuilderBeanIdentity(withResolverBuilder.value(), withResolverBuilder.qualifierValue(), withResolverBuilder.qualifierType()));
            } else if (operationSourceBeanClass.isAnnotationPresent(WithResolverBuilders.class)) {
                for (WithResolverBuilder withResolverBuilder : operationSourceBeanClass.getAnnotation(WithResolverBuilders.class).value()) {
                    result.get(beanName).resolverBuilders.add(new ResolverBuilderBeanIdentity(withResolverBuilder.value(), withResolverBuilder.qualifierValue(), withResolverBuilder.qualifierType()));
                }

            }
        }
        return result;
    }

    private static class SpqrBean {
        private final BeanScope scope;
        private final Object springBean;
        private final AnnotatedType type;
        private final List<ResolverBuilderBeanIdentity> resolverBuilders;

        SpqrBean(ApplicationContext context, String beanName, AnnotatedType type) {
            this.springBean = context.getBean(beanName);
            this.scope = BeanScope.findBeanScope(context, beanName);
            this.type = type;
            this.resolverBuilders = new ArrayList<>();
        }

        BeanScope getScope() {
            return scope;
        }

        Object getSpringBean() {
            return springBean;
        }

        public AnnotatedType getType() {
            return type;
        }

        List<ResolverBuilderBeanIdentity> getResolverBuilders() {
            return resolverBuilders;
        }
    }

    private class ResolverBuilderBeanIdentity {
        private final Class<? extends ResolverBuilder> resolverType;
        private final String value;
        private final Class<? extends Annotation> qualifierType;

        private ResolverBuilderBeanIdentity(Class<? extends ResolverBuilder> resolverType, String value, Class<? extends Annotation> qualifierType) {
            this.resolverType = resolverType;
            this.value = value;
            this.qualifierType = qualifierType;
        }

        String getValue() {
            return value;
        }

        Class<? extends Annotation> getQualifierType() {
            return qualifierType;
        }

        Class<? extends ResolverBuilder> getResolverType() {
            return resolverType;
        }
    }

    private enum BeanScope {
        SINGLETON,
        PROTOTYPE;

        static BeanScope findBeanScope(ApplicationContext context, String beanName) {
            if (context.isSingleton(beanName)) {
                return BeanScope.SINGLETON;
            } else if (context.isPrototype(beanName)) {
                return BeanScope.PROTOTYPE;
            } else {
                //TODO log warning and proceed
                throw new RuntimeException("Unsupported bean scope");
            }
        }
    }

}