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

import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableMap;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

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.YaGroup;
import ru.yandex.qe.dispenser.domain.dao.SqlDaoBase;
import ru.yandex.qe.dispenser.domain.dao.project.ProjectDao;
import ru.yandex.qe.dispenser.domain.dao.project.role.ProjectRoleCache;
import ru.yandex.qe.dispenser.domain.dao.validation.Validate;
import ru.yandex.qe.dispenser.domain.hierarchy.Role;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;

public class SqlPersonDao extends SqlDaoBase implements PersonDao {
    private static final String PROJECTS_BY_USER_QUERY = "SELECT project.id FROM project, person_membership WHERE project.id = person_membership.project_id AND person_membership.person_id = :personId AND person_membership.project_role_id = :projectRoleId";

    private static final String GET_ALL_QUERY = "SELECT * FROM person";
    private static final String INSERT_QUERY = "INSERT INTO person (login, uid, is_robot, is_dismissed, is_deleted, affiliation) VALUES (:login, :uid, :is_robot, :is_dismissed, :is_deleted, cast(:affiliation as person_affiliation))";
    private static final String CREATE_IF_ABSENT_QUERY = "INSERT INTO person (login, uid, is_robot, is_dismissed, is_deleted, affiliation) VALUES (:login, :uid, :is_robot, :is_dismissed, :is_deleted, cast(:affiliation as person_affiliation)) ON CONFLICT DO NOTHING";
    private static final String SELECT_QUERY = "SELECT * FROM person WHERE login = :login";
    private static final String READ_BY_LOGINS_QUERY = "SELECT * FROM person WHERE login IN (:logins)";
    private static final String UPDATE_QUERY = "UPDATE person SET login = :login, uid = :uid, is_robot = :is_robot, is_dismissed = :is_dismissed, is_deleted = :is_deleted, affiliation = cast(:affiliation as person_affiliation) WHERE id = :userId";
    private static final String DELETE_QUERY = "DELETE FROM person WHERE id = :userId";
    private static final String CLEAR_QUERY = "TRUNCATE person CASCADE";
    private static final String SELECT_BY_ID_QUERY = "SELECT * FROM person WHERE id = :userId";
    private static final String SELECT_BY_IDS_QUERY = "SELECT * FROM person WHERE id IN (:userIds)";
    private static final String SELECT_BY_UID_QUERY = "SELECT * FROM person WHERE uid = :uid";
    private static final String SELECT_BY_UIDS_QUERY = "SELECT * FROM person WHERE uid IN (:uids)";
    private static final String GET_ALL_ACTIVE_QUERY = "SELECT * FROM person WHERE is_dismissed = FALSE AND is_deleted = FALSE";

    private static final String FIND_PROJECTS_BY_PERSON_GROUP_ROLE = "SELECT DISTINCT(gm.project_id) as project_id FROM person_group_membership pm " +
            "JOIN yandex_group_membership gm ON pm.group_id=gm.yandex_group_id JOIN yandex_group g ON gm.yandex_group_id = g.id WHERE " +
            "gm.project_role_id = :projectRoleId AND g.deleted = FALSE AND pm.person_id = :person_id";
    private static final String FIND_GROUPS_FOR_ACTIVE_PERSONS = "SELECT p.id as person_id, g.* FROM person p JOIN person_group_membership pm ON p.id = pm.person_id " +
            "JOIN yandex_group g ON pm.group_id = g.id WHERE p.is_dismissed = FALSE AND p.is_deleted = FALSE";

    private static final String PERSON_MEMBERSHIP_CURSOR_QUERY = "SELECT pe.id, pe.login, pr.abc_service_id, r.key as role_key FROM person pe LEFT JOIN " +
            "person_membership pm ON pe.id = pm.person_id LEFT JOIN project_role r ON r.id = pm.project_role_id LEFT JOIN project pr ON pr.id = pm.project_id WHERE pe.id IN " +
            "(SELECT id FROM person%s ORDER BY id ASC LIMIT :limit_person_id) ORDER BY pe.id ASC";
    private static final String PERSON_MEMBERSHIP_CURSOR_CONSTRAINT = " WHERE id > :from_person_id";

    @Autowired
    private ProjectDao projectDao;

    @Autowired
    private ProjectRoleCache projectRoleCache;

