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

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Table;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.postgresql.util.PGobject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import ru.yandex.qe.dispenser.api.v1.DiMetaValueSet;
import ru.yandex.qe.dispenser.domain.Person;
import ru.yandex.qe.dispenser.domain.PersonAffiliation;
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.Service;
import ru.yandex.qe.dispenser.domain.YaGroup;
import ru.yandex.qe.dispenser.domain.dao.SqlDaoBase;
import ru.yandex.qe.dispenser.domain.dao.SqlUtils;
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.dao.validation.Validate;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.domain.hierarchy.Role;
import ru.yandex.qe.dispenser.domain.index.LongIndexable;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;
import ru.yandex.qe.dispenser.domain.util.StreamUtils;

public class SqlProjectDao extends SqlDaoBase implements ProjectDao {
    private static final String GET_ALL_QUERY = "SELECT project.*, person.login, person.uid, person.is_robot, person.is_dismissed, person.is_deleted, person.affiliation FROM project LEFT OUTER JOIN person ON project.person_id = person.id";
    private static final String GET_ROOT_QUERY = "SELECT * FROM project WHERE parent_id IS NULL";
    private static final String CREATE_QUERY = "INSERT INTO project (short_name, key, description, parent_id, person_id, abc_service_id, synced_with_abc, mail_list, value_stream_abc_service_id) VALUES (:shortName, :key, :description, :parentId, :personId, :abcServiceId, :syncedWithAbc, :mailList, :valueStreamAbcServiceId)";
    private static final String CREATE_IF_ABSENT_QUERY = CREATE_QUERY + " ON CONFLICT DO NOTHING";
    private static final String SELECT_QUERY = "WITH RECURSIVE r AS ( "
            + "  SELECT id, parent_id, key, short_name, description, 1 AS level "
            + "  FROM project "
            + "      WHERE key = :key"
            + "      UNION ALL "
            + "      SELECT project.id, project.parent_id, project.key, project.short_name, project.description, r.level + 1 AS level "
            + "      FROM project "
            + "      JOIN r "
            + "      ON project.id = r.parent_id "
            + ") "
            + "SELECT * FROM r ORDER BY level DESC";
    private static final String UPDATE_QUERY = "UPDATE project SET key = :key, short_name = :shortName, description = :description, parent_id = :parentId, abc_service_id = :abcServiceId, removed = :removed, synced_with_abc = :syncedWithAbc, mail_list = :mailList, value_stream_abc_service_id = :valueStreamAbcServiceId  WHERE id = :projectId";
    private static final String DELETE_QUERY = "DELETE FROM project WHERE id = :id";
    private static final String CLEAR_QUERY = "TRUNCATE project CASCADE";
    private static final String SELECT_BY_ID_QUERY = "WITH RECURSIVE r AS ( "
            + "  SELECT id, parent_id, key, short_name, description, 1 AS level "
            + "  FROM project "
            + "      WHERE id = :projectId"
            + "      UNION ALL "
            + "      SELECT project.id, project.parent_id, project.key, project.short_name, project.description, r.level + 1 AS LEVEL "
            + "      FROM project "
            + "      JOIN r "
            + "      ON project.id = r.parent_id "
            + ") "
            + "SELECT * FROM r ORDER BY level DESC";

    private static final String GET_ALL_PROJECT_PERSONS_QUERY = "SELECT project_id, person.id, login, person.uid, person.is_robot, person.is_dismissed, person.is_deleted, person.affiliation, pr.key as role_key FROM person JOIN person_membership ON person.id = person_membership.person_id JOIN project_role pr ON pr.id = person_membership.project_role_id";

    private static final String GET_ALL_PROJECT_PERSONS_WITH_ROLE_QUERY = GET_ALL_PROJECT_PERSONS_QUERY + " WHERE pr.key in (:roleKey)";

