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

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;

import org.springframework.dao.DuplicateKeyException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import ru.yandex.qe.dispenser.api.v1.DiResourceType;
import ru.yandex.qe.dispenser.domain.base_resources.BaseResourceType;
import ru.yandex.qe.dispenser.domain.dao.SqlDaoBase;

public class SqlBaseResourceTypeDao extends SqlDaoBase implements BaseResourceTypeDao {

    private static final String INSERT_QUERY = "INSERT INTO base_resource_type" +
            " (key, name, service_id, resource_type)" +
            " VALUES (:key, :name, :serviceId, cast(:resourceType as resource_type)) RETURNING id";
    private static final String GET_BY_ID_QUERY = "SELECT id, key, name, service_id, resource_type FROM" +
            " base_resource_type WHERE id = :id";
    private static final String GET_BY_IDS_QUERY = "SELECT id, key, name, service_id, resource_type FROM" +
            " base_resource_type WHERE id IN (:ids)";
    private static final String GET_FIRST_PAGE_QUERY = "SELECT id, key, name, service_id, resource_type FROM" +
            " base_resource_type ORDER BY id ASC LIMIT :limit";
    private static final String GET_PAGE_QUERY = "SELECT id, key, name, service_id, resource_type FROM" +
            " base_resource_type WHERE id > :idFrom ORDER BY id ASC LIMIT :limit";
    private static final String GET_BY_SERVICE_ID_QUERY = "SELECT id, key, name, service_id, resource_type FROM" +
            " base_resource_type WHERE service_id = :serviceId";
    private static final String GET_BY_ID_FOR_UPDATE_QUERY = "SELECT id, key, name, service_id, resource_type FROM" +
            " base_resource_type WHERE id = :id FOR UPDATE";
    private static final String GET_BY_SERVICE_ID_FOR_UPDATE_QUERY = "SELECT id, key, name, service_id, resource_type" +
            " FROM base_resource_type WHERE service_id = :serviceId FOR UPDATE";
    private static final String CLEAR_QUERY = "TRUNCATE base_resource_type CASCADE";

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public BaseResourceType create(BaseResourceType.Builder builder) {
        try {
            long id = jdbcTemplate.queryForObject(INSERT_QUERY, toInsertParams(builder), Long.class);
            return builder.build(id);
        } catch (DuplicateKeyException e) {
            throw new IllegalArgumentException("Conflicting base resource type already exists", e);
        }
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public Optional<BaseResourceType> getById(long id) {
        return jdbcTemplate.queryForOptional(GET_BY_ID_QUERY, Map.of("id", id), this::toModel);
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public Set<BaseResourceType> getByIds(Collection<? extends Long> ids) {
        if (ids.isEmpty()) {
            return Set.of();
        }
        return jdbcTemplate.queryForSet(GET_BY_IDS_QUERY, Map.of("ids", ids), this::toModel);
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public List<BaseResourceType> getPage(Long idFrom, int size) {
        return Optional.ofNullable(idFrom).map(from -> jdbcTemplate.query(GET_PAGE_QUERY,
                        Map.of("idFrom", from, "limit", (long) size), this::toModel))
                .orElseGet(() -> jdbcTemplate.query(GET_FIRST_PAGE_QUERY,
                        Map.of("limit", (long) size), this::toModel));
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public synchronized Set<BaseResourceType> getByServiceId(long serviceId) {
        return jdbcTemplate.queryForSet(GET_BY_SERVICE_ID_QUERY, Map.of("serviceId", serviceId), this::toModel);
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public Optional<BaseResourceType> getByIdForUpdate(long id) {
        return jdbcTemplate.queryForOptional(GET_BY_ID_FOR_UPDATE_QUERY, Map.of("id", id), this::toModel);
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public Set<BaseResourceType> getByServiceIdForUpdate(long serviceId) {
        return jdbcTemplate.queryForSet(GET_BY_SERVICE_ID_FOR_UPDATE_QUERY, Map.of("serviceId", serviceId),
                this::toModel);
    }

    @Override
    @Transactional(propagation = Propagation.MANDATORY)
    public void clear() {
        jdbcTemplate.update(CLEAR_QUERY);
    }

    private Map<String, ?> toInsertParams(BaseResourceType.Builder builder) {
        return Map.of(
                "key", builder.getKey().orElseThrow(() -> new NoSuchElementException("Key is required")),
                "name", builder.getName().orElseThrow(() -> new NoSuchElementException("Name is required")),
                "serviceId", builder.getServiceId()
                        .orElseThrow(() -> new NoSuchElementException("Service id is required")),
                "resourceType", builder.getResourceType()
                        .orElseThrow(() -> new NoSuchElementException("Key is required")).name()
        );
    }

    private BaseResourceType toModel(ResultSet rs, int i) throws SQLException {
        return BaseResourceType.builder()
                .id(rs.getLong("id"))
                .key(rs.getString("key"))
                .name(rs.getString("name"))
                .serviceId(rs.getLong("service_id"))
                .resourceType(DiResourceType.valueOf(rs.getString("resource_type")))
                .build();
    }

}
