package graphql.normalized.nf;

import com.google.common.collect.ImmutableList;
import graphql.Assert;
import graphql.ExperimentalApi;
import graphql.introspection.Introspection;
import graphql.language.Argument;
import graphql.language.Directive;
import graphql.language.Document;
import graphql.language.Field;
import graphql.language.InlineFragment;
import graphql.language.OperationDefinition;
import graphql.language.Selection;
import graphql.language.SelectionSet;
import graphql.language.TypeName;
import graphql.schema.GraphQLCompositeType;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLUnmodifiedType;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static graphql.collect.ImmutableKit.emptyList;
import static graphql.language.Field.newField;
import static graphql.language.InlineFragment.newInlineFragment;
import static graphql.language.SelectionSet.newSelectionSet;
import static graphql.language.TypeName.newTypeName;
import static graphql.schema.GraphQLTypeUtil.unwrapAll;

/**
 * This class can take a list of {@link NormalizedField}s and compiling out a
 * normalised operation {@link Document} that would represent how those fields
 * may be executed.
 * <p>
 * This is essentially the reverse of {@link NormalizedDocumentFactory} which takes
 * operation text and makes {@link NormalizedField}s from it, this takes {@link NormalizedField}s
 * and makes operation text from it.
 * <p>
 * You could for example send that operation text onto to some other graphql server if it
 * has the same schema as the one provided.
 */
@ExperimentalApi
public class NormalizedOperationToAstCompiler {

    /**
     * The result is a {@link Document} and a map of variables
     * that would go with that document.
     */
    public static class CompilerResult {
        private final Document document;
        private final Map<String, Object> variables;

        public CompilerResult(Document document, Map<String, Object> variables) {
            this.document = document;
            this.variables = variables;
        }

        public Document getDocument() {
            return document;
        }

        public Map<String, Object> getVariables() {
            return variables;
        }
    }

    public static CompilerResult compileToDocument(GraphQLSchema graphQLSchema,
                                                   GraphQLObjectType rootType,
                                                   List<NormalizedField> rootFields,
                                                   @Nullable String operationName,
                                                   OperationDefinition.Operation operationKind) {

        return compileToDocumentImpl(graphQLSchema, rootType, rootFields, operationName, operationKind);
    }

    public static CompilerResult compileToDocument(GraphQLSchema graphQLSchema,
                                                   GraphQLObjectType rootType,
                                                   NormalizedField singleRootField,
                                                   @Nullable String operationName,
                                                   OperationDefinition.Operation operationKind) {
        return compileToDocumentImpl(graphQLSchema, rootType, ImmutableList.of(singleRootField), operationName, operationKind);


    }


    public static CompilerResult compileToDocument(GraphQLSchema schema,
                                                   NormalizedOperation normalizedOperation) {
        GraphQLObjectType operationType = getOperationType(schema, normalizedOperation.getOperation());

        return compileToDocumentImpl(
                schema,
                operationType,
                normalizedOperation.getRootFields(),
                normalizedOperation.getOperationName(),
                normalizedOperation.getOperation()
        );
    }

    private static CompilerResult compileToDocumentImpl(GraphQLSchema schema,
                                                        GraphQLObjectType rootType,
                                                        List<NormalizedField> rootFields,
                                                        @Nullable String operationName,
                                                        OperationDefinition.Operation operationKind) {

        List<Selection<?>> selections = subSelectionsForNormalizedFields(schema, rootType.getName(), rootFields);
        SelectionSet selectionSet = new SelectionSet(selections);

        OperationDefinition.Builder definitionBuilder = OperationDefinition.newOperationDefinition()
                .name(operationName)
                .operation(operationKind)
                .selectionSet(selectionSet);

//        definitionBuilder.variableDefinitions(variableAccumulator.getVariableDefinitions());

        return new CompilerResult(
                Document.newDocument()
                        .definition(definitionBuilder.build())
                        .build(),
                null
        );
    }