    private static final String GET_ALL_PROJECT_GROUPS_QUERY = "SELECT yandex_group.*, ygm.project_id,  pr.key as role_key FROM yandex_group JOIN yandex_group_membership ygm ON yandex_group.id = ygm.yandex_group_id JOIN project_role pr ON pr.id = ygm.project_role_id";

    private static final String ATTACH_USER_TO_PROJECT_QUERY = "INSERT INTO person_membership (person_id, project_id, project_role_id) VALUES (:personId, :projectId, :projectRoleId)";

    private static final String ATTACH_GROUP_TO_PROJECT_QUERY = "INSERT INTO yandex_group_membership (yandex_group_id, project_id, project_role_id) VALUES (:staffGroupId, :projectId, :projectRoleId)";

    private static final String DETACH_USER_FROM_PROJECT_QUERY = "DELETE FROM person_membership WHERE person_id = :personId AND project_id = :projectId AND project_role_id = :projectRoleId";

    private static final String DETACH_GROUP_FROM_PROJECT_QUERY = "DELETE FROM yandex_group_membership WHERE yandex_group_id = :staffGroupId AND project_id = :projectId AND project_role_id = :projectRoleId";

    private static final String REMOVE_PERSON_PROJECT_RELATIONS_QUERY = "DELETE FROM person_membership WHERE project_id = :projectId";

    private static final String REMOVE_GROUP_PROJECT_RELATIONS_QUERY = "DELETE FROM yandex_group_membership WHERE project_id = :projectId";

    private static final String GET_ALL_PROJECT_META_QUERY = "SELECT * FROM project_service_meta";
    private static final String GET_PROJECT_META_QUERY = "SELECT data FROM project_service_meta WHERE project_id = :projectId AND service_id = :serviceId";
    private static final String PUT_PROJECT_META_QUERY = "INSERT INTO project_service_meta (project_id, service_id, data) VALUES (:projectId, :serviceId, :data) ON CONFLICT (project_id, service_id) DO UPDATE SET data = :data";
    private static final String SELECT_FOR_UPDATE_QUERY = "SELECT * FROM project WHERE id in (:projectIds) FOR UPDATE";
    private static final String REMOVE_PROJECT_META_QUERY = "DELETE FROM project_service_meta WHERE project_id = :projectId";

    private static final String CHECK_PERSON_GROUP_ROLE_IN_PROJECT = "SELECT EXISTS (SELECT id FROM person_group_membership WHERE " +
            "person_id = :person_id AND group_id IN (SELECT g.id FROM yandex_group_membership gm JOIN yandex_group g ON gm.yandex_group_id = g.id WHERE " +
            "gm.project_role_id = :projectRoleId AND gm.project_id IN (:project_ids) AND g.deleted = FALSE)) AS has_role";


    @Autowired
    private StaffCache staffCache;

    @Autowired
    private ProjectRoleCache projectRoleCache;

    @Autowired
    @Qualifier("quotaDao")
    private QuotaDao quotaDao;

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

    public Table<Project, String, Set<Person>> getLinkedPersons(@NotNull final Collection<Project> projects) {
        final HashBasedTable<Project, String, Set<Person>> result = HashBasedTable.create();

        if (projects.isEmpty()) {
            return result;
        }
        final Map<Long, Project> id2project = CollectionUtils.index(projects);
        final Map<Long, Person> personById = new HashMap<>();

        jdbcTemplate.query(GET_ALL_PROJECT_PERSONS_QUERY, rch -> {
            final Project project = id2project.get(rch.getLong("project_id"));
            if (project != null) {
                final String roleKey = rch.getString("role_key");
                Set<Person> persons = result.get(project, roleKey);
                if (persons == null) {
                    persons = new HashSet<>();
                    result.put(project, roleKey, persons);
                }
                persons.add(toPerson(rch, personById));
            }
        });
        return result;
    }

