package ru.yandex.qe.dispenser.ws.api.domain.distribution;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import ru.yandex.qe.dispenser.api.v1.DiAmount;
import ru.yandex.qe.dispenser.api.v1.DiQuotaChangeRequest;
import ru.yandex.qe.dispenser.domain.Resource;
import ru.yandex.qe.dispenser.domain.Segment;
import ru.yandex.qe.dispenser.domain.Service;
import ru.yandex.qe.dispenser.domain.bot.BigOrder;

public final class QuotaDistribution {

    @NotNull
    private final List<Change> changes;
    @Nullable
    private final String comment;
    @NotNull
    private final Service service;
    private final @Nullable BigOrder bigOrder;

    private QuotaDistribution(@NotNull final List<Change> changes, @Nullable final String comment, @NotNull final Service service, @Nullable BigOrder bigOrder) {
        this.changes = changes;
        this.comment = comment;
        this.service = service;
        this.bigOrder = bigOrder;
    }

    public static Builder builder() {
        return new Builder();
    }

    @NotNull
    public List<Change> getChanges() {
        return changes;
    }

    @Nullable
    public String getComment() {
        return comment;
    }

    @NotNull
    public Service getService() {
        return service;
    }

    @Nullable
    public BigOrder getBigOrder() {
        return bigOrder;
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final QuotaDistribution that = (QuotaDistribution) o;
        return changes.equals(that.changes) &&
                Objects.equals(comment, that.comment) &&
                service.equals(that.service) &&
                Objects.equals(bigOrder, that.bigOrder);
    }

    @Override
    public int hashCode() {
        return Objects.hash(changes, comment, service, bigOrder);
    }

    @Override
    public String toString() {
        return "QuotaDistribution{" +
                "changes=" + changes +
                ", comment='" + comment + '\'' +
                ", service=" + service +
                '}';
    }

    public static final class Change {

        private final long id;
        private final long requestId;
        @NotNull
        private final Resource resource;
        @NotNull
        private final Set<Segment> segments;
        private final long amount;
        private final long amountReady;
        private final long amountAllocated;
        private final long amountReadyIncrement;
        private final long amountAllocatedIncrement;
        private final long amountAllocating;

        private Change(final long id,
                       final long requestId,
                       @NotNull final Resource resource,
                       @NotNull final Set<Segment> segments,
                       final long amount,
                       final long amountReady,
                       final long amountAllocated,
                       final long amountReadyIncrement,
                       final long amountAllocatedIncrement,
                       final long amountAllocating) {
            this.id = id;
            this.requestId = requestId;
            this.resource = resource;
            this.segments = segments;
            this.amount = amount;
            this.amountReady = amountReady;
            this.amountAllocated = amountAllocated;
            this.amountReadyIncrement = amountReadyIncrement;
            this.amountAllocatedIncrement = amountAllocatedIncrement;
            this.amountAllocating = amountAllocating;
        }

        public static Builder builder() {
            return new Builder();
        }

        public long getId() {
            return id;
        }

        public long getRequestId() {
            return requestId;
        }

        @NotNull
        public Resource getResource() {
            return resource;
        }

        @NotNull
        public Set<Segment> getSegments() {
            return segments;
        }

        public long getAmount() {
            return amount;
        }

        public long getAmountReady() {
            return amountReady;
        }

        public long getAmountAllocated() {
            return amountAllocated;
        }

        public long getAmountReadyIncrement() {
            return amountReadyIncrement;
        }

        public long getAmountAllocatedIncrement() {
            return amountAllocatedIncrement;
        }

        public long getAmountAllocating() {
            return amountAllocating;
        }

        public DiQuotaChangeRequest.Change toChangeViewBefore(@NotNull final Service service, @Nullable final DiQuotaChangeRequest.Order bigOrder) {
            return new DiQuotaChangeRequest.Change(
                    bigOrder,
                    new DiQuotaChangeRequest.Service(service.getKey(), service.getName()),
                    new DiQuotaChangeRequest.Resource(resource.getPublicKey(), resource.getName()),
                    segments.stream().map(Segment::getPublicKey).collect(Collectors.toSet()),
                    DiAmount.of(this.amount, resource.getType().getBaseUnit()),
                    DiAmount.of(this.amountReady, resource.getType().getBaseUnit()),
                    DiAmount.of(this.amountAllocated, resource.getType().getBaseUnit()),
                    DiAmount.of(this.amountAllocating, resource.getType().getBaseUnit()),
                    null,
                    null
            );
        }

