package graphql.execution.values.legacycoercing;

import graphql.GraphQLContext;
import graphql.Scalars;
import graphql.execution.values.InputInterceptor;
import graphql.scalar.CoercingUtil;
import graphql.schema.GraphQLInputType;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import java.math.BigDecimal;
import java.util.Locale;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;

import static graphql.Assert.assertNotNull;
import static graphql.scalar.CoercingUtil.isNumberIsh;

public class LegacyCoercingInputInterceptor implements InputInterceptor {

    /**
     * This will ONLY observe legacy values and invoke the callback when it gets one.  you can use this to enumerate how many
     * legacy values are hitting you graphql implementation
     *
     * @param observerCallback a callback allowing you to observe a legacy scalar value
     *
     * @return an InputInterceptor that only observes values
     */
    public static LegacyCoercingInputInterceptor observesValues(BiConsumer<Object, GraphQLInputType> observerCallback) {
        return new LegacyCoercingInputInterceptor(((input, graphQLInputType) -> {
            observerCallback.accept(input, graphQLInputType);
            return input;
        }));
    }

    /**
     * This will change legacy values as it encounters them to something acceptable to the more strict coercion rules.
     *
     * @return an InputInterceptor that migrates values to a more strict value
     */
    public static LegacyCoercingInputInterceptor migratesValues() {
        return migratesValues((input, type) -> {
        });
    }

    /**
     * This will change legacy values as it encounters them to something acceptable to the more strict coercion rules.
     * The observer callback will be invoked if it detects a legacy value that it will change.
     *
     * @param observerCallback a callback allowing you to observe a legacy scalar value before it is migrated
     *
     * @return an InputInterceptor that both observes values and migrates them to a more strict value
     */
    public static LegacyCoercingInputInterceptor migratesValues(BiConsumer<Object, GraphQLInputType> observerCallback) {
        return new LegacyCoercingInputInterceptor(((input, graphQLInputType) -> {
            observerCallback.accept(input, graphQLInputType);
            if (Scalars.GraphQLBoolean.equals(graphQLInputType)) {
                return coerceLegacyBooleanValue(input);
            }
            if (Scalars.GraphQLFloat.equals(graphQLInputType)) {
                return coerceLegacyFloatValue(input);
            }
            if (Scalars.GraphQLInt.equals(graphQLInputType)) {
                return coerceLegacyIntValue(input);
            }
            if (Scalars.GraphQLString.equals(graphQLInputType)) {
                return coerceLegacyStringValue(input);
            }
            return input;
        }));
    }

    private final BiFunction<Object, GraphQLInputType, Object> behavior;

    private LegacyCoercingInputInterceptor(BiFunction<Object, GraphQLInputType, Object> behavior) {
        this.behavior = assertNotNull(behavior);
    }

    @Override
    public Object intercept(@Nullable Object input, @NonNull GraphQLInputType graphQLType, @NonNull GraphQLContext graphqlContext, @NonNull Locale locale) {
        if (isLegacyValue(input, graphQLType)) {
            // we ONLY apply the new behavior IF it's an old acceptable legacy value.
            // so for compliant values - we change nothing and invoke no behaviour
            // and for values that would not coerce anyway, we also invoke no behavior
            return behavior.apply(input, graphQLType);
        }
        return input;
    }

    @SuppressWarnings("RedundantIfStatement")
    static boolean isLegacyValue(Object input, GraphQLInputType graphQLType) {
        if (Scalars.GraphQLBoolean.equals(graphQLType)) {
            return isLegacyBooleanValue(input);
        } else if (Scalars.GraphQLFloat.equals(graphQLType)) {
            return isLegacyFloatValue(input);
        } else if (Scalars.GraphQLInt.equals(graphQLType)) {
            return isLegacyIntValue(input);
        } else if (Scalars.GraphQLString.equals(graphQLType)) {
            return isLegacyStringValue(input);
        } else {
            return false;
        }
    }

    static boolean isLegacyBooleanValue(Object input) {
        return input instanceof String || CoercingUtil.isNumberIsh(input);
    }

    static boolean isLegacyFloatValue(Object input) {
        return input instanceof String;
    }

    static boolean isLegacyIntValue(Object input) {
        return input instanceof String;
    }

    static boolean isLegacyStringValue(Object input) {
        return !(input instanceof String);
    }

    static Object coerceLegacyBooleanValue(Object input) {
        if (input instanceof String) {
            String lStr = ((String) input).toLowerCase();
            if (lStr.equals("true")) {
                return true;
            }
            if (lStr.equals("false")) {
                return false;
            }
            return input;
        } else if (isNumberIsh(input)) {
            BigDecimal value;
            try {
                value = new BigDecimal(input.toString());
            } catch (NumberFormatException e) {
                // this should never happen because String is handled above
                return input;
            }
            return value.compareTo(BigDecimal.ZERO) != 0;
        }
        // unchanged
        return input;
    }

    static Object coerceLegacyFloatValue(Object input) {
        if (isNumberIsh(input)) {
            BigDecimal value;
            try {
                value = new BigDecimal(input.toString());
            } catch (NumberFormatException e) {
                return input;
            }
            return value.doubleValue();
        }
        return input;
    }

    static Object coerceLegacyIntValue(Object input) {
        if (isNumberIsh(input)) {
            BigDecimal value;
            try {
                value = new BigDecimal(input.toString());
            } catch (NumberFormatException e) {
                return input;
            }
            try {
                return value.intValueExact();
            } catch (ArithmeticException e) {
                return input;
            }
        }
        return input;
    }


    static Object coerceLegacyStringValue(Object input) {
        return String.valueOf(input);
    }
}
