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

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

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

import ru.yandex.qe.dispenser.api.v1.DiQuotingMode;
import ru.yandex.qe.dispenser.api.v1.DiResourceType;
import ru.yandex.qe.dispenser.domain.Resource;
import ru.yandex.qe.dispenser.domain.ResourceGroup;
import ru.yandex.qe.dispenser.domain.Service;
import ru.yandex.qe.dispenser.domain.dao.SqlDaoBase;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.domain.index.LongIndexBase;

public class SqlResourceDao extends SqlDaoBase implements ResourceDao {
    private static final String GET_ALL_QUERY = "SELECT * FROM resource";

    private static final String CREATE_QUERY = "INSERT INTO resource (key, name, service_id, resource_type, quoting_mode, description, resource_group_id, priority) VALUES (:key, :name, :serviceId, cast(:resourceType as resource_type), cast(:quotingMode as quoting_mode), :description, :resourceGroupId, :priority)";
    private static final String READ_QUERY = "SELECT * FROM resource WHERE id = :id";
    private static final String READ_MANY_QUERY = "SELECT * FROM resource WHERE id IN (:ids)";
    private static final String READ_BY_KEY_QUERY = "SELECT * FROM resource WHERE service_id = :serviceId AND key = :key";
    private static final String UPDATE_QUERY = "UPDATE resource SET key = :key, name = :name, description = :description, service_id = :serviceId, resource_type = cast(:resourceType as resource_type), quoting_mode = cast(:quotingMode as quoting_mode), resource_group_id = :resourceGroupId, priority = :priority WHERE id = :resourceId";
    private static final String DELETE_QUERY = "DELETE FROM resource WHERE id = :resourceId";
    private static final String CLEAR_QUERY = "TRUNCATE resource CASCADE";
    private static final String CLEAR_GROUP_QUERY = "UPDATE resource SET resource_group_id = NULL WHERE resource_group_id = :resourceGroupId";
    private static final String SELECT_BY_IDS = "SELECT * FROM resource WHERE id IN (:ids)";

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public Set<Resource> getAll() {
        return new HashSet<>(jdbcTemplate.query(GET_ALL_QUERY, this::toResource));
    }

    @NotNull
    @Override
    public Map<Long, Resource> tryGetByIds(@NotNull final Set<Long> ids) {
        if (ids.isEmpty()) {
            return Collections.emptyMap();
        }
        final Set<Resource> resources = jdbcTemplate.queryForSet(SELECT_BY_IDS,
                ImmutableMap.of("ids", ids), this::toResource);
        return resources.stream().collect(Collectors.toMap(LongIndexBase::getId, Function.identity()));
    }

    @Override
    public Set<Resource> readByIds(Set<Long> ids) {
        if (ids.isEmpty()) {
            return Set.of();
        }
        return jdbcTemplate.queryForSet(READ_MANY_QUERY, Collections.singletonMap("ids", ids), this::toResource);
    }

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

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public @NotNull Resource read(final @NotNull Resource.Key key) throws EmptyResultDataAccessException {
        return jdbcTemplate.queryForOptional(READ_BY_KEY_QUERY, toParams(key), this::toResource)
                .orElseThrow(() -> new EmptyResultDataAccessException("No resource with key " + key, 1));
    }

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED) // only for tests
    public Resource read(@NotNull final Long id) throws EmptyResultDataAccessException {
        return jdbcTemplate.queryForOptional(READ_QUERY, ImmutableMap.of("id", id), this::toResource)
                .orElseThrow(() -> new EmptyResultDataAccessException("No resource with id " + id, 1));
    }

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

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

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

    @NotNull
    public Resource toResource(@NotNull final ResultSet rs, final int i) throws SQLException {
        final Service service = Hierarchy.get().getServiceReader().read(rs.getLong("service_id"));

        final long groupId = rs.getLong("resource_group_id");
        final ResourceGroup group = rs.wasNull() ? null : Hierarchy.get().getResourceGroupReader().read(groupId);

        final Resource resource = new Resource.Builder(rs.getString("key"), service)
                .name(rs.getString("name"))
                .type(DiResourceType.valueOf(rs.getString("resource_type")))
                .mode(DiQuotingMode.valueOf(rs.getString("quoting_mode")))
                .description(rs.getString("description"))
                .group(group)
                .priority(getInteger(rs, "priority"))
                .build();
        resource.setId(rs.getLong("id"));
        return resource;
    }
}
