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

import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

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

import com.yandex.direct.api.v5.ads.AdGetItem;
import com.yandex.direct.api.v5.ads.AdTypeEnum;
import com.yandex.direct.api.v5.ads.GetRequest;
import com.yandex.direct.api.v5.ads.GetResponse;
import com.yandex.direct.api.v5.ads.TextAdFieldEnum;
import one.util.streamex.StreamEx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import ru.yandex.direct.api.v5.common.ApiPathConverter;
import ru.yandex.direct.api.v5.common.validation.GetRequestGeneralValidator;
import ru.yandex.direct.api.v5.entity.GenericGetRequest;
import ru.yandex.direct.api.v5.entity.GetApiServiceDelegate;
import ru.yandex.direct.api.v5.entity.ads.container.AdsGetContainer;
import ru.yandex.direct.api.v5.entity.ads.converter.GetResponseConverter;
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.core.entity.addition.callout.repository.CalloutRepository;
import ru.yandex.direct.core.entity.adgroup.model.AdGroupType;
import ru.yandex.direct.core.entity.adgroup.model.ContentPromotionAdgroupType;
import ru.yandex.direct.core.entity.adgroup.repository.AdGroupRepository;
import ru.yandex.direct.core.entity.adgroup.service.AdGroupService;
import ru.yandex.direct.core.entity.banner.container.AdsSelectionCriteria;
import ru.yandex.direct.core.entity.banner.model.Banner;
import ru.yandex.direct.core.entity.banner.model.BannerWithAdGroupId;
import ru.yandex.direct.core.entity.banner.model.BannerWithBannerImage;
import ru.yandex.direct.core.entity.banner.model.BannerWithCallouts;
import ru.yandex.direct.core.entity.banner.model.BannerWithCreative;
import ru.yandex.direct.core.entity.banner.model.BannerWithCreativeModeration;
import ru.yandex.direct.core.entity.banner.model.BannerWithDisplayHref;
import ru.yandex.direct.core.entity.banner.model.BannerWithSitelinks;
import ru.yandex.direct.core.entity.banner.model.BannerWithSystemFields;
import ru.yandex.direct.core.entity.banner.model.BannerWithVcard;
import ru.yandex.direct.core.entity.banner.repository.BannerCommonRepository;
import ru.yandex.direct.core.entity.banner.service.BannerService;
import ru.yandex.direct.core.entity.banner.service.validation.type.BannerTypeValidationPredicates;
import ru.yandex.direct.core.entity.banner.type.creative.BannerCreativeRepository;
import ru.yandex.direct.core.entity.campaign.model.Campaign;
import ru.yandex.direct.core.entity.campaign.service.CampaignService;
import ru.yandex.direct.core.entity.creative.model.Creative;
import ru.yandex.direct.core.entity.creative.model.CreativeType;
import ru.yandex.direct.core.entity.creative.repository.CreativeRepository;
import ru.yandex.direct.core.entity.mobilecontent.model.MobileContent;
import ru.yandex.direct.core.entity.mobilecontent.repository.MobileContentRepository;
import ru.yandex.direct.core.entity.moderationdiag.model.ModerationDiag;
import ru.yandex.direct.core.entity.moderationreason.model.ModerationReasonObjectType;
import ru.yandex.direct.core.entity.moderationreason.service.ModerationReasonService;
import ru.yandex.direct.dbutil.model.ClientId;
import ru.yandex.direct.dbutil.sharding.ShardHelper;
import ru.yandex.direct.validation.builder.Constraint;
import ru.yandex.direct.validation.builder.ItemValidationBuilder;
import ru.yandex.direct.validation.builder.ListValidationBuilder;
import ru.yandex.direct.validation.builder.When;
import ru.yandex.direct.validation.result.ValidationResult;

