package io.wealthwizards.appsynctesting.utils.dynamodb;

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

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.vavr.Tuple;
import io.vavr.Tuple2;

public final class DynamoDB {
    private final Gson gson;

    public DynamoDB() {
        gson = new GsonBuilder()
                .serializeNulls()
                .setFieldNamingStrategy(new FieldNamingStrategy())
                .create();
    }

    public Map<String, ?> toDynamoDB(Object object) {
        if (object == null) {
            return toNull();
        }

        if (object instanceof String) {
            return toString((String) object);
        }

        if (object instanceof Number) {
            return toNumber((Number) object);
        }

        if (object instanceof Boolean) {
            return toBoolean((Boolean) object);
        }

        if (object instanceof Collection) {
            return toList((Collection<?>) object);
        }

        if (object instanceof Map) {
            @SuppressWarnings("unchecked")
            var map = toMap((Map<String, ?>) object);

            return map;
        }

        throw new RuntimeException("Unable to convert to DynamoDB, not instance of expectations");
    }

    public String toDynamoDBJson(Object object) {
        return gson.toJson(toDynamoDB(object));
    }

    public Map<String, Map<String, ?>> toMap(Map<String, ?> map) {
        var values = toMapValues(map);

        return new HashMap<>(Map.of("M", values));
    }

    public String toMapJson(Map<String, ?> map) {
        return gson.toJson(toMap(map));
    }

    public Map<String, ?> toMapValues(Map<String, ?> map) {
        var values = map.entrySet()
                .stream()
                .map(entry -> Tuple.of(entry.getKey(), toDynamoDB(entry.getValue())))
                .collect(Collectors.toMap(Tuple2::_1, Tuple2::_2));

        return new HashMap<>(values);
    }

    public String toMapValuesJson(Map<String, ?> map) {
        return gson.toJson(toMapValues(map));
    }

    public Map<String, String> toString(String string) {
        return new HashMap<>(Map.of("S", string));
    }

    public String toStringJson(String string) {
        return gson.toJson(toString(string));
    }

    public Map<String, List<String>> toStringSet(List<String> strings) {
        return new HashMap<>(Map.of("SS", strings));
    }

    public String toStringSetJson(List<String> strings) {
        return gson.toJson(toStringSet(strings));
    }

    public Map<String, String> toNumber(Number number) {
        return new HashMap<>(Map.of("N", number.toString()));
    }

    public String toNumberJson(Number number) {
        return gson.toJson(toNumber(number));
    }

    public Map<String, List<String>> toNumberSet(List<Number> numbers) {
        var strings = numbers.stream()
                .map(Number::toString)
                .collect(Collectors.toList());

        return new HashMap<>(Map.of("NS", strings));
    }

    public String toNumberSetJson(List<Number> numbers) {
        return gson.toJson(toNumberSet(numbers));
    }

    public Map<String, String> toBinary(String data) {
        return new HashMap<>(Map.of("B", data));
    }

    public String toBinaryJson(String data) {
        return gson.toJson(toBinary(data));
    }

    public Map<String, List<String>> toBinarySet(List<String> data) {
        return new HashMap<>(Map.of("BS", data));
    }

    public String toBinarySetJson(List<String> data) {
        return gson.toJson(toBinarySet(data));
    }

    public Map<String, Boolean> toBoolean(boolean bool) {
        return new HashMap<>(Map.of("BOOL", bool));
    }

    public String toBooleanJson(boolean bool) {
        return gson.toJson(toBoolean(bool));
    }

    public Map<String, Object> toNull() {
        var result = new HashMap<String, Object>();

        result.put("NULL", null);

        return result;
    }

    public String toNullJson() {
        return gson.toJson(toNull());
    }

    public Map<String, Object> toList(Collection<?> list) {
        var elements = list.stream()
                .map(this::toDynamoDB)
                .collect(Collectors.toList());

        return new HashMap<>(
                Map.of(
                        "L", new ArrayList<>(elements)
                )
        );
    }

    public String toListJson(Collection<?> list) {
        return gson.toJson(toList(list));
    }

    public Map<String, String> toS3Object(String key, String bucket, String region, String version) {
        Objects.requireNonNull(key);
        Objects.requireNonNull(bucket);
        Objects.requireNonNull(region);

        var pairs = new HashMap<>(
                Map.of(
                        "key", key,
                        "bucket", bucket,
                        "region", region
                )
        );

        // AppSync does not serialise any null values
        if (version != null) {
            pairs.put("version", version);
        }

        var s3Json = gson.toJson(
                // use a HashMap to get this to serialise consistently
                new HashMap<>(
                        Map.of("s3", pairs)
                )
        );

        return toString(s3Json);
    }

    public Map<String, String> toS3Object(String key, String bucket, String region) {
        return toS3Object(key, bucket, region, null);
    }

    public String toS3ObjectJson(String key, String bucket, String region, String version) {
        return gson.toJson(toS3Object(key, bucket, region, version));
    }

    public String toS3ObjectJson(String key, String bucket, String region) {
        return toS3ObjectJson(key, bucket, region, null);
    }

    @SuppressWarnings("unchecked")
    public Map<String, String> fromS3ObjectJson(String json) {
        var stringMap = (Map<String, String>) gson.fromJson(json, Map.class);
        var encodedS3 = (Map<String, Map<String, String>>) gson.fromJson(stringMap.get("S"), Map.class);
        var s3Object = encodedS3.get("s3");

        var result = new HashMap<>(Map.of(
                "key", s3Object.get("key"),
                "bucket", s3Object.get("bucket"),
                "region", s3Object.get("region")
        ));

        var version = s3Object.get("version");

        if (version != null) {
            result.put("version", version);
        }

        return result;
    }
}