    @NotNull
    @Override
    @Validate(exists = true)
    @Transactional(propagation = Propagation.REQUIRED)
    public Set<Project> getLinkedProjects(@NotNull final Person member, @NotNull final Role role) {

        final long roleId = projectRoleCache.getByKey(role.getKey()).getId();

        final Map<String, Object> params = ImmutableMap.of("personId", member.getId(), "projectRoleId", roleId);
        final Set<Long> projectIds = jdbcTemplate.queryForSet(PROJECTS_BY_USER_QUERY, params, this::toId);

        final ImmutableMap<String, ? extends Serializable> queryParams = ImmutableMap.of("projectRoleId", roleId, "person_id", member.getId());
        final Set<Long> linkedByGroupProjectIds = jdbcTemplate.queryForSet(FIND_PROJECTS_BY_PERSON_GROUP_ROLE, queryParams, (rs, i) -> {
            return rs.getLong("project_id");
        });
        projectIds.addAll(linkedByGroupProjectIds);

        return new HashSet<>(projectDao.read(projectIds).values());
    }

    @Override
    @NotNull
    @Transactional(propagation = Propagation.REQUIRED)
    public Optional<Person> tryReadPersonByUid(final long uid) {
        return jdbcTemplate.queryForOptional(SELECT_BY_UID_QUERY, ImmutableMap.of("uid", uid), this::toPerson);
    }

    @Override
    @NotNull
    @Transactional(propagation = Propagation.REQUIRED)
    public Set<Person> tryReadPersonsByUids(@NotNull final Collection<Long> uids) {
        if (uids.isEmpty()) {
            return Collections.emptySet();
        }
        return jdbcTemplate.queryForSet(SELECT_BY_UIDS_QUERY, Collections.singletonMap("uids", uids), this::toPerson);
    }

    @Override
    @NotNull
    @Transactional(propagation = Propagation.REQUIRED)
    public Optional<Person> tryReadPersonByLogin(@NotNull final String login) {
        return jdbcTemplate.queryForOptional(SELECT_QUERY, ImmutableMap.of("login", login), this::toPerson);
    }

    @Override
    @NotNull
    @Transactional(propagation = Propagation.REQUIRED)
    public Set<Person> tryReadPersonsByLogins(@NotNull final Collection<String> logins) {
        if (logins.isEmpty()) {
            return Collections.emptySet();
        }
        return jdbcTemplate.queryForSet(READ_BY_LOGINS_QUERY, Collections.singletonMap("logins", logins), this::toPerson);
    }

    @Override
    @NotNull
    @Transactional(propagation = Propagation.REQUIRED)
    public Optional<Person> tryReadPersonById(final long id) {
        return jdbcTemplate.queryForOptional(SELECT_BY_ID_QUERY, ImmutableMap.of("userId", id), this::toPerson);
    }

    @Override
    @NotNull
    @Transactional(propagation = Propagation.REQUIRED)
    public Set<Person> tryReadPersonsByIds(@NotNull final Collection<Long> ids) {
        return jdbcTemplate.queryForSet(SELECT_BY_IDS_QUERY, ImmutableMap.of("userIds", ids), this::toPerson);
    }

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public Set<Person> getAll() {
        return jdbcTemplate.queryForSet(GET_ALL_QUERY, this::toPerson);
    }

