package ru.yandex.direct.web.entity.adgroup.service;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import one.util.streamex.StreamEx;
import org.springframework.stereotype.Service;

import ru.yandex.direct.core.entity.keyword.model.Keyword;
import ru.yandex.direct.core.entity.keyword.repository.KeywordRepository;
import ru.yandex.direct.core.entity.relevancematch.model.RelevanceMatch;
import ru.yandex.direct.core.entity.relevancematch.repository.RelevanceMatchRepository;
import ru.yandex.direct.core.entity.retargeting.model.RetargetingSimple;
import ru.yandex.direct.core.entity.retargeting.model.TargetInterest;
import ru.yandex.direct.core.entity.retargeting.repository.RetargetingRepository;
import ru.yandex.direct.dbutil.model.ClientId;
import ru.yandex.direct.model.Model;
import ru.yandex.direct.model.ModelProperty;
import ru.yandex.direct.multitype.entity.LimitOffset;

import static org.apache.commons.collections4.CollectionUtils.isEmpty;
import static ru.yandex.direct.utils.CommonUtils.ifNotNull;
import static ru.yandex.direct.utils.FunctionalUtils.filterList;
import static ru.yandex.direct.utils.FunctionalUtils.listToMap;
import static ru.yandex.direct.utils.FunctionalUtils.listToSet;
import static ru.yandex.direct.utils.FunctionalUtils.mapList;

/**
 * Заполняет данные группы при копировании из исходной группы
 */
@Service
public class CopyAdGroupDataFiller {
    private final KeywordRepository keywordRepository;
    private final RetargetingRepository retargetingRepository;
    private final RelevanceMatchRepository relevanceMatchRepository;

    public CopyAdGroupDataFiller(KeywordRepository keywordRepository,
                                 RetargetingRepository retargetingRepository,
                                 RelevanceMatchRepository relevanceMatchRepository) {
        this.keywordRepository = keywordRepository;
        this.retargetingRepository = retargetingRepository;
        this.relevanceMatchRepository = relevanceMatchRepository;
    }

    public void fillKeywordPrices(int shard, ClientId clientId, List<List<Keyword>> adGroupsKeywords,
                                  List<Double> generalPrices) {
        Function<Integer, List<ShowConditionAdapter>> requestObjectsGetter =
                index -> {
                    List<Keyword> adGroupKeywords = adGroupsKeywords.get(index);
                    if (adGroupKeywords == null) {
                        return null;
                    }
                    return StreamEx.of(adGroupKeywords)
                            .nonNull()
                            .map(CopyAdGroupDataFiller::keywordAdapter)
                            .toList();
                };

        Function<Collection<Long>, Collection<ShowConditionAdapter>> sourcesSupplier =
                ids -> {
                    List<Keyword> keywords = keywordRepository.getKeywordsByIds(shard, clientId, ids);
                    return mapList(keywords, CopyAdGroupDataFiller::keywordAdapter);
                };

        fillShowConditionPrices(adGroupsKeywords.size(), generalPrices, requestObjectsGetter, sourcesSupplier);
    }

    public void fillRetargetingPrices(int shard, List<List<TargetInterest>> adGroupsTargetInterests,
                                      List<Double> generalPrices) {
        Function<Integer, List<ShowConditionAdapter>> requestObjectsGetter =
                index -> {
                    List<TargetInterest> targetInterests = adGroupsTargetInterests.get(index);
                    if (targetInterests == null) {
                        return null;
                    }
                    return StreamEx.of(targetInterests)
                            .nonNull()
                            .map(CopyAdGroupDataFiller::targetInterestAdapter)
                            .toList();
                };

        Function<Collection<Long>, Collection<ShowConditionAdapter>> sourcesSupplier =
                ids -> {
                    List<RetargetingSimple> retargetings = retargetingRepository
                            .getRetargetingsSimple(shard, ids, LimitOffset.maxLimited());
                    return mapList(retargetings, CopyAdGroupDataFiller::retargetingAdapter);
                };

        fillShowConditionPrices(adGroupsTargetInterests.size(), generalPrices, requestObjectsGetter, sourcesSupplier);
    }

