package ru.yandex.qe.dispenser.domain;

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

import javax.annotation.Nonnull;

import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import ru.yandex.qe.dispenser.api.v1.DiEntitySpec;
import ru.yandex.qe.dispenser.domain.index.NormalizedPrimaryKeyBase;
import ru.yandex.qe.dispenser.domain.util.StreamUtils;
import ru.yandex.qe.dispenser.domain.util.ValidationUtils;

import static ru.yandex.qe.dispenser.api.v1.DiQuotingMode.ENTITIES_ONLY;

/**
 * Спецификация {@link Entity сущности}.
 * <p>Описывает набор ресурсов, {@link Service провайдера}, временность сущности.
 */
public final class EntitySpec extends NormalizedPrimaryKeyBase<EntitySpec.Key> {
    @NotNull
    private final String description;
    @NotNull
    private final Set<Resource> resources;
    private final boolean expirable;
    @Nonnull
    private final String tag;

    private EntitySpec(@Nonnull final Builder builder) {
        super(builder.computeKey());
        description = ValidationUtils.requireNonNull(builder.description, "Entity specification description is required!");
        tag = ValidationUtils.requireNonNull(builder.tag, "Entity specification tag required!");
        resources = ImmutableSet.copyOf(builder.resources);
        if (resources.stream().anyMatch(r -> r.getMode() != ENTITIES_ONLY)) {
            throw new IllegalArgumentException("Entity specification resources must be in '" + ENTITIES_ONLY + "' quoting mode!");
        }
        expirable = builder.expirable;
    }

    @Nonnull
    public static EntitySpec.Builder builder() {
        return new Builder();
    }

    @Nonnull
    public String getTag() {
        return tag;
    }

    @NotNull
    public Service getService() {
        return getKey().getService();
    }

    @NotNull
    public String getDescription() {
        return description;
    }

    @NotNull
    public Set<Resource> getResources() {
        return resources;
    }

    public boolean isExpirable() {
        return expirable;
    }

    @NotNull
    public DiEntitySpec toView() {
        return DiEntitySpec.builder(getKey().getPublicKey())
                .inService(getService().toView())
                .withDescription(getDescription())
                .overResources(getResources().stream().map(r -> r.getKey().getPublicKey()).toArray(String[]::new))
                .build();
    }

    public static final class Key implements Comparable<Key> {
        @NotNull
        private final String key;
        @NotNull
        private final Service service;

        private final int hash;

        public Key(@NotNull final String key, @NotNull final Service service) {
            this.key = key;
            this.service = service;
            this.hash = 31 * key.hashCode() + service.hashCode();
        }

        @NotNull
        public String getPublicKey() {
            return key;
        }

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

        @Override
        public int compareTo(@NotNull final Key key) {
            return new CompareToBuilder()
                    .append(getService(), key.getService())
                    .append(getPublicKey(), key.getPublicKey())
                    .build();
        }

        @Override
        public boolean equals(@Nullable final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            return key.equals(((Key) o).key) && service.equals(((Key) o).service);

        }

        @Override
        public int hashCode() {
            return hash;
        }

        @NotNull
        @Override
        public String toString() {
            return "Key{key='" + key + "', service.key=" + service.getKey() + '}';
        }
    }

    public static final class Builder {
        @Nullable
        private String key;
        @Nullable
        private String description;
        @NotNull
        private final Set<Resource> resources = new HashSet<>();
        private boolean expirable;
        @Nonnull
        private String tag = "common";

        private Builder() {
        }

        @Nonnull
        public Builder withKey(@Nonnull final String key) {
            this.key = key;
            return this;
        }

        @Nonnull
        public Builder withDescription(@NotNull final String description) {
            this.description = description;
            return this;
        }

        @Nonnull
        public Builder overResource(@Nonnull final Resource resource) {
            resources.add(resource);
            return this;
        }

        @Nonnull
        public Builder overResources(@Nonnull final Collection<Resource> resources) {
            this.resources.addAll(resources);
            return this;
        }

        @Nonnull
        public Builder expirable(final boolean expirable) {
            this.expirable = expirable;
            return this;
        }

        @Nonnull
        public Builder withCustomTag(@Nonnull final String tag) {
            this.tag = tag;
            return this;
        }

        @Nonnull
        public EntitySpec build() {
            return new EntitySpec(this);
        }

        @Nonnull
        private Key computeKey() {
            return new Key(ValidationUtils.requireNonNull(key, "Key required!"), extractService(resources));
        }

        @NotNull
        private Service extractService(@NotNull final Collection<Resource> resources) {
            return StreamUtils.requireSingle(resources.stream().map(Resource::getService).distinct());
        }
    }
}