    private static List<Selection<?>> subSelectionsForNormalizedFields(GraphQLSchema schema,
                                                                       @NonNull String parentOutputType,
                                                                       List<NormalizedField> normalizedFields
    ) {
        ImmutableList.Builder<Selection<?>> selections = ImmutableList.builder();

        // All conditional fields go here instead of directly to selections, so they can be grouped together
        // in the same inline fragment in the output
        Map<String, List<Field>> fieldsByTypeCondition = new LinkedHashMap<>();

        for (NormalizedField nf : normalizedFields) {
            if (nf.isConditional(schema)) {
                selectionForNormalizedField(schema, nf)
                        .forEach((objectTypeName, field) ->
                                fieldsByTypeCondition
                                        .computeIfAbsent(objectTypeName, ignored -> new ArrayList<>())
                                        .add(field));
            } else {
                selections.add(selectionForNormalizedField(schema, parentOutputType, nf));
            }
        }

        fieldsByTypeCondition.forEach((objectTypeName, fields) -> {
            TypeName typeName = newTypeName(objectTypeName).build();
            InlineFragment inlineFragment = newInlineFragment()
                    .typeCondition(typeName)
                    .selectionSet(selectionSet(fields))
                    .build();
            selections.add(inlineFragment);
        });

        return selections.build();
    }

    /**
     * @return Map of object type names to list of fields
     */
    private static Map<String, Field> selectionForNormalizedField(GraphQLSchema schema,
                                                                  NormalizedField normalizedField
    ) {
        Map<String, Field> groupedFields = new LinkedHashMap<>();

        for (String objectTypeName : normalizedField.getObjectTypeNames()) {
            groupedFields.put(objectTypeName, selectionForNormalizedField(schema, objectTypeName, normalizedField));
        }

        return groupedFields;
    }

    /**
     * @return Map of object type names to list of fields
     */
    private static Field selectionForNormalizedField(GraphQLSchema schema,
                                                     String objectTypeName,
                                                     NormalizedField normalizedField) {

        final List<Selection<?>> subSelections;
        if (normalizedField.getChildren().isEmpty()) {
            subSelections = emptyList();
        } else {
            GraphQLFieldDefinition fieldDef = getFieldDefinition(schema, objectTypeName, normalizedField);
            GraphQLUnmodifiedType fieldOutputType = unwrapAll(fieldDef.getType());

            subSelections = subSelectionsForNormalizedFields(
                    schema,
                    fieldOutputType.getName(),
                    normalizedField.getChildren()
            );
        }

        SelectionSet selectionSet = selectionSetOrNullIfEmpty(subSelections);
//        List<Argument> arguments = createArguments(executableNormalizedField, variableAccumulator);
        List<Argument> arguments = normalizedField.getAstArguments();
        List<Directive> directives = normalizedField.getAstDirectives();


        Field.Builder builder = newField()
                .name(normalizedField.getFieldName())
                .alias(normalizedField.getAlias())
                .selectionSet(selectionSet)
                .directives(directives)
                .arguments(arguments);
        return builder.build();
    }

    @Nullable
    private static SelectionSet selectionSetOrNullIfEmpty(List<Selection<?>> selections) {
        return selections.isEmpty() ? null : newSelectionSet().selections(selections).build();
    }

    private static SelectionSet selectionSet(List<Field> fields) {
        return newSelectionSet().selections(fields).build();
    }


    @NonNull
    private static GraphQLFieldDefinition getFieldDefinition(GraphQLSchema schema,
                                                             String parentType,
                                                             NormalizedField nf) {
        return Introspection.getFieldDef(schema, (GraphQLCompositeType) schema.getType(parentType), nf.getName());
    }


    private static GraphQLObjectType getOperationType(@NonNull GraphQLSchema schema,
                                                      OperationDefinition.@NonNull Operation operationKind) {
        switch (operationKind) {
            case QUERY:
                return schema.getQueryType();
            case MUTATION:
                return schema.getMutationType();
            case SUBSCRIPTION:
                return schema.getSubscriptionType();
        }

        return Assert.assertShouldNeverHappen("Unknown operation kind " + operationKind);
    }

}