    public void fillRelevanceMatchPrices(int shard, ClientId clientId,
                                         List<List<RelevanceMatch>> adGroupsRelevanceMatches, List<Double> generalPrices) {
        Function<Integer, List<ShowConditionAdapter>> requestObjectsGetter =
                index -> {
                    List<RelevanceMatch> adGroupRelevanceMatches = adGroupsRelevanceMatches.get(index);
                    if (adGroupRelevanceMatches == null) {
                        return null;
                    }
                    return StreamEx.of(adGroupRelevanceMatches)
                            .nonNull()
                            .map(CopyAdGroupDataFiller::relevanceMatchAdapter)
                            .toList();
                };

        Function<Collection<Long>, Collection<ShowConditionAdapter>> sourcesSupplier =
                ids -> {
                    Collection<RelevanceMatch> relevanceMatches =
                            relevanceMatchRepository.getRelevanceMatchesByIds(shard, clientId, ids).values();
                    return mapList(relevanceMatches, CopyAdGroupDataFiller::relevanceMatchAdapter);
                };

        fillShowConditionPrices(adGroupsRelevanceMatches.size(), generalPrices, requestObjectsGetter, sourcesSupplier);
    }

    private void fillShowConditionPrices(
            Integer complexAdGroupsCount, List<Double> generalPrices,
            Function<Integer, List<ShowConditionAdapter>> requestObjectsGetter,
            Function<Collection<Long>, Collection<ShowConditionAdapter>> sourcesSupplier) {
        List<ShowConditionAdapter> showConditionsToCopyPriorities = new ArrayList<>();
        List<ShowConditionAdapter> showConditionsToCopyPrices = new ArrayList<>();

        // Выставляем единую ставку в группах, где она задана.
        // Вместе с этим запоминаем условия показа, для которых нужно скопировать
        // ставки и приоритет из исходных
        for (int adGroupIndex = 0; adGroupIndex < complexAdGroupsCount; adGroupIndex++) {
            List<ShowConditionAdapter> requestShowConditions = requestObjectsGetter.apply(adGroupIndex);
            if (isEmpty(requestShowConditions)) {
                continue;
            }

            List<ShowConditionAdapter> copiedShowConditionsOfAdGroup =
                    filterList(requestShowConditions, sc -> !sc.isNew());
            showConditionsToCopyPriorities.addAll(copiedShowConditionsOfAdGroup);

            BigDecimal generalPrice = ifNotNull(generalPrices.get(adGroupIndex), BigDecimal::valueOf);

            if (generalPrice != null) {
                requestShowConditions.forEach(sc -> sc.withPrice(generalPrice).withPriceContext(generalPrice));
            } else {
                showConditionsToCopyPrices.addAll(copiedShowConditionsOfAdGroup);
            }
        }

        if (showConditionsToCopyPriorities.isEmpty()) {
            return;
        }

        // достаем из базы исходные условия показа, из которых нужно скопировать приоритет и возможно ставки
        Set<Long> ids = listToSet(showConditionsToCopyPriorities, ShowConditionAdapter::getId);
        Collection<ShowConditionAdapter> sourceShowConditions = sourcesSupplier.apply(ids);
        Map<Long, ShowConditionAdapter> sourceShowConditionsMap =
                listToMap(sourceShowConditions, ShowConditionAdapter::getId);

        // приоритет копируется для всех условий показа
        showConditionsToCopyPriorities.forEach(showConditionToCopyPriority -> {
            ShowConditionAdapter sourceShowCondition = sourceShowConditionsMap.get(showConditionToCopyPriority.getId());
            if (sourceShowCondition != null) {
                showConditionToCopyPriority.withAutobudgetPriority(sourceShowCondition.getAutobudgetPriority());
            }
        });

        // ставки копируются для тех копируемых условий показа,
        // для которых на уровне группы не задана единая ставка
        showConditionsToCopyPrices.forEach(showConditionToCopyPrice -> {
            ShowConditionAdapter sourceShowCondition = sourceShowConditionsMap.get(showConditionToCopyPrice.getId());
            if (sourceShowCondition != null) {
                showConditionToCopyPrice.withPrice(sourceShowCondition.getPrice())
                        .withPriceContext(sourceShowCondition.getPriceContext());
            }
        });

        // если в группе у всех условий показа одинаковая ставка или приоритет,
        // то выставляем это значение для всех новых условий показа
        for (int adGroupIndex = 0; adGroupIndex < complexAdGroupsCount; adGroupIndex++) {

            if (isEmpty(requestObjectsGetter.apply(adGroupIndex))) {
                continue;
            }

            Set<BigDecimal> prices = new HashSet<>();
            Set<BigDecimal> contextPrices = new HashSet<>();
            Set<Integer> priorities = new HashSet<>();
            List<ShowConditionAdapter> newShowConditions = new ArrayList<>();

            requestObjectsGetter.apply(adGroupIndex).forEach(showCondition -> {
                if (!showCondition.isNew()) {
                    prices.add(showCondition.getPrice());
                    contextPrices.add(showCondition.getPriceContext());
                    priorities.add(showCondition.getAutobudgetPriority());
                } else {
                    newShowConditions.add(showCondition);
                }
            });

            if (prices.size() == 1) {
                BigDecimal price = prices.iterator().next();
                newShowConditions.forEach(newShowCondition -> newShowCondition.withPrice(price));
            }

            if (contextPrices.size() == 1) {
                BigDecimal contextPrice = contextPrices.iterator().next();
                newShowConditions.forEach(newShowCondition -> newShowCondition.withPriceContext(contextPrice));
            }

            if (priorities.size() == 1) {
                Integer priority = priorities.iterator().next();
                newShowConditions.forEach(newShowCondition -> newShowCondition.withAutobudgetPriority(priority));
            }
        }
    }

