package ru.yandex.qe.dispenser.domain.dao.bot.settings;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.jetbrains.annotations.NotNull;
import org.postgresql.util.PGobject;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowCallbackHandler;
import ru.yandex.qe.dispenser.domain.BotCampaignGroup;
import ru.yandex.qe.dispenser.domain.BotCampaignGroupUpdate;
import ru.yandex.qe.dispenser.domain.Campaign;
import ru.yandex.qe.dispenser.domain.CampaignForBot;
import ru.yandex.qe.dispenser.domain.bot.BigOrder;
import ru.yandex.qe.dispenser.domain.bot.BigOrderConfig;
import ru.yandex.qe.dispenser.domain.dao.SqlDaoBase;
import ru.yandex.qe.dispenser.domain.dao.SqlUtils;
import ru.yandex.qe.dispenser.domain.dao.bot.bigorder.SqlBigOrderDao;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;

public class SqlBotCampaignGroupDao extends SqlDaoBase implements BotCampaignGroupDao {
    private static final String GET_ALL_GROUP_WITH_ORDERS = "SELECT bcg.*, bo.id as big_order_id, bo.date, bo.configs FROM bot_campaign_group bcg " +
            "LEFT JOIN bot_campaign_group_big_order gbo ON bcg.id = gbo.bot_campaign_group_id " +
            "LEFT JOIN bot_big_order bo ON gbo.bot_big_order_id = bo.id AND bo.deleted != TRUE";

    private static final String FOR_GROUP_UPDATE = " FOR UPDATE OF bcg";

    private static final String GROUP_ORDER_ORDER_BY = " ORDER BY bcg.id, bo.id";

    private static final String WHERE_GROUP_ACTIVE_STATEMENT = " WHERE bcg.active IS TRUE";

    private static final String GET_GROUP_WITH_ORDERS_BY_ID = GET_ALL_GROUP_WITH_ORDERS + " WHERE bcg.id = :id";

    private static final String GET_GROUP_BY_CAMPAIGN_ID = "SELECT bcg.*, bo.id as big_order_id, bo.date, bo.configs FROM bot_campaign_group_campaign bcgc " +
            "LEFT JOIN bot_campaign_group bcg ON bcg.id = bcgc.bot_campaign_group_id " +
            "LEFT JOIN bot_campaign_group_big_order gbo ON bcg.id = gbo.bot_campaign_group_id " +
            "LEFT JOIN bot_big_order bo ON gbo.bot_big_order_id = bo.id AND bo.deleted != TRUE " +
            "WHERE bcgc.campaign_id = :campaignId";

    private static final String INSERT_GROUP_QUERY = "INSERT INTO bot_campaign_group(key, name, bot_pre_order_issue_key, active) VALUES (:key, :name, :botPreOrderIssueKey, :active)";

    private static final String DELETE_GROUP = "DELETE FROM bot_campaign_group WHERE id = :id";
    private static final String ATTACH_GROUP_ORDERS = "INSERT INTO bot_campaign_group_big_order(bot_campaign_group_id, bot_big_order_id) VALUES (:botCampaignGroupId, :orderId)";
    private static final String DETACH_GROUP_ORDERS = "DELETE FROM bot_campaign_group_big_order WHERE bot_big_order_id IN (:orderIds) AND bot_campaign_group_id = :botCampaignGroupId";

    private static final String UPDATE_GROUP_BODY = "UPDATE bot_campaign_group SET key = :key, name = :name, bot_pre_order_issue_key = :botPreOrderIssueKey, active = :active WHERE id = :id";

    private static final String ATTACH_GROUP_CAMPAIGN = "INSERT INTO bot_campaign_group_campaign(bot_campaign_group_id, campaign_id) VALUES (:botCampaignGroupId, :campaignId)";
    private static final String DETACH_GROUP_CAMPAIGN = "DELETE FROM bot_campaign_group_campaign WHERE campaign_id IN (:campaignIds) AND bot_campaign_group_id = :botCampaignGroupId";

    private static final String GET_ALL_GROUPED_CAMPAIGNS = "SELECT bcgc.bot_campaign_group_id, c.id, c.name, c.key, "
            + "c.start_date, c.status, c.campaign_type, bo.id as big_order_id, bo.date, bo.configs FROM bot_campaign_group_campaign bcgc LEFT JOIN "
            + "campaign c ON bcgc.campaign_id = c.id LEFT JOIN campaign_big_order cb ON c.id = cb.campaign_id AND cb.deleted != TRUE LEFT JOIN "
            + "bot_big_order bo ON cb.big_order_id = bo.id WHERE c.deleted != TRUE";

