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

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

import javax.annotation.ParametersAreNonnullByDefault;

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.domain.Segment;
import ru.yandex.qe.dispenser.domain.Segmentation;
import ru.yandex.qe.dispenser.domain.dao.SqlDaoBase;
import ru.yandex.qe.dispenser.domain.dao.quota.segment.SqlQuotaSegmentDao;
import ru.yandex.qe.dispenser.domain.dao.segmentation.SqlSegmentationDao;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;

@ParametersAreNonnullByDefault
public class SqlSegmentDao extends SqlDaoBase implements SegmentDao {
    private static final String GET_ALL_QUERY = "SELECT * FROM segment";

    private static final String CREATE_QUERY = "INSERT INTO segment (key, name, description, segmentation_id, priority) VALUES (:key, :name, :description, :segmentationId, :priority)";

    private static final String READ_QUERY = "SELECT * FROM segment WHERE id = :id";
    private static final String READ_MANY_QUERY = "SELECT * FROM segment WHERE id IN (:ids)";
    private static final String READ_FOR_UPDATE_QUERY = "SELECT * FROM segment WHERE id = :id FOR UPDATE";

    private static final String UPDATE_QUERY = "UPDATE segment SET key = :key, name = :name, description = :description, priority = :priority WHERE id = :segmentId";

    private static final String DELETE_QUERY = "DELETE FROM segment WHERE id = :segmentId";
    private static final String GET_BY_SEGMENTATION_QUERY = "SELECT * FROM segment WHERE segmentation_id = :segmentationId";
    private static final String GET_BY_SEGMENTATION_MAX_PRIORITY_QUERY = "SELECT MAX(priority) FROM segment WHERE segmentation_id = :segmentationId";
    private static final String GET_BY_KEY_QUERY = "SELECT * FROM segment WHERE key = :key";
    private static final String GET_BY_SEGMENTATIONS_QUERY = "SELECT * FROM segment WHERE segmentation_id IN (:segmentation_ids)";

    @Autowired
    private SqlQuotaSegmentDao quotaSegmentDao;

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

    @NotNull
    @Override
    public Set<Segment> get(final Segmentation segmentation) {
        return jdbcTemplate.queryForSet(GET_BY_SEGMENTATION_QUERY, SqlSegmentationDao.toParams(segmentation), this::toSegment);
    }

    @NotNull
    @Override
    public Map<Long, Set<Segment>> getBySegmentationIds(@NotNull final Set<Long> segmentationIds) {
        if (segmentationIds.isEmpty()) {
            return Collections.emptyMap();
        }
        final Set<Segment> segments = jdbcTemplate.queryForSet(GET_BY_SEGMENTATIONS_QUERY,
                ImmutableMap.of("segmentation_ids", segmentationIds), this::toSegment);
        final Map<Long, Set<Segment>> result = segments.stream()
                .collect(Collectors.groupingBy(s -> s.getSegmentation().getId(), HashMap::new, Collectors.toSet()));
        segmentationIds.forEach(id -> result.computeIfAbsent(id, k -> new HashSet<>()));
        return result;
    }

    @NotNull
    @Override
    public Segment read(final String publicKey) {
        return jdbcTemplate.queryForObject(GET_BY_KEY_QUERY, Collections.singletonMap("key", publicKey), this::toSegment);
    }

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public Segment create(@NotNull final Segment segment) {
        final long id = jdbcTemplate.insert(CREATE_QUERY, toParams(segment));
        return read(id);
    }

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public Segment read(@NotNull final Long id) throws EmptyResultDataAccessException {
        return jdbcTemplate.queryForOptional(READ_QUERY, Collections.singletonMap("id", id), this::toSegment)
                .orElseThrow(() -> new EmptyResultDataAccessException("No segment with id " + id, 1));
    }

    @NotNull
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public Segment readForUpdate(final Long id) {
        return jdbcTemplate.queryForOptional(READ_FOR_UPDATE_QUERY, Collections.singletonMap("id", id), this::toSegment)
                .orElseThrow(() -> new EmptyResultDataAccessException("No segment with id " + id, 1));
    }

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

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

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public boolean delete(@NotNull final Segment segment) {
        final int update = jdbcTemplate.update(DELETE_QUERY, toParams(segment));
        quotaSegmentDao.removeBySegment(segment);
        return update > 0;
    }

    @NotNull
    @Override
    public Short getMaxPriority(final Segmentation segmentation) {
        return jdbcTemplate.queryForObject(GET_BY_SEGMENTATION_MAX_PRIORITY_QUERY, SqlSegmentationDao.toParams(segmentation),
                this::toPriorityMax);
    }

    @NotNull
    private Segment toSegment(final ResultSet rs, final int i) throws SQLException {
        final Segmentation segmentation = Hierarchy.get().getSegmentationReader().read(rs.getLong("segmentation_id"));
        final Segment segment = new Segment.Builder(rs.getString("key"), segmentation)
                .name(rs.getString("name"))
                .description(rs.getString("description"))
                .priority(rs.getShort("priority"))
                .build();
        segment.setId(rs.getLong("id"));
        return segment;
    }

    private short toPriorityMax(final ResultSet rs, final int i) throws SQLException {
        return rs.getShort("max");
    }

    @NotNull
    public static Map<String, Object> toParams(final Segment segment) {
        final ImmutableMap.Builder<String, Object> paramMap = ImmutableMap.<String, Object>builder()
                .put("segmentId", segment.getId())
                .put("segmentationId", segment.getKey().getSegmentation().getId())
                .put("key", segment.getKey().getPublicKey())
                .put("name", segment.getName())
                .put("description", segment.getDescription())
                .put("priority", segment.getPriority());

        return paramMap.build();
    }
}
