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

import java.io.IOException;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
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.SerializationUtils;
import ru.yandex.qe.dispenser.api.util.ValidationUtils;

@JsonSerialize(using = DiQuota.Serializer.class)
@JsonDeserialize(using = DiQuota.Deserializer.class)
public final class DiQuota {
    @NotNull
    private final DiQuotaSpec specification;
    @NotNull
    private final DiProject project;

    @NotNull
    private final DiAmount max;
    @NotNull
    private final DiAmount actual;
    @NotNull
    private final DiAmount ownMax;
    @NotNull
    private final DiAmount ownActual;

    @Nullable
    private final Long lastOverquotingTs;
    @NotNull
    private final String statisticsLink;
    @NotNull
    private final Set<String> segmentKeys;

    private DiQuota(@NotNull final Builder builder) {
        specification = builder.specification;
        project = builder.project;
        max = ValidationUtils.requireNonNull(builder.max, "Max and actual values required!");
        actual = ValidationUtils.requireNonNull(builder.actual, "Max and actual values required!");
        ownMax = ValidationUtils.requireNonNull(builder.ownMax, "Max and actual values required!");
        ownActual = ValidationUtils.requireNonNull(builder.ownActual, "Max and actual values required!");
        lastOverquotingTs = builder.lastOverquotingTs;
        statisticsLink = ValidationUtils.requireNonNull(builder.statisticsLink, "Statistics link is required!");
        segmentKeys = ValidationUtils.requireNonNull(builder.segmentKeys, "Segment keys are required");
    }

    @NotNull
    public static DiQuota.Builder builder(final @NotNull DiQuotaSpec specification, @NotNull final DiProject project) {
        return new Builder(specification, project);
    }

    @NotNull
    public DiQuotaSpec getSpecification() {
        return specification;
    }

    @NotNull
    public DiProject getProject() {
        return project;
    }

    @NotNull
    public DiAmount getMax() {
        return max;
    }

    @NotNull
    public DiAmount getOwnMax() {
        return ownMax;
    }

    public long getMax(@NotNull final DiUnit unit) {
        return unit.convert(max);
    }

    @NotNull
    public long getOwnMax(@NotNull final DiUnit unit) {
        return unit.convert(ownMax);
    }

    @NotNull
    public DiAmount getActual() {
        return actual;
    }

    @NotNull
    public DiAmount getOwnActual() {
        return ownActual;
    }

    public long getActual(@NotNull final DiUnit unit) {
        return unit.convert(actual);
    }

    @Nullable
    public Long getLastOverquotingTs() {
        return lastOverquotingTs;
    }

    @NotNull
    public String getStatisticsLink() {
        return statisticsLink;
    }

    @NotNull
    public Set<String> getSegmentKeys() {
        return segmentKeys;
    }

    public boolean canAcquire(@NotNull final DiAmount amount) {
        final DiUnit baseUnit = getSpecification().getResource().getType().getBaseUnit();
        return baseUnit.convert(actual) + baseUnit.convert(amount) <= baseUnit.convert(max);
    }

    @Override
    public boolean equals(@Nullable final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final DiQuota quota = (DiQuota) o;
        return specification.equals(quota.specification)
                && project.equals(quota.project)
                && segmentKeys.equals(quota.segmentKeys);
    }

    @Override
    public int hashCode() {
        return Objects.hash(specification, project, segmentKeys);
    }

    @Override
    public String toString() {
        return "DiQuota{" +
                "project = " + project.getKey() +
                ", service = " + specification.getResource().getService().getKey() +
                ", resource = " + specification.getResource().getKey() +
                ", specification = " + specification.getKey() +
                ", segments = " + segmentKeys +
                ", max = " + max +
                ", actual = " + actual +
                '}';
    }

    @NotNull
    public DiQuotaKey createKey() {
        return new DiQuotaKey.Builder()
                .projectKey(project.getKey())
                .serviceKey(specification.getResource().getService().getKey())
                .resourceKey(specification.getResource().getKey())
                .quotaSpecKey(specification.getKey())
                .segmentKeys(segmentKeys)
                .build();
    }

