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

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import javax.annotation.ParametersAreNonnullByDefault;

import com.fasterxml.jackson.core.type.TypeReference;
import org.jetbrains.annotations.Nullable;
import org.postgresql.util.PGobject;
import org.springframework.beans.factory.annotation.Autowired;

import ru.yandex.qe.dispenser.domain.dao.DiJdbcTemplate;
import ru.yandex.qe.dispenser.domain.dao.SqlUtils;
import ru.yandex.qe.dispenser.domain.resources_model.DeliveryResult;
import ru.yandex.qe.dispenser.domain.resources_model.ExternalResource;
import ru.yandex.qe.dispenser.domain.resources_model.InternalResource;
import ru.yandex.qe.dispenser.domain.resources_model.QuotaRequestDelivery;
import ru.yandex.qe.dispenser.domain.resources_model.QuotaRequestDeliveryResolveStatus;

@ParametersAreNonnullByDefault
public class SqlDeliveryDao implements DeliveryDao {
    public static final TypeReference<List<ExternalResource>> EXTERNAL_RESOURCE_TYPE = new TypeReference<>() {
    };
    public static final TypeReference<List<InternalResource>> INTERNAL_RESOURCE_TYPE = new TypeReference<>() {
    };
    public static final TypeReference<List<DeliveryResult>> DELIVERY_RESULT_TYPE = new TypeReference<>() {
    };
    private static final String GET_ALL_UNRESOLVED_FOR_PROVIDER_AND_REQUEST_QUERY = "SELECT * FROM delivery" +
            " WHERE service_id = :serviceId AND request_id = :requestId AND resolve_status != 'RESOLVED'";
    private static final String SELECT_BY_ID_FOR_UPDATE = "SELECT * FROM delivery WHERE unique_id = :uniqueId FOR UPDATE";
    private static final String CREATE_DELIVERY_QUERY = "INSERT INTO delivery (service_id, request_id, unique_id," +
            " author_id, author_uid, abc_service_id, campaign_id, created_at, resolved, resolved_at, external_resources," +
            " internal_resources, delivery_results, resolve_status) VALUES (:serviceId, :requestId, :uniqueId, :authorId, :authorUid," +
            " :abcServiceId, :campaignId, :createdAt, :resolved, :resolvedAt, :externalResources, :internalResources, :deliveryResults, :resolveStatus::resolve_status)";
    private static final String UPDATE_DELIVERY_QUERY = "UPDATE delivery SET resolved_at = :resolvedAt," +
            " delivery_results = :deliveryResults, resolved = :resolved, resolve_status = :resolveStatus::resolve_status WHERE unique_id = :uniqueId;";
    private static final String REMOVE_DELIVERY_QUERY = "DELETE FROM delivery WHERE unique_id = :uniqueId";
    private static final String CLEAR_QUERY = "TRUNCATE delivery";
    private static final String GET_ALL_QUERY = "SELECT * FROM delivery";
    private static final String GET_BY_REQUEST_IDS = "SELECT * FROM delivery WHERE request_id IN (:requestIds)";
    private static final String GET_UNRESOLVED_FIRST_PAGE = "SELECT * FROM delivery WHERE resolve_status != 'RESOLVED' " +
            "ORDER BY unique_id ASC LIMIT :limit";
    private static final String GET_UNRESOLVED_NEXT_PAGE = "SELECT * FROM delivery WHERE resolve_status != 'RESOLVED' " +
            "AND unique_id > :fromUniqueId ORDER BY unique_id ASC LIMIT :limit";

    @Autowired
    protected DiJdbcTemplate jdbcTemplate;

    @Override
    public List<QuotaRequestDelivery> getUnresolvedByQuotaRequestIdAndProviderId(long quotaRequestId, long providerId) {
        return jdbcTemplate.query(GET_ALL_UNRESOLVED_FOR_PROVIDER_AND_REQUEST_QUERY, Map.of("serviceId", providerId,
                "requestId", quotaRequestId), this::toQuotaRequestDelivery);
    }

    @Override
    public QuotaRequestDelivery readForUpdate(UUID id) {
        return jdbcTemplate.queryForObject(SELECT_BY_ID_FOR_UPDATE, Map.of("uniqueId", id),
                this::toQuotaRequestDelivery);
    }

    @Override
    public List<QuotaRequestDelivery> readAll() {
        return jdbcTemplate.query(GET_ALL_QUERY, this::toQuotaRequestDelivery);
    }

