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

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import org.jetbrains.annotations.NotNull;
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.domain.Person;
import ru.yandex.qe.dispenser.domain.Service;
import ru.yandex.qe.dispenser.domain.dao.SqlDaoBase;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;

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

public class SqlServiceDao extends SqlDaoBase implements ServiceDao {
    private static final String GET_ALL_QUERY = "SELECT * FROM service";
    private static final String DELETE_QUERY = "DELETE FROM service WHERE id = :serviceId";
    private static final String CLEAR_QUERY = "TRUNCATE service CASCADE";
    private static final String UPDATE_QUERY = "UPDATE service SET key = :key, name = :name, " +
            "abc_service_id = :abcServiceId, priority = :priority, " +
            "account_actual_values_in_quota_distribution = :accountActualValuesInQuotaDistribution, " +
            "require_zero_quota_usage_for_project_deletion = :requireZeroQuotaUsageForProjectDeletion, " +
            "uses_project_hierarchy = :usesProjectHierarchy, " +
            "manual_quota_allocation = :manualQuotaAllocation, " +
            "resources_mapping_bean_name = :resourcesMappingBeanName " +
            "WHERE id = :serviceId";
    private static final String GET_BY_PK_QUERY = "SELECT * FROM service WHERE id = :id";
    private static final String GET_BY_PKS_QUERY = "SELECT * FROM service WHERE id IN (:ids)";
    private static final String GET_BY_KEY_QUERY = "SELECT * FROM service WHERE key = :key";
    private static final String CREATE_QUERY = "INSERT INTO service (key, name, abc_service_id, priority, account_actual_values_in_quota_distribution, require_zero_quota_usage_for_project_deletion, uses_project_hierarchy, manual_quota_allocation, resources_mapping_bean_name) " +
            "VALUES (:key, :name, :abcServiceId, :priority, :accountActualValuesInQuotaDistribution, :requireZeroQuotaUsageForProjectDeletion, :usesProjectHierarchy, :manualQuotaAllocation, :resourcesMappingBeanName)";
    private static final String GET_ADMINS_QUERY = "SELECT service_id, person.id, login, person.uid, person.is_robot, person.is_dismissed, person.is_deleted, person.affiliation FROM person, service_admin WHERE person.id = service_admin.person_id AND service_admin.service_id IN (:serviceIds)";
    private static final String GET_ADMIN_SERVICES_QUERY = "SELECT service.* FROM service, service_admin, person WHERE service.id = service_admin.service_id AND person.id = service_admin.person_id AND person.id = :personId";
    private static final String GET_TRUSTEES_QUERY = "SELECT service_id, person.id, login, person.uid, person.is_robot, person.is_dismissed, person.is_deleted, person.affiliation FROM person, service_trustee WHERE person.id = service_trustee.person_id AND service_trustee.service_id IN (:serviceIds)";
    private static final String ATTACH_ADMIN_QUERY = "INSERT INTO service_admin VALUES (:personId, :serviceId)";
    private static final String DEATACH_ADMIN_QUERY = "DELETE FROM service_admin WHERE person_id = :personId AND service_id = :serviceId";
    private static final String ATTACH_TRUSTEE_QUERY = "INSERT INTO service_trustee VALUES (:personId, :serviceId)";
    private static final String DEATACH_TRUSTEE_QUERY = "DELETE FROM service_trustee WHERE person_id = :personId AND service_id = :serviceId";
    private static final String DEATACH_ALL_TRUSTEES_QUERY = "DELETE FROM service_trustee WHERE service_id = :serviceId";
    private static final String DEATACH_ALL_ADMINS_QUERY = "DELETE FROM service_admin WHERE service_id = :serviceId";

    @NotNull
    @Override
    public Multimap<Service, Person> getAdmins(@NotNull final Collection<Service> services) {
        return getPersons(GET_ADMINS_QUERY, services);
    }

    @NotNull
    @Override
    public Multimap<Service, Person> getTrustees(@NotNull final Collection<Service> services) {
        return getPersons(GET_TRUSTEES_QUERY, services);
    }

