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

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

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;

import ru.yandex.qe.dispenser.domain.Campaign;
import ru.yandex.qe.dispenser.domain.Resource;
import ru.yandex.qe.dispenser.domain.Segment;
import ru.yandex.qe.dispenser.domain.dao.SqlDaoBase;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;
import ru.yandex.qe.dispenser.domain.hierarchy.HierarchySupplier;
import ru.yandex.qe.dispenser.domain.index.LongIndexBase;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;

public class SqlBotServiceReserveDao extends SqlDaoBase implements BotServiceReserveDao {

    private static final String SELECT_FROM_STATEMENT = "bot_service_reserve r LEFT JOIN bot_service_reserve_segment s ON r.id = s.bot_service_reserve_id";
    public static final String GET_ALL_RESERVES_QUERY = "SELECT r.*, s.segment_id FROM " + SELECT_FROM_STATEMENT;
    public static final String READ_QUERY = GET_ALL_RESERVES_QUERY + " WHERE r.id = :id";
    public static final String GET_RESERVES_BY_ORDER_IDS_QUERY = GET_ALL_RESERVES_QUERY + " WHERE %s";
    public static final String INSERT_RESERVE_QUERY = "INSERT INTO bot_service_reserve(resource_id, big_order_id, campaign_id, amount) VALUES (:resource_id, :big_order_id, :campaign_id, :amount)";
    public static final String INSERT_RESERVE_SEGMENT_QUERY = "INSERT INTO bot_service_reserve_segment(bot_service_reserve_id, segment_id) VALUES (:reserve_id, :segment_id)";
    public static final String UPDATE_RESERVE_QUERY = "UPDATE bot_service_reserve SET big_order_id = :big_order_id, amount = :amount WHERE id = :id";
    public static final String READ_BY_KEY_WITHOUT_SEGMENT_QUERY = GET_ALL_RESERVES_QUERY + " WHERE r.resource_id = :resource_id AND r.big_order_id = :big_order_id AND r.campaign_id = :campaign_id";
    public static final String DELETE_QUERY = "DELETE FROM bot_service_reserve WHERE id = :id";
    public static final String CLEAR_QUERY = "TRUNCATE bot_service_reserve CASCADE";
    private static final String EXISTS_BY_CAMPAIGN_ID = "SELECT EXISTS(SELECT 1 FROM bot_service_reserve WHERE campaign_id = :campaign_id) AS exists";
    private static final String EXISTS_BY_CAMPAIGN_ID_AND_ORDER_ID_NOT_IN = "SELECT EXISTS(SELECT 1 FROM bot_service_reserve WHERE campaign_id = :campaign_id AND big_order_id NOT IN (:order_ids)) as exists";

    private static final String UPDATE_SEGMENTS = "WITH removed AS (DELETE FROM bot_service_reserve_segment WHERE bot_service_reserve_id = :reserve_id AND segment_id NOT IN (:segment_id)) INSERT INTO bot_service_reserve_segment (bot_service_reserve_id, segment_id) SELECT :reserve_id, s.id FROM segment s WHERE s.id IN (:segment_id) ON CONFLICT DO NOTHING";

    @Autowired
    private HierarchySupplier hierarchySupplier;

    @Override
    public @NotNull Set<BotServiceReserve> getAll() {
        final ReserveCollector reserveCollector = new ReserveCollector(hierarchySupplier.get());
        jdbcTemplate.query(GET_ALL_RESERVES_QUERY, reserveCollector::processRow);
        return reserveCollector.get();
    }

    @NotNull
    private Set<BotServiceReserve> queryForSet(final String query, final Map<String, ?> params) {
        final ReserveCollector reserveCollector = new ReserveCollector(hierarchySupplier.get());
        jdbcTemplate.query(query, params, reserveCollector::processRow);
        return reserveCollector.get();
    }

