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

import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import com.google.common.collect.Multimap;
import net.jcip.annotations.NotThreadSafe;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;

import ru.yandex.qe.dispenser.domain.Person;
import ru.yandex.qe.dispenser.domain.PersonGroupMembership;
import ru.yandex.qe.dispenser.domain.Project;
import ru.yandex.qe.dispenser.domain.YaGroup;
import ru.yandex.qe.dispenser.domain.dao.InMemoryLongKeyDaoImpl;
import ru.yandex.qe.dispenser.domain.hierarchy.Role;
import ru.yandex.qe.dispenser.domain.index.LongIndexBase;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;

@NotThreadSafe
public class PersonDaoImpl extends InMemoryLongKeyDaoImpl<Person> implements PersonDao {
    private PersonProjectRelations relations;
    private PersonGroupMembershipDao groupMembershipDao;
    // Write access to id2obj and uidToId is not synchronized, should not matter much in practice
    private final Map<Long, Long> uidToId = new ConcurrentHashMap<>();
    // Write access to id2obj and loginToId is not synchronized, should not matter much in practice
    private final Map<String, Long> loginToId = new ConcurrentHashMap<>();

    @NotNull
    @Override
    public Person create(@NotNull final Person newInstance) {
        final Person createdInstance = super.create(newInstance);
        if (createdInstance.getId() >= 0) {
            uidToId.put(createdInstance.getUid(), createdInstance.getId());
            loginToId.put(createdInstance.getLogin(), createdInstance.getId());
        }
        return createdInstance;
    }

    @NotNull
    @Override
    public Person createIfAbsent(@NotNull final Person obj) {
        final Person createdInstance = super.createIfAbsent(obj);
        if (createdInstance.getId() >= 0) {
            uidToId.put(createdInstance.getUid(), createdInstance.getId());
            loginToId.put(createdInstance.getLogin(), createdInstance.getId());
        }
        return createdInstance;
    }

    @Override
    public void createIfAbsent(@NotNull final Collection<Person> persons) {
        persons.forEach(this::createIfAbsent);
    }

    @NotNull
    @Override
    public Set<Person> getAllActive() {
        return filter(p -> !p.isDismissed() && !p.isDeleted()).collect(Collectors.toSet());
    }

    @NotNull
    @Override
    public Map<Long, Set<YaGroup>> getGroupsForAllActiveUsers() {
        final Map<Long, Set<YaGroup>> result = new HashMap<>();
        filter(p -> !p.isDismissed() && !p.isDeleted()).forEach(person -> {
            groupMembershipDao.filter(m -> m.getPerson().getId() == person.getId()).forEach(m -> {
                result.computeIfAbsent(person.getId(), k -> new HashSet<>()).add(m.getGroup());
            });
        });
        return result;
    }

    @Override
    public boolean update(@NotNull final Person obj) {
        // Uid should never change
        final boolean result = super.update(obj);
        if (result && obj.getId() >= 0) {
            uidToId.put(obj.getUid(), obj.getId());
            loginToId.put(obj.getLogin(), obj.getId());
        }
        return result;
    }

    @Override
    public boolean delete(final @NotNull Person user) {
        relations.detach(user);
        groupMembershipDao.deleteAll(groupMembershipDao.filter(m -> m.getPerson().getId() == user.getId()).collect(Collectors.toList()));
        final boolean result = super.delete(user);
        if (result) {
            uidToId.remove(user.getUid());
            loginToId.remove(user.getLogin());
        }
        return result;
    }

    @NotNull
    @Override
    public Set<Project> getLinkedProjects(@NotNull final Person member, @NotNull final Role role) {
        final Set<Project> result = new HashSet<>();
        result.addAll(relations.getLinkedProjects(member, role));
        final Multimap<Project, YaGroup> project2groups = relations.getLinkedPersonGroups(PersonProjectRelations.EntityType.GROUP, role);
        final Set<YaGroup> personGroups = groupMembershipDao.filter(m -> m.getPerson().getId() == member.getId())
                .map(PersonGroupMembership::getGroup).collect(Collectors.toSet());
        final Set<Long> personGroupIds = personGroups.stream().filter(g -> !g.isDeleted()).map(LongIndexBase::getId).collect(Collectors.toSet());
        result.addAll(CollectionUtils.filter(project2groups, (p, g) -> personGroupIds.contains(g.getId())).keySet());
        return result;
    }

