package ru.yandex.direct.api.v5.entity.bidmodifiers.delegate;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;

import com.google.common.collect.Multimap;
import com.yandex.direct.api.v5.bidmodifiers.BidModifierFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.BidModifierGetItem;
import com.yandex.direct.api.v5.bidmodifiers.BidModifierLevelEnum;
import com.yandex.direct.api.v5.bidmodifiers.BidModifierTypeEnum;
import com.yandex.direct.api.v5.bidmodifiers.BidModifiersSelectionCriteria;
import com.yandex.direct.api.v5.bidmodifiers.DemographicsAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.DemographicsAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.DesktopAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.DesktopAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.DesktopOnlyAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.DesktopOnlyAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.GetRequest;
import com.yandex.direct.api.v5.bidmodifiers.GetResponse;
import com.yandex.direct.api.v5.bidmodifiers.IncomeGradeAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.IncomeGradeAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.MobileAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.MobileAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.ObjectFactory;
import com.yandex.direct.api.v5.bidmodifiers.OperatingSystemTypeEnum;
import com.yandex.direct.api.v5.bidmodifiers.RegionalAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.RegionalAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.RetargetingAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.RetargetingAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.SerpLayoutAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.SerpLayoutAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.SmartAdAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.SmartAdAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.SmartTvAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.SmartTvAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.TabletAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.TabletAdjustmentGet;
import com.yandex.direct.api.v5.bidmodifiers.VideoAdjustmentFieldEnum;
import com.yandex.direct.api.v5.bidmodifiers.VideoAdjustmentGet;
import com.yandex.direct.api.v5.general.AgeRangeEnum;
import com.yandex.direct.api.v5.general.GenderEnum;
import com.yandex.direct.api.v5.general.IncomeGradeEnum;
import com.yandex.direct.api.v5.general.SerpLayoutEnum;
import com.yandex.direct.api.v5.general.YesNoEnum;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import ru.yandex.direct.api.v5.common.EnumPropertyFilter;
import ru.yandex.direct.api.v5.entity.GenericGetRequest;
import ru.yandex.direct.api.v5.entity.GetApiServiceDelegate;
import ru.yandex.direct.api.v5.entity.bidmodifiers.validation.GetBidModifiersValidationService;
import ru.yandex.direct.api.v5.result.ApiResult;
import ru.yandex.direct.api.v5.security.ApiAuthenticationSource;
import ru.yandex.direct.api.v5.validation.DefectType;
import ru.yandex.direct.common.util.PropertyFilter;
import ru.yandex.direct.core.entity.adgroup.model.AdGroupType;
import ru.yandex.direct.core.entity.adgroup.service.AdGroupService;
import ru.yandex.direct.core.entity.bidmodifier.AgeType;
import ru.yandex.direct.core.entity.bidmodifier.BidModifier;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierDemographics;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierDesktop;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierDesktopOnly;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierGeo;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierMobile;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierMobileAdjustment;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierPerformanceTgo;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierPrismaIncomeGrade;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierPrismaIncomeGradeAdjustment;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierRetargeting;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierSmartTV;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierTablet;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierTabletAdjustment;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierTrafaretPosition;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierType;
import ru.yandex.direct.core.entity.bidmodifier.BidModifierVideo;
import ru.yandex.direct.core.entity.bidmodifier.GenderType;
import ru.yandex.direct.core.entity.bidmodifier.OsType;
import ru.yandex.direct.core.entity.bidmodifier.TabletOsType;
import ru.yandex.direct.core.entity.bidmodifier.TrafaretPosition;
import ru.yandex.direct.core.entity.bidmodifiers.repository.BidModifierLevel;
import ru.yandex.direct.core.entity.bidmodifiers.service.BidModifierService;
import ru.yandex.direct.dbutil.model.ClientId;
import ru.yandex.direct.multitype.entity.LimitOffset;
import ru.yandex.direct.validation.result.PathConverter;
import ru.yandex.direct.validation.result.ValidationResult;

import static com.google.common.base.Preconditions.checkState;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static ru.yandex.direct.api.v5.common.GeneralUtil.yesNoFromBool;
import static ru.yandex.direct.api.v5.entity.bidmodifiers.Constants.INCOME_GRADE_ENUM_BY_INCOME_GRADE_LITERAL_VALUE;
import static ru.yandex.direct.core.entity.bidmodifiers.service.BidModifierService.getExternalId;
import static ru.yandex.direct.core.entity.bidmodifiers.service.BidModifierService.getRealIdsGroupedByType;
import static ru.yandex.direct.utils.CommonUtils.ifNotNull;
import static ru.yandex.direct.utils.FunctionalUtils.flatMap;
import static ru.yandex.direct.utils.FunctionalUtils.mapList;

