package io.wealthwizards.appsynctesting.utils.transform;

import com.amazonaws.services.dynamodbv2.document.ItemUtils;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.google.common.collect.Multiset;
import io.vavr.Tuple;
import io.vavr.Tuple2;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@AllArgsConstructor
class Expression {
    private String name;
    private String comparator;
    private List<Object> values;
    private String prefix;

    private static final Map<String, BiFunction<String, List<String>, String>> formatters;
    private static final List<String> rangeComparators = List.of("between");

    static {
        formatters = new HashMap<>();

        Function<String, BiFunction<String, List<String>, String>> takeFirstValueName = (format) ->
                (name, valueNames) -> String.format(format, name, valueNames.get(0));

        formatters.put("contains", takeFirstValueName.apply("(contains(%s,%s))"));
        formatters.put("notContains", takeFirstValueName.apply("(NOT contains(%s,%s))"));
        formatters.put("beginsWith", takeFirstValueName.apply("(begins_with(%s,%s))"));
        formatters.put("eq", takeFirstValueName.apply("(%s \u003d %s)"));
        formatters.put("ne", takeFirstValueName.apply("(%s \u003c\u003e %s)"));
        formatters.put("le", takeFirstValueName.apply("(%s \u003c\u003d %s)"));
        formatters.put("lt", takeFirstValueName.apply("(%s \u003c %s)"));
        formatters.put("ge", takeFirstValueName.apply("(%s \u003e\u003d %s)"));
        formatters.put("gt", takeFirstValueName.apply("(%s \u003e %s)"));
        formatters.put("in", (name, valueNames) -> {
            var group = valueNames.stream()
                    .collect(Collectors.joining(", ", "(", ")"));

            return String.format("(%s IN %s)", name, group);
        });
        formatters.put("between", (name, valueNames) -> String.format(
            "(%s BETWEEN %s AND %s)",
            name,
            valueNames.get(0),
            valueNames.get(1)
        ));
    }

    public Expression(String name, String comparator, String value, String prefix) {
        this.name = name;
        this.comparator = comparator;
        this.values = List.of(value);
        this.prefix = prefix;
    }

    public FilterExpression getFilterExpression() {
        var formatter = formatters.get(comparator);

        var expressionName = String.format("#%s", name);

        var isRangeComparator = rangeComparators.contains(comparator);

        // TODO: check if need to use internal DymamoDB implementation :(
        Function<Tuple2<Object, String>, Map.Entry<String, AttributeValue>> buildEntry = (pair) -> {
            var singleValueName = String.format("%s%s_%s", prefix, name, comparator);
            var value = ItemUtils.toAttributeValue(pair._1());

            if (values.size() == 1) {
                return Map.entry(singleValueName, value);
            }

            var indexedValueName = String.format("%s_%s", singleValueName, pair._2());

            return Map.entry(indexedValueName, value);
        };

        var valueStream = isRangeComparator
                ? Stream.of(
                        Tuple.of(values.get(0), "start"),
                        Tuple.of(values.get(1), "end")
                  )
                : values.stream()
                    .map((value) -> Tuple.of(value, String.valueOf(values.indexOf(value))));

        var entries = valueStream.map(buildEntry)
                    .collect(Collectors.toList());

        var expressionValueNames = entries.stream()
                .sequential()
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());

        var expressionValues = entries.stream()
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        var expression = formatter.apply(expressionName, expressionValueNames);

        return new FilterExpression(
                expression,
                Map.of(expressionName, name),
                expressionValues
        );
    }
}
