package ru.yandex.webmaster3.storage.util.yt;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Preconditions;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import static ru.yandex.webmaster3.core.util.functional.ThrowingFunction.rethrowingUnchecked;

/**
 * @author akhazhoyan 02/2018
 */
@Slf4j
public final class YtSchema {
    private static final ObjectMapper OM = new ObjectMapper();
    private final Collection<YtColumn<?>> columns = new ArrayList<>();
    private final ArrayNode jsonSchema = OM.createArrayNode();
    private final Set<String> columnNames = new HashSet<>();
    private final boolean isStrict;

    public YtSchema() {
        this(true);
    }

    public YtSchema(boolean isStrict) {
        this.isStrict = isStrict;
    }

    public <T> YtColumn<T> addColumn(String name, YtColumn.Type<T> type) {
        Preconditions.checkArgument(columnNames.add(name), "Column with name " + name + " already exists in schema");
        YtColumn<T> ytColumn = new YtColumn<>(name, type);
        columns.add(ytColumn);
        ObjectNode column = OM.createObjectNode();
        column.put("name", name);
        column.put("type", type.getName());
        jsonSchema.add(column);
        return ytColumn;
    }

    @Nullable
    public static YtSchema fromJsonNode(JsonNode schemaNode) {
        var attrsNode = schemaNode.get("$attributes");
        if (attrsNode == null || attrsNode.isNull()) {
            log.error("$attributes node not found");
            return null;
        }

        var strictNode = attrsNode.get("strict");
        if (strictNode == null || strictNode.isNull()) {
            log.error("strict node not found");
            return null;
        }

        var valueNode = schemaNode.get("$value");
        if (valueNode == null || valueNode.isNull()) {
            log.error("$value node not found");
            return null;
        }

        YtSchema schema = new YtSchema(strictNode.asBoolean());
        ArrayNode schemaArr = (ArrayNode)valueNode;
        for (int i = 0; i < schemaArr.size(); ++i) {
            var columnSchemaNode = schemaArr.get(i);

            var nameNode = columnSchemaNode.get("name");
            if (nameNode == null || nameNode.isNull()) {
                log.error("column name node is not found");
                return null;
            }

            var typeNode = columnSchemaNode.get("type");
            if (typeNode == null || typeNode.isNull()) {
                log.error("column type node is not found");
                return null;
            }

            String nameStr = nameNode.asText();
            String typeStr = typeNode.asText();
            YtColumn.Type<?> type = YtColumn.Type.fromString(typeStr);
            if (type == null) {
                log.error("Failed to create type for {}", typeStr);
                return null;
            }

            schema.addColumn(nameStr, type);
        }

        return schema;
    }

    public boolean isStrict() {
        return isStrict;
    }

    public String schema() {
        return rethrowingUnchecked(OM::writeValueAsString).apply(columns);
    }

    public ArrayNode getJsonSchema() {
        return jsonSchema;
    }

    public Collection<YtColumn<?>> getColumns() {
        return columns;
    }
}