import static com.yandex.direct.api.v5.ads.AdsSelectionCriteria.PropInfo.AD_EXTENSION_IDS;
import static com.yandex.direct.api.v5.ads.AdsSelectionCriteria.PropInfo.AD_GROUP_IDS;
import static com.yandex.direct.api.v5.ads.AdsSelectionCriteria.PropInfo.AD_IMAGE_HASHES;
import static com.yandex.direct.api.v5.ads.AdsSelectionCriteria.PropInfo.CAMPAIGN_IDS;
import static com.yandex.direct.api.v5.ads.AdsSelectionCriteria.PropInfo.IDS;
import static com.yandex.direct.api.v5.ads.AdsSelectionCriteria.PropInfo.SITELINK_SET_IDS;
import static com.yandex.direct.api.v5.ads.AdsSelectionCriteria.PropInfo.TYPES;
import static com.yandex.direct.api.v5.ads.AdsSelectionCriteria.PropInfo.V_CARD_IDS;
import static com.yandex.direct.api.v5.ads.GetRequest.PropInfo.SELECTION_CRITERIA;
import static com.yandex.direct.api.v5.ads.GetRequest.PropInfo.TEXT_AD_FIELD_NAMES;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static ru.yandex.direct.api.v5.common.constants.GetRequestCommonConstants.DEFAULT_MAX_IDS_COUNT;
import static ru.yandex.direct.api.v5.common.constants.GetRequestCommonConstants.DEFAULT_RELATED_OBJECT_IDS_COUNT;
import static ru.yandex.direct.api.v5.entity.ads.Constants.MAX_ADGROUP_IDS_COUNT;
import static ru.yandex.direct.api.v5.entity.ads.Constants.MAX_CAMPAIGN_IDS_COUNT;
import static ru.yandex.direct.api.v5.entity.ads.converter.GetRequestConverter.convertExtensionStatuses;
import static ru.yandex.direct.api.v5.entity.ads.converter.GetRequestConverter.convertMobile;
import static ru.yandex.direct.api.v5.entity.ads.converter.GetRequestConverter.convertStates;
import static ru.yandex.direct.api.v5.entity.ads.converter.GetRequestConverter.convertStatuses;
import static ru.yandex.direct.api.v5.entity.ads.converter.GetRequestConverter.convertToContentPromotionTypes;
import static ru.yandex.direct.api.v5.entity.ads.converter.GetRequestConverter.convertTypes;
import static ru.yandex.direct.api.v5.entity.ads.converter.GetRequestConverter.getSupportedTypes;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.AD_STATUS_CLARIFICATION;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.CPC_VIDEO_AD_BUILDER_AD_CREATIVE;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.CPM_BANNER_AD_BUILDER_AD_CREATIVE;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.CPM_VIDEO_AD_BUILDER_AD_CREATIVE;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.DYNAMIC_TEXT_AD_EXTENSIONS;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.DYNAMIC_TEXT_AD_IMAGE_MODERATION;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.DYNAMIC_TEXT_AD_SITELINKS_MODERATION;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.DYNAMIC_TEXT_AD_V_CARD_MODERATION;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.MOBILE_APP_AD_BUILDER_AD_CREATIVE;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.MOBILE_APP_AD_FEATURES;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.MOBILE_APP_AD_IMAGE_MODERATION;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.MOBILE_APP_AD_VIDEO_EXTENSION;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.MOBILE_APP_CPC_VIDEO_AD_BUILDER_AD_CREATIVE;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.SMART_AD_BUILDER_AD_CREATIVE;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.TEXT_AD_BUILDER_AD_CREATIVE;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.TEXT_AD_DISPLAY_URL_PATH_MODERATION;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.TEXT_AD_EXTENSIONS;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.TEXT_AD_IMAGE_MODERATION;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.TEXT_AD_SITELINKS_MODERATION;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.TEXT_AD_VIDEO_EXTENSION;
import static ru.yandex.direct.api.v5.entity.ads.delegate.AdAnyFieldEnum.TEXT_AD_V_CARD_MODERATION;
import static ru.yandex.direct.api.v5.validation.DefectTypes.invalidUseOfField;
import static ru.yandex.direct.api.v5.validation.DefectTypes.maxIdsInSelection;
import static ru.yandex.direct.api.v5.validation.DefectTypes.missedParamsInSelection;
import static ru.yandex.direct.api.v5.validation.constraints.Constraints.maxListSize;
import static ru.yandex.direct.api.v5.validation.constraints.Constraints.notNull;
import static ru.yandex.direct.core.entity.adgroup.container.AccessibleAdGroupTypes.API5_ALLOWED_CONTENT_PROMOTION_AD_GROUP_TYPES;
import static ru.yandex.direct.core.entity.banner.service.validation.type.BannerTypeValidationPredicates.isCpmBanner;
import static ru.yandex.direct.core.entity.moderationreason.model.ModerationReasonObjectType.BANNER;
import static ru.yandex.direct.core.entity.moderationreason.model.ModerationReasonObjectType.CONTACTINFO;
import static ru.yandex.direct.core.entity.moderationreason.model.ModerationReasonObjectType.DISPLAY_HREF;
import static ru.yandex.direct.core.entity.moderationreason.model.ModerationReasonObjectType.IMAGE;
import static ru.yandex.direct.core.entity.moderationreason.model.ModerationReasonObjectType.SITELINKS_SET;
import static ru.yandex.direct.utils.FunctionalUtils.filterAndMapList;
import static ru.yandex.direct.utils.FunctionalUtils.listToMap;
import static ru.yandex.direct.utils.FunctionalUtils.listToSet;
import static ru.yandex.direct.utils.FunctionalUtils.mapList;
import static ru.yandex.direct.validation.builder.Constraint.fromPredicate;

