package ru.yandex.qe.dispenser.api.v1.request;

import java.io.IOException;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import ru.yandex.qe.dispenser.api.DtoBuilder;
import ru.yandex.qe.dispenser.api.util.JsonDeserializerBase;
import ru.yandex.qe.dispenser.api.util.JsonSerializerBase;
import ru.yandex.qe.dispenser.api.util.ValidationUtils;
import ru.yandex.qe.dispenser.api.v1.DiAmount;
import ru.yandex.qe.dispenser.api.v1.DiMetaField;
import ru.yandex.qe.dispenser.api.v1.DiMetaValueSet;

import static ru.yandex.qe.dispenser.api.util.SerializationUtils.convertValue;

@JsonSerialize(using = DiEntity.Serializer.class)
@JsonDeserialize(using = DiEntity.Deserializer.class)
public final class DiEntity extends DiEntityReference {
    @NotNull
    private final Set<DiResourceAmount> dimensions;
    @Nullable
    private final Duration ttl;
    @NotNull
    private final DiMetaValueSet metaValues;

    private DiEntity(@NotNull final Builder builder) {
        super(builder);
        dimensions = Collections.unmodifiableSet(new HashSet<>(builder.dimensions));
        ttl = builder.ttl;
        metaValues = builder.metaValues;
    }

    @NotNull
    public static Builder withKey(@NotNull final String key) {
        return new Builder(key);
    }

    @NotNull
    public Collection<DiResourceAmount> getDimensions() {
        return dimensions;
    }

    @NotNull
    @Deprecated
    public DiAmount getSize(@NotNull final String resourceKey) {
        return getSize(resourceKey, Collections.emptySet());
    }

    @NotNull
    public DiAmount getSize(@NotNull final String resourceKey, @NotNull final Set<String> segments) {
        final DiResourceAmount resourceAmount = dimensions.stream()
                .filter(d -> d.getResourceKey().equals(resourceKey) & d.getSegmentKeys().equals(segments))
                .findFirst()
                .orElse(null);
        return ValidationUtils.requireNonNull(resourceAmount, "No dimension of resource '" + resourceKey + "'!").getAmount();
    }

    @Nullable
    public Duration getTtl() {
        return ttl;
    }

    public Optional<String> tryGetHostIdentifier() {
        return tryGet("identifier");
    }

    public Optional<String> tryGetServiceKey() {
        return tryGet("serviceKey");
    }

    public Optional<Long> tryGetCreationTime() {
        return tryGet("creationTime");
    }

    @NotNull
    public DiMetaValueSet getMetaValues() {
        return metaValues;
    }

    private <T> Optional<T> tryGet(@NotNull final String name) {
        for (final DiMetaField<?> field : metaValues.getKeys()) {
            if (name.equals(field.getKey())) {
                return Optional.of(metaValues.getValue((DiMetaField<T>) field));
            }
        }
        return Optional.empty();
    }

    public static final class Builder extends DiEntityReference.Builder implements DtoBuilder<DiEntity> {
        @NotNull
        private final Set<DiResourceAmount> dimensions = new HashSet<>();
        @Nullable
        private Duration ttl;
        @NotNull
        private DiMetaValueSet metaValues = DiMetaValueSet.EMPTY;

        private Builder(@NotNull final String key) {
            super(key);
        }

        @NotNull
        @Override
        public Builder bySpecification(@NotNull final String specificationKey) {
            super.bySpecification(specificationKey);
            return this;
        }

        @NotNull
        @Deprecated
        public Builder occupies(@NotNull final String resourceKey, @NotNull final DiAmount amount) {
            return occupies(DiResourceAmount.ofResource(resourceKey).withAmount(amount).build());
        }

        @NotNull
        public Builder occupies(@NotNull final DiResourceAmount resourceAmount) {
            this.dimensions.add(resourceAmount);
            return this;
        }

        @NotNull
        public Builder occupies(@NotNull final Collection<DiResourceAmount> resourceAmounts) {
            resourceAmounts.forEach(this::occupies);
            return this;
        }

        @NotNull
        public Builder withTtl(@NotNull final Duration ttl) {
            this.ttl = ttl;
            return this;
        }

        @NotNull
        public Builder meta(@NotNull final DiMetaValueSet metaValues) {
            this.metaValues = metaValues;
            return this;
        }

        @NotNull
        @Override
        public DiEntity build() {
            return new DiEntity(this);
        }
    }

    static final class Serializer extends JsonSerializerBase<DiEntity> {
        @Override
        public void serialize(@NotNull final DiEntity entity,
                              @NotNull final JsonGenerator jg,
                              @NotNull final SerializerProvider sp) throws IOException {
            jg.writeStartObject();
            jg.writeStringField("key", entity.getKey());
            jg.writeStringField("specificationKey", entity.getSpecificationKey());
            if (entity.metaValues != DiMetaValueSet.EMPTY) {
                jg.writeObjectField("meta", entity.metaValues);
            }
            jg.writeObjectField("dimensions", entity.dimensions);
            if (entity.getTtl() != null) {
                jg.writeObjectField("ttl", entity.getTtl().toMillis());
            }
            jg.writeEndObject();
        }
    }

    static final class Deserializer extends JsonDeserializerBase<DiEntity> {
        @NotNull
        @Override
        public DiEntity deserialize(@NotNull final JsonParser jp,
                                    @NotNull final DeserializationContext dc) throws IOException {
            final JsonNode json = toJson(jp);
            final DiEntity.Builder builder = withKey(json.get("key").asText())
                    .bySpecification(json.get("specificationKey").asText())
                    .meta(json.has("meta") ? convertValue(json.get("meta"), DiMetaValueSet.class) : DiMetaValueSet.EMPTY)
                    .occupies(convertValue(json.get("dimensions"), new TypeReference<Collection<DiResourceAmount>>() {
                    }));
            if (json.hasNonNull("ttl")) {
                builder.withTtl(Duration.ofMillis(json.get("ttl").asInt()));
            }
            return builder.build();
        }
    }
}