    public static class Builder implements DtoBuilder<DiQuota> {
        @NotNull
        private final DiQuotaSpec specification;
        @NotNull
        private final DiProject project;
        @Nullable
        private DiAmount max;
        @Nullable
        private DiAmount actual;
        @Nullable
        private DiAmount ownMax;
        @Nullable
        private DiAmount ownActual;
        @Nullable
        private Long lastOverquotingTs;
        @Nullable
        private String statisticsLink;
        @Nullable
        private Set<String> segmentKeys;

        private Builder(@NotNull final DiQuotaSpec specification, @NotNull final DiProject project) {
            this.specification = specification;
            this.project = project;
            this.segmentKeys = Collections.emptySet();
        }

        @NotNull
        public Builder max(@NotNull final DiAmount max) {
            if (specification.getResource().getType() != null) {
                max.getUnit().checkConverting(specification.getResource().getType().getBaseUnit());
            }
            this.max = max;
            return this;
        }

        @NotNull
        public Builder actual(@NotNull final DiAmount actual) {
            if (specification.getResource().getType() != null) {
                actual.getUnit().checkConverting(specification.getResource().getType().getBaseUnit());
            }
            this.actual = actual;
            return this;
        }

        @NotNull
        public Builder ownMax(@NotNull final DiAmount ownMax) {
            this.ownMax = ownMax;
            return this;
        }

        @NotNull
        public Builder ownActual(@NotNull final DiAmount actual) {
            this.ownActual = actual;
            return this;
        }

        @NotNull
        public Builder lastOverquotingTs(@Nullable final Long lastOverquotingTs) {
            this.lastOverquotingTs = lastOverquotingTs;
            return this;
        }

        @NotNull
        public Builder statisticsLink(@NotNull final String statisticsLink) {
            this.statisticsLink = statisticsLink;
            return this;
        }

        @NotNull
        public Builder segments(@NotNull final Set<String> segments) {
            this.segmentKeys = segments;
            return this;
        }

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

    static final class Serializer extends JsonSerializerBase<DiQuota> {
        @Override
        public void serialize(@NotNull final DiQuota quota,
                              @NotNull final JsonGenerator jg,
                              @NotNull final SerializerProvider sp) throws IOException {
            jg.writeStartObject();
            jg.writeObjectField("specification", quota.getSpecification());
            jg.writeObjectField("project", quota.getProject());
            jg.writeObjectField("max", quota.getMax());
            jg.writeObjectField("actual", quota.getActual());
            jg.writeObjectField("ownMax", quota.getOwnMax());
            jg.writeObjectField("ownActual", quota.getOwnActual());
            jg.writeObjectField("lastOverquotingTs", quota.getLastOverquotingTs());
            jg.writeObjectFieldStart("statistics");
            jg.writeObjectField("link", quota.getStatisticsLink());
            jg.writeEndObject();
            jg.writeObjectField("segments", quota.getSegmentKeys());
            jg.writeEndObject();
        }
    }

    static final class Deserializer extends JsonDeserializerBase<DiQuota> {
        @NotNull
        @Override
        public DiQuota deserialize(@NotNull final JsonParser jp,
                                   @Nullable final DeserializationContext dc) throws IOException {
            final JsonNode json = toJson(jp);
            final JsonNode segments = json.get("segments");
            return builder(SerializationUtils.convertValue(json.get("specification"), DiQuotaSpec.class), SerializationUtils.convertValue(json.get("project"), DiProject.class))
                    .max(SerializationUtils.convertValue(json.get("max"), DiAmount.class))
                    .ownMax(SerializationUtils.convertValue(json.get("ownMax"), DiAmount.class))
                    .actual(SerializationUtils.convertValue(json.get("actual"), DiAmount.class))
                    .ownActual(SerializationUtils.convertValue(json.get("ownActual"), DiAmount.class))
                    .lastOverquotingTs(json.hasNonNull("lastOverquotingTs") ? json.get("lastOverquotingTs").longValue() : null)
                    .statisticsLink(json.hasNonNull("statistics") ? json.get("statistics").get("link").asText() : "")
                    .segments(segments == null ? Collections.emptySet() : SerializationUtils.toStringSet(segments))
                    .build();
        }
    }
}
