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

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

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Multimap;
import com.google.common.collect.Table;
import net.jcip.annotations.NotThreadSafe;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.lookup.DataSourceLookupFailureException;
import ru.yandex.qe.dispenser.api.v1.DiMetaValueSet;
import ru.yandex.qe.dispenser.api.v1.DiYandexGroupType;
import ru.yandex.qe.dispenser.domain.Person;
import ru.yandex.qe.dispenser.domain.Project;
import ru.yandex.qe.dispenser.domain.ProjectRole;
import ru.yandex.qe.dispenser.domain.ProjectServiceMeta;
import ru.yandex.qe.dispenser.domain.Quota;
import ru.yandex.qe.dispenser.domain.Service;
import ru.yandex.qe.dispenser.domain.YaGroup;
import ru.yandex.qe.dispenser.domain.dao.InMemoryLongKeyDaoImpl;
import ru.yandex.qe.dispenser.domain.dao.person.PersonProjectRelations;
import ru.yandex.qe.dispenser.domain.dao.person.StaffCache;
import ru.yandex.qe.dispenser.domain.dao.project.role.ProjectRoleCache;
import ru.yandex.qe.dispenser.domain.dao.quota.QuotaDao;
import ru.yandex.qe.dispenser.domain.hierarchy.Role;
import ru.yandex.qe.dispenser.domain.index.NormalizedPrimaryKeyBase;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;

import static ru.yandex.qe.dispenser.domain.util.ValidationUtils.validateProjectKey;

@NotThreadSafe
public class ProjectDaoImpl extends InMemoryLongKeyDaoImpl<Project> implements ProjectDao {
    private PersonProjectRelations userProjects;
    private QuotaDao quotaDao;
    private StaffCache staffCache;
    private ProjectRoleCache roleCache;
    @Nullable
    protected volatile Project root;

    @NotNull
    private final Table<Project, Service, ProjectServiceMeta> projectMetas = HashBasedTable.create();

    @NotNull
    @Override
    public Project create(@NotNull final Project newProject) {
        validateProjectKey(newProject.getPublicKey());
        if (!newProject.isRoot() && !contains(newProject.getParent().getId())) {
            throw new DataSourceLookupFailureException(
                    "Parent '" + newProject.getParent().getName() + "' of project '" + newProject.getName() + "' not exists!");
        }
        final Project project = super.create(newProject);
        quotaDao.createZeroQuotasFor(Collections.singleton(project));
        if (project.isRoot()) {
            root = project;
        }
        return project;
    }

    @NotNull
    @Override
    public Project createIfAbsent(@NotNull final Project project) {
        validateProjectKey(project.getPublicKey());
        Project result = ProjectDao.super.createIfAbsent(project);
        if (result.isRoot()) {
            root = result;
        }
        return result;
    }

    @Override
    public boolean delete(final @NotNull Project project) {
        deleteAll(project.getSubprojectsInternal());
        userProjects.detach(project);
        if (super.delete(project)) {
            quotaDao.deleteAll(quotaDao.getQuotas(project));
            if (project.isRoot()) {
                root = null;
            }
            return true;
        }
        return false;
    }

    @Override
    public boolean clear() {
        quotaDao.clear();
        projectMetas.clear();
        getAll().forEach(userProjects::detach);
        getAll().forEach(super::delete);
        root = null;
        return true;
    }

    @Override
    public @NotNull Project getRoot() {
        Project currentRoot = root;
        if (currentRoot != null) {
            return currentRoot;
        }
        return ProjectUtils.root(id2obj.values());
    }

    @NotNull
    @Override
    public Map<Project, Set<Person>> getLinkedMembers(@NotNull final Collection<Project> projects) {
        return getLinked(projects, PersonProjectRelations.EntityType.PERSON, Role.MEMBER);
    }

    @Override
    public Set<Person> getLinkedMembers(@NotNull final Project project) {
        return getLinked(project, PersonProjectRelations.EntityType.PERSON, Role.MEMBER);
    }

    @NotNull
    @Override
    public Map<Project, Set<YaGroup>> getLinkedMemberGroups(@NotNull final Collection<Project> projects) {
        return getLinked(projects, PersonProjectRelations.EntityType.GROUP, Role.MEMBER);
    }

    @NotNull
    @Override
    public Map<Project, Set<Person>> getLinkedResponsibles(@NotNull final Collection<Project> projects) {
        return getLinked(projects, PersonProjectRelations.EntityType.PERSON, Role.RESPONSIBLE);
    }

    @Override
    @NotNull
    public Set<Person> getAllLinkedResponsibles(@NotNull final Collection<Project> projects) {
        if (projects.isEmpty()) {
            return Collections.emptySet();
        }

        final Multimap<Project, Person> ret =
                userProjects.getLinkedPersonGroups(PersonProjectRelations.EntityType.PERSON, projects, Role.RESPONSIBLE);
        return new HashSet<>(ret.values());
    }