    public Table<Project, String, Set<YaGroup>> getLinkedGroups(@NotNull final Collection<Project> projects) {
        final HashBasedTable<Project, String, Set<YaGroup>> result = HashBasedTable.create();

        if (projects.isEmpty()) {
            return result;
        }
        final Map<Long, Project> id2project = CollectionUtils.index(projects);
        final Map<Long, YaGroup> groupById = new HashMap<>();

        jdbcTemplate.query(GET_ALL_PROJECT_GROUPS_QUERY, rch -> {
            final Project project = id2project.get(rch.getLong("project_id"));
            if (project != null) {
                final String roleKey = rch.getString("role_key");
                Set<YaGroup> groups = result.get(project, roleKey);
                if (groups == null) {
                    groups = new HashSet<>();
                    result.put(project, roleKey, groups);
                }
                groups.add(toGroup(rch, groupById));
            }
        });
        return result;
    }

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

    @Override
    public @NotNull Set<Person> getLinkedResponsibles(final @NotNull Project project) {
        return getLinkedResponsibles(Collections.singleton(project)).get(project);
    }

    @NotNull
    private Map<Project, Set<YaGroup>> getLinkedGroups(final @NotNull Collection<Project> projects, final Role role) {
        if (projects.isEmpty()) {
            return Collections.emptyMap();
        }

        return getLinkedGroups(projects, role.getKey());
    }

    @NotNull
    @Override
    public Map<Project, Set<YaGroup>> getLinkedGroups(final @NotNull Collection<Project> projects, final String roleKey) {
        if (projects.isEmpty()) {
            return Collections.emptyMap();
        }
        final Map<Long, Project> id2project = CollectionUtils.index(projects);
        final Map<Long, YaGroup> groupById = new HashMap<>();

        final Map<Project, Set<YaGroup>> ret = projects.stream()
                .collect(Collectors.toMap(Function.identity(), p -> new HashSet<>()));
        jdbcTemplate.query(GET_ALL_PROJECT_GROUPS_QUERY, ImmutableMap.of("roleKey", roleKey), rs -> {
            final Project project = id2project.get(rs.getLong("project_id"));
            if (project != null) {
                ret.get(project).add(toGroup(rs, groupById));
            }
        });

        return ret;
    }

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

    @Override
    public @NotNull Set<Person> getAllLinkedResponsibles(@NotNull final Collection<Project> projects) {
        return getLinkedResponsibles(projects).values().stream().flatMap(Set::stream).collect(Collectors.toSet());
    }

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

    private Map<Project, Set<Person>> getLinkedPersons(@NotNull final Collection<Project> projects, @NotNull final Role role) {
        if (projects.isEmpty()) {
            return Collections.emptyMap();
        }

        return getLinkedPersons(projects, role.getKey());
    }

    @Override
    @NotNull
    public Map<Project, Set<Person>> getLinkedPersons(@NotNull final Collection<Project> projects, final String roleKey) {
        if (projects.isEmpty()) {
            return Collections.emptyMap();
        }

        final Map<Long, Project> id2project = CollectionUtils.index(projects);
        final Map<Project, Set<Person>> result = projects.stream()
                .collect(Collectors.toMap(Function.identity(), p -> new HashSet<>()));

        final Map<Long, Person> personById = new HashMap<>();
        jdbcTemplate.query(GET_ALL_PROJECT_PERSONS_WITH_ROLE_QUERY, ImmutableMap.of("roleKey", roleKey), rch -> {
            final Project project = id2project.get(rch.getLong("project_id"));
            if (project != null) {
                result.get(project).add(toPerson(rch, personById));
            }
        });

        return result;
    }

    @Override
    @NotNull
    public Set<YaGroup> getLinkedGroups(final Project project, final String roleKey) {
        return getLinkedGroups(Collections.singleton(project), roleKey).get(project);
    }

