package cz.moneta.test.harness.connectors.messaging.kafkautils;

import com.google.gson.*;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;

import java.util.ArrayList;
import java.util.List;

public class JsonToAvroConverter {

    public static GenericRecord processJson(String json, Schema schema) throws IllegalArgumentException, JsonSchemaException {
        GenericRecord result = (GenericRecord) jsonElementToAvro(JsonParser.parseString(json), schema);
        return result;
    }

    private static Object jsonElementToAvro(JsonElement element, Schema schema) throws JsonSchemaException {
        boolean schemaIsNullable = isNullable(schema);
        if (schemaIsNullable) {
            schema = typeFromNullable(schema);
        }
        if (element == null || element.isJsonNull()) {
            if (!schemaIsNullable) {
                throw new JsonSchemaException("The element is not nullable in Avro schema.");
            }
            return null;
        } else if (element.isJsonObject()) {
            if (schema.getType() != Schema.Type.RECORD) {
                throw new JsonSchemaException(
                        String.format("The element `%s` doesn't match Avro type RECORD", element));
            }
            return jsonObjectToAvro(element.getAsJsonObject(), schema);
        } else if (element.isJsonArray()) {
            if (schema.getType() != Schema.Type.ARRAY) {
                throw new JsonSchemaException(
                        String.format("The element `%s` doesn't match Avro type ARRAY", element));
            }
            JsonArray jsonArray = element.getAsJsonArray();
            List<Object> avroArray = new ArrayList<>(jsonArray.size());
            for (JsonElement e : element.getAsJsonArray()) {
                avroArray.add(jsonElementToAvro(e, schema.getElementType()));
            }
            return avroArray;
        } else if (element.isJsonPrimitive()) {
            return jsonPrimitiveToAvro(element.getAsJsonPrimitive(), schema);
        } else {
            throw new JsonSchemaException(
                    String.format(
                            "The Json element `%s` is of an unknown class %s", element, element.getClass()));
        }
    }

    private static GenericRecord jsonObjectToAvro(JsonObject jsonObject, Schema schema) throws JsonSchemaException {
        GenericRecord avroRecord = new GenericData.Record(schema);

        for (Schema.Field field : schema.getFields()) {
            avroRecord.put(field.name(), jsonElementToAvro(jsonObject.get(field.name()), field.schema()));
        }
        return avroRecord;
    }

    private static boolean isNullable(Schema type) {
        return type.getType() == Schema.Type.NULL
                || type.getType() == Schema.Type.UNION
                && type.getTypes().stream().anyMatch(JsonToAvroConverter::isNullable);
    }

    private static Schema typeFromNullable(Schema type) {
        if (type.getType() == Schema.Type.UNION) {
            return typeFromNullable(
                    type.getTypes().stream()
                            .filter(t -> t.getType() != Schema.Type.NULL)
                            .findFirst()
                            .orElseThrow(
                                    () ->
                                            new IllegalStateException(
                                                    String.format("Type `%s` should have a non null subtype", type))));
        }
        return type;
    }

    private static Object jsonPrimitiveToAvro(JsonPrimitive primitive, Schema schema) {
        switch (schema.getType()) {
            case NULL:
                return null;
            case STRING:
                return primitive.getAsString();
            case BOOLEAN:
                return primitive.getAsBoolean();
            case INT:
                return primitive.getAsInt();
            case LONG:
                return primitive.getAsLong();
            case FLOAT:
                return primitive.getAsFloat();
            case DOUBLE:
                return primitive.getAsDouble();
            default:
                return primitive.getAsString();
        }
    }

    private static class JsonSchemaException extends Exception {
        JsonSchemaException(String message) {
            super(message);
        }
    }
}
