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

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.inject.Inject;

import com.google.common.collect.Maps;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Lazy;
import org.springframework.dao.EmptyResultDataAccessException;
import ru.yandex.qe.dispenser.domain.Campaign;
import ru.yandex.qe.dispenser.domain.CampaignForBot;
import ru.yandex.qe.dispenser.domain.CampaignUpdate;
import ru.yandex.qe.dispenser.domain.QuotaChangeRequest;
import ru.yandex.qe.dispenser.domain.bot.BigOrder;
import ru.yandex.qe.dispenser.domain.dao.InMemoryLongKeyDaoImpl;
import ru.yandex.qe.dispenser.domain.dao.bot.bigorder.BigOrderDao;
import ru.yandex.qe.dispenser.domain.dao.bot.settings.BotCampaignGroupDao;
import ru.yandex.qe.dispenser.domain.dao.quota.request.QuotaChangeRequestDao;
import ru.yandex.qe.dispenser.domain.index.LongIndexBase;

public class CampaignDaoImpl extends InMemoryLongKeyDaoImpl<Campaign> implements CampaignDao {

    private final QuotaChangeRequestDao quotaChangeRequestDao;
    private final BigOrderDao bigOrderDao;
    private final BotCampaignGroupDao botCampaignGroupDao;

    @Inject
    public CampaignDaoImpl(@Lazy @NotNull final QuotaChangeRequestDao quotaChangeRequestDao,
                           @NotNull final BigOrderDao bigOrderDao,
                           @Lazy final BotCampaignGroupDao botCampaignGroupDao) {
        this.quotaChangeRequestDao = quotaChangeRequestDao;
        this.bigOrderDao = bigOrderDao;
        this.botCampaignGroupDao = botCampaignGroupDao;
    }

    @NotNull
    @Override
    public Campaign read(@NotNull final Long id) {
        final Campaign result = super.read(id);
        result.getBigOrders().sort(Comparator.comparing(Campaign.BigOrder::getBigOrderId));
        return result;
    }

    @NotNull
    @Override
    public Campaign create(@NotNull final Campaign newInstance) {
        long nextId = getAll().stream().flatMapToLong(c -> c.getBigOrders().stream().filter(o -> o.getId() >= 0)
                .mapToLong(Campaign.BigOrder::getId)).max().orElse(-1L) + 1L;
        for (final Campaign.BigOrder o : newInstance.getBigOrders()) {
            o.setId(nextId++);
        }
        final Campaign result = super.create(newInstance);
        result.getBigOrders().sort(Comparator.comparing(Campaign.BigOrder::getBigOrderId));
        return result;
    }

    @Override
    public boolean update(final @NotNull Campaign obj) {
        long nextId = getAll().stream().flatMapToLong(c -> c.getBigOrders().stream().filter(o -> o.getId() >= 0)
                .mapToLong(Campaign.BigOrder::getId)).max().orElse(-1L) + 1L;
        for (final Campaign.BigOrder o : obj.getBigOrders()) {
            if (o.getId() < 0) {
                o.setId(nextId++);
            }
        }
        final List<QuotaChangeRequest> updates = new ArrayList<>();
        quotaChangeRequestDao.getAll().forEach(r -> {
            if (Long.valueOf(obj.getId()).equals(r.getCampaignId())) {

                final Set<Long> orderIds = obj.getBigOrders().stream()
                        .map(Campaign.BigOrder::getBigOrderId)
                        .collect(Collectors.toSet());

                updates.add(r.copyBuilder().campaign(QuotaChangeRequest.Campaign.from(obj)).campaignType(obj.getType())
                        .changes(r.getChanges().stream()
                                .map(c -> {
                                    final boolean inCampaign = c.getKey().getBigOrder() != null && orderIds.contains(c.getKey().getBigOrder().getId());
                                    final QuotaChangeRequest.BigOrder order = c.getKey().getBigOrder() == null ? null
                                            : new QuotaChangeRequest.BigOrder(c.getKey().getBigOrder().getId(),
                                            c.getKey().getBigOrder().getDate(), inCampaign);

                                    return c.copyBuilder()
                                            .order(order)
                                            .build();
                                })
                                .collect(Collectors.toList()))
                        .build());
            }
        });
        updates.forEach(quotaChangeRequestDao::update);
        return super.update(obj);
    }

