package ru.yandex.qe.dispenser.ws.common.domain.errors;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.jetbrains.annotations.NotNull;

public final class ErrorCollection<K, E> {

    @NotNull
    private final Set<E> errors;
    @NotNull
    private final Map<K, Set<E>> fieldErrors;
    @NotNull
    private final Map<String, Set<Object>> details;

    private ErrorCollection(@NotNull final Set<E> errors, @NotNull final Map<K, Set<E>> fieldErrors,
                            @NotNull final Map<String, Set<Object>> details) {
        this.errors = errors;
        this.fieldErrors = fieldErrors;
        this.details = details;
    }

    @NotNull
    public static <I, F> Builder<I, F> builder() {
        return new Builder<>();
    }

    @NotNull
    public static Builder<String, TypedError<String>> typedStringBuilder() {
        return new Builder<>();
    }

    @NotNull
    public Set<E> getErrors() {
        return errors;
    }

    @NotNull
    public Map<K, Set<E>> getFieldErrors() {
        return fieldErrors;
    }

    @NotNull
    public Map<String, Set<Object>> getDetails() {
        return details;
    }

    public boolean hasAnyErrors() {
        return !errors.isEmpty() || (!fieldErrors.isEmpty() && fieldErrors.values().stream().anyMatch(e -> !e.isEmpty()));
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final ErrorCollection<?, ?> that = (ErrorCollection<?, ?>) o;
        return errors.equals(that.errors) &&
                fieldErrors.equals(that.fieldErrors) &&
                details.equals(that.details);
    }

    @Override
    public int hashCode() {
        return Objects.hash(errors, fieldErrors, details);
    }

    @Override
    public String toString() {
        return "ErrorCollection{" +
                "errors=" + errors +
                ", fieldErrors=" + fieldErrors +
                ", details=" + details +
                '}';
    }

    public static final class Builder<I, F> {

        @NotNull
        private final Set<F> errors = new HashSet<>();
        @NotNull
        private final Map<I, Set<F>> fieldErrors = new HashMap<>();
        @NotNull
        private final Map<String, Set<Object>> details = new HashMap<>();

        private Builder() {
        }

        @NotNull
        public Builder<I, F> addError(@NotNull final F error) {
            Objects.requireNonNull(error, "Error is required.");
            errors.add(error);
            return this;
        }

        @NotNull
        public Builder<I, F> addError(@NotNull final I field, @NotNull final F error) {
            Objects.requireNonNull(field, "Field is required.");
            Objects.requireNonNull(error, "Error is required.");
            fieldErrors.computeIfAbsent(field, k -> new HashSet<>()).add(error);
            return this;
        }

        public Builder<I, F> addDetail(@NotNull final String key, @NotNull final Object value) {
            Objects.requireNonNull(key, "Key is required.");
            Objects.requireNonNull(value, "Value is required.");
            details.computeIfAbsent(key, k -> new HashSet<>()).add(value);
            return this;
        }

        public Builder<I, F> add(@NotNull final ErrorCollection<I, F> errorCollection) {
            Objects.requireNonNull(errorCollection, "Error collection is required.");
            errors.addAll(errorCollection.getErrors());
            errorCollection.getFieldErrors().forEach((k, v) -> {
                fieldErrors.computeIfAbsent(k, x -> new HashSet<>()).addAll(v);
            });
            errorCollection.getDetails().forEach((k, v) -> {
                details.computeIfAbsent(k, x -> new HashSet<>()).addAll(v);
            });
            return this;
        }

        public Builder<I, F> add(@NotNull final ErrorCollection.Builder<I, F> errorCollectionBuilder) {
            Objects.requireNonNull(errorCollectionBuilder, "Error collection is required.");
            errors.addAll(errorCollectionBuilder.errors);
            errorCollectionBuilder.fieldErrors.forEach((k, v) -> {
                fieldErrors.computeIfAbsent(k, x -> new HashSet<>()).addAll(v);
            });
            errorCollectionBuilder.details.forEach((k, v) -> {
                details.computeIfAbsent(k, x -> new HashSet<>()).addAll(v);
            });
            return this;
        }

        @NotNull
        public ErrorCollection<I, F> build() {
            return new ErrorCollection<>(errors, fieldErrors, details);
        }

        public boolean hasAnyErrors() {
            return !errors.isEmpty() || (!fieldErrors.isEmpty() && fieldErrors.values().stream().anyMatch(e -> !e.isEmpty()));
        }

        @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(errors, builder.errors) &&
                    Objects.equals(fieldErrors, builder.fieldErrors);
        }

        @Override
        public int hashCode() {
            return Objects.hash(errors, fieldErrors);
        }

        @Override
        public String toString() {
            return "Builder{" +
                    "errors=" + errors +
                    ", fieldErrors=" + fieldErrors +
                    '}';
        }

    }

}