    private static final String CAMPAIGNS_GROUP_FILTER = " AND bcgc.bot_campaign_group_id IN (:groupIds)";
    private static final String CAMPAIGNS_ORDER_BY = " ORDER BY c.start_date ASC, c.id ASC, cb.big_order_id ASC";
    private static final String FOR_CAMPAIGN_GROUP_UPDATE = " FOR UPDATE OF bcgc";

    private static final String GET_GROUPED_CAMPAIGNS_BY_GROUP_IDS = GET_ALL_GROUPED_CAMPAIGNS + CAMPAIGNS_GROUP_FILTER;
    private static final String CLEAR_QUERY = "TRUNCATE bot_campaign_group CASCADE";

    private static final String UPDATE_ACTIVE_STATUS = "UPDATE bot_campaign_group SET active = :active WHERE id = :id";
    private static final String HAS_GROUP_BY_CAMPAIGN = "SELECT EXISTS (SELECT 1 FROM bot_campaign_group_campaign WHERE campaign_id = :campaignId)";

    @Override
    @NotNull
    public Set<BotCampaignGroup> getAll() {
        return query(GET_ALL_GROUP_WITH_ORDERS + GROUP_ORDER_ORDER_BY,
                GET_ALL_GROUPED_CAMPAIGNS + CAMPAIGNS_ORDER_BY);
    }

    private Set<BotCampaignGroup> query(final String groupWithOrdersQuery, final String campaignsWithOrdersQuery) {
        return query(groupWithOrdersQuery, campaignsWithOrdersQuery, Collections.emptyMap());
    }

    private Set<BotCampaignGroup> query(final String groupWithOrdersQuery, final String campaignsWithOrdersQuery, final Map<String, ?> groupParams) {
        final GroupCollector groupCollector = new GroupCollector();
        jdbcTemplate.query(groupWithOrdersQuery, groupParams, groupCollector);
        final Map<Long, BotCampaignGroup.Builder> builderByGroupId = groupCollector.getBuilderByGroupId();

        if (builderByGroupId.isEmpty()) {
            return Collections.emptySet();
        }

        final CampaignsCollector campaignsCollector = new CampaignsCollector();
        jdbcTemplate.query(campaignsWithOrdersQuery, Collections.singletonMap("groupIds", builderByGroupId.keySet()), campaignsCollector);
        final Map<Long, List<CampaignForBot>> campaignsByGroupId = campaignsCollector.getCampaignsByGroupId();

        return builderByGroupId.entrySet().stream()
                .map(e -> {
                    final Long groupId = e.getKey();
                    final BotCampaignGroup.Builder builder = e.getValue();
                    final List<CampaignForBot> campaigns = campaignsByGroupId.getOrDefault(groupId, Collections.emptyList());
                    return builder
                            .setCampaigns(campaigns)
                            .build();
                })
                .collect(Collectors.toSet());
    }

    @NotNull
    @Override
    public BotCampaignGroup create(@NotNull final BotCampaignGroup body) {
        final long id;
        try {
            id = jdbcTemplate.insert(INSERT_GROUP_QUERY, toParams(body));
        } catch (DuplicateKeyException e) {
            throw duplicateGroupKeyException(e);
        }
        body.setId(id);
        attachBigOrders(id, CollectionUtils.ids(body.getBigOrders()));

        attachCampaigns(id, CollectionUtils.ids(body.getCampaigns()));

        return body;
    }

    @Override
    public void attachCampaigns(final long groupId, final Collection<Long> campaigns) {
        try {
            jdbcTemplate.batchUpdate(ATTACH_GROUP_CAMPAIGN, campaigns.stream()
                    .map(c -> ImmutableMap.of("botCampaignGroupId", groupId, "campaignId", c))
                    .collect(Collectors.toList())
            );
        } catch (DuplicateKeyException e) {
            throw new IllegalArgumentException("Campaigns cannot be used in several campaign groups", e);
        }
    }

    @Override
    public void detachCampaigns(final long groupId, final Collection<Long> campaigns) {
        jdbcTemplate.update(DETACH_GROUP_CAMPAIGN, ImmutableMap.of("campaignIds", campaigns, "botCampaignGroupId", groupId));
    }

    @Override
    public void attachBigOrders(final long groupId, final Collection<Long> bigOrders) {
        try {
            jdbcTemplate.batchUpdate(ATTACH_GROUP_ORDERS, toGroupOrdersParams(groupId, bigOrders));
        } catch (DuplicateKeyException e) {
            throw new IllegalArgumentException("Big orders cannot be used in several campaign groups", e);
        }
    }

    @Override
    public void detachBigOrders(final long groupId, final Collection<Long> bigOrders) {
        jdbcTemplate.update(DETACH_GROUP_ORDERS, ImmutableMap.of("orderIds", bigOrders, "botCampaignGroupId", groupId));
    }