    @NotNull
    @Override
    public List<Campaign> getAllSorted(@NotNull final Set<Campaign.Status> statuses) {
        final Comparator<Campaign> comparator = Comparator.comparing(Campaign::getStartDate).reversed()
                .thenComparing(Comparator.comparing(LongIndexBase::getId).reversed());
        final List<Campaign> sortedCampaigns;
        if (statuses.isEmpty()) {
            sortedCampaigns = getAll().stream().sorted(comparator).collect(Collectors.toList());
        } else {
            sortedCampaigns = getAll().stream().filter(c -> statuses.contains(c.getStatus())).sorted(comparator).collect(Collectors.toList());
        }
        sortedCampaigns.forEach(c -> c.getBigOrders().sort(Comparator.comparing(Campaign.BigOrder::getBigOrderId)));
        return sortedCampaigns;
    }

    @NotNull
    @Override
    public Campaign readForUpdate(@NotNull final Long id) throws EmptyResultDataAccessException {
        return read(id);
    }

    @Override
    public void partialUpdate(@NotNull final Campaign current, @NotNull final CampaignUpdate update) {
        final Campaign updatedCampaign = update.apply(current);
        if (update.getBigOrders() != null) {
            long nextId = getAll().stream().flatMapToLong(c -> c.getBigOrders().stream().filter(o -> o.getId() >= 0)
                    .mapToLong(Campaign.BigOrder::getId)).max().orElse(-1L) + 1L;
            final Map<Long, Long> idsByBigOrder = current.getBigOrders().stream().filter(o -> o.getId() >= 0L)
                    .collect(Collectors.toMap(Campaign.BigOrder::getBigOrderId, LongIndexBase::getId));
            for (final Campaign.BigOrder o : updatedCampaign.getBigOrders()) {
                if (idsByBigOrder.containsKey(o.getBigOrderId())) {
                    o.setId(idsByBigOrder.get(o.getBigOrderId()));
                } else {
                    o.setId(nextId++);
                }
            }
        }
        update(updatedCampaign);
        final List<QuotaChangeRequest> updates = new ArrayList<>();
        quotaChangeRequestDao.getAll().forEach(r -> {
            if (Long.valueOf(updatedCampaign.getId()).equals(r.getCampaignId())) {
                final Set<Long> orderIds = updatedCampaign.getBigOrders().stream()
                        .map(Campaign.BigOrder::getBigOrderId)
                        .collect(Collectors.toSet());

                updates.add(r.copyBuilder()
                        .campaign(QuotaChangeRequest.Campaign.from(updatedCampaign))
                        .campaignType(updatedCampaign.getType())
                        .changes(r.getChanges().stream()
                                .map(c -> {
                                    final boolean inCampaign = c.getKey().getBigOrder() != null && orderIds.contains(c.getKey().getBigOrder().getId());
                                    final QuotaChangeRequest.BigOrder order = c.getKey().getBigOrder() == null ? null
                                            : new QuotaChangeRequest.BigOrder(c.getKey().getBigOrder().getId(),
                                            c.getKey().getBigOrder().getDate(), inCampaign);

                                    return c.copyBuilder()
                                            .order(order)
                                            .build();
                                })
                                .collect(Collectors.toList()))
                        .build());
            }
        });
        updates.forEach(quotaChangeRequestDao::update);
    }

    @Override
    public List<Campaign.CampaignOrder> getCampaignOrders(final @NotNull Set<Long> ids) {
        if (ids.isEmpty()) {
            return List.of();
        }
        return this.getAll().stream().flatMap(c -> c.getBigOrders().stream().filter(o -> ids.contains(o.getId()))
                .map(o -> new Campaign.CampaignOrder(o.getId(), c.getId(), o.getBigOrderId()))).distinct()
                .sorted(Comparator.comparing(Campaign.CampaignOrder::getId)).collect(Collectors.toList());
    }