    @Override
    public @NotNull Optional<Person> tryReadPersonByUid(final long uid) {
        final Long id = uidToId.get(uid);
        if (id == null) {
            return Optional.empty();
        }
        return Optional.ofNullable(id2obj.get(id));
    }

    @Override
    @NotNull
    public Set<Person> tryReadPersonsByUids(@NotNull final Collection<Long> uids) {
        final Set<Long> ids = new HashSet<>();
        uids.forEach(uid -> {
            final Long id = uidToId.get(uid);
            if (id != null) {
                ids.add(id);
            }
        });
        final Set<Person> persons = new HashSet<>();
        ids.forEach(id -> {
            final Person person = id2obj.get(id);
            if (person != null) {
                persons.add(person);
            }
        });
        return persons;
    }

    @Override
    @NotNull
    public Optional<Person> tryReadPersonByLogin(@NotNull final String login) {
        final Long id = loginToId.get(login);
        if (id == null) {
            return Optional.empty();
        }
        return Optional.ofNullable(id2obj.get(id));
    }

    @Override
    @NotNull
    public Set<Person> tryReadPersonsByLogins(@NotNull final Collection<String> logins) {
        final Set<Long> ids = new HashSet<>();
        logins.forEach(login -> {
            final Long id = loginToId.get(login);
            if (id != null) {
                ids.add(id);
            }
        });
        final Set<Person> persons = new HashSet<>();
        ids.forEach(id -> {
            final Person person = id2obj.get(id);
            if (person != null) {
                persons.add(person);
            }
        });
        return persons;
    }

    @Override
    @NotNull
    public Optional<Person> tryReadPersonById(final long id) {
        return Optional.ofNullable(id2obj.get(id));
    }

    @Override
    @NotNull
    public Set<Person> tryReadPersonsByIds(@NotNull final Collection<Long> ids) {
        final Set<Person> result = new HashSet<>();
        ids.forEach(id -> {
            final Person person = id2obj.get(id);
            if (person != null) {
                result.add(person);
            }
        });
        return result;
    }

    @Autowired
    public void setRelations(@NotNull final PersonProjectRelations relations) {
        this.relations = relations;
    }

    @Autowired
    public void setGroupMembershipDao(final PersonGroupMembershipDao groupMembershipDao) {
        this.groupMembershipDao = groupMembershipDao;
    }

    @Override
    protected Person createUnsafe(@NotNull final Long id, @NotNull final Person newInstance) {
        final Person createdInstance = super.createUnsafe(id, newInstance);
        if (createdInstance.getId() >= 0) {
            uidToId.put(createdInstance.getUid(), createdInstance.getId());
            loginToId.put(createdInstance.getLogin(), createdInstance.getId());
        }
        return createdInstance;
    }

    @NotNull
    @Override
    public PersonMembershipPage getPersonMembershipsPage(final Long fromPersonId, final long limit) {
        final List<Person> personsSubset = getAll().stream().sorted(Comparator.comparing(LongIndexBase::getId))
                .filter(p -> fromPersonId == null || p.getId() > fromPersonId).limit(limit).collect(Collectors.toList());
        final Map<String, Map<String, Set<Long>>> result = new HashMap<>();
        personsSubset.forEach(person -> {
            final Map<String, Set<Project>> linkedProjects = relations.getLinkedProjects(person);

            final Map<String, Set<Long>> projectsByRole = result.computeIfAbsent(person.getLogin(), l -> new HashMap<>());

            linkedProjects.forEach((roleKey, projects) -> {

                final Set<Long> abcIds = projects.stream().filter(p -> p.getAbcServiceId() != null)
                        .map(p -> p.getAbcServiceId().longValue()).collect(Collectors.toSet());

                projectsByRole.computeIfAbsent(roleKey, r -> new HashSet<>()).addAll(abcIds);

            });

        });
        return new PersonMembershipPage(result, personsSubset.isEmpty() ? null : personsSubset.get(personsSubset.size() - 1).getId());
    }

}