@Component
@ParametersAreNonnullByDefault
public class GetAdsDelegate
        extends GetApiServiceDelegate<GetRequest, GetResponse, AdAnyFieldEnum, AdsSelectionCriteria, AdsGetContainer> {

    private static final Logger logger = LoggerFactory.getLogger(GetAdsDelegate.class);

    private final ShardHelper shardHelper;
    private final BannerService bannerService;
    private final BannerCommonRepository bannerRepository;
    private final CampaignService campaignService;
    private final CalloutRepository calloutRepository;
    private final BannerCreativeRepository bannerCreativeRepository;
    private final CreativeRepository creativeRepository;
    private final ModerationReasonService moderationReasonService;
    private final MobileContentRepository mobileContentRepository;
    private final GetResponseConverter getResponseConverter;
    private final AdGroupService adGroupService;
    private final AdGroupRepository adGroupRepository;

    @Autowired
    public GetAdsDelegate(
            ApiAuthenticationSource auth,
            ShardHelper shardHelper,
            BannerService bannerService,
            BannerCommonRepository bannerRepository,
            CampaignService campaignService,
            CalloutRepository calloutRepository,
            BannerCreativeRepository bannerCreativeRepository,
            CreativeRepository creativeRepository,
            ModerationReasonService moderationReasonService,
            MobileContentRepository mobileContentRepository,
            GetResponseConverter getResponseConverter,
            AdGroupService adGroupService,
            AdGroupRepository adGroupRepository
    ) {
        super(ApiPathConverter.forAds(), auth);
        this.shardHelper = shardHelper;
        this.bannerService = bannerService;
        this.bannerRepository = bannerRepository;
        this.campaignService = campaignService;
        this.calloutRepository = calloutRepository;
        this.bannerCreativeRepository = bannerCreativeRepository;
        this.creativeRepository = creativeRepository;
        this.moderationReasonService = moderationReasonService;
        this.mobileContentRepository = mobileContentRepository;
        this.getResponseConverter = getResponseConverter;
        this.adGroupService = adGroupService;
        this.adGroupRepository = adGroupRepository;
    }

    @Override
    public ValidationResult<GetRequest, DefectType> validateRequest(GetRequest externalRequest) {
        logger.debug("validate request {}", externalRequest);

        ItemValidationBuilder<com.yandex.direct.api.v5.ads.GetRequest, DefectType> vb =
                ItemValidationBuilder.of(externalRequest);

        vb.item(externalRequest.getSelectionCriteria(), SELECTION_CRITERIA.schemaName.getLocalPart())
                .check(notNull())
                .checkBy(this::validateSelectionCriteria, When.isValid());

        vb.item(externalRequest.getTextAdFieldNames(), TEXT_AD_FIELD_NAMES.schemaName.getLocalPart())
                .checkBy(this::validateTextAdFieldNames, When.notNull());

        vb.checkBy(GetRequestGeneralValidator::validateRequestWithDefectTypes, When.notNull());

        return vb.getResult();
    }

    @Override
    public List<Long> returnedCampaignIds(ApiResult<List<AdsGetContainer>> result) {
        return mapList(result.getResult(), container -> container.getAd().getCampaignId());
    }

    private ValidationResult<com.yandex.direct.api.v5.ads.AdsSelectionCriteria, DefectType> validateSelectionCriteria(
            com.yandex.direct.api.v5.ads.AdsSelectionCriteria selectionCriteria) {
        logger.debug("validate selection criteria {}", selectionCriteria);

        ItemValidationBuilder<com.yandex.direct.api.v5.ads.AdsSelectionCriteria, DefectType> vb =
                ItemValidationBuilder.of(selectionCriteria);

        vb.check(fromPredicate(
                sc -> !sc.getIds().isEmpty() || !sc.getAdGroupIds().isEmpty() || !sc.getCampaignIds().isEmpty(),
                missedParamsInSelection(String.join(
                        ", ", asList(IDS.schemaName.getLocalPart(), AD_GROUP_IDS.schemaName.getLocalPart(),
                                CAMPAIGN_IDS.schemaName.getLocalPart())))));

        vb.item(selectionCriteria.getIds(), IDS.schemaName.getLocalPart())
                .check(maxListSize(DEFAULT_MAX_IDS_COUNT), maxIdsInSelection());

        vb.item(selectionCriteria.getAdGroupIds(), AD_GROUP_IDS.schemaName.getLocalPart())
                .check(maxListSize(MAX_ADGROUP_IDS_COUNT), maxIdsInSelection());

        vb.item(selectionCriteria.getCampaignIds(), CAMPAIGN_IDS.schemaName.getLocalPart())
                .check(maxListSize(MAX_CAMPAIGN_IDS_COUNT), maxIdsInSelection());

        vb.item(selectionCriteria.getVCardIds(), V_CARD_IDS.schemaName.getLocalPart())
                .check(maxListSize(DEFAULT_RELATED_OBJECT_IDS_COUNT), maxIdsInSelection());

        vb.item(selectionCriteria.getSitelinkSetIds(), SITELINK_SET_IDS.schemaName.getLocalPart())
                .check(maxListSize(DEFAULT_RELATED_OBJECT_IDS_COUNT), maxIdsInSelection());

        vb.item(selectionCriteria.getAdImageHashes(), AD_IMAGE_HASHES.schemaName.getLocalPart())
                .check(maxListSize(DEFAULT_RELATED_OBJECT_IDS_COUNT), maxIdsInSelection());

        vb.item(selectionCriteria.getAdExtensionIds(), AD_EXTENSION_IDS.schemaName.getLocalPart())
                .check(maxListSize(DEFAULT_RELATED_OBJECT_IDS_COUNT), maxIdsInSelection());

        vb.item(selectionCriteria.getTypes(), TYPES.schemaName.getLocalPart())
                .check(supportedContentPromotion());

        return vb.getResult();
    }

    private ValidationResult<List<TextAdFieldEnum>, DefectType> validateTextAdFieldNames(List<TextAdFieldEnum> textAdFieldNames) {
        boolean isLeadformAtributesAllowed = getAuth().isLeadformAttributesAllowed();
        ListValidationBuilder<TextAdFieldEnum, DefectType> vb = ListValidationBuilder.of(textAdFieldNames);
        vb.checkEach(fromPredicate(
                name -> isLeadformAtributesAllowed, invalidUseOfField()
        ), When.valueIs(name -> name == TextAdFieldEnum.LF_HREF || name == TextAdFieldEnum.LF_BUTTON_TEXT));
        return vb.getResult();
    }

    private Constraint<List<AdTypeEnum>, DefectType> supportedContentPromotion() {
        boolean isServicesApp = getAuth().isServicesApplication();
        return fromPredicate(
                types -> isServicesApp || !types.contains(AdTypeEnum.CONTENT_PROMOTION_SERVICE_AD),
                invalidUseOfField()
        );
    }

    @Override
    public Set<AdAnyFieldEnum> extractFieldNames(GetRequest externalRequest) {
        logger.debug("extract field names");

        return Stream.of(
                externalRequest.getFieldNames().stream()
                        .map(AdAnyFieldEnum::fromAdFieldEnum),
                externalRequest.getTextAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromTextAdFieldEnum),
                externalRequest.getTextAdPriceExtensionFieldNames().stream()
                        .map(AdAnyFieldEnum::fromTextAdPriceExtensionFieldEnum),
                externalRequest.getMobileAppAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromMobileAppAdFieldEnum),
                externalRequest.getDynamicTextAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromDynamicTextAdFieldEnum),
                externalRequest.getTextImageAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromTextImageAdFieldEnum),
                externalRequest.getMobileAppImageAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromMobileAppImageAdFieldEnum),
                externalRequest.getMobileAppCpcVideoAdBuilderAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromMobileAppCpcVideoAdBuilderAdFieldEnum),
                externalRequest.getTextAdBuilderAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromTextAdBuilderAdFieldEnum),
                externalRequest.getMobileAppAdBuilderAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromMobileAppAdBuilderAdFieldEnum),
                externalRequest.getCpmBannerAdBuilderAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromCpmBannerAdBuilderAdFieldEnum),
                externalRequest.getCpcVideoAdBuilderAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromCpcVideoAdBuilderAdFieldEnum),
                externalRequest.getCpmVideoAdBuilderAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromCpmVideoAdBuilderAdFieldEnum),
                externalRequest.getSmartAdBuilderAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromSmartAdBuilderAdFieldEnum),
                externalRequest.getContentPromotionVideoAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromContentPromotionVideoAdFieldEnum),
                externalRequest.getContentPromotionCollectionAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromContentPromotionCollectionAdFieldEnum),
                externalRequest.getContentPromotionServiceAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromContentPromotionServiceAdFieldEnum),
                externalRequest.getContentPromotionEdaAdFieldNames().stream()
                        .map(AdAnyFieldEnum::fromContentPromotionEdaAdFieldEnum)
        ).flatMap(Function.identity()).collect(toSet());
    }

    @Override
    public AdsSelectionCriteria extractSelectionCriteria(GetRequest externalRequest) {
        logger.debug("extract selection criteria");

        com.yandex.direct.api.v5.ads.AdsSelectionCriteria selectionCriteria = externalRequest.getSelectionCriteria();

        return new AdsSelectionCriteria()
                .withAdIds(new HashSet<>(selectionCriteria.getIds()))
                .withAdGroupIds(new HashSet<>(selectionCriteria.getAdGroupIds()))
                .withCampaignIds(new HashSet<>(selectionCriteria.getCampaignIds()))
                .withVCardIds(new HashSet<>(selectionCriteria.getVCardIds()))
                .withSitelinkSetIds(new HashSet<>(selectionCriteria.getSitelinkSetIds()))
                .withAdExtensionIds(new HashSet<>(selectionCriteria.getAdExtensionIds()))
                .withAdImageHashes(new HashSet<>(selectionCriteria.getAdImageHashes()))
                .withTypes(convertTypes(selectionCriteria.getTypes()))
                .withStates(convertStates(selectionCriteria.getStates()))
                .withStatuses(convertStatuses(selectionCriteria.getStatuses()))
                .withVCardStatuses(convertExtensionStatuses(selectionCriteria.getVCardModerationStatuses()))
                .withSitelinksStatuses(convertExtensionStatuses(selectionCriteria.getSitelinksModerationStatuses()))
                .withAdImageStatuses(convertExtensionStatuses(selectionCriteria.getAdImageModerationStatuses()))
                .withMobile(convertMobile(selectionCriteria.getMobile()))
                .withSelectCpmVideo(selectionCriteria.getTypes().isEmpty() ||
                        selectionCriteria.getTypes().contains(AdTypeEnum.CPM_VIDEO_AD))
                .withSelectCpmBanner(selectionCriteria.getTypes().isEmpty() ||
                        selectionCriteria.getTypes().contains(AdTypeEnum.CPM_BANNER_AD))
                .withContentPromotionAdgroupTypes(convertToContentPromotionTypes(selectionCriteria.getTypes()));
    }

    @Override
    public List<AdsGetContainer> get(GenericGetRequest<AdAnyFieldEnum, AdsSelectionCriteria> getRequest) {
        logger.debug("seek ads {}", getRequest);

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

        int shard = shardHelper.getShardByClientIdStrictly(clientId);

        Set<AdAnyFieldEnum> requestedFields = getRequest.getRequestedFields();
        AdsSelectionCriteria selectionCriteria = getRequest.getSelectionCriteria();

        if (selectionCriteria.getTypes().isEmpty()) {
            selectionCriteria.withTypes(getSupportedTypes());
        }

        var ads = bannerService
                .getBannersBySelectionCriteria(operatorUid, clientId, selectionCriteria, getRequest.getLimitOffset());
        Set<Long> adGroupIds = StreamEx.of(ads)
                .map(BannerWithAdGroupId::getAdGroupId)
                .toSet();
        Map<Long, AdGroupType> adGroupTypes = adGroupService.getAdGroupTypes(clientId, adGroupIds);
        Map<Long, ContentPromotionAdgroupType> contentPromotionAdgroupTypes = adGroupRepository
                .getContentPromotionAdGroupTypesByIds(shard, adGroupIds);

        Set<AdGroupType> allowedAdGroupTypes = getAllowedAdGroupTypes();
        ads.removeIf(b -> adGroupTypes.get(b.getAdGroupId()) != null
                && !allowedAdGroupTypes.contains(adGroupTypes.get(b.getAdGroupId())));

        // отфильтровываем неподдерживаемые типы продвижения контента
        boolean isServicesApp = getAuth().isServicesApplication();
        ads.removeIf(b -> badContentPromotionGroups(contentPromotionAdgroupTypes, isServicesApp, b));

        if (!selectionCriteria.selectCpmVideo()) {
            ads.removeIf(b -> AdGroupType.CPM_VIDEO.equals(adGroupTypes.get(b.getAdGroupId())));
        }

        if (!selectionCriteria.selectCpmBanner()) {
            ads.removeIf(b -> AdGroupType.CPM_BANNER.equals(adGroupTypes.get(b.getAdGroupId())));
        }

        // не нашли подходящих баннеров - не стоит собирать связанные подобъекты
        if (ads.isEmpty()) {
            return emptyList();
        }

        Map<Long, Boolean> isBannerStoppedByMonitoring = bannerRepository
                .getEffectiveStopByMonitoringStatusByBannerIds(shard,
                        ads.stream().map(Banner::getId).collect(toList()));

        if (requestedFields.contains(TEXT_AD_EXTENSIONS) || requestedFields.contains(DYNAMIC_TEXT_AD_EXTENSIONS)) {
            Map<Long, BannerWithCallouts> adById = StreamEx.of(ads)
                    .select(BannerWithCallouts.class)
                    .collect(toMap(Banner::getId, identity()));

            Map<Long, List<Long>> calloutIdsByAdId =
                    calloutRepository.getExistingCalloutIdsByBannerIds(shard, adById.keySet());

            if (!calloutIdsByAdId.isEmpty()) {
                for (var ad : adById.values()) {
                    ad.withCalloutIds(calloutIdsByAdId.get(ad.getId()));
                }
            }
        }

        Collection<Long> campaignIds = listToSet(ads, BannerWithSystemFields::getCampaignId);
        Map<Long, Campaign> campaignById =
                listToMap(campaignService.getCampaigns(clientId, campaignIds), Campaign::getId);

        Map<Long, BannerWithCreativeModeration> bannerCreativeByAdId = new HashMap<>();
        Map<Long, Creative> creativeById = new HashMap<>();

        if (requestedFields.contains(TEXT_AD_VIDEO_EXTENSION)) {
            List<Long> textAdIds = filterAndMapList(ads, BannerTypeValidationPredicates::isTextBanner,
                    Banner::getId);
            readVideoCreatives(CreativeType.VIDEO_ADDITION_CREATIVE, textAdIds, bannerCreativeByAdId, creativeById,
                    clientId, shard);
        }

        if (requestedFields.contains(MOBILE_APP_AD_VIDEO_EXTENSION)) {
            List<Long> mobileAppAdIds = filterAndMapList(ads, BannerTypeValidationPredicates::isMobileAppBanner,
                    Banner::getId);
            readVideoCreatives(CreativeType.VIDEO_ADDITION_CREATIVE, mobileAppAdIds, bannerCreativeByAdId, creativeById,
                    clientId, shard);
        }

        if (requestedFields.contains(CPC_VIDEO_AD_BUILDER_AD_CREATIVE)
                || requestedFields.contains(MOBILE_APP_CPC_VIDEO_AD_BUILDER_AD_CREATIVE)) {
            List<Long> cpcVideoAdIds = filterAndMapList(ads, BannerTypeValidationPredicates::isCpcVideoBanner,
                    Banner::getId);
            readVideoCreatives(CreativeType.CPC_VIDEO_CREATIVE, cpcVideoAdIds, bannerCreativeByAdId, creativeById,
                    clientId, shard);
        }

        if (requestedFields.contains(CPM_VIDEO_AD_BUILDER_AD_CREATIVE)) {
            List<Long> cpmVideoAdIds = filterAndMapList(
                    ads,
                    ad -> AdGroupType.CPM_VIDEO.equals(adGroupTypes.get(ad.getAdGroupId())) &&
                            isCpmBanner(ad),
                    Banner::getId);
            readVideoCreatives(CreativeType.CPM_VIDEO_CREATIVE, cpmVideoAdIds, bannerCreativeByAdId, creativeById,
                    clientId, shard);
        }

        Map<Long, MobileContent> mobileContentByAdGroupId = emptyMap();
        if (requestedFields.contains(MOBILE_APP_AD_FEATURES)) {
            List<Long> adgroupIds = filterAndMapList(ads, BannerTypeValidationPredicates::isMobileAppBanner,
                    BannerWithAdGroupId::getAdGroupId);
            mobileContentByAdGroupId = mobileContentRepository.getMobileContentByAdGroupIds(shard, adgroupIds);
        }

        if (requestedFields.contains(TEXT_AD_BUILDER_AD_CREATIVE)
                || requestedFields.contains(MOBILE_APP_AD_BUILDER_AD_CREATIVE)
                || requestedFields.contains(CPM_BANNER_AD_BUILDER_AD_CREATIVE)
                || requestedFields.contains(SMART_AD_BUILDER_AD_CREATIVE)) {
            Predicate<Banner> isImageCreativeBanner =
                    BannerTypeValidationPredicates::isImageCreativeBanner;
            Predicate<Banner> isRequestedBannerType = isImageCreativeBanner
                    .or(BannerTypeValidationPredicates.isCpmBanner())
                    .or(BannerTypeValidationPredicates.isPerformanceBanner());
            List<Long> canvasAdIds = filterAndMapList(ads, isRequestedBannerType, Banner::getId);
            Map<Long, BannerWithCreativeModeration> canvasBannerCreativeByAdId = bannerCreativeRepository
                    .getBannersWithNotNullCreativeByBannerId(shard, canvasAdIds);

            if (!canvasBannerCreativeByAdId.isEmpty()) {
                bannerCreativeByAdId.putAll(canvasBannerCreativeByAdId);

                List<Long> creativeIds = mapList(canvasBannerCreativeByAdId.values(),
                        BannerWithCreative::getCreativeId);
                List<Creative> creativesWithTypes = creativeRepository.getCreativesWithTypes(shard,
                        clientId,
                        creativeIds,
                        asList(CreativeType.CANVAS, CreativeType.HTML5_CREATIVE, CreativeType.PERFORMANCE));
                Map<Long, Creative> canvasCreativeById = listToMap(creativesWithTypes, Creative::getId);

                if (!canvasCreativeById.isEmpty()) {
                    creativeById.putAll(canvasCreativeById);
                }
            }
        }

        Map<ModerationReasonObjectType, List<Long>> moderationReasonsByObjectTypeAndId
                = getModerationReasonsByTypeAndIdFilter(ads, requestedFields);

        return buildAdsGetContainerListFromCoreModels(
                clientId,
                ads,
                moderationReasonsByObjectTypeAndId,
                bannerCreativeByAdId,
                campaignById,
                isBannerStoppedByMonitoring,
                mobileContentByAdGroupId,
                creativeById,
                adGroupTypes,
                contentPromotionAdgroupTypes);
    }

    /**
     * Метод для определения "плохих" групп типа content_promotion.
     * "Плохая" == не поддерживается в API или поддерживается, но нет доступа у текущего оператора
     */
    private boolean badContentPromotionGroups(
            Map<Long, ContentPromotionAdgroupType> contentPromotionAdgroupTypes,
            boolean isServicesApp,
            BannerWithAdGroupId banner) {
        if (contentPromotionAdgroupTypes.get(banner.getAdGroupId()) == null) {
            // не content promotion -- всё хорошо
            return false;
        }
        ContentPromotionAdgroupType type = contentPromotionAdgroupTypes.get(banner.getAdGroupId());
        boolean allowedContentType = API5_ALLOWED_CONTENT_PROMOTION_AD_GROUP_TYPES.contains(type);
        if (!allowedContentType) {
            // не поддерживается в API
            return true;
        }
        boolean noAccessToTypeService = type == ContentPromotionAdgroupType.SERVICE && !isServicesApp;
        if (noAccessToTypeService) {
            // тип продвижения "Услуги" недоступен текущему приложению
            return true;
        }
        return false;
    }

    private static Map<ModerationReasonObjectType, List<Long>> getModerationReasonsByTypeAndIdFilter(
            List<BannerWithSystemFields> ads,
            Set<AdAnyFieldEnum> requestedFields) {
        Map<ModerationReasonObjectType, List<Long>> moderationReasonsByObjectTypeAndId =
                new EnumMap<>(ModerationReasonObjectType.class);

        if (requestedFields.contains(AD_STATUS_CLARIFICATION)) {
            List<Long> textAdIds = filterAndMapList(ads, BannerTypeValidationPredicates::isTextBanner,
                    Banner::getId);
            moderationReasonsByObjectTypeAndId.put(BANNER, textAdIds);
        }

        if (requestedFields.contains(TEXT_AD_V_CARD_MODERATION)
                || requestedFields.contains(DYNAMIC_TEXT_AD_V_CARD_MODERATION)) {
            List<Long> withVcardAdIds = StreamEx.of(ads)
                    .select(BannerWithVcard.class)
                    .filter(ad -> ad.getVcardId() != null)
                    .map(Banner::getId).collect(toList());
            moderationReasonsByObjectTypeAndId.put(CONTACTINFO, withVcardAdIds);
        }

        if (requestedFields.contains(TEXT_AD_SITELINKS_MODERATION)
                || requestedFields.contains(DYNAMIC_TEXT_AD_SITELINKS_MODERATION)) {
            List<Long> withSitelinksAdIds = StreamEx.of(ads)
                    .select(BannerWithSitelinks.class)
                    .filter(ad -> ad.getSitelinksSetId() != null)
                    .map(Banner::getId).collect(toList());
            moderationReasonsByObjectTypeAndId.put(SITELINKS_SET, withSitelinksAdIds);
        }

        if (requestedFields.contains(TEXT_AD_DISPLAY_URL_PATH_MODERATION)) {
            List<Long> withDisplayUrlAdIds = StreamEx.of(ads)
                    .select(BannerWithDisplayHref.class)
                    .filter(ad -> ad.getDisplayHref() != null)
                    .map(Banner::getId).collect(toList());
            moderationReasonsByObjectTypeAndId.put(DISPLAY_HREF, withDisplayUrlAdIds);
        }

        if (requestedFields.contains(TEXT_AD_IMAGE_MODERATION)
                || requestedFields.contains(MOBILE_APP_AD_IMAGE_MODERATION)
                || requestedFields.contains(DYNAMIC_TEXT_AD_IMAGE_MODERATION)) {
            List<Long> withImageAdIds = StreamEx.of(ads)
                    .select(BannerWithBannerImage.class)
                    .map(Banner::getId).collect(toList());
            moderationReasonsByObjectTypeAndId.put(IMAGE, withImageAdIds);
        }
        return moderationReasonsByObjectTypeAndId;
    }

    private List<AdsGetContainer> buildAdsGetContainerListFromCoreModels(
            ClientId clientId,
            List<BannerWithSystemFields> ads,
            Map<ModerationReasonObjectType, List<Long>> moderationReasonsByObjectTypeAndId,
            Map<Long, BannerWithCreativeModeration> bannerCreativeByAdId,
            Map<Long, Campaign> campaignById,
            Map<Long, Boolean> isBannerStoppedByMonitoring,
            Map<Long, MobileContent> mobileContentByAdGroupId,
            Map<Long, Creative> creativeById,
            Map<Long, AdGroupType> adGroupTypes,
            Map<Long, ContentPromotionAdgroupType> contentPromotionAdgroupTypes) {
        Map<Long, Map<ModerationReasonObjectType, List<ModerationDiag>>> moderationReasonsByAdId =
                moderationReasonService.getRejectReasonDiags(clientId, moderationReasonsByObjectTypeAndId);

        List<AdsGetContainer> results = new ArrayList<>();
        for (var ad : ads) {
            Long adId = ad.getId();

            BannerWithCreativeModeration bannerCreative = bannerCreativeByAdId.get(adId);

            AdsGetContainer.Builder builder = new AdsGetContainer.Builder()
                    .withAd(ad)
                    .isStoppedByMonitoring(isBannerStoppedByMonitoring.get(adId))
                    .withCampaign(campaignById.get(ad.getCampaignId()))
                    .withBannerCreative(bannerCreative)
                    .withMobileContent(mobileContentByAdGroupId.get(ad.getAdGroupId()))
                    .withCreative(
                            bannerCreative != null ? creativeById.get(bannerCreative.getCreativeId()) : null)
                    .withAdGroupType(adGroupTypes.get(ad.getAdGroupId()))
                    .withContentPromotionAdgroupType(contentPromotionAdgroupTypes.get(ad.getAdGroupId()));

            Map<ModerationReasonObjectType, List<ModerationDiag>> reasonsGroupedByType =
                    moderationReasonsByAdId.get(adId);
            builder.setReasonsByType(reasonsGroupedByType);
            results.add(builder.build());
        }

        return results;
    }

    private void readVideoCreatives(CreativeType type,
                                    List<Long> adIds,
                                    Map<Long, BannerWithCreativeModeration> bannerCreativeByAdId,
                                    Map<Long, Creative> creativeById,
                                    ClientId clientId, int shard) {
        Map<Long, BannerWithCreativeModeration> videoBannerCreativeByAdId =
                bannerCreativeRepository.getBannersWithNotNullCreativeByBannerId(shard, adIds);

        if (!videoBannerCreativeByAdId.isEmpty()) {
            bannerCreativeByAdId.putAll(videoBannerCreativeByAdId);

            List<Long> ids = mapList(videoBannerCreativeByAdId.values(),
                    BannerWithCreativeModeration::getCreativeId);
            creativeById.putAll(listToMap(creativeRepository
                    .getCreativesWithType(shard, clientId, ids, type), Creative::getId));
        }
    }

    @Override
    public GetResponse convertGetResponse(List<AdsGetContainer> result, Set<AdAnyFieldEnum> requestedFields,
                                          @Nullable Long limitedBy) {
        logger.debug("convert response - result {}, requestedFields {}, limitedBy {}", result, requestedFields,
                limitedBy);

        GetResponse response = new GetResponse().withLimitedBy(limitedBy);

        if (!result.isEmpty()) {
            List<AdGetItem> getItems = mapList(result, getResponseConverter::convertResponseItem);
            getResponseConverter.filterProperties(getItems, requestedFields);
            response.withAds(getItems);
        }

        return response;
    }
}
