package io.leangen.graphql.metadata.strategy.query;

import graphql.execution.batched.Batched;
import graphql.language.OperationDefinition;
import io.leangen.graphql.annotations.GraphQLComplexity;
import io.leangen.graphql.generator.JavaDeprecationMappingConfig;
import io.leangen.graphql.metadata.Resolver;
import io.leangen.graphql.metadata.TypedElement;
import io.leangen.graphql.metadata.execution.FieldAccessor;
import io.leangen.graphql.metadata.execution.FixedMethodInvoker;
import io.leangen.graphql.metadata.execution.MethodInvoker;
import io.leangen.graphql.metadata.messages.MessageBundle;
import io.leangen.graphql.metadata.strategy.value.Property;
import io.leangen.graphql.util.ClassUtils;
import io.leangen.graphql.util.ReservedStrings;
import io.leangen.graphql.util.Utils;
import org.reactivestreams.Publisher;

import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * A resolver builder that exposes all public methods
 */
@SuppressWarnings("WeakerAccess")
public class PublicResolverBuilder extends AbstractResolverBuilder {

    private String[] basePackages;
    private JavaDeprecationMappingConfig javaDeprecationConfig;

    public PublicResolverBuilder() {
        this(new String[0]);
    }

    public PublicResolverBuilder(String... basePackages) {
        this.operationInfoGenerator = new DeprecationAwareOperationInfoGenerator();
        this.argumentBuilder = new AnnotatedArgumentBuilder();
        this.propertyElementReducer = AbstractResolverBuilder::mergePropertyElements;
        withBasePackages(basePackages);
        withJavaDeprecation(new JavaDeprecationMappingConfig(true, "Deprecated"));
        withDefaultFilters();
    }

    public PublicResolverBuilder withBasePackages(String... basePackages) {
        this.basePackages = basePackages;
        return this;
    }

    /**
     * Sets whether the {@code Deprecated} annotation should map to GraphQL deprecation
     *
     * @param javaDeprecation Whether the {@code Deprecated} maps to GraphQL deprecation
     * @return This builder instance to allow chained calls
     */
    public PublicResolverBuilder withJavaDeprecationRespected(boolean javaDeprecation) {
        this.javaDeprecationConfig = new JavaDeprecationMappingConfig(javaDeprecation, "Deprecated");
        return this;
    }

    /**
     * Sets whether and how the {@code Deprecated} annotation should map to GraphQL deprecation
     *
     * @param javaDeprecationConfig Configures if and how {@code Deprecated} maps to GraphQL deprecation
     * @return This builder instance to allow chained calls
     */
    public PublicResolverBuilder withJavaDeprecation(JavaDeprecationMappingConfig javaDeprecationConfig) {
        this.javaDeprecationConfig = javaDeprecationConfig;
        return this;
    }

    @Override
    public Collection<Resolver> buildQueryResolvers(ResolverBuilderParams params) {
        Set<Property> properties = ClassUtils.getProperties(ClassUtils.getRawType(params.getBeanType().getType()));
        Collection<Resolver> propertyAccessors = buildPropertyAccessors(properties.stream(), params);
        Collection<Resolver> methodInvokers = buildMethodInvokers(params, (method, par) -> isQuery(method, par) && properties.stream().noneMatch(prop -> prop.getGetter().equals(method)), OperationDefinition.Operation.QUERY, true);
        Collection<Resolver> fieldAccessors = buildFieldAccessors(params);
        return Utils.concat(methodInvokers.stream(), propertyAccessors.stream(), fieldAccessors.stream()).collect(Collectors.toSet());
    }

    @Override
    public Collection<Resolver> buildMutationResolvers(ResolverBuilderParams params) {
        return buildMethodInvokers(params, this::isMutation, OperationDefinition.Operation.MUTATION, false);
    }

    @Override
    public Collection<Resolver> buildSubscriptionResolvers(ResolverBuilderParams params) {
        return buildMethodInvokers(params, this::isSubscription, OperationDefinition.Operation.SUBSCRIPTION, false);
    }