    private Map<Project, Set<Person>> getLinkedPersonsBySql(@NotNull final Collection<Project> projects, @NotNull final String sql) {
        if (projects.isEmpty()) {
            return Collections.emptyMap();
        }
        final Map<Long, Project> id2project = CollectionUtils.index(projects);
        final Map<Long, Person> personById = new HashMap<>();
        final Map<Project, Set<Person>> ret = projects.stream()
                .collect(Collectors.toMap(Function.identity(), p -> new HashSet<>()));
        jdbcTemplate.query(sql, rch -> {
            final Project project = id2project.get(rch.getLong("project_id"));
            if (project != null) {
                ret.get(project).add(toPerson(rch, personById));
            }
        });
        return ret;
    }

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public Set<Project> getAll() {
        final List<ProjectRow> rows = jdbcTemplate.query(GET_ALL_QUERY, this::toProjectRow);
        final Map<Long, ProjectRow> id2row = CollectionUtils.index(rows);
        final Map<Long, Project> id2project = new HashMap<>();
        id2row.values().forEach(row -> row.upstream(id2row, id2project));
        return new HashSet<>(id2project.values());
    }

    @Override
    public Set<Person> getLinkedMembers(final @NotNull Project project) {
        return getLinkedMembers(Collections.singleton(project)).get(project);
    }

    @Override
    @NotNull
    public Set<YaGroup> getLinkedMemberGroups(final @NotNull Project project) {
        return getLinkedMemberGroups(Collections.singleton(project)).get(project);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    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 Person person, @NotNull Project project, @NotNull String roleKey) {
        if (getLinkedPersons(project.getPathToRoot(), roleKey).values()
                .stream()
                .flatMap(Collection::stream)
                .anyMatch(person::equals)) {
            return true;
        }

        final ProjectRole role = projectRoleCache.getByKey(roleKey);

        final Map<String, ?> params = ImmutableMap.of(
                "project_ids", CollectionUtils.ids(project.getPathToRoot()),
                "projectRoleId", role.getId(),
                "person_id", person.getId());
        return jdbcTemplate.queryForObject(CHECK_PERSON_GROUP_ROLE_IN_PROJECT, params, (rs, i) -> rs.getBoolean("has_role"));
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    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 Person person, @NotNull Project project, @NotNull String roleKey) {
        if (getLinkedPersons(List.of(project), roleKey).values()
                .stream()
                .flatMap(Collection::stream)
                .anyMatch(person::equals)) {
            return true;
        }

        final ProjectRole role = projectRoleCache.getByKey(roleKey);

        final Map<String, ?> params = ImmutableMap.of(
                "project_ids", CollectionUtils.ids(List.of(project)),
                "projectRoleId", role.getId(),
                "person_id", person.getId());
        return jdbcTemplate.queryForObject(CHECK_PERSON_GROUP_ROLE_IN_PROJECT, params, (rs, i) -> rs.getBoolean("has_role"));
    }

    @NotNull
    @Override
    @Validate(exists = false)
    @Transactional(propagation = Propagation.REQUIRED)
    public Project create(@NotNull final Project project) {
        return createImpl(CREATE_QUERY, project);
    }

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public Project createIfAbsent(@NotNull final Project project) {
        return createImpl(CREATE_IF_ABSENT_QUERY, project);
    }

    @NotNull
    @Transactional(propagation = Propagation.MANDATORY)
    protected Project createImpl(@NotNull final String sql, @NotNull final Project newInstance) {
        if (newInstance.getId() >= 0) {
            throw new IllegalStateException("Instance already exists (id = " + newInstance.getId() + ")");
        }

        if (!newInstance.isRoot() && newInstance.getParent().getId() < 0) {
            create(newInstance.getParent());
        }

        final boolean created = jdbcTemplate.update(sql, toParams(newInstance)) > 0;
        final Project project = read(newInstance.getKey());
        if (created) {
            quotaDao.createZeroQuotasFor(Collections.singleton(project));
        }

        return project;
    }