    @NotNull
    @Override
    public BotCampaignGroup read(@NotNull final Long id) throws EmptyResultDataAccessException {
        final Set<BotCampaignGroup> res = query(GET_GROUP_WITH_ORDERS_BY_ID + GROUP_ORDER_ORDER_BY,
                GET_GROUPED_CAMPAIGNS_BY_GROUP_IDS + CAMPAIGNS_ORDER_BY, Collections.singletonMap("id", id));
        return res.stream()
                .findFirst()
                .orElseThrow(() -> new EmptyResultDataAccessException("No campaign group for id " + id, 1));

    }

    @Override
    public Set<BotCampaignGroup> getActiveGroups() {
        return query(GET_ALL_GROUP_WITH_ORDERS + WHERE_GROUP_ACTIVE_STATEMENT + GROUP_ORDER_ORDER_BY,
                GET_GROUPED_CAMPAIGNS_BY_GROUP_IDS + CAMPAIGNS_ORDER_BY);
    }

    @Override
    public Optional<BotCampaignGroup> readOptional(long id) {
        final Set<BotCampaignGroup> res = query(GET_GROUP_WITH_ORDERS_BY_ID + GROUP_ORDER_ORDER_BY,
                GET_GROUPED_CAMPAIGNS_BY_GROUP_IDS + CAMPAIGNS_ORDER_BY, Collections.singletonMap("id", id));
        return res.stream()
                .findFirst();
    }

    @Override
    public boolean update(@NotNull final BotCampaignGroup trainsientObject) {
        try {
            return jdbcTemplate.update(UPDATE_GROUP_BODY, toParams(trainsientObject)) > 0;
        } catch (DuplicateKeyException e) {
            throw duplicateGroupKeyException(e);
        }
    }

    private static IllegalArgumentException duplicateGroupKeyException(final Throwable cause) {
        return new IllegalArgumentException("Campaign group with this key or name already exists", cause);
    }

    @Override
    public boolean delete(@NotNull final BotCampaignGroup persistentObject) {
        return jdbcTemplate.update(DELETE_GROUP, Collections.singletonMap("id", persistentObject.getId())) > 0;
    }

    @Override
    public BotCampaignGroup partialUpdate(final BotCampaignGroup group, final BotCampaignGroupUpdate update) {
        if (update.hasSimpleFieldsUpdate()) {
            try {
                jdbcTemplate.update(UPDATE_GROUP_BODY, toBodyUpdateParams(group, update));
            } catch (DuplicateKeyException e) {
                throw duplicateGroupKeyException(e);
            }
        }
        if (update.hasBigOrdersUpdate()) {
            final Set<Long> currentOrders = CollectionUtils.ids(group.getBigOrders());
            final Set<Long> newOrders = Sets.difference(update.getBigOrders(), currentOrders);
            final Set<Long> deletedOrders = Sets.difference(currentOrders, update.getBigOrders());
            if (!newOrders.isEmpty()) {
                attachBigOrders(group.getId(), newOrders);
            }
            if (!deletedOrders.isEmpty()) {
                detachBigOrders(group.getId(), deletedOrders);
            }
        }
        if (update.hasCampaignsUpdate()) {
            final Set<Long> currentCampaigns = CollectionUtils.ids(group.getCampaigns());
            final Set<Long> newCampaigns = Sets.difference(update.getCampaigns(), currentCampaigns);
            final Set<Long> deletedCampaigns = Sets.difference(currentCampaigns, update.getCampaigns());
            if (!newCampaigns.isEmpty()) {
                attachCampaigns(group.getId(), newCampaigns);
            }
            if (!deletedCampaigns.isEmpty()) {
                detachCampaigns(group.getId(), deletedCampaigns);
            }
        }
        return read(group.getId());
    }

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

    @Override
    public BotCampaignGroup readForUpdate(final long id) throws EmptyResultDataAccessException {
        return query(GET_GROUP_WITH_ORDERS_BY_ID + GROUP_ORDER_ORDER_BY + FOR_GROUP_UPDATE,
                GET_GROUPED_CAMPAIGNS_BY_GROUP_IDS + CAMPAIGNS_ORDER_BY + FOR_CAMPAIGN_GROUP_UPDATE, Collections.singletonMap("id", id)).stream()
                .findFirst()
                .orElseThrow(() -> new EmptyResultDataAccessException("No campaign group for id " + id, 1));
    }

    @Override
    public Optional<BotCampaignGroup> getByCampaign(final long campaignId) {
        return query(GET_GROUP_BY_CAMPAIGN_ID + GROUP_ORDER_ORDER_BY,
                GET_GROUPED_CAMPAIGNS_BY_GROUP_IDS + CAMPAIGNS_ORDER_BY,
                Collections.singletonMap("campaignId", campaignId)).stream().findFirst();
    }