    private Collection<Resolver> buildMethodInvokers(ResolverBuilderParams params, BiPredicate<Method, ResolverBuilderParams> filter, OperationDefinition.Operation operation, boolean batchable) {
        MessageBundle messageBundle = params.getEnvironment().messageBundle;
        AnnotatedType beanType = params.getBeanType();
        Supplier<Object> querySourceBean = params.getQuerySourceBeanSupplier();
        Class<?> rawType = ClassUtils.getRawType(beanType.getType());
        if (rawType.isArray() || rawType.isPrimitive()) return Collections.emptyList();

        return Arrays.stream(rawType.getMethods())
                .filter(method -> filter.test(method, params))
                .filter(method -> params.getInclusionStrategy().includeOperation(Collections.singletonList(method), beanType))
                .filter(getFilters().stream().reduce(Predicate::and).orElse(ACCEPT_ALL))
                .map(method -> {
                    TypedElement element = new TypedElement(getReturnType(method, params), method);
                    OperationInfoGeneratorParams infoParams = new OperationInfoGeneratorParams(element, beanType, querySourceBean, messageBundle, operation);
                    return new Resolver(
                            messageBundle.interpolate(operationInfoGenerator.name(infoParams)),
                            messageBundle.interpolate(operationInfoGenerator.description(infoParams)),
                            messageBundle.interpolate(ReservedStrings.decode(operationInfoGenerator.deprecationReason(infoParams))),
                            batchable && method.isAnnotationPresent(Batched.class),
                            querySourceBean == null ? new MethodInvoker(method, beanType) : new FixedMethodInvoker(querySourceBean, method, beanType),
                            element,
                            argumentBuilder.buildResolverArguments(
                                    new ArgumentBuilderParams(method, beanType, params.getInclusionStrategy(), params.getTypeTransformer(), params.getEnvironment())),
                            method.isAnnotationPresent(GraphQLComplexity.class) ? method.getAnnotation(GraphQLComplexity.class).value() : null
                    );
                })
                .collect(Collectors.toList());
    }

    private Collection<Resolver> buildPropertyAccessors(Stream<Property> properties, ResolverBuilderParams params) {
        MessageBundle messageBundle = params.getEnvironment().messageBundle;
        AnnotatedType beanType = params.getBeanType();
        Predicate<Member> mergedFilters = getFilters().stream().reduce(Predicate::and).orElse(ACCEPT_ALL);

        return properties
                .filter(prop -> isQuery(prop, params))
                .filter(prop -> mergedFilters.test(prop.getField()) && mergedFilters.test(prop.getGetter()))
                .filter(prop -> params.getInclusionStrategy().includeOperation(Arrays.asList(prop.getField(), prop.getGetter()), beanType))
                .map(prop -> {
                    TypedElement element = propertyElementReducer.apply(new TypedElement(getFieldType(prop.getField(), params), prop.getField()), new TypedElement(getReturnType(prop.getGetter(), params), prop.getGetter()));
                    OperationInfoGeneratorParams infoParams = new OperationInfoGeneratorParams(element, beanType, params.getQuerySourceBeanSupplier(), messageBundle, OperationDefinition.Operation.QUERY);
                    return new Resolver(
                            messageBundle.interpolate(operationInfoGenerator.name(infoParams)),
                            messageBundle.interpolate(operationInfoGenerator.description(infoParams)),
                            messageBundle.interpolate(ReservedStrings.decode(operationInfoGenerator.deprecationReason(infoParams))),
                            element.isAnnotationPresent(Batched.class),
                            params.getQuerySourceBeanSupplier() == null ? new MethodInvoker(prop.getGetter(), beanType) : new FixedMethodInvoker(params.getQuerySourceBeanSupplier(), prop.getGetter(), beanType),
                            element,
                            argumentBuilder.buildResolverArguments(new ArgumentBuilderParams(prop.getGetter(), beanType, params.getInclusionStrategy(), params.getTypeTransformer(), params.getEnvironment())),
                            element.isAnnotationPresent(GraphQLComplexity.class) ? element.getAnnotation(GraphQLComplexity.class).value() : null
                    );
                })
                .collect(Collectors.toSet());
    }