    @NotNull
    @Override
    public Set<Person> getLinkedResponsibles(@NotNull final Project project) {
        return getLinked(project, PersonProjectRelations.EntityType.PERSON, Role.RESPONSIBLE);
    }

    @Override
    @NotNull
    public Set<YaGroup> getLinkedMemberGroups(final @NotNull Project project) {
        return getLinked(project, PersonProjectRelations.EntityType.GROUP, Role.MEMBER);
    }

    public Set<YaGroup> getLinkedResponsibleGroups(final @NotNull Project project) {
        return getLinked(project, PersonProjectRelations.EntityType.GROUP, Role.RESPONSIBLE);
    }

    @Override
    public @NotNull Map<Project, Set<YaGroup>> getLinkedResponsibleGroups(@NotNull final Collection<Project> projects) {
        return getLinked(projects, PersonProjectRelations.EntityType.GROUP, Role.RESPONSIBLE);
    }

    @NotNull
    private <T extends NormalizedPrimaryKeyBase<String>> Set<T> getLinked(final @NotNull Project project, final PersonProjectRelations.EntityType<T> type, final Role member) {
        return userProjects.getLinkedPersonGroups(type, project, member);
    }

    @NotNull
    private <T extends NormalizedPrimaryKeyBase<String>> Map<Project, Set<T>> getLinked(final @NotNull Collection<Project> projects, final PersonProjectRelations.EntityType<T> type, final Role member) {
        return CollectionUtils.multimapAsMap(userProjects.getLinkedPersonGroups(type, projects, member), projects);
    }

    @Override
    public boolean hasRole(@NotNull final Person person, @NotNull final Project project, @NotNull final Role role) {
        return hasRole(person, project, role.getKey());
    }

    @Override
    public boolean hasRole(@NotNull final Person person, @NotNull final Project project, @NotNull final String roleKey) {
        final Collection<Person> linkedPersons =
                userProjects.getLinkedEntities(PersonProjectRelations.EntityType.PERSON, project.getPathToRoot(), roleKey).values();

        if (linkedPersons.contains(person)) {
            return true;
        }

        final Set<String> groupUrls =
                userProjects.getLinkedEntities(PersonProjectRelations.EntityType.GROUP, project.getPathToRoot(), roleKey)
                        .values()
                        .stream()
                        .filter(group -> group.getType() == DiYandexGroupType.DEPARTMENT)
                        .map(YaGroup::getUrl)
                        .collect(Collectors.toSet());
        if (groupUrls.isEmpty()) {
            return false;
        }
        return staffCache.getPersonGroups(person).stream().filter(g -> !g.isDeleted()).anyMatch(g -> groupUrls.contains(g.getUrl()));
    }

    @Override
    public boolean hasRoleNoInheritance(@NotNull final Person person, @NotNull final Project project, @NotNull final Role role) {
        return hasRoleNoInheritance(person, project, role.getKey());
    }

    @Override
    public boolean hasRoleNoInheritance(@NotNull final Person person, @NotNull final Project project, @NotNull final String roleKey) {
        final Collection<Person> linkedPersons =
                userProjects.getLinkedEntities(PersonProjectRelations.EntityType.PERSON, List.of(project), roleKey).values();

        if (linkedPersons.contains(person)) {
            return true;
        }

        final Set<String> groupUrls =
                userProjects.getLinkedEntities(PersonProjectRelations.EntityType.GROUP, List.of(project), roleKey)
                        .values()
                        .stream()
                        .filter(group -> group.getType() == DiYandexGroupType.DEPARTMENT)
                        .map(YaGroup::getUrl)
                        .collect(Collectors.toSet());
        if (groupUrls.isEmpty()) {
            return false;
        }
        return staffCache.getPersonGroups(person).stream().filter(g -> !g.isDeleted()).anyMatch(g -> groupUrls.contains(g.getUrl()));
    }

    @Override
    public boolean attachAll(@NotNull final Collection<Person> persons,
                             @NotNull final Collection<YaGroup> groups,
                             final @NotNull Project project,
                             @NotNull final Role role) {
        persons.forEach(u -> userProjects.attach(u, project, role));
        groups.forEach(g -> userProjects.attach(g, project, role));
        return true;
    }

    @Override
    public boolean attachAll(@NotNull final Collection<Person> persons, @NotNull final Collection<YaGroup> groups, @NotNull final Project project, final long roleId) {
        final ProjectRole role = roleCache.getById(roleId);
        persons.forEach(u -> userProjects.attach(PersonProjectRelations.EntityType.PERSON, u, project, role.getKey()));
        groups.forEach(g -> userProjects.attach(PersonProjectRelations.EntityType.GROUP, g, project, role.getKey()));
        return true;
    }