    @Override
    public boolean hasGroupForCampaign(final long campaignId) {
        return jdbcTemplate.queryForObject(HAS_GROUP_BY_CAMPAIGN, Collections.singletonMap("campaignId", campaignId), Boolean.class);
    }

    private static Map<String, Object> toBodyUpdateParams(final BotCampaignGroup body, final BotCampaignGroupUpdate update) {
        final Map<String, Object> params = new HashMap<>(toParams(body));
        if (update.getKey() != null) {
            params.put("key", update.getKey());
        }
        if (update.getName() != null) {
            params.put("name", update.getName());
        }
        if (update.getBotPreOrderIssueKey() != null) {
            params.put("botPreOrderIssueKey", update.getBotPreOrderIssueKey());
        }
        if (update.getActive() != null) {
            params.put("active", update.getActive());
        }
        return params;
    }

    private static Map<String, Object> toParams(final BotCampaignGroup group) {
        final Map<String, Object> params = new HashMap<>();
        params.put("id", group.getId());
        params.put("key", group.getKey());
        params.put("name", group.getName());
        params.put("active", group.isActive());
        params.put("botPreOrderIssueKey", group.getBotPreOrderIssueKey());
        return params;
    }

    private static List<Map<String, ?>> toGroupOrdersParams(final long groupId, final Collection<Long> orders) {
        return orders.stream()
                .map(o -> ImmutableMap.of("botCampaignGroupId", groupId, "orderId", o))
                .collect(Collectors.toList());
    }

    public static class GroupCollector implements RowCallbackHandler {
        private final Map<Long, BotCampaignGroup.Builder> groupBuilderById = new HashMap<>();

        @Override
        public void processRow(final ResultSet rs) throws SQLException {
            final long id = rs.getLong("id");
            if (!groupBuilderById.containsKey(id)) {
                final BotCampaignGroup.Builder builder = new BotCampaignGroup.Builder();
                final String key = rs.getString("key");
                final String name = rs.getString("name");
                final String botPreOrderIssueKey = rs.getString("bot_pre_order_issue_key");
                final boolean active = rs.getBoolean("active");
                builder.setId(id)
                        .setActive(active)
                        .setName(name)
                        .setKey(key)
                        .setBotPreOrderIssueKey(botPreOrderIssueKey);
                groupBuilderById.put(id, builder);
            }
            final BigOrder simpleBigOrder = simpleBigOrderForRow(rs);
            if (simpleBigOrder != null) {
                groupBuilderById.get(id).addBigOrder(simpleBigOrder);
            }
        }

        public Map<Long, BotCampaignGroup.Builder> getBuilderByGroupId() {
            return groupBuilderById;
        }
    }

    public static class CampaignsCollector implements RowCallbackHandler {
        private final Map<Long, CampaignForBot.Builder> builderByCampaignId = new HashMap<>();
        private final Map<Long, List<CampaignForBot.Builder>> campaignsByGroupId = new HashMap<>();

        @Override
        public void processRow(final ResultSet rs) throws SQLException {
            final long groupId = rs.getLong("bot_campaign_group_id");
            campaignsByGroupId.computeIfAbsent(groupId, x -> new ArrayList<>());

            final Long campaignId = getLong(rs, "id");
            if (campaignId == null) {
                return;
            }
            if (!builderByCampaignId.containsKey(campaignId)) {
                final CampaignForBot.Builder builder = new CampaignForBot.Builder();
                builder.setId(campaignId)
                        .setKey(rs.getString("key"))
                        .setName(rs.getString("name"))
                        .setStartDate(rs.getDate("start_date").toLocalDate())
                        .setStatus(Campaign.Status.valueOf(rs.getString("status")))
                        .setType(Campaign.Type.valueOf(rs.getString("campaign_type")));
                campaignsByGroupId.get(groupId).add(builder);
                builderByCampaignId.put(campaignId, builder);
            }
            final BigOrder simpleBigOrder = simpleBigOrderForRow(rs);
            if (simpleBigOrder != null) {
                builderByCampaignId.get(campaignId).addBigOrder(simpleBigOrder);
            }
        }

        public Map<Long, List<CampaignForBot>> getCampaignsByGroupId() {
            return Maps.transformValues(campaignsByGroupId, builders -> builders.stream()
                    .map(CampaignForBot.Builder::build)
                    .collect(Collectors.toList()));
        }
    }

    private static BigOrder simpleBigOrderForRow(final ResultSet rs) throws SQLException {
        final Long botBigOrderId = getLong(rs, "big_order_id");
        if (botBigOrderId == null) {
            return null;
        }
        final String date = rs.getString("date");
        final List<BigOrderConfig> configs = SqlUtils.fromJsonb((PGobject) rs.getObject("configs"), SqlBigOrderDao.CONFIG_TYPE);
        return BigOrder.builder(LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE))
                .configs(configs)
                .build(botBigOrderId);
    }
}