    @Override
    public List<QuotaRequestDelivery> getByRequestIds(Set<Long> quotaRequestIds) {
        if (quotaRequestIds.isEmpty()) {
            return List.of();
        }
        return jdbcTemplate.query(GET_BY_REQUEST_IDS, Map.of("requestIds", quotaRequestIds),
                this::toQuotaRequestDelivery);
    }

    @Override
    public List<QuotaRequestDelivery> getUnresolved(long limit, @Nullable UUID fromId) {
        if (limit <= 0) {
            return List.of();
        }
        if (fromId == null) {
            return jdbcTemplate.query(GET_UNRESOLVED_FIRST_PAGE, Map.of("limit", limit),
                    this::toQuotaRequestDelivery);
        } else {
            return jdbcTemplate.query(GET_UNRESOLVED_NEXT_PAGE, Map.of("limit", limit, "fromUniqueId", fromId),
                    this::toQuotaRequestDelivery);
        }
    }

    @Override
    public void create(QuotaRequestDelivery delivery) {
        Map<String, Object> paramMap = toParams(delivery);
        jdbcTemplate.update(CREATE_DELIVERY_QUERY, paramMap);
    }

    @Override
    public void update(QuotaRequestDelivery delivery) {
        Map<String, Object> paramMap = toParams(delivery);
        jdbcTemplate.update(UPDATE_DELIVERY_QUERY, paramMap);
    }

    @Override
    public void remove(QuotaRequestDelivery delivery) {
        Map<String, Object> paramMap = toParams(delivery);
        jdbcTemplate.update(REMOVE_DELIVERY_QUERY, paramMap);
    }

    @Override
    public void clear() {
        jdbcTemplate.update(CLEAR_QUERY);
    }

    private QuotaRequestDelivery toQuotaRequestDelivery(final ResultSet rs, final int ignored) throws SQLException {
        final Timestamp resolved_at = rs.getTimestamp("resolved_at");
        final PGobject externalResources = (PGobject) rs.getObject("external_resources");
        final PGobject internalResources = (PGobject) rs.getObject("internal_resources");
        final PGobject deliveryResults = (PGobject) rs.getObject("delivery_results");
        return QuotaRequestDelivery.builder()
                .providerId(rs.getLong("service_id"))
                .quotaRequestId(rs.getLong("request_id"))
                .id(UUID.fromString(rs.getString("unique_id")))
                .authorId(rs.getLong("author_id"))
                .authorUid(rs.getLong("author_uid"))
                .abcServiceId(rs.getLong("abc_service_id"))
                .campaignId(rs.getLong("campaign_id"))
                .createdAt(rs.getTimestamp("created_at").toInstant())
                .resolvedAt(resolved_at != null ? resolved_at.toInstant() : null)
                .resolved(rs.getBoolean("resolved"))
                .addExternalResources(SqlUtils.fromJsonb(externalResources, EXTERNAL_RESOURCE_TYPE))
                .addInternalResources(SqlUtils.fromJsonb(internalResources, INTERNAL_RESOURCE_TYPE))
                .addDeliveryResults(SqlUtils.fromJsonb(deliveryResults, DELIVERY_RESULT_TYPE))
                .resolveStatus(QuotaRequestDeliveryResolveStatus.valueOf(rs.getString("resolve_status")))
                .build();
    }

    private Map<String, Object> toParams(final QuotaRequestDelivery quotaRequestDelivery) {
        Map<String, Object> params = new HashMap<>();

        params.put("serviceId", quotaRequestDelivery.getProviderId());
        params.put("requestId", quotaRequestDelivery.getQuotaRequestId());
        params.put("uniqueId", quotaRequestDelivery.getId());
        params.put("authorId", quotaRequestDelivery.getAuthorId());
        params.put("authorUid", quotaRequestDelivery.getAuthorUid());
        params.put("abcServiceId", quotaRequestDelivery.getAbcServiceId());
        params.put("campaignId", quotaRequestDelivery.getCampaignId());
        params.put("createdAt", Timestamp.from(quotaRequestDelivery.getCreatedAt()));
        params.put("resolved", quotaRequestDelivery.isResolved());
        params.put("resolvedAt", quotaRequestDelivery.getResolvedAt().map(Timestamp::from).orElse(null));
        params.put("externalResources", SqlUtils.toJsonb(quotaRequestDelivery.getExternalResources()));
        params.put("internalResources", SqlUtils.toJsonb(quotaRequestDelivery.getInternalResources()));
        params.put("deliveryResults", SqlUtils.toJsonb(quotaRequestDelivery.getDeliveryResults()));
        params.put("resolveStatus", quotaRequestDelivery.getResolveStatus().name());

        return params;
    }
}