    @Override
    public Set<BotServiceReserve> getReservesByBigOrders(final Set<Campaign.CampaignIdOrderId> campaignOrderIds) {
        if (campaignOrderIds.isEmpty()) {
            return Collections.emptySet();
        }
        final List<Campaign.CampaignIdOrderId> campaignOrders = new ArrayList<>(campaignOrderIds);
        final StringBuilder conditionBuilder = new StringBuilder();
        final Map<String, Object> params = new HashMap<>();
        conditionBuilder.append("(");
        for (int i = 0; i < campaignOrders.size(); i++) {
            params.put("big_order_id_" + i, campaignOrders.get(i).getBigOrderId());
            params.put("campaign_id_" + i, campaignOrders.get(i).getCampaignId());
            if (i > 0) {
                conditionBuilder.append(" OR ");
            }
            conditionBuilder.append("(r.big_order_id = :big_order_id_");
            conditionBuilder.append(i);
            conditionBuilder.append(" AND r.campaign_id = :campaign_id_");
            conditionBuilder.append(i);
            conditionBuilder.append(")");
        }
        conditionBuilder.append(")");
        final String query = String.format(GET_RESERVES_BY_ORDER_IDS_QUERY, conditionBuilder.toString());
        return queryForSet(query, params);
    }

    @Override
    public Stream<BotServiceReserve> getReserveStreamFiltered(final BotServiceReserveFilter filter) {
        final Map<String, Object> params = new HashMap<>();
        final String query = query(filter, params);
        return queryForSet(query, params).stream();
    }

    @Override
    public boolean hasReservesInCampaign(final long campaignId) {
        return jdbcTemplate.queryForObject(EXISTS_BY_CAMPAIGN_ID, new MapSqlParameterSource("campaign_id", campaignId), (rs, rn) -> {
            return rs.getBoolean("exists");
        });
    }

    @Override
    public boolean hasReservesInCampaignForOrdersOtherThan(final long campaignId, final Set<Long> orderIds) {
        final MapSqlParameterSource params = new MapSqlParameterSource(ImmutableMap.of("campaign_id", campaignId, "order_ids", orderIds));
        return jdbcTemplate.queryForObject(EXISTS_BY_CAMPAIGN_ID_AND_ORDER_ID_NOT_IN, params, (rs, rn) -> {
            return rs.getBoolean("exists");
        });
    }

    @Override
    public void updateSegments(final long reserveId, final Set<Segment> segments) {
        final List<Long> segmentIds = segments.stream().map(LongIndexBase::getId).collect(Collectors.toList());
        jdbcTemplate.update(UPDATE_SEGMENTS, ImmutableMap.of("reserve_id", reserveId, "segment_id", segmentIds));
    }

    private String query(final BotServiceReserveFilter filter, final Map<String, Object> params) {
        final StringBuilder builder = new StringBuilder("SELECT * FROM ");
        builder.append(from(filter));
        builder.append(where(filter, params));
        return builder.toString();
    }

    private String from(final BotServiceReserveFilter filter) {
        if (!filter.getServices().isEmpty()) {
            return SELECT_FROM_STATEMENT + " JOIN resource AS res ON r.resource_id = res.id JOIN service AS srv ON res.service_id = srv.id";
        } else {
            return SELECT_FROM_STATEMENT;
        }
    }

    private String where(final BotServiceReserveFilter filter, final Map<String, Object> params) {
        final List<String> conditions = new ArrayList<>(3);
        if (!filter.getServices().isEmpty()) {
            conditions.add("service_id IN (:service_ids)");
            params.put("service_ids", CollectionUtils.ids(filter.getServices()));
        }
        if (!filter.getBigOrderIds().isEmpty()) {
            conditions.add("big_order_id IN (:big_order_ids)");
            params.put("big_order_ids", filter.getBigOrderIds());
        }
        if (!filter.getCampaignIds().isEmpty()) {
            conditions.add("campaign_id IN (:campaign_ids)");
            params.put("campaign_ids", filter.getCampaignIds());
        }
        if (!filter.getCampaignBigOrders().isEmpty()) {
            final List<Campaign.CampaignOrder> campaignOrders = new ArrayList<>(filter.getCampaignBigOrders());
            final StringBuilder conditionBuilder = new StringBuilder();
            conditionBuilder.append("(");
            for (int i = 0; i < campaignOrders.size(); i++) {
                params.put("big_order_id_" + i, campaignOrders.get(i).getBigOrderId());
                params.put("campaign_id_" + i, campaignOrders.get(i).getCampaignId());
                if (i > 0) {
                    conditionBuilder.append(" OR ");
                }
                conditionBuilder.append("(r.big_order_id = :big_order_id_");
                conditionBuilder.append(i);
                conditionBuilder.append(" AND r.campaign_id = :campaign_id_");
                conditionBuilder.append(i);
                conditionBuilder.append(")");
            }
            conditionBuilder.append(")");
            conditions.add(conditionBuilder.toString());
        }
        if (conditions.isEmpty()) {
            return "";
        } else {
            return " WHERE " + StringUtils.join(conditions, " AND ");
        }
    }