    @Override
    public List<Campaign.CampaignOrder> getCampaignOrdersByOrderIds(final @NotNull Set<Long> bigOrderIds) {
        if (bigOrderIds.isEmpty()) {
            return List.of();
        }
        return this.getAll().stream().flatMap(c -> c.getBigOrders().stream().filter(o -> bigOrderIds.contains(o.getBigOrderId()))
                        .map(o -> new Campaign.CampaignOrder(o.getId(), c.getId(), o.getBigOrderId()))).distinct()
                .sorted(Comparator.comparing(Campaign.CampaignOrder::getId)).collect(Collectors.toList());
    }

    @NotNull
    @Override
    public Optional<CampaignForBot> getActiveForBotIntegration() {
        return this.filter(c -> c.getStatus() == Campaign.Status.ACTIVE).map(this::convertForBot).findFirst();
    }

    @Override
    public Map<Long, CampaignForBot> readForBot(final @NotNull Set<Long> ids) {
        return Maps.transformValues(read(ids), this::convertForBot);
    }

    private CampaignForBot convertForBot(final Campaign campaign) {
        final List<BigOrder> bigOrders = campaign.getBigOrders().stream()
                .map(o -> {
                    BigOrder bigOrder = bigOrderDao.getById(o.getBigOrderId());
                    if (bigOrder == null) {
                        throw new EmptyResultDataAccessException("No big order with id " + o.getBigOrderId(), 1);
                    }
                    return bigOrder;
                }).collect(Collectors.toList());
        return new CampaignForBot(campaign.getId(), campaign.getKey(), bigOrders, campaign.getName(), campaign.getStatus(), campaign.getStartDate(),
                campaign.getType());
    }

    @NotNull
    @Override
    public Campaign readWithDeleted(final @NotNull Long id) throws EmptyResultDataAccessException {
        return read(id);
    }

    @NotNull
    @Override
    public Optional<Campaign> readOptional(final long id) {
        final Optional<Campaign> result = filter(c -> c.getId() == id).findFirst();
        result.ifPresent(c -> c.getBigOrders().sort(Comparator.comparing(Campaign.BigOrder::getBigOrderId)));
        return result;
    }

    @Override
    public Optional<Campaign> getByKey(String key) {
        final Optional<Campaign> result = filter(c -> Objects.equals(c.getKey(), key)).findFirst();
        result.ifPresent(c -> c.getBigOrders().sort(Comparator.comparing(Campaign.BigOrder::getBigOrderId)));
        return result;
    }

    @Override
    public boolean delete(final @NotNull Campaign obj) {
        final List<QuotaChangeRequest> updates = new ArrayList<>();
        quotaChangeRequestDao.getAll().forEach(r -> {
            if (Long.valueOf(obj.getId()).equals(r.getCampaignId())) {
                updates.add(r.copyBuilder().campaign(new QuotaChangeRequest.Campaign(obj.getId(), obj.getKey(), obj.getName(), obj.getStatus(),
                        obj.isRequestModificationDisabledForNonManagers(), true,
                        obj.isAllowedRequestModificationWhenClosed(),
                        obj.isAllowedModificationOnMissingAdditionalFields(),
                        obj.isForcedAllowingRequestStatusChange(),
                        obj.isSingleProviderRequestModeEnabled(),
                        obj.isAllowedRequestCreationForProviderAdmin(),
                        obj.isRequestCreationDisabled(),
                        obj.isAllowedRequestCreationForCapacityPlanner(),
                        obj.getType(),
                        obj.isRequestModificationDisabled()))
                        .campaignType(obj.getType())
                        .build());
            }
        });
        updates.forEach(quotaChangeRequestDao::update);
        return super.delete(obj);
    }

    @Override
    public Campaign insertForTest(Campaign campaign) {
        return create(campaign);
    }
}