    @NotNull
    @Transactional(propagation = Propagation.REQUIRED)
    protected Multimap<Service, Person> getPersons(@NotNull final String sql, @NotNull final Collection<Service> services) {
        if (services.isEmpty()) {
            return ImmutableMultimap.of();
        }
        final Map<Long, Service> id2service = CollectionUtils.index(services);
        final Multimap<Service, Person> persons = HashMultimap.create();
        jdbcTemplate.query(sql, ImmutableMap.of("serviceIds", CollectionUtils.ids(services)), rch -> {
            persons.put(id2service.get(rch.getLong("service_id")), toPerson(rch));
        });
        return persons;
    }

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

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public Set<Service> getAdminServices(@NotNull final Person admin) {
        return jdbcTemplate.queryForSet(GET_ADMIN_SERVICES_QUERY, ImmutableMap.of("personId", admin.getId()), this::toService);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public @NotNull Service create(final @NotNull Service newInstance) {
        jdbcTemplate.update(CREATE_QUERY, toParams(newInstance));
        return read(newInstance.getKey());
    }

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public Service read(@NotNull final Long id) throws EmptyResultDataAccessException {
        return jdbcTemplate.queryForOptional(GET_BY_PK_QUERY, ImmutableMap.of("id", id), this::toService)
                .orElseThrow(() -> new EmptyResultDataAccessException("No service with id " + id, 1));
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public @NotNull Service read(@NotNull final String key) throws EmptyResultDataAccessException {
        return jdbcTemplate.queryForOptional(GET_BY_KEY_QUERY, ImmutableMap.of("key", key), this::toService)
                .orElseThrow(() -> new EmptyResultDataAccessException("No service with id " + key, 1));
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean update(final @NotNull Service trainsientObject) {
        return jdbcTemplate.update(UPDATE_QUERY, toParams(trainsientObject)) > 0;
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean delete(final @NotNull Service persistentObject) {
        return jdbcTemplate.update(DELETE_QUERY, toParams(persistentObject)) > 0;
    }

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

    @Override
    public boolean attachAdmins(@NotNull final Service service, @NotNull final Collection<Person> admins) {
        return changePersons(ATTACH_ADMIN_QUERY, service, admins);
    }

    @Override
    public boolean detachAdmins(@NotNull final Service service, @NotNull final Collection<Person> admins) {
        return changePersons(DEATACH_ADMIN_QUERY, service, admins);
    }

    @Override
    public void detachAllAdmins(@NotNull final Service service) {
        changePersons(DEATACH_ALL_ADMINS_QUERY, service);
    }

    @Override
    public boolean attachTrustees(@NotNull final Service service, @NotNull final Collection<Person> trustees) {
        return changePersons(ATTACH_TRUSTEE_QUERY, service, trustees);
    }

    @Override
    public boolean detachTrustees(@NotNull final Service service, @NotNull final Collection<Person> trustees) {
        return changePersons(DEATACH_TRUSTEE_QUERY, service, trustees);
    }

    @Override
    public Set<Service> readByIds(Set<Long> ids) {
        if (ids.isEmpty()) {
            return Set.of();
        }
        return jdbcTemplate.queryForSet(GET_BY_PKS_QUERY, ImmutableMap.of("ids", ids), this::toService);
    }

    @Override
    public void detachAllTrustees(@NotNull final Service service) {
        changePersons(DEATACH_ALL_TRUSTEES_QUERY, service);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    protected boolean changePersons(@NotNull final String sql, @NotNull final Service service,
                                    @NotNull final Collection<Person> persons) {
        final SqlParameterSource[] modifications = persons.stream()
                .map(person -> new MapSqlParameterSource(ImmutableMap.of("personId", person.getId(), "serviceId", service.getId())))
                .toArray(SqlParameterSource[]::new);
        return jdbcTemplate.batchUpdate(sql, modifications).length > 0;
    }

    @Transactional(propagation = Propagation.REQUIRED)
    protected boolean changePersons(@NotNull final String sql, @NotNull final Service service) {
        final SqlParameterSource modification = new MapSqlParameterSource(ImmutableMap.of("serviceId", service.getId()));
        return jdbcTemplate.batchUpdate(sql, modification).length > 0;
    }


    private @NotNull Service toService(@NotNull final ResultSet rs, final int i) throws SQLException {
        final Service.Settings settings = Service.Settings.builder()
                .accountActualValuesInQuotaDistribution(rs.getBoolean("account_actual_values_in_quota_distribution"))
                .requireZeroQuotaUsageForProjectDeletion(rs.getBoolean("require_zero_quota_usage_for_project_deletion"))
                .usesProjectHierarchy(rs.getBoolean("uses_project_hierarchy"))
                .manualQuotaAllocation(rs.getBoolean("manual_quota_allocation"))
                .resourcesMappingBeanName(rs.getString("resources_mapping_bean_name"))
                .build();
        final int abcServiceId = rs.getInt("abc_service_id");
        final Integer priority = getInteger(rs, "priority");
        final Service service = Service.withKey(rs.getString("key"))
                .withName(rs.getString("name"))
                .withAbcServiceId(abcServiceId > 0 ? abcServiceId : null)
                .withSettings(settings)
                .withPriority(priority)
                .build();
        service.setId(rs.getLong("id"));
        return service;
    }

    public static Map<String, Object> toParams(final @NotNull Service service) {
        validateServiceKey(service.getKey());
        final Map<String, Object> parameters = new HashMap<>();
        parameters.put("serviceId", service.getId());
        parameters.put("key", service.getKey());
        parameters.put("name", service.getName());
        parameters.put("abcServiceId", service.getAbcServiceId());
        parameters.put("priority", service.getPriority());
        parameters.put("accountActualValuesInQuotaDistribution", service.getSettings().accountActualValuesInQuotaDistribution());
        parameters.put("requireZeroQuotaUsageForProjectDeletion", service.getSettings().requireZeroQuotaUsageForProjectDeletion());
        parameters.put("usesProjectHierarchy", service.getSettings().usesProjectHierarchy());
        parameters.put("manualQuotaAllocation", service.getSettings().isManualQuotaAllocation());
        parameters.put("resourcesMappingBeanName", service.getSettings().getResourcesMappingBeanName());
        return parameters;
    }
}
