package graphql.extensions;

import com.google.common.collect.ImmutableMap;
import graphql.ExecutionResult;
import graphql.PublicApi;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

import static graphql.Assert.assertNotNull;

/**
 * This class can be used to help build the graphql `extensions` map.  A series of changes to the extensions can
 * be added and these will be merged together via a {@link ExtensionsMerger} implementation and that resultant
 * map can be used as the `extensions`
 * <p>
 * The engine will place a {@link ExtensionsBuilder} into the {@link graphql.GraphQLContext} (if one is not manually placed there)
 * and hence {@link graphql.schema.DataFetcher}s can use it to build up extensions progressively.
 * <p>
 * At the end of the execution, the {@link ExtensionsBuilder} will be used to build a graphql `extensions` map that
 * is placed in the {@link ExecutionResult}
 */
@PublicApi
public class ExtensionsBuilder {

    // thread safe since there can be many changes say in DFs across threads
    private final List<Map<Object, Object>> changes = new CopyOnWriteArrayList<>();
    private final ExtensionsMerger extensionsMerger;


    private ExtensionsBuilder(ExtensionsMerger extensionsMerger) {
        this.extensionsMerger = extensionsMerger;
    }

    /**
     * @return a new ExtensionsBuilder using a default merger
     */
    public static ExtensionsBuilder newExtensionsBuilder() {
        return new ExtensionsBuilder(ExtensionsMerger.DEFAULT);
    }

    /**
     * This creates a new ExtensionsBuilder with the provided {@link ExtensionsMerger}
     *
     * @param extensionsMerger the merging code to use
     *
     * @return a new ExtensionsBuilder using the provided merger
     */
    public static ExtensionsBuilder newExtensionsBuilder(ExtensionsMerger extensionsMerger) {
        return new ExtensionsBuilder(extensionsMerger);
    }

    /**
     * @return how many extension changes have been made so far
     */
    public int getChangeCount() {
        return changes.size();
    }

    /**
     * Adds new values into the extension builder
     *
     * @param newValues the new values to add
     *
     * @return this builder for fluent style reasons
     */
    public ExtensionsBuilder addValues(@NonNull Map<Object, Object> newValues) {
        assertNotNull(newValues);
        if (!newValues.isEmpty()) {
            changes.add(newValues);
        }
        return this;
    }

    /**
     * Adds a single new value into the extension builder
     *
     * @param key   the key in the extensions
     * @param value the value in the extensions
     *
     * @return this builder for fluent style reasons
     */
    public ExtensionsBuilder addValue(@NonNull Object key, @Nullable Object value) {
        assertNotNull(key);
        return addValues(Collections.singletonMap(key, value));
    }

    /**
     * This builds an extensions map from this builder, merging together the values provided
     *
     * @return a new extensions map
     */
    public Map<Object, Object> buildExtensions() {
        if (changes.isEmpty()) {
            return ImmutableMap.of();
        }
        Map<Object, Object> firstChange = changes.get(0);
        if (changes.size() == 1) {
            return firstChange;
        }
        Map<Object, Object> outMap = new LinkedHashMap<>(firstChange);
        for (int i = 1; i < changes.size(); i++) {
            Map<Object, Object> newMap = extensionsMerger.merge(outMap, changes.get(i));
            assertNotNull(outMap, () -> "You MUST provide a non null Map from ExtensionsMerger.merge()");
            outMap = newMap;
        }
        return outMap;
    }

    /**
     * This sets new  extensions into the provided {@link ExecutionResult}, overwriting any previous values
     *
     * @param executionResult the result to set these extensions into
     *
     * @return a new ExecutionResult with the extensions values in this builder
     */
    public ExecutionResult setExtensions(ExecutionResult executionResult) {
        assertNotNull(executionResult);
        Map<Object, Object> currentExtensions = executionResult.getExtensions();
        Map<Object, Object> builderExtensions = buildExtensions();
        // if there was no extensions map before, and we are not adding anything new
        // then leave it null
        if (currentExtensions == null && builderExtensions.isEmpty()) {
            return executionResult;
        }
        return executionResult.transform(builder -> builder.extensions(builderExtensions));
    }
}