        public DiQuotaChangeRequest.Change toChangeViewAfter(@NotNull final Service service, @Nullable final DiQuotaChangeRequest.Order bigOrder) {
            return new DiQuotaChangeRequest.Change(
                    bigOrder,
                    new DiQuotaChangeRequest.Service(service.getKey(), service.getName()),
                    new DiQuotaChangeRequest.Resource(resource.getPublicKey(), resource.getName()),
                    segments.stream().map(Segment::getPublicKey).collect(Collectors.toSet()),
                    DiAmount.of(this.amount, resource.getType().getBaseUnit()),
                    DiAmount.of(this.amountReady + this.amountReadyIncrement, resource.getType().getBaseUnit()),
                    DiAmount.of(this.amountAllocated + this.amountAllocatedIncrement, resource.getType().getBaseUnit()),
                    DiAmount.of(this.amountAllocating + this.amountAllocatedIncrement, resource.getType().getBaseUnit()),
                    null,
                    null
            );
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            final Change change = (Change) o;
            return id == change.id &&
                    requestId == change.requestId &&
                    amount == change.amount &&
                    amountReady == change.amountReady &&
                    amountAllocated == change.amountAllocated &&
                    amountAllocating == change.amountAllocating &&
                    amountReadyIncrement == change.amountReadyIncrement &&
                    amountAllocatedIncrement == change.amountAllocatedIncrement &&
                    resource.equals(change.resource) &&
                    segments.equals(change.segments);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, requestId, resource, segments, amount, amountReady, amountAllocated, amountReadyIncrement, amountAllocatedIncrement, amountAllocating);
        }

        @Override
        public String toString() {
            return "Change{" +
                    "id=" + id +
                    ", requestId=" + requestId +
                    ", resource=" + resource +
                    ", segments=" + segments +
                    ", amount=" + amount +
                    ", amountReady=" + amountReady +
                    ", amountAllocated=" + amountAllocated +
                    ", amountAllocating=" + amountAllocating +
                    ", amountReadyIncrement=" + amountReadyIncrement +
                    ", amountAllocatedIncrement=" + amountAllocatedIncrement +
                    '}';
        }

        public static final class Builder {

            @Nullable
            private Long id;
            @Nullable
            private Long requestId;
            @Nullable
            private Resource resource;
            @Nullable
            private Set<Segment> segments;
            @Nullable
            private Long amount;
            @Nullable
            private Long amountReady;
            @Nullable
            private Long amountAllocated;
            @Nullable
            private Long amountReadyIncrement;
            @Nullable
            private Long amountAllocatedIncrement;
            @Nullable
            private Long amountAllocating;

            private Builder() {
            }

            @NotNull
            public Builder id(final long id) {
                this.id = id;
                return this;
            }

            @NotNull
            public Builder requestId(final long requestId) {
                this.requestId = requestId;
                return this;
            }

            @NotNull
            public Builder resource(@NotNull final Resource resource) {
                Objects.requireNonNull(resource, "Resource is required.");
                this.resource = resource;
                return this;
            }

            @NotNull
            public Builder segments(@NotNull final Set<Segment> segments) {
                Objects.requireNonNull(segments, "Segments are required.");
                this.segments = segments;
                return this;
            }

            @NotNull
            public Builder amount(@NotNull final Long amount) {
                Objects.requireNonNull(amount, "Amount is required.");
                this.amount = amount;
                return this;
            }

            @NotNull
            public Builder amountReady(final long amountReady) {
                this.amountReady = amountReady;
                return this;
            }

            @NotNull
            public Builder amountAllocated(final long amountAllocated) {
                this.amountAllocated = amountAllocated;
                return this;
            }

            @NotNull
            public Builder amountReadyIncrement(final long amountReadyIncrement) {
                this.amountReadyIncrement = amountReadyIncrement;
                return this;
            }

            @NotNull
            public Builder amountAllocatedIncrement(final long amountAllocatedIncrement) {
                this.amountAllocatedIncrement = amountAllocatedIncrement;
                return this;
            }

            @NotNull
            public Builder amountAllocating(final long amountAllocating) {
                this.amountAllocating = amountAllocating;
                return this;
            }


            @Nullable
            public Long getId() {
                return id;
            }

            @Nullable
            public Long getRequestId() {
                return requestId;
            }

            @Nullable
            public Resource getResource() {
                return resource;
            }

            @Nullable
            public Set<Segment> getSegments() {
                return segments;
            }

            @Nullable
            public Long getAmount() {
                return amount;
            }

            @Nullable
            public Long getAmountReady() {
                return amountReady;
            }