    @NotNull
    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public Map<Project.Key, Project> createAllIfAbsent(@NotNull final Collection<Project> projects) {
        if (projects.isEmpty()) {
            return Collections.emptyMap();
        }
        final SqlParameterSource[] insetions = projects.stream()
                .sorted(Comparator.comparing(Project::getKey))
                .map(this::toParams)
                .map(MapSqlParameterSource::new)
                .toArray(SqlParameterSource[]::new);
        final List<Boolean> created = Arrays.stream(jdbcTemplate.batchUpdate(CREATE_IF_ABSENT_QUERY, insetions)).mapToObj(c -> c
                > 0).collect(Collectors.toList());
        final Map<Project.Key, Project> result = readAll(CollectionUtils.keys(projects));

        final Map<Project.Key, Project> forZero = new HashMap<>();

        final int[] index = {0};
        projects.stream()
                .sorted(Comparator.comparing(Project::getKey))
                .forEach(p -> {
                    final boolean isCreated = created.get(index[0]);
                    if (isCreated) {
                        forZero.put(p.getKey(), result.get(p.getKey()));
                    }
                    index[0]++;
                });
        if (!forZero.isEmpty()) {
            quotaDao.createZeroQuotasFor(forZero.values());
        }
        return result;
    }

    @NotNull
    @Override
    public Project read(@NotNull final Long id) throws EmptyResultDataAccessException {
        return filter(p -> p.getId() == id)
                .findFirst()
                .orElseThrow(() -> new EmptyResultDataAccessException("No project with id = " + id, 1));
    }

    @NotNull
    @Override
    public Map<Long, Project> read(@NotNull final Collection<Long> ids) {
        return StreamUtils.toMap(filter(p -> ids.contains(p.getId())), Project::getId);
    }

    @Override
    @Validate(exists = true)
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean update(final @NotNull Project project) {
        if (project.getId() < 0) {
            throw new IllegalStateException("Instance does not exist (name = " + project.getName() + ")");
        }
        return jdbcTemplate.update(UPDATE_QUERY, toParams(project)) > 0;
    }