    private Collection<Resolver> buildFieldAccessors(ResolverBuilderParams params) {
        MessageBundle messageBundle = params.getEnvironment().messageBundle;
        AnnotatedType beanType = params.getBeanType();

        return Arrays.stream(ClassUtils.getRawType(beanType.getType()).getFields())
                .filter(field -> isQuery(field, params))
                .filter(getFilters().stream().reduce(Predicate::and).orElse(ACCEPT_ALL))
                .filter(field -> params.getInclusionStrategy().includeOperation(Collections.singletonList(field), beanType))
                .map(field -> {
                    TypedElement element = new TypedElement(getFieldType(field, params), field);
                    OperationInfoGeneratorParams infoParams = new OperationInfoGeneratorParams(element, beanType, params.getQuerySourceBeanSupplier(), messageBundle, OperationDefinition.Operation.QUERY);
                    return new Resolver(
                            messageBundle.interpolate(operationInfoGenerator.name(infoParams)),
                            messageBundle.interpolate(operationInfoGenerator.description(infoParams)),
                            messageBundle.interpolate(ReservedStrings.decode(operationInfoGenerator.deprecationReason(infoParams))),
                            false,
                            new FieldAccessor(field, beanType),
                            element,
                            Collections.emptyList(),
                            field.isAnnotationPresent(GraphQLComplexity.class) ? field.getAnnotation(GraphQLComplexity.class).value() : null
                    );
                })
                .collect(Collectors.toSet());
    }

    protected boolean isQuery(Method method, ResolverBuilderParams params) {
        return isPackageAcceptable(method, params) && !isMutation(method, params) && !isSubscription(method, params);
    }

    protected boolean isQuery(Field field, ResolverBuilderParams params) {
        return isPackageAcceptable(field, params);
    }

    protected boolean isQuery(Property property, ResolverBuilderParams params) {
        return isQuery(property.getGetter(), params);
    }

    protected boolean isMutation(Method method, ResolverBuilderParams params) {
        return isPackageAcceptable(method, params) && method.getReturnType() == void.class;
    }

    protected boolean isSubscription(Method method, ResolverBuilderParams params) {
        return isPackageAcceptable(method, params) && Publisher.class.isAssignableFrom(method.getReturnType());
    }

    protected boolean isPackageAcceptable(Member method, ResolverBuilderParams params) {
        Class<?> beanType = ClassUtils.getRawType(params.getBeanType().getType());
        String[] defaultPackages = params.getBasePackages();
        String[] basePackages = new String[0];
        if (Utils.isArrayNotEmpty(this.basePackages)) {
            basePackages = this.basePackages;
        } else if (Utils.isArrayNotEmpty(defaultPackages)) {
            basePackages = defaultPackages;
        } else if (beanType.getPackage() != null) {
            basePackages = new String[] {beanType.getPackage().getName()};
        }
        basePackages = Arrays.stream(basePackages).filter(Utils::isNotEmpty).toArray(String[]::new); //remove the default package
        return method.getDeclaringClass().equals(beanType)
                || Arrays.stream(basePackages).anyMatch(basePackage -> ClassUtils.isSubPackage(method.getDeclaringClass().getPackage(), basePackage));
    }

    private class DeprecationAwareOperationInfoGenerator extends DefaultOperationInfoGenerator {
        @Override
        public String deprecationReason(OperationInfoGeneratorParams params) {
            String explicit = ReservedStrings.decode(super.deprecationReason(params));
            if (explicit == null && javaDeprecationConfig.enabled && params.getElement().isAnnotationPresent(Deprecated.class)) {
                return javaDeprecationConfig.deprecationReason;
            }
            return explicit;
        }
    }
}