            @Nullable
            public Long getAmountAllocated() {
                return amountAllocated;
            }

            @Nullable
            public Long getAmountReadyIncrement() {
                return amountReadyIncrement;
            }

            @Nullable
            public Long getAmountAllocatedIncrement() {
                return amountAllocatedIncrement;
            }

            @Nullable
            public Long getAmountAllocating() {
                return amountAllocating;
            }

            @NotNull
            public Change build() {
                Objects.requireNonNull(id, "Id is required.");
                Objects.requireNonNull(requestId, "Request id is required.");
                Objects.requireNonNull(resource, "Resource is required.");
                Objects.requireNonNull(segments, "Segments are required.");
                Objects.requireNonNull(amount, "Amount is required.");
                Objects.requireNonNull(amountReady, "Amount ready is required.");
                Objects.requireNonNull(amountAllocated, "Amount allocated is required.");
                Objects.requireNonNull(amountReadyIncrement, "Amount ready increment is required.");
                Objects.requireNonNull(amountAllocatedIncrement, "Amount allocated increment is required.");
                Objects.requireNonNull(amountAllocating, "Amount allocating increment is required.");
                return new Change(id, requestId, resource, segments, amount,
                        amountReady, amountAllocated, amountReadyIncrement, amountAllocatedIncrement, amountAllocating);
            }

            @Override
            public boolean equals(final Object o) {
                if (this == o) {
                    return true;
                }
                if (o == null || getClass() != o.getClass()) {
                    return false;
                }
                final Builder builder = (Builder) o;
                return Objects.equals(id, builder.id) &&
                        Objects.equals(requestId, builder.requestId) &&
                        Objects.equals(resource, builder.resource) &&
                        Objects.equals(segments, builder.segments) &&
                        Objects.equals(amount, builder.amount) &&
                        Objects.equals(amountReady, builder.amountReady) &&
                        Objects.equals(amountAllocated, builder.amountAllocated) &&
                        Objects.equals(amountAllocating, builder.amountAllocating) &&
                        Objects.equals(amountReadyIncrement, builder.amountReadyIncrement) &&
                        Objects.equals(amountAllocatedIncrement, builder.amountAllocatedIncrement);
            }

            @Override
            public int hashCode() {
                return Objects.hash(id, requestId, resource, segments, amount, amountReady, amountAllocated, amountReadyIncrement, amountAllocatedIncrement, amountAllocating);
            }

            @Override
            public String toString() {
                return "Builder{" +
                        "id=" + id +
                        ", requestId=" + requestId +
                        ", resource=" + resource +
                        ", segments=" + segments +
                        ", amount=" + amount +
                        ", amountReady=" + amountReady +
                        ", amountAllocated=" + amountAllocated +
                        ", amountAllocating=" + amountAllocating +
                        ", amountReadyIncrement=" + amountReadyIncrement +
                        ", amountAllocatedIncrement=" + amountAllocatedIncrement +
                        '}';
            }

        }

    }

    public static final class Builder {

        @NotNull
        private final List<Change> changes = new ArrayList<>();
        @Nullable
        private String comment;
        @Nullable
        private Service service;
        @Nullable
        private BigOrder bigOrder;

        private Builder() {
        }

        @NotNull
        public Builder addChange(@NotNull final Change change) {
            Objects.requireNonNull(change, "Change is required.");
            changes.add(change);
            return this;
        }

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

        @NotNull
        public Builder service(@NotNull final Service service) {
            Objects.requireNonNull(service, "Service is required.");
            this.service = service;
            return this;
        }

        @NotNull
        public Builder bigOrder(@NotNull final BigOrder bigOrder) {
            this.bigOrder = bigOrder;
            return this;
        }

        @NotNull
        public List<Change> getChanges() {
            return changes;
        }

        @Nullable
        public String getComment() {
            return comment;
        }

        @Nullable
        public Service getService() {
            return service;
        }

        @NotNull
        public QuotaDistribution build() {
            Objects.requireNonNull(service, "Service is required.");
            return new QuotaDistribution(changes, comment, service, bigOrder);
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            final Builder builder = (Builder) o;
            return changes.equals(builder.changes) &&
                    Objects.equals(comment, builder.comment) &&
                    Objects.equals(service, builder.service);
        }

        @Override
        public int hashCode() {
            return Objects.hash(changes, comment, service);
        }

        @Override
        public String toString() {
            return "Builder{" +
                    "changes=" + changes +
                    ", comment='" + comment + '\'' +
                    ", service=" + service +
                    '}';
        }

    }

}
