package io.wealthwizards.appsynctesting.utils.transform;

import com.amazonaws.services.dynamodbv2.document.ItemUtils;
import io.vavr.Tuple;
import lombok.EqualsAndHashCode;

import java.util.*;
import java.util.stream.Collectors;

@EqualsAndHashCode
class ExpressionsTree {
    private String operation;
    private List<ExpressionsTree> branches;
    private Expression leaf;

    public ExpressionsTree(String operation, List<ExpressionsTree> branches) {
        this.operation = operation;
        this.branches = branches;
    }

    public ExpressionsTree(Expression leaf) {
        this.leaf = leaf;
    }

    protected FilterExpression buildFilterExpression(String expressionValuePrefix, boolean isTopLevel) {
        if (branches == null) {
            return leaf.getFilterExpression();
        }

        var branchExpressions = branches.stream()
                .map(tree -> tree.buildFilterExpression(expressionValuePrefix, false))
                .collect(Collectors.toCollection(TreeSet::new));

        var delimiter = String.format(" %s ", operation);

        var expressionNames = branchExpressions.stream()
                .sequential()
                .map(FilterExpression::getExpressionNames)
                .map(TreeMap::new)
                .map(Map::entrySet)
                .flatMap(Set::stream)
                // if duplicated (possible) just take a, as they should be identical
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a));

        var expressionValues = branchExpressions.stream()
                .sequential()
                .map(FilterExpression::getExpressionValues)
                .map(TreeMap::new)
                .map(Map::entrySet)
                .flatMap(Set::stream)
                // shouldn't need a merge function, throw if it does
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        var names = expressionNames.keySet().size();

        var expressions = branchExpressions.stream()
                .sequential()
                .map(FilterExpression::getExpression)
                .collect(Collectors.toList());

        var hasEmpty = expressions.contains("");

        var expressionCollector = branchExpressions.size() > 1 && !isTopLevel || branchExpressions.size() > 1 && names == 1 && !hasEmpty
                ? Collectors.joining(delimiter, "(", ")")
                : Collectors.joining(delimiter);

        var expression = expressions.stream()
                .collect(expressionCollector);

        return new FilterExpression(expression, expressionNames, expressionValues);
    }

    public FilterExpression getFilterExpression() {
        return buildFilterExpression(":", true);
    }

    public static ExpressionsTree fromEntries(Set<Map.Entry<String, Object>> entries, String prefixExpressionName) {
        if (entries.size() < 1) {
            throw new IllegalStateException("Cannot build from entries without entries");
        }

        // build a branch for each entry at this level
        var branches = entries.stream()
                .map(entry -> {
                    // recursively build child branches for each item in the list
                    if (entry.getValue() instanceof List) {
                        @SuppressWarnings("unchecked")
                        var children = (List<Map<String, Object>>) entry.getValue();

                        var subTrees = children.stream()
                                .map(child -> Tuple.of(children.indexOf(child), child))
                                .map(tuple -> tuple.map2(Map::entrySet))
                                .map(tuple -> {
                                    var position = tuple._1();
                                    var subTree = tuple._2();
                                    var newPrefix = String.format(
                                            "%s%s_%s_",
                                            prefixExpressionName,
                                            entry.getKey(),
                                            position
                                    );

                                    return ExpressionsTree.fromEntries(subTree, newPrefix);
                                })
                                .collect(Collectors.toList());

                        return new ExpressionsTree(entry.getKey(), subTrees);
                    }

                    var name = entry.getKey();
                    // otherwise this entry is map of leaves (conditions -> values)
                    @SuppressWarnings("unchecked")
                    var value = (Map<String, Object>) entry.getValue();

                    @SuppressWarnings("unchecked")
                    var leaves = value.entrySet()
                            .stream()
                            .map(expressionEntry -> {
                                var expressionEntryValue = expressionEntry.getValue();

                                if (expressionEntryValue instanceof List) {
                                    return new Expression(
                                            name,
                                            expressionEntry.getKey(),
                                            (List<Object>) expressionEntryValue,
                                            prefixExpressionName
                                    );
                                }

                                return new Expression(
                                        name,
                                        expressionEntry.getKey(),
                                        (String) expressionEntryValue,
                                        prefixExpressionName
                                );
                            })
                            .map(ExpressionsTree::new)
                            .collect(Collectors.toList());

                    // if there's only one leaf, return it
                    if (leaves.size() == 1) {
                        return leaves.get(0);
                    }

                    return new ExpressionsTree("AND", leaves);
                })
                .collect(Collectors.toList());

        // if there's only one branch, return it
        if (branches.size() == 1) {
            return branches.get(0);
        }

        return new ExpressionsTree("AND", branches);
    }
}
