package ru.yandex.qe.dispenser.domain.dao.project;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.collect.HashMultimap;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.EmptyResultDataAccessException;

import ru.yandex.qe.dispenser.domain.Person;
import ru.yandex.qe.dispenser.domain.Project;
import ru.yandex.qe.dispenser.domain.YaGroup;
import ru.yandex.qe.dispenser.domain.dao.ReadOnlyDao;
import ru.yandex.qe.dispenser.domain.dao.person.StaffCache;
import ru.yandex.qe.dispenser.domain.hierarchy.Role;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;

public interface ProjectReader extends ReadOnlyDao.Normalized<Project, Project.Key> {
    @NotNull
    default Project readExisting(@NotNull final String publicKey) {
        final Project project = read(Project.Key.of(publicKey, null));
        if (project.isRemoved()) {
            throw new EmptyResultDataAccessException("No project with key = [" + publicKey + "]!", 1);
        }
        return project;
    }

    @NotNull
    default Project read(@NotNull final String publicKey) {
        return read(Project.Key.of(publicKey, null));
    }

    @NotNull
    default Map<String, Project> readByPublicKeys(@NotNull final Collection<String> publicKeys) {
        final Collection<Project.Key> keys = publicKeys.stream()
                .map(publicKey -> Project.Key.of(publicKey, null))
                .collect(Collectors.toSet());
        return CollectionUtils.toMap(readAll(keys).values(), p -> p.getKey().getPublicKey());
    }

    @NotNull
    Project createIfAbsent(@NotNull Project project);

    @NotNull
    Map<Project.Key, Project> createAllIfAbsent(@NotNull Collection<Project> projects);

    @NotNull
    Set<Project> getAll();

    @NotNull
    default Set<Project> getAllReal() {
        return getAll().stream()
                .filter(Project::isReal)
                .filter(project -> !project.isRemoved())
                .collect(Collectors.toSet());
    }

    @NotNull
    default Project readByAbcServiceId(final Integer abcServiceId) {
        return getAll().stream()
                .filter(Project::isReal)
                .filter(project -> !project.isRemoved())
                .filter(project -> Objects.equals(project.getAbcServiceId(), abcServiceId))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("No project with abcServiceId = " + abcServiceId));
    }

    @NotNull
    default Set<Project> tryReadByAbcServiceIds(final Set<Integer> abcServiceIds) {
        return getAll().stream()
                .filter(Project::isReal)
                .filter(project -> !project.isRemoved())
                .filter(project -> abcServiceIds.contains(project.getAbcServiceId())).collect(Collectors.toSet());
    }

    @NotNull
    default Project getRoot() {
        return ProjectUtils.root(getAll());
    }

    Set<Person> getLinkedMembers(final @NotNull Project project);

    Map<Project, Set<Person>> getLinkedMembers(@NotNull Collection<Project> projects);

    @NotNull Set<YaGroup> getLinkedMemberGroups(@NotNull Project project);

    @NotNull
    Map<Project, Set<YaGroup>> getLinkedMemberGroups(@NotNull Collection<Project> projects);

    @NotNull
    Set<Person> getLinkedResponsibles(final @NotNull Project project);

    @NotNull
    Map<Project, Set<Person>> getLinkedResponsibles(@NotNull Collection<Project> project);

    @NotNull
    Set<Person> getAllLinkedResponsibles(@NotNull Collection<Project> project);

    @NotNull
    Map<Project, Set<YaGroup>> getLinkedResponsibleGroups(@NotNull Collection<Project> projects);

    boolean hasRole(@NotNull Person user, @NotNull Project project, @NotNull Role role);

    boolean hasRole(@NotNull Person user, @NotNull Project project, @NotNull String roleKey);

    boolean hasRoleNoInheritance(@NotNull Person user, @NotNull Project project, @NotNull Role role);

    boolean hasRoleNoInheritance(@NotNull Person user, @NotNull Project project, @NotNull String roleKey);

    /**
     * Returns set of person logins (not of {@link Person} objects) in order not to create all absent perosns in DB.
     */
    @NotNull
    default Set<String> getAllMembers(@NotNull final Project project) {
        return getAllMembers(Collections.singleton(project)).get(project);
    }

    @NotNull
    default HashMultimap<Project, String> getAllMembers(@NotNull final Collection<Project> projects) {
        final StaffCache staffCache = getStaffCache();

        final Map<Project, Set<Person>> linkedResponsibles = getLinkedResponsibles(projects);
        final Map<Project, Set<YaGroup>> linkedMemberGroups = getLinkedMemberGroups(projects);
        final Map<Project, Set<Person>> linkedMembers = getLinkedMembers(projects);

        final HashMultimap<Project, String> resultMultimap = HashMultimap.create();

        for (final Map.Entry<Project, Set<YaGroup>> entry : linkedMemberGroups.entrySet()) {
            resultMultimap.putAll(entry.getKey(),
                    staffCache.getPersonsInGroups(entry.getValue().stream().filter(g -> !g.isDeleted()).collect(Collectors.toSet()))
                            .stream().map(Person::getLogin).collect(Collectors.toSet()));
        }

        Stream.of(linkedMembers, linkedResponsibles)
                .flatMap(mm -> mm.entrySet().stream())
                .forEach(entry ->
                        resultMultimap.putAll(entry.getKey(), entry.getValue().stream().map(Person::getLogin).collect(Collectors.toList())));

        return resultMultimap;
    }

    StaffCache getStaffCache();

    @NotNull
    Set<Person> getLinkedPersons(final Project project, final String roleKey);

    @NotNull
    Map<Project, Set<Person>> getLinkedPersons(final Collection<Project> project, final String roleKey);

    @NotNull
    Set<YaGroup> getLinkedGroups(final Project project, final String roleKey);

    @NotNull
    Map<Project, Set<YaGroup>> getLinkedGroups(final Collection<Project> project, final String roleKey);
}