    @Override
    public boolean detachAll(@NotNull final Collection<Person> persons,
                             @NotNull final Collection<YaGroup> groups,
                             final @NotNull Project project,
                             @NotNull final Role role) {
        persons.forEach(u -> userProjects.detach(u, project, role));
        groups.forEach(g -> userProjects.detach(g, project, role));
        return true;  // TODO
    }

    @Override
    public boolean detachAll(@NotNull Collection<Person> persons, @NotNull Collection<YaGroup> groups, @NotNull Project project, long roleId) {
        final ProjectRole role = roleCache.getById(roleId);
        persons.forEach(u -> userProjects.detach(u, PersonProjectRelations.EntityType.PERSON, project, role.getKey()));
        groups.forEach(g -> userProjects.detach(g, PersonProjectRelations.EntityType.GROUP, project, role.getKey()));
        return true;
    }

    @Override
    public void detachAll(final @NotNull Project project) {
        userProjects.detach(project);
    }

    @Override
    public void detachAllPersons(@NotNull final Project project) {
        final Stream<Person> members =
                userProjects.<Person>getLinkedPersonGroups(PersonProjectRelations.EntityType.PERSON, Collections.singleton(project),
                        Role.MEMBER).get(project).stream();
        final Stream<Person> responsibles =
                userProjects.<Person>getLinkedPersonGroups(PersonProjectRelations.EntityType.PERSON, Collections.singleton(project),
                        Role.RESPONSIBLE).get(project).stream();

        Stream.concat(members, responsibles)
                .forEach(userProjects::detach);
    }

    @Nullable
    @Override
    public DiMetaValueSet getProjectMeta(@NotNull final Project project, @NotNull final Service service) {
        final ProjectServiceMeta projectMeta = projectMetas.get(project, service);
        return projectMeta != null ? projectMeta.getMetaValues() : null;
    }

    @NotNull
    @Override
    public Set<ProjectServiceMeta> getAllProjectMetas() {
        return new HashSet<>(projectMetas.values());
    }

    @Override
    public boolean putProjectMeta(@NotNull final ProjectServiceMeta projectMeta) {
        projectMetas.put(projectMeta.getProject(), projectMeta.getService(), projectMeta);
        return true;
    }

    @NotNull
    @Override
    public Project lockForUpdate(final @NotNull Project project) {
        return project;
    }

    @Override
    @NotNull
    public Collection<Project> lockForUpdate(final @NotNull Collection<Project> projects) {
        return projects;
    }

    @Autowired
    public void setUserProjects(final @NotNull PersonProjectRelations userProjects) {
        this.userProjects = userProjects;
    }

    @Autowired
    public void setQuotaDao(@NotNull final QuotaDao quotaDao) {
        this.quotaDao = quotaDao;
    }

    @Autowired
    public void setStaffCache(@NotNull final StaffCache staffCache) {
        this.staffCache = staffCache;
    }

    @Autowired
    public void setStaffCache(@NotNull final ProjectRoleCache roleCache) {
        this.roleCache = roleCache;
    }

    @Override
    public StaffCache getStaffCache() {
        return staffCache;
    }

    @Override
    @NotNull
    public Set<Person> getLinkedPersons(final Project project, final String roleKey) {
        return userProjects.getLinkedEntities(PersonProjectRelations.EntityType.PERSON, project, roleKey);
    }

    @Override
    @NotNull
    public Map<Project, Set<Person>> getLinkedPersons(final Collection<Project> projects, final String roleKey) {
        return CollectionUtils.multimapAsMap(userProjects.getLinkedEntities(PersonProjectRelations.EntityType.PERSON, projects, roleKey), projects);
    }

    @Override
    @NotNull
    public Set<YaGroup> getLinkedGroups(final Project project, final String roleKey) {
        return userProjects.getLinkedEntities(PersonProjectRelations.EntityType.GROUP, project, roleKey);
    }

    @Override
    @NotNull
    public Map<Project, Set<YaGroup>> getLinkedGroups(final Collection<Project> projects, final String roleKey) {
        return CollectionUtils.multimapAsMap(userProjects.getLinkedEntities(PersonProjectRelations.EntityType.GROUP, projects, roleKey), projects);
    }

    @Override
    public boolean update(final @NotNull Project project) {
        final Project previous = read(primaryKeyProducer.apply(project));
        final boolean update = super.update(project);
        if (update) {
            if (!previous.isRoot()) {
                previous.getParent().getSubprojectsInternal().remove(previous);
            }
            if (!project.isRoot()) {
                project.getParent().getSubprojectsInternal().add(project);
            }

            project.getSubProjects().forEach(subProject -> update(Project.copyOf(subProject).parent(project).build()));

            quotaDao.getQuotas(project).forEach(quota -> {
                final Quota updatedQuota = Quota.builder(quota, project).build();
                quotaDao.update(updatedQuota);
            });
        }
        if (update && project.isRoot()) {
            root = project;
        }
        return update;
    }
}