    @NotNull
    @Override
    public BotServiceReserve create(@NotNull final BotServiceReserve reserve) {
        final Long id = jdbcTemplate.insert(INSERT_RESERVE_QUERY, toParams(reserve));

        if (!reserve.getSegments().isEmpty()) {
            final List<Map<String, ?>> params = reserve.getSegments().stream()
                    .map(segment -> ImmutableMap.of("reserve_id", id, "segment_id", segment.getId()))
                    .collect(Collectors.toList());

            jdbcTemplate.batchUpdate(INSERT_RESERVE_SEGMENT_QUERY, params);
        }

        return read(id);
    }

    @NotNull
    @Override
    public BotServiceReserve read(@NotNull final Long id) throws EmptyResultDataAccessException {
        return queryForSet(READ_QUERY, Collections.singletonMap("id", id))
                .stream().findFirst()
                .orElseThrow(() -> new EmptyResultDataAccessException("No reserve with id " + id, 1));
    }

    @NotNull
    @Override
    public BotServiceReserve read(final BotServiceReserve.@NotNull Key key) throws EmptyResultDataAccessException {
        String query = READ_BY_KEY_WITHOUT_SEGMENT_QUERY;
        if (key.getSegments().isEmpty()) {
            query += " AND s.segment_id IS NULL";
        } else {
            query += " AND s.segment_id IN (:segment_id)";
        }
        return queryForSet(query, toParams(key))
                .stream().findFirst()
                .orElseThrow(() -> new EmptyResultDataAccessException("No reserve with key " + key, 1));
    }

    @Override
    public boolean update(@NotNull final BotServiceReserve transientObject) {
        return jdbcTemplate.update(UPDATE_RESERVE_QUERY, toParams(transientObject)) > 0;
    }

    @Override
    public boolean delete(@NotNull final BotServiceReserve persistentObject) {
        return jdbcTemplate.update(DELETE_QUERY, toParams(persistentObject)) > 0;
    }

    @Override
    public boolean clear() {
        return jdbcTemplate.update(CLEAR_QUERY) > 0;
    }

    public static Map<String, Object> toParams(final BotServiceReserve.Key reserveKey) {
        return ImmutableMap.<String, Object>builder()
                .put("resource_id", reserveKey.getResource().getId())
                .put("big_order_id", reserveKey.getBigOrderId())
                .put("campaign_id", reserveKey.getCampaignId())
                .put("segment_id", CollectionUtils.ids(reserveKey.getSegments()))
                .build();
    }

    public static Map<String, Object> toParams(final BotServiceReserve reserve) {
        return ImmutableMap.<String, Object>builder()
                .put("id", reserve.getId())
                .put("resource_id", reserve.getResource().getId())
                .put("big_order_id", reserve.getBigOrderId())
                .put("campaign_id", reserve.getCampaignId())
                .put("amount", reserve.getAmount())
                .build();
    }

    private static class ReserveCollector {
        private final Map<Long, Set<Segment>> segmentByReserveId = new HashMap<>();
        private final Set<BotServiceReserve> result = new HashSet<>();
        private final Hierarchy hierarchy;

        private ReserveCollector(final Hierarchy hierarchy) {
            this.hierarchy = hierarchy;
        }

        private void processRow(final ResultSet rs) throws SQLException {
            final long reserveId = rs.getLong("id");
            final Long segmentId = getLong(rs, "segment_id");
            final Segment segment = segmentId == null ? null : hierarchy.getSegmentReader().read(segmentId);

            if (segmentByReserveId.containsKey(reserveId)) {
                segmentByReserveId.get(reserveId).add(segment);
                return;
            }

            final Set<Segment> segments;
            if (segment == null) {
                segments = Collections.emptySet();
            } else {
                segments = Sets.newHashSet(segment);
                segmentByReserveId.put(reserveId, segments);
            }

            final Resource resource = hierarchy.getResourceReader().read(rs.getLong("resource_id"));
            final BotServiceReserve.Key key = new BotServiceReserve.Key(resource, segments,
                    rs.getLong("big_order_id"), rs.getLong("campaign_id"));
            final BotServiceReserve reserve = new BotServiceReserve(key, rs.getLong("amount"));

            reserve.setId(rs.getLong("id"));

            result.add(reserve);
        }

        public Set<BotServiceReserve> get() {
            return result;
        }
    }
}