    private static ShowConditionAdapter keywordAdapter(Keyword keyword) {
        return new ShowConditionAdapter<>(keyword, Keyword.ID, Keyword.PRICE,
                Keyword.PRICE_CONTEXT, Keyword.AUTOBUDGET_PRIORITY);
    }

    private static ShowConditionAdapter targetInterestAdapter(TargetInterest targetInterest) {
        return new ShowConditionAdapter<>(targetInterest, TargetInterest.ID, null,
                TargetInterest.PRICE_CONTEXT, TargetInterest.AUTOBUDGET_PRIORITY);
    }

    private static ShowConditionAdapter retargetingAdapter(RetargetingSimple retargetingSimple) {
        return new ShowConditionAdapter<>(retargetingSimple, RetargetingSimple.ID, null,
                RetargetingSimple.PRICE_CONTEXT, RetargetingSimple.AUTOBUDGET_PRIORITY);
    }

    private static ShowConditionAdapter relevanceMatchAdapter(RelevanceMatch relevanceMatch) {
        return new ShowConditionAdapter<>(relevanceMatch, RelevanceMatch.ID, RelevanceMatch.PRICE,
                RelevanceMatch.PRICE_CONTEXT, RelevanceMatch.AUTOBUDGET_PRIORITY);
    }

    // адаптер для разных условий показа
    // нужен, чтобы работать с ними единообразно
    // учитывает, что цены на поиске может не быть по определению, как например для ретаргетинга
    private static class ShowConditionAdapter<M extends Model> {
        private M model;
        private ModelProperty<? super M, Long> idProp;
        private ModelProperty<? super M, BigDecimal> priceProp;
        private ModelProperty<? super M, BigDecimal> priceContextProp;
        private ModelProperty<? super M, Integer> autobudgetPriorityProp;

        public ShowConditionAdapter(M model, ModelProperty<? super M, Long> idProp,
                                    ModelProperty<? super M, BigDecimal> priceProp,
                                    ModelProperty<? super M, BigDecimal> priceContextProp,
                                    ModelProperty<? super M, Integer> autobudgetPriorityProp) {
            this.model = model;
            this.idProp = idProp;
            this.priceProp = priceProp;
            this.priceContextProp = priceContextProp;
            this.autobudgetPriorityProp = autobudgetPriorityProp;
        }

        public Long getId() {
            return idProp.get(model);
        }

        public boolean isNew() {
            return getId() == null;
        }

        public BigDecimal getPrice() {
            return priceProp != null ? priceProp.get(model) : null;
        }

        public ShowConditionAdapter withPrice(BigDecimal price) {
            if (priceProp != null) {
                priceProp.set(model, price);
            }
            return this;
        }

        public BigDecimal getPriceContext() {
            return priceContextProp.get(model);
        }

        public ShowConditionAdapter withPriceContext(BigDecimal priceContext) {
            priceContextProp.set(model, priceContext);
            return this;
        }

        public Integer getAutobudgetPriority() {
            return autobudgetPriorityProp.get(model);
        }

        public ShowConditionAdapter withAutobudgetPriority(Integer autobudgetPriority) {
            autobudgetPriorityProp.set(model, autobudgetPriority);
            return this;
        }
    }
}