@ParametersAreNonnullByDefault
@Component
public class GetBidModifiersDelegate extends GetApiServiceDelegate<GetRequest, GetResponse, BidModifierAnyFieldEnum,
        BidModifiersSelectionCriteria, BidModifierGetItem> {

    private final BidModifierService bidModifierService;
    private final GetBidModifiersValidationService validationService;
    private final AdGroupService adGroupService;

    private final EnumPropertyFilter<BidModifierFieldEnum> propertyFilter;
    private final EnumPropertyFilter<MobileAdjustmentFieldEnum> mobilePropertyFilter;
    private final EnumPropertyFilter<TabletAdjustmentFieldEnum> tabletPropertyFilter;
    private final EnumPropertyFilter<RegionalAdjustmentFieldEnum> regionalPropertyFilter;
    private final EnumPropertyFilter<DemographicsAdjustmentFieldEnum> demographyPropertyFilter;
    private final EnumPropertyFilter<RetargetingAdjustmentFieldEnum> retargetingPropertyFilter;
    private final EnumPropertyFilter<VideoAdjustmentFieldEnum> videoPropertyFilter;
    private final EnumPropertyFilter<DesktopAdjustmentFieldEnum> desktopPropertyFilter;
    private final EnumPropertyFilter<DesktopOnlyAdjustmentFieldEnum> desktopOnlyPropertyFilter;
    private final EnumPropertyFilter<SmartTvAdjustmentFieldEnum> smartTvPropertyFilter;
    private final EnumPropertyFilter<SmartAdAdjustmentFieldEnum> smartAdPropertyFilter;
    private final EnumPropertyFilter<SerpLayoutAdjustmentFieldEnum> serpLayoutPropertyFilter;
    private final EnumPropertyFilter<IncomeGradeAdjustmentFieldEnum> incomeGradePropertyFilter;

    private static final ObjectFactory FACTORY = new ObjectFactory();

    @Autowired
    public GetBidModifiersDelegate(ApiAuthenticationSource auth,
                                   GetBidModifiersValidationService validationService,
                                   BidModifierService bidModifierService,
                                   PropertyFilter propertyFilter,
                                   AdGroupService adGroupService) {
        super(PathConverter.identity(), auth);
        //
        this.validationService = validationService;
        this.bidModifierService = bidModifierService;
        this.adGroupService = adGroupService;
        //
        this.propertyFilter = EnumPropertyFilter.from(BidModifierFieldEnum.class, propertyFilter);
        this.mobilePropertyFilter = EnumPropertyFilter.from(MobileAdjustmentFieldEnum.class, propertyFilter);
        this.tabletPropertyFilter = EnumPropertyFilter.from(TabletAdjustmentFieldEnum.class, propertyFilter);
        this.regionalPropertyFilter = EnumPropertyFilter.from(RegionalAdjustmentFieldEnum.class, propertyFilter);
        this.demographyPropertyFilter = EnumPropertyFilter.from(DemographicsAdjustmentFieldEnum.class, propertyFilter);
        this.retargetingPropertyFilter = EnumPropertyFilter.from(RetargetingAdjustmentFieldEnum.class, propertyFilter);
        this.videoPropertyFilter = EnumPropertyFilter.from(VideoAdjustmentFieldEnum.class, propertyFilter);
        this.desktopPropertyFilter = EnumPropertyFilter.from(DesktopAdjustmentFieldEnum.class, propertyFilter);
        this.desktopOnlyPropertyFilter = EnumPropertyFilter.from(DesktopOnlyAdjustmentFieldEnum.class, propertyFilter);
        this.smartTvPropertyFilter = EnumPropertyFilter.from(SmartTvAdjustmentFieldEnum.class, propertyFilter);
        this.smartAdPropertyFilter = EnumPropertyFilter.from(SmartAdAdjustmentFieldEnum.class, propertyFilter);
        this.serpLayoutPropertyFilter = EnumPropertyFilter.from(SerpLayoutAdjustmentFieldEnum.class, propertyFilter);
        this.incomeGradePropertyFilter = EnumPropertyFilter.from(IncomeGradeAdjustmentFieldEnum.class, propertyFilter);
    }

    @Nullable
    @Override
    public ValidationResult<GetRequest, DefectType> validateRequest(GetRequest externalRequest) {
        return validationService.validateRequest(externalRequest);
    }

    @Override
    public List<Long> returnedCampaignIds(ApiResult<List<BidModifierGetItem>> result) {
        return mapList(result.getResult(), BidModifierGetItem::getCampaignId);
    }

    @Override
    public Set<BidModifierAnyFieldEnum> extractFieldNames(GetRequest externalRequest) {
        return Stream.of(
                externalRequest.getFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromBidModifierFieldEnum),
                externalRequest.getMobileAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromMobileAdjustmentFieldEnum),
                externalRequest.getDesktopAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromDesktopAdjustmentFieldEnum),
                externalRequest.getTabletAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromTabletAdjustmentFieldEnum),
                externalRequest.getDesktopOnlyAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromDesktopOnlyAdjustmentFieldEnum),
                externalRequest.getSmartTvAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromSmartTvAdjustmentFieldEnum),
                externalRequest.getRegionalAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromRegionalAdjustmentFieldEnum),
                externalRequest.getDemographicsAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromDemographicsAdjustmentFieldEnum),
                externalRequest.getRetargetingAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromRetargetingAdjustmentFieldEnum),
                externalRequest.getVideoAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromVideoAdjustmentFieldEnum),
                externalRequest.getSmartAdAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromSmartAdjustmentFieldEnum),
                externalRequest.getSerpLayoutAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromSerpLayoutAdjustmentFieldEnum),
                externalRequest.getIncomeGradeAdjustmentFieldNames().stream()
                        .map(BidModifierAnyFieldEnum::fromIncomeGradeAdjustmentFieldEnum)
        ).flatMap(Function.identity()).collect(toSet());
    }

    @Override
    public BidModifiersSelectionCriteria extractSelectionCriteria(GetRequest externalRequest) {
        return externalRequest.getSelectionCriteria();
    }

    @Override
    public List<BidModifierGetItem> get(
            GenericGetRequest<BidModifierAnyFieldEnum, BidModifiersSelectionCriteria> request) {
        BidModifiersSelectionCriteria selectionCriteria = request.getSelectionCriteria();

        Set<BidModifierType> allowedBidModifierTypes = getAllowedBidModifierTypes();
        Set<BidModifierType> types = getAllowedTypes(selectionCriteria, allowedBidModifierTypes);

        Set<BidModifierLevel> levels =
                selectionCriteria.getLevels().stream().map(GetBidModifiersDelegate::convertLevel).collect(toSet());

        long operatorUid = auth.getOperator().getUid();
        ClientId clientId = auth.getChiefSubclient().getClientId();

        List<BidModifier> bidModifiers;
        LimitOffset limitOffset = request.getLimitOffset();
        Set<Long> campaignIds = new HashSet<>(selectionCriteria.getCampaignIds());
        Set<Long> adGroupIds = new HashSet<>(selectionCriteria.getAdGroupIds());
        if (!selectionCriteria.getIds().isEmpty()) {
            // Преобразуем внешние id в настоящие
            Multimap<BidModifierType, Long> idsByType = getRealIdsGroupedByType(selectionCriteria.getIds());
            if (idsByType.isEmpty()) {
                return emptyList();
            }
            bidModifiers = bidModifierService.getByIds(clientId, idsByType, campaignIds, adGroupIds,
                    types, levels, operatorUid);
        } else if (!adGroupIds.isEmpty()) {
            bidModifiers = bidModifierService.getByAdGroupIds(
                    clientId, adGroupIds, campaignIds, types, levels, operatorUid);
        } else {
            checkState(!campaignIds.isEmpty());
            bidModifiers = bidModifierService.getByCampaignIds(
                    clientId, campaignIds, types, levels, operatorUid);
        }

        Map<Long, AdGroupType> adGroupTypes = adGroupService.getAdGroupTypes(clientId, mapList(bidModifiers,
                BidModifier::getAdGroupId));

        // Удаляем корректировку, если она привязана к группе с неподдерживаемым типом
        Set<AdGroupType> allowedAdGroupTypes = getAllowedAdGroupTypes();
        bidModifiers.removeIf(bm -> adGroupTypes.get(bm.getAdGroupId()) != null
                && !allowedAdGroupTypes.contains(adGroupTypes.get(bm.getAdGroupId())));
        return convertAll(bidModifiers, limitOffset);
    }

    private Set<BidModifierType> getAllowedTypes(BidModifiersSelectionCriteria selectionCriteria,
                                                 Set<BidModifierType> allowedBidModifierTypes) {
        if (selectionCriteria.getTypes().isEmpty()) {
            return allowedBidModifierTypes;
        }
        return selectionCriteria.getTypes().stream()
                .map(GetBidModifiersDelegate::hierarchicalMultipliersTypeFromExternal)
                .filter(allowedBidModifierTypes::contains)
                .collect(toSet());
    }

    private List<BidModifierGetItem> convertAll(List<BidModifier> bidModifiers, LimitOffset limitOffset) {
        return bidModifiers.stream()
                .map(bm -> convertToExternal(bm))
                .flatMap(Collection::stream)
                .sorted(Comparator.comparingLong(BidModifierGetItem::getId))
                .skip(limitOffset.offset())
                .limit(limitOffset.limit())
                .collect(toList());
    }

    // TODO : beautify
    List<BidModifierGetItem> convertToExternal(BidModifier bidModifier) {
        switch (bidModifier.getType()) {
            case MOBILE_MULTIPLIER: {
                BidModifierMobile modifier = (BidModifierMobile) bidModifier;
                BidModifierMobileAdjustment mobileAdjustment = modifier.getMobileAdjustment();
                return Collections.singletonList(
                        initGetItem(modifier, BidModifierTypeEnum.MOBILE_ADJUSTMENT)
                                .withId(getExternalId(mobileAdjustment.getId(), modifier.getType()))
                                .withMobileAdjustment(new MobileAdjustmentGet()
                                        .withOperatingSystemType(FACTORY.createMobileAdjustmentGetOperatingSystemType(
                                                ifNotNull(mobileAdjustment.getOsType(),
                                                        GetBidModifiersDelegate::osTypeToExternal)))
                                        .withBidModifier(mobileAdjustment.getPercent())));
            }
            case DESKTOP_MULTIPLIER: {
                BidModifierDesktop modifier = (BidModifierDesktop) bidModifier;
                return Collections.singletonList(
                        initGetItem(modifier, BidModifierTypeEnum.DESKTOP_ADJUSTMENT)
                                .withId(getExternalId(modifier.getId(), modifier.getType()))
                                .withDesktopAdjustment(new DesktopAdjustmentGet()
                                        .withBidModifier(modifier.getDesktopAdjustment().getPercent())));
            }
            case TABLET_MULTIPLIER: {
                BidModifierTablet modifier = (BidModifierTablet) bidModifier;
                BidModifierTabletAdjustment tabletAdjustment = modifier.getTabletAdjustment();
                return Collections.singletonList(
                        initGetItem(modifier, BidModifierTypeEnum.TABLET_ADJUSTMENT)
                                .withId(getExternalId(tabletAdjustment.getId(), modifier.getType()))
                                .withTabletAdjustment(new TabletAdjustmentGet()
                                        .withOperatingSystemType(FACTORY.createTabletAdjustmentGetOperatingSystemType(
                                                ifNotNull(tabletAdjustment.getOsType(),
                                                        GetBidModifiersDelegate::tabletOsTypeToExternal)))
                                        .withBidModifier(tabletAdjustment.getPercent())));
            }
            case DESKTOP_ONLY_MULTIPLIER: {
                BidModifierDesktopOnly modifier = (BidModifierDesktopOnly) bidModifier;
                return Collections.singletonList(
                        initGetItem(modifier, BidModifierTypeEnum.DESKTOP_ONLY_ADJUSTMENT)
                                .withId(getExternalId(modifier.getId(), modifier.getType()))
                                .withDesktopOnlyAdjustment(new DesktopOnlyAdjustmentGet()
                                        .withBidModifier(modifier.getDesktopOnlyAdjustment().getPercent())));
            }
            case SMARTTV_MULTIPLIER: {
                BidModifierSmartTV modifier = (BidModifierSmartTV) bidModifier;
                return Collections.singletonList(
                        initGetItem(modifier, BidModifierTypeEnum.SMART_TV_ADJUSTMENT)
                                .withId(getExternalId(modifier.getId(), modifier.getType()))
                                .withSmartTvAdjustment(new SmartTvAdjustmentGet()
                                        .withBidModifier(modifier.getSmartTVAdjustment().getPercent())));
            }
            case VIDEO_MULTIPLIER: {
                BidModifierVideo modifier = (BidModifierVideo) bidModifier;
                return Collections.singletonList(
                        initGetItem(modifier, BidModifierTypeEnum.VIDEO_ADJUSTMENT)
                                .withId(getExternalId(modifier.getId(), modifier.getType()))
                                .withVideoAdjustment(new VideoAdjustmentGet()
                                        .withBidModifier(modifier.getVideoAdjustment().getPercent())));
            }
            case DEMOGRAPHY_MULTIPLIER: {
                BidModifierDemographics modifier = (BidModifierDemographics) bidModifier;
                return modifier.getDemographicsAdjustments().stream()
                        .map(adjustment ->
                                initGetItem(modifier, BidModifierTypeEnum.DEMOGRAPHICS_ADJUSTMENT)
                                        .withId(getExternalId(adjustment.getId(), modifier.getType()))
                                        .withDemographicsAdjustment(
                                                new DemographicsAdjustmentGet()
                                                        .withEnabled(yesNoFromBool(modifier.getEnabled()))
                                                        .withGender(
                                                                FACTORY.createDemographicsAdjustmentGetGender(
                                                                        adjustment.getGender() != null
                                                                                ? genderTypeToExternal(
                                                                                adjustment.getGender()) : null))
                                                        .withAge(
                                                                FACTORY.createDemographicsAdjustmentGetAge(
                                                                        adjustment.getAge() != null ? ageTypeToExternal(
                                                                                adjustment.getAge()) : null))
                                                        .withBidModifier(adjustment.getPercent())))
                        .collect(toList());
            }
            case GEO_MULTIPLIER: {
                BidModifierGeo modifier = (BidModifierGeo) bidModifier;
                return modifier.getRegionalAdjustments().stream()
                        // Скрытые корректировки не возвращаются наружу из API
                        .filter(it -> !it.getHidden())
                        .map(adjustment ->
                                initGetItem(modifier, BidModifierTypeEnum.REGIONAL_ADJUSTMENT)
                                        .withId(getExternalId(adjustment.getId(), modifier.getType()))
                                        .withRegionalAdjustment(
                                                new RegionalAdjustmentGet()
                                                        .withEnabled(yesNoFromBool(modifier.getEnabled()))
                                                        .withBidModifier(adjustment.getPercent())
                                                        .withRegionId(adjustment.getRegionId())))
                        .collect(toList());
            }
            case RETARGETING_MULTIPLIER: {
                BidModifierRetargeting modifier = (BidModifierRetargeting) bidModifier;
                return modifier.getRetargetingAdjustments().stream().map(adjustment ->
                        initGetItem(modifier, BidModifierTypeEnum.RETARGETING_ADJUSTMENT)
                                .withId(getExternalId(adjustment.getId(), modifier.getType()))
                                .withRetargetingAdjustment(
                                        new RetargetingAdjustmentGet()
                                                .withEnabled(yesNoFromBool(modifier.getEnabled()))
                                                .withRetargetingConditionId(adjustment.getRetargetingConditionId())
                                                .withBidModifier(adjustment.getPercent())
                                                .withAccessible(yesNoFromBool(adjustment.getAccessible()))))
                        .collect(toList());
            }
            case PERFORMANCE_TGO_MULTIPLIER: {
                BidModifierPerformanceTgo modifier = (BidModifierPerformanceTgo) bidModifier;
                return Collections.singletonList(
                        initGetItem(modifier, BidModifierTypeEnum.SMART_AD_ADJUSTMENT)
                                .withId(getExternalId(modifier.getId(), modifier.getType()))
                                .withSmartAdAdjustment(new SmartAdAdjustmentGet()
                                        .withBidModifier(modifier.getPerformanceTgoAdjustment().getPercent())));
            }
            case PRISMA_INCOME_GRADE_MULTIPLIER: {
                BidModifierPrismaIncomeGrade modifier = (BidModifierPrismaIncomeGrade) bidModifier;
                return mapList(modifier.getExpressionAdjustments(), adjustment -> {
                    BidModifierPrismaIncomeGradeAdjustment prismaIncomeGradeAdjustment =
                            (BidModifierPrismaIncomeGradeAdjustment) adjustment;
                    return initGetItem(modifier, BidModifierTypeEnum.INCOME_GRADE_ADJUSTMENT)
                            .withId(getExternalId(adjustment.getId(), modifier.getType()))
                            .withIncomeGradeAdjustment(toIncomeGradeAdjustmentGet(prismaIncomeGradeAdjustment,
                                    yesNoFromBool(modifier.getEnabled())));
                });

            }
            case TRAFARET_POSITION_MULTIPLIER: {
                BidModifierTrafaretPosition modifier = (BidModifierTrafaretPosition) bidModifier;
                return modifier.getTrafaretPositionAdjustments().stream()
                        .map(adjustment ->
                                initGetItem(modifier, BidModifierTypeEnum.SERP_LAYOUT_ADJUSTMENT)
                                        .withId(getExternalId(adjustment.getId(), modifier.getType()))
                                        .withSerpLayoutAdjustment(
                                                new SerpLayoutAdjustmentGet()
                                                        .withEnabled(yesNoFromBool(modifier.getEnabled()))
                                                        .withSerpLayout(positionToExternal(adjustment.getTrafaretPosition()))
                                                        .withBidModifier(adjustment.getPercent())))
                        .collect(toList());
            }
            default: {
                throw new IllegalStateException("Unsupported type");
            }
        }
    }

    private IncomeGradeAdjustmentGet toIncomeGradeAdjustmentGet(BidModifierPrismaIncomeGradeAdjustment adjustment,
                                                                YesNoEnum enabled) {
        var literals = flatMap(adjustment.getCondition(), list -> list);
        checkState(literals.size() == 1, "Unexpected conditions for income grade adjustment");
        return new IncomeGradeAdjustmentGet()
                .withBidModifier(adjustment.getPercent())
                .withEnabled(enabled)
                .withGrade(incomeGradeEnumToExternal(literals.get(0).getValueString()));
    }

    private BidModifierGetItem initGetItem(BidModifier modifier, BidModifierTypeEnum type) {
        return new BidModifierGetItem()
                .withCampaignId(modifier.getCampaignId())
                .withAdGroupId(FACTORY.createBidModifierGetItemAdGroupId(modifier.getAdGroupId()))
                .withType(type)
                .withLevel(getLevelOf(modifier));
    }

    private BidModifierLevelEnum getLevelOf(BidModifier bidModifier) {
        return bidModifier.getAdGroupId() != null ? BidModifierLevelEnum.AD_GROUP : BidModifierLevelEnum.CAMPAIGN;
    }

    @Override
    public GetResponse convertGetResponse(List<BidModifierGetItem> items,
                                          Set<BidModifierAnyFieldEnum> requestedFields,
                                          @Nullable Long limitedBy) {
        // Сначала очищаем от лишних полей дочерние структуры (при их наличии)
        Map<? extends Class<?>, List<BidModifierAnyFieldEnum>> grouped =
                requestedFields.stream().collect(groupingBy(BidModifierAnyFieldEnum::getEnumClazz));
        if (grouped.containsKey(MobileAdjustmentFieldEnum.class)) {
            mobilePropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.MOBILE_ADJUSTMENT)
                            .map(BidModifierGetItem::getMobileAdjustment).collect(toList()),
                    grouped.get(MobileAdjustmentFieldEnum.class).stream()
                            .map(e -> (MobileAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(MobileAdjustmentFieldEnum.class))));
        }
        if (grouped.containsKey(TabletAdjustmentFieldEnum.class)) {
            tabletPropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.TABLET_ADJUSTMENT)
                            .map(BidModifierGetItem::getTabletAdjustment).collect(toList()),
                    grouped.get(TabletAdjustmentFieldEnum.class).stream()
                            .map(e -> (TabletAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(TabletAdjustmentFieldEnum.class))));
        }
        if (grouped.containsKey(RegionalAdjustmentFieldEnum.class)) {
            regionalPropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.REGIONAL_ADJUSTMENT)
                            .map(BidModifierGetItem::getRegionalAdjustment).collect(toList()),
                    grouped.get(RegionalAdjustmentFieldEnum.class).stream()
                            .map(e -> (RegionalAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(RegionalAdjustmentFieldEnum.class))));
        }
        if (grouped.containsKey(DemographicsAdjustmentFieldEnum.class)) {
            demographyPropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.DEMOGRAPHICS_ADJUSTMENT)
                            .map(BidModifierGetItem::getDemographicsAdjustment).collect(toList()),
                    grouped.get(DemographicsAdjustmentFieldEnum.class).stream()
                            .map(e -> (DemographicsAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(DemographicsAdjustmentFieldEnum.class))));
        }
        if (grouped.containsKey(RetargetingAdjustmentFieldEnum.class)) {
            retargetingPropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.RETARGETING_ADJUSTMENT)
                            .map(BidModifierGetItem::getRetargetingAdjustment).collect(toList()),
                    grouped.get(RetargetingAdjustmentFieldEnum.class).stream()
                            .map(e -> (RetargetingAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(RetargetingAdjustmentFieldEnum.class))));
        }
        if (grouped.containsKey(VideoAdjustmentFieldEnum.class)) {
            videoPropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.VIDEO_ADJUSTMENT)
                            .map(BidModifierGetItem::getVideoAdjustment).collect(toList()),
                    grouped.get(VideoAdjustmentFieldEnum.class).stream()
                            .map(e -> (VideoAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(VideoAdjustmentFieldEnum.class))));
        }
        if (grouped.containsKey(DesktopAdjustmentFieldEnum.class)) {
            desktopPropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.DESKTOP_ADJUSTMENT)
                            .map(BidModifierGetItem::getDesktopAdjustment).collect(toList()),
                    grouped.get(DesktopAdjustmentFieldEnum.class).stream()
                            .map(e -> (DesktopAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(DesktopAdjustmentFieldEnum.class))));
        }
        if (grouped.containsKey(DesktopOnlyAdjustmentFieldEnum.class)) {
            desktopOnlyPropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.DESKTOP_ONLY_ADJUSTMENT)
                            .map(BidModifierGetItem::getDesktopOnlyAdjustment).collect(toList()),
                    grouped.get(DesktopOnlyAdjustmentFieldEnum.class).stream()
                            .map(e -> (DesktopOnlyAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(DesktopOnlyAdjustmentFieldEnum.class))));
        }
        if (grouped.containsKey(SmartTvAdjustmentFieldEnum.class)) {
            smartTvPropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.SMART_TV_ADJUSTMENT)
                            .map(BidModifierGetItem::getSmartTvAdjustment).collect(toList()),
                    grouped.get(SmartTvAdjustmentFieldEnum.class).stream()
                            .map(e -> (SmartTvAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(SmartTvAdjustmentFieldEnum.class))));
        }
        if (grouped.containsKey(SmartAdAdjustmentFieldEnum.class)) {
            smartAdPropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.SMART_AD_ADJUSTMENT)
                            .map(BidModifierGetItem::getSmartAdAdjustment).collect(toList()),
                    grouped.get(SmartAdAdjustmentFieldEnum.class).stream()
                            .map(e -> (SmartAdAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(SmartAdAdjustmentFieldEnum.class))));
        }
        if (grouped.containsKey(SerpLayoutAdjustmentFieldEnum.class)) {
            serpLayoutPropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.SERP_LAYOUT_ADJUSTMENT)
                            .map(BidModifierGetItem::getSerpLayoutAdjustment).collect(toList()),
                    grouped.get(SerpLayoutAdjustmentFieldEnum.class).stream()
                            .map(e -> (SerpLayoutAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(SerpLayoutAdjustmentFieldEnum.class))));
        }

        if (grouped.containsKey(IncomeGradeAdjustmentFieldEnum.class)) {
            incomeGradePropertyFilter.filterProperties(
                    items.stream()
                            .filter(item -> item.getType() == BidModifierTypeEnum.INCOME_GRADE_ADJUSTMENT)
                            .map(BidModifierGetItem::getIncomeGradeAdjustment).collect(toList()),
                    grouped.get(IncomeGradeAdjustmentFieldEnum.class).stream()
                            .map(e -> (IncomeGradeAdjustmentFieldEnum) e.getValue())
                            .collect(toCollection(() -> EnumSet.noneOf(IncomeGradeAdjustmentFieldEnum.class))));
        }


        // Теперь очищаем от лишних полей основную сущность
        List<String> propertyNames = new ArrayList<>();
        if (grouped.containsKey(MobileAdjustmentFieldEnum.class)) {
            propertyNames.add("mobileAdjustment");
        }
        if (grouped.containsKey(TabletAdjustmentFieldEnum.class)) {
            propertyNames.add("tabletAdjustment");
        }
        if (grouped.containsKey(RegionalAdjustmentFieldEnum.class)) {
            propertyNames.add("regionalAdjustment");
        }
        if (grouped.containsKey(DemographicsAdjustmentFieldEnum.class)) {
            propertyNames.add("demographicsAdjustment");
        }
        if (grouped.containsKey(RetargetingAdjustmentFieldEnum.class)) {
            propertyNames.add("retargetingAdjustment");
        }
        if (grouped.containsKey(VideoAdjustmentFieldEnum.class)) {
            propertyNames.add("videoAdjustment");
        }
        if (grouped.containsKey(DesktopAdjustmentFieldEnum.class)) {
            propertyNames.add("desktopAdjustment");
        }
        if (grouped.containsKey(DesktopOnlyAdjustmentFieldEnum.class)) {
            propertyNames.add("desktopOnlyAdjustment");
        }
        if (grouped.containsKey(SmartTvAdjustmentFieldEnum.class)) {
            propertyNames.add("smartTvAdjustment");
        }
        if (grouped.containsKey(SmartAdAdjustmentFieldEnum.class)) {
            propertyNames.add("smartAdAdjustment");
        }
        if (grouped.containsKey(SerpLayoutAdjustmentFieldEnum.class)) {
            propertyNames.add("serpLayoutAdjustment");
        }
        if (grouped.containsKey(IncomeGradeAdjustmentFieldEnum.class)) {
            propertyNames.add("incomeGradeAdjustment");
        }

        Set<BidModifierFieldEnum> fields =
                ObjectUtils.defaultIfNull(grouped.get(BidModifierFieldEnum.class),
                        Collections.<BidModifierAnyFieldEnum>emptyList())
                        .stream().map(e -> (BidModifierFieldEnum) e.getValue())
                        .collect(toSet());
        propertyNames.addAll(mapList(fields, propertyFilter.getEnumToFieldMap()::get));
        propertyFilter.filterPropertiesByNames(items, propertyNames);

        return new GetResponse().withBidModifiers(items).withLimitedBy(limitedBy);
    }

    static BidModifierLevel convertLevel(BidModifierLevelEnum level) {
        switch (level) {
            case AD_GROUP:
                return BidModifierLevel.ADGROUP;
            case CAMPAIGN:
                return BidModifierLevel.CAMPAIGN;
            default:
                throw new IllegalStateException("Invalid level: " + level);
        }
    }

    static GenderEnum genderTypeToExternal(GenderType genderType) {
        switch (genderType) {
            case MALE:
                return GenderEnum.GENDER_MALE;
            case FEMALE:
                return GenderEnum.GENDER_FEMALE;
            default:
                throw new IllegalStateException("Invalid value: " + genderType);
        }
    }

    static OperatingSystemTypeEnum osTypeToExternal(OsType osType) {
        switch (osType) {
            case ANDROID:
                return OperatingSystemTypeEnum.ANDROID;
            case IOS:
                return OperatingSystemTypeEnum.IOS;
            default:
                throw new IllegalStateException("Invalid value: " + osType);
        }
    }

    static OperatingSystemTypeEnum tabletOsTypeToExternal(TabletOsType osType) {
        switch (osType) {
            case ANDROID:
                return OperatingSystemTypeEnum.ANDROID;
            case IOS:
                return OperatingSystemTypeEnum.IOS;
            default:
                throw new IllegalStateException("Invalid value: " + osType);
        }
    }

    static AgeRangeEnum ageTypeToExternal(AgeType ageType) {
        switch (ageType) {
            case _0_17:
                return AgeRangeEnum.AGE_0_17;
            case _18_24:
                return AgeRangeEnum.AGE_18_24;
            case _25_34:
                return AgeRangeEnum.AGE_25_34;
            case _35_44:
                return AgeRangeEnum.AGE_35_44;
            case _45_54:
                return AgeRangeEnum.AGE_45_54;
            case _45_:
                return AgeRangeEnum.AGE_45;
            case _55_:
                return AgeRangeEnum.AGE_55;
            case UNKNOWN:
                // API не поддерживает этот возраст
                return null;
            default:
                throw new IllegalStateException("Invalid value: " + ageType);
        }
    }

    static SerpLayoutEnum positionToExternal(TrafaretPosition position) {
        switch (position) {
            case ALONE:
                return SerpLayoutEnum.ALONE;
            case SUGGEST:
                return SerpLayoutEnum.SUGGEST;
            default:
                throw new IllegalStateException("Invalid value: " + position);
        }
    }

    static IncomeGradeEnum incomeGradeEnumToExternal(String incomeGradeLiteralValue) {
        if (INCOME_GRADE_ENUM_BY_INCOME_GRADE_LITERAL_VALUE.containsKey(incomeGradeLiteralValue)) {
            return INCOME_GRADE_ENUM_BY_INCOME_GRADE_LITERAL_VALUE.get(incomeGradeLiteralValue);
        }
        throw new IllegalStateException("Unknown income grade " + incomeGradeLiteralValue);
    }

    static BidModifierType hierarchicalMultipliersTypeFromExternal(BidModifierTypeEnum bidModifierTypeEnum) {
        switch (bidModifierTypeEnum) {
            case MOBILE_ADJUSTMENT:
                return BidModifierType.MOBILE_MULTIPLIER;
            case DESKTOP_ADJUSTMENT:
                return BidModifierType.DESKTOP_MULTIPLIER;
            case TABLET_ADJUSTMENT:
                return BidModifierType.TABLET_MULTIPLIER;
            case DESKTOP_ONLY_ADJUSTMENT:
                return BidModifierType.DESKTOP_ONLY_MULTIPLIER;
            case SMART_TV_ADJUSTMENT:
                return BidModifierType.SMARTTV_MULTIPLIER;
            case DEMOGRAPHICS_ADJUSTMENT:
                return BidModifierType.DEMOGRAPHY_MULTIPLIER;
            case REGIONAL_ADJUSTMENT:
                return BidModifierType.GEO_MULTIPLIER;
            case RETARGETING_ADJUSTMENT:
                return BidModifierType.RETARGETING_MULTIPLIER;
            case VIDEO_ADJUSTMENT:
                return BidModifierType.VIDEO_MULTIPLIER;
            case SMART_AD_ADJUSTMENT:
                return BidModifierType.PERFORMANCE_TGO_MULTIPLIER;
            case INCOME_GRADE_ADJUSTMENT:
                return BidModifierType.PRISMA_INCOME_GRADE_MULTIPLIER;
            case SERP_LAYOUT_ADJUSTMENT:
                return BidModifierType.TRAFARET_POSITION_MULTIPLIER;
            default:
                throw new UnsupportedOperationException();
        }
    }
}