    @Override
    @Validate(exists = true)
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean delete(final @NotNull Project project) {
        if (jdbcTemplate.update(DELETE_QUERY, ImmutableMap.of("id", project.getId())) > 0) {
            quotaDao.deleteAll(quotaDao.getQuotas(project));
            return true;
        }
        return false;
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public boolean clear() {
        return jdbcTemplate.update(CLEAR_QUERY) > 0;
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean attachAll(@NotNull final Collection<Person> persons,
                             @NotNull final Collection<YaGroup> groups,
                             final @NotNull Project project,
                             @NotNull final Role role) {
        if (!persons.isEmpty() || !groups.isEmpty()) {
            final long roleId = projectRoleCache.getByKey(role.getKey()).getId();
            attachAll(persons, groups, project, roleId);
        }
        return true;
    }

    @Override
    public boolean attachAll(@NotNull final Collection<Person> persons, @NotNull final Collection<YaGroup> groups, @NotNull final Project project, final long roleId) {
        if (!persons.isEmpty()) {
            jdbcTemplate.batchUpdate(ATTACH_USER_TO_PROJECT_QUERY, toBatchParams(persons, u -> toParams(u, project, roleId)));
        }
        if (!groups.isEmpty()) {
            jdbcTemplate.batchUpdate(ATTACH_GROUP_TO_PROJECT_QUERY, toBatchParams(groups, g -> toParams(g, project, roleId)));
        }
        return true;
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean detachAll(@NotNull final Collection<Person> persons,
                             @NotNull final Collection<YaGroup> groups,
                             final @NotNull Project project,
                             @NotNull final Role role) {
        if (!persons.isEmpty() || !groups.isEmpty()) {
            final long roleId = projectRoleCache.getByKey(role.getKey()).getId();
            detachAll(persons, groups, project, roleId);
        }
        return true;
    }

    @Override
    public boolean detachAll(@NotNull final Collection<Person> persons, @NotNull final Collection<YaGroup> groups, @NotNull final Project project, final long roleId) {
        if (!persons.isEmpty()) {
            jdbcTemplate.batchUpdate(DETACH_USER_FROM_PROJECT_QUERY, toBatchParams(persons, u -> toParams(u, project, roleId)));
        }
        if (!groups.isEmpty()) {
            jdbcTemplate.batchUpdate(DETACH_GROUP_FROM_PROJECT_QUERY, toBatchParams(groups, g -> toParams(g, project, roleId)));
        }
        return true;
    }

    @Override
    public void detachAll(final @NotNull Project project) {
        detachAllPersons(project);
        jdbcTemplate.update(REMOVE_GROUP_PROJECT_RELATIONS_QUERY, ImmutableMap.of("projectId", project.getId()));
    }

    @Override
    public void detachAllPersons(@NotNull final Project project) {
        jdbcTemplate.update(REMOVE_PERSON_PROJECT_RELATIONS_QUERY, ImmutableMap.of("projectId", project.getId()));
    }

    @Nullable
    @Override
    public DiMetaValueSet getProjectMeta(@NotNull final Project project, @NotNull final Service service) {
        final Map<String, ?> params = ImmutableMap.of("projectId", project.getId(), "serviceId", service.getId());
        final List<DiMetaValueSet> metaValues = jdbcTemplate.query(GET_PROJECT_META_QUERY, params, this::toMetaValues);
        if (metaValues.isEmpty()) {
            return null;
        }
        return StreamUtils.requireSingle(metaValues.stream(), "There are two metas!");
    }

    @NotNull
    @Override
    public Set<ProjectServiceMeta> getAllProjectMetas() {
        return jdbcTemplate.queryForSet(GET_ALL_PROJECT_META_QUERY, (rs, i) -> {
            final Project project = Hierarchy.get().getProjectReader().read(rs.getLong("project_id"));
            final Service service = Hierarchy.get().getServiceReader().read(rs.getLong("service_id"));
            return new ProjectServiceMeta(project, service, toMetaValues(rs, i));
        });
    }

    @Override
    public boolean putProjectMeta(@NotNull final ProjectServiceMeta projectMeta) {
        final Map<String, ?> params = ImmutableMap.<String, Object>builder()
                .put("projectId", projectMeta.getProject().getId())
                .put("serviceId", projectMeta.getService().getId())
                .put("data", SqlUtils.toJsonb(projectMeta.getMetaValues()))
                .build();
        return jdbcTemplate.update(PUT_PROJECT_META_QUERY, params) > 0;
    }

    @NotNull
    @Override
    public Project lockForUpdate(@NotNull final Project project) {
        final long id = project.getId();
        final Map<String, Object> params = Collections.singletonMap("projectIds", Collections.singleton(id));
        final List<Project> queriedProjects = jdbcTemplate.query(SELECT_FOR_UPDATE_QUERY, params, this::toSimpleProject);
        final Project queriedProject = Iterables.getFirst(queriedProjects, null);
        if (queriedProject == null) {
            throw new EmptyResultDataAccessException("No project with key = " + project.getPublicKey(), 1);
        }
        return queriedProject;
    }

    @Override
    @NotNull
    public Collection<Project> lockForUpdate(final @NotNull Collection<Project> projects) {
        if (projects.isEmpty()) {
            return Collections.emptySet();
        }
        final Map<String, Object> params = ImmutableMap.of("projectIds", CollectionUtils.ids(projects));
        return jdbcTemplate.query(SELECT_FOR_UPDATE_QUERY, params, this::toSimpleProject);
    }

    @Override
    public void lockForChanges() {
        acquireRowExclusiveLockOnTable("project");
    }

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

    @Override
    @NotNull
    public Set<Person> getLinkedPersons(final Project project, final String roleKey) {
        return getLinkedPersons(Collections.singleton(project), roleKey).get(project);
    }

    @NotNull
    private Map<String, ?> toParams(final @NotNull Person user, final @NotNull Project project, final long roleId) {
        return ImmutableMap.of("personId", user.getId(), "projectId", project.getId(), "projectRoleId", roleId);
    }

    @NotNull
    private Map<String, ?> toParams(@NotNull final YaGroup group, final @NotNull Project project, final long roleId) {
        return ImmutableMap.of("staffGroupId", group.getId(), "projectId", project.getId(), "projectRoleId", roleId);
    }

    @NotNull
    private Project toSimpleProject(@NotNull final ResultSet rs, final int i) throws SQLException {
        final long id = rs.getLong("id");
        final int abcServiceId = rs.getInt("abc_service_id");
        final long parentId = rs.getLong("parent_id");
        final Project parent = parentId == 0 ? null : Hierarchy.get().getProjectReader().read(parentId);

        return Project.withKey(Project.Key.of(rs.getString("key")))
                .id(id)
                .name(rs.getString("short_name"))
                .description(rs.getString("description"))
                .abcServiceId(abcServiceId > 0 ? abcServiceId : null)
                .removed(rs.getBoolean("removed"))
                .syncedWithAbc(rs.getBoolean("synced_with_abc"))
                .mailList(rs.getString("mail_list"))
                .valueStreamAbcServiceId(getLong(rs, "value_stream_abc_service_id"))
                .parent(parent)
                .build();
    }

    @NotNull
    private ProjectRow toProjectRow(@NotNull final ResultSet rs, final int i) throws SQLException {
        final long personId = rs.getLong("person_id");
        final Person person = personId > 0 ? new Person(personId, rs.getString("login"), rs.getLong("uid"),
                rs.getBoolean("is_robot"), rs.getBoolean("is_dismissed"), rs.getBoolean("is_deleted"),
                PersonAffiliation.valueOf(rs.getString("affiliation"))) : null;
        final long id = rs.getLong("id");
        final int abcServiceId = rs.getInt("abc_service_id");
        final Project.Builder builder = Project.withKey(Project.Key.of(rs.getString("key"), person))
                .id(id)
                .name(rs.getString("short_name"))
                .description(rs.getString("description"))
                .abcServiceId(abcServiceId > 0 ? abcServiceId : null)
                .removed(rs.getBoolean("removed"))
                .syncedWithAbc(rs.getBoolean("synced_with_abc"))
                .mailList(rs.getString("mail_list"))
                .valueStreamAbcServiceId(getLong(rs, "value_stream_abc_service_id"));
        return new ProjectRow(id, builder, rs.getLong("parent_id"));
    }

    @NotNull
    private DiMetaValueSet toMetaValues(@NotNull final ResultSet rs, final int i) throws SQLException {
        return SqlUtils.fromJsonb((PGobject) rs.getObject("data"), DiMetaValueSet.class);
    }

    private static final class ProjectRow implements LongIndexable {
        private final long id;
        @NotNull
        private final Project.Builder projectBuilder;
        private final long parentId;

        private ProjectRow(final long id, @NotNull final Project.Builder projectBuilder, final long parentId) {
            this.id = id;
            this.projectBuilder = projectBuilder;
            this.parentId = parentId;
        }

        @Override
        public long getId() {
            return id;
        }

        private void upstream(@NotNull final Map<Long, ProjectRow> id2row, @NotNull final Map<Long, Project> id2project) {
            if (id2project.containsKey(id)) {
                return;
            }
            if (parentId <= 0) {
                id2project.put(id, projectBuilder.build());
                return;
            }
            id2row.get(parentId).upstream(id2row, id2project);
            id2project.put(id, projectBuilder.parent(id2project.get(parentId)).build());
        }
    }

    @NotNull
    private Person toPerson(@NotNull final ResultSet rs, @NotNull final Map<Long, Person> personById) throws SQLException {
        final long personId = rs.getLong("id");
        Person person = personById.get(personId);

        if (person == null) {
            person = toPerson(rs);
            personById.put(personId, person);
        }

        return person;
    }

    @NotNull
    private YaGroup toGroup(@NotNull final ResultSet rs, @NotNull final Map<Long, YaGroup> groupById) throws SQLException {
        final long groupId = rs.getLong("id");
        YaGroup group = groupById.get(groupById);

        if (group == null) {
            group = toGroup(rs);
            groupById.put(groupId, group);
        }

        return group;
    }
}