    @Override
    @Validate(exists = false)
    @Transactional(propagation = Propagation.REQUIRED)
    public @NotNull Person create(final @NotNull Person newInstance) {
        jdbcTemplate.update(INSERT_QUERY, toCreateParams(newInstance));
        return read(newInstance.getLogin());
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void createIfAbsent(@NotNull final Collection<Person> persons) {
        if (persons.isEmpty()) {
            return;
        }
        jdbcTemplate.batchUpdate(CREATE_IF_ABSENT_QUERY, persons.stream().map(this::toCreateParams).collect(Collectors.toList()));
    }

    @NotNull
    @Override
    public Set<Person> getAllActive() {
        return jdbcTemplate.queryForSet(GET_ALL_ACTIVE_QUERY, this::toPerson);
    }

    @NotNull
    @Override
    public Map<Long, Set<YaGroup>> getGroupsForAllActiveUsers() {
        final Map<Long, Set<YaGroup>> result = new HashMap<>();
        final Map<Long, YaGroup> groupCache = new HashMap<>();
        jdbcTemplate.query(FIND_GROUPS_FOR_ACTIVE_PERSONS, rs -> {
            final long personId = rs.getLong("person_id");
            final Set<YaGroup> groups = result.computeIfAbsent(personId, k -> new HashSet<>());
            final long groupId = rs.getLong("id");
            if (groupCache.containsKey(groupId)) {
                groups.add(groupCache.get(groupId));
            } else {
                final YaGroup group = toGroup(rs, groupId);
                groups.add(group);
                groupCache.put(groupId, group);
            }
        });
        return result;
    }

    @Override
    @NotNull
    @Transactional(propagation = Propagation.REQUIRED)
    public Person read(@NotNull final Long id) throws EmptyResultDataAccessException {
        return readPersonById(id);
    }

    @Override
    @NotNull
    @Transactional(propagation = Propagation.REQUIRED)
    public Person read(@NotNull final String login) throws EmptyResultDataAccessException {
        return readPersonByLogin(login);
    }

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public Map<String, Person> readAll(@NotNull final Collection<String> logins) {
        return CollectionUtils.keyIndex(tryReadPersonsByLogins(logins));
    }

    @Override
    @Validate(exists = true)
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean update(final @NotNull Person user) {
        return jdbcTemplate.update(UPDATE_QUERY, toUpdateParams(user)) > 0;
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean updateAll(final Collection<Person> persons) {
        if (persons.isEmpty()) {
            return false;
        }
        final int[] updatedArray = jdbcTemplate.batchUpdate(UPDATE_QUERY,
                persons.stream().map(this::toUpdateParams).collect(Collectors.toList()));
        boolean result = false;
        for (final int updated : updatedArray) {
            result |= updated > 0;
        }
        return result;
    }

    @Override
    @Validate(exists = true)
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean delete(final @NotNull Person user) {
        return jdbcTemplate.update(DELETE_QUERY, toDeleteParams(user)) > 0;
    }

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

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public PersonMembershipPage getPersonMembershipsPage(final Long fromPersonId, final long limit) {
        final String constraint = fromPersonId != null ? PERSON_MEMBERSHIP_CURSOR_CONSTRAINT : "";
        final String query = String.format(PERSON_MEMBERSHIP_CURSOR_QUERY, constraint);
        final Map<String, Long> params = fromPersonId != null
                ? ImmutableMap.of("from_person_id", fromPersonId, "limit_person_id", limit)
                : ImmutableMap.of("limit_person_id", limit);
        final Map<String, Map<String, Set<Long>>> result = new HashMap<>();
        final Long[] lastPersonId = {null};
        jdbcTemplate.query(query, params, rs -> {
            final String login = rs.getString("login");
            final Long abcServiceId = getLong(rs, "abc_service_id");
            final String roleKey = rs.getString("role_key");
            final long personId = rs.getLong("id");
            if (abcServiceId != null && roleKey != null) {
                final Map<String, Set<Long>> projectsByRole = result.computeIfAbsent(login, l -> new HashMap<>());
                projectsByRole.computeIfAbsent(roleKey, r -> new HashSet<>()).add(abcServiceId);
            } else {
                result.computeIfAbsent(login, l -> new HashMap<>());
            }
            if (lastPersonId[0] == null || personId > lastPersonId[0]) {
                lastPersonId[0] = personId;
            }
        });
        return new PersonMembershipPage(result, lastPersonId[0]);
    }

    @NotNull
    private Map<String, Object> toCreateParams(final @NotNull Person user) {
        final Map<String, Object> params = new HashMap<>();
        params.put("login", user.getLogin());
        params.put("uid", user.getUid());
        params.put("is_robot", user.isRobot());
        params.put("is_dismissed", user.isDismissed());
        params.put("is_deleted", user.isDeleted());
        params.put("affiliation", user.getAffiliation().name());
        return params;
    }

    @NotNull
    private Map<String, Object> toUpdateParams(final @NotNull Person user) {
        final Map<String, Object> params = new HashMap<>();
        params.put("login", user.getLogin());
        params.put("userId", user.getId());
        params.put("uid", user.getUid());
        params.put("is_robot", user.isRobot());
        params.put("is_dismissed", user.isDismissed());
        params.put("is_deleted", user.isDeleted());
        params.put("affiliation", user.getAffiliation().name());
        return params;
    }

    @NotNull
    private Map<String, Object> toDeleteParams(final @NotNull Person user) {
        final Map<String, Object> params = new HashMap<>();
        params.put("userId", user.getId());
        return params;
    }

    private YaGroup toGroup(@NotNull final ResultSet rs, final long groupId) throws SQLException {
        final String groupUrl = rs.getString("url_acceptable_key");
        final DiYandexGroupType groupType = DiYandexGroupType.valueOf(rs.getString("group_type"));
        final long groupStaffId = rs.getLong("staff_id");
        final boolean groupDeleted = rs.getBoolean("deleted");
        final YaGroup group = new YaGroup(groupUrl, groupType, groupStaffId, groupDeleted);
        group.setId(groupId);
        return group;
    }

}
