package ru.yandex.direct.web.entity.uac.service

import java.math.BigDecimal
import org.slf4j.Logger
import ru.yandex.direct.core.entity.adgroup.container.ComplexDynamicAdGroup
import ru.yandex.direct.core.entity.adgroup.container.ComplexMobileContentAdGroup
import ru.yandex.direct.core.entity.adgroup.container.ComplexPerformanceAdGroup
import ru.yandex.direct.core.entity.adgroup.container.ComplexTextAdGroup
import ru.yandex.direct.core.entity.adgroup.model.AdGroup
import ru.yandex.direct.core.entity.adgroup.model.TextAdGroup
import ru.yandex.direct.core.entity.banner.container.ComplexBanner
import ru.yandex.direct.core.entity.banner.model.TextBanner
import ru.yandex.direct.core.entity.bidmodifiers.service.BidModifierService
import ru.yandex.direct.core.entity.campaign.model.CampaignSource
import ru.yandex.direct.core.entity.campaign.model.DbStrategy
import ru.yandex.direct.core.entity.campaign.model.DbStrategyBase
import ru.yandex.direct.core.entity.campaign.model.MobileContentCampaign
import ru.yandex.direct.core.entity.campaign.model.StrategyData
import ru.yandex.direct.core.entity.campaign.model.TextCampaign
import ru.yandex.direct.core.entity.campaign.service.CampaignService
import ru.yandex.direct.core.entity.campaign.service.uc.UcCampaignService
import ru.yandex.direct.core.entity.campaign.service.validation.CampaignDefects
import ru.yandex.direct.core.entity.campaign.service.validation.DisableDomainValidationService
import ru.yandex.direct.core.entity.client.service.ClientGeoService
import ru.yandex.direct.core.entity.client.service.ClientLimitsService
import ru.yandex.direct.core.entity.client.service.ClientService
import ru.yandex.direct.core.entity.feature.service.FeatureService
import ru.yandex.direct.core.entity.feature.service.enabled
import ru.yandex.direct.core.entity.feed.service.FeedService
import ru.yandex.direct.core.entity.image.repository.BannerImageFormatRepository
import ru.yandex.direct.core.entity.keyword.model.Keyword
import ru.yandex.direct.core.entity.keyword.service.validation.phrase.minusphrase.MinusPhraseConstraints.CAMPAIGN_MINUS_KEYWORDS_MAX_LENGTH
import ru.yandex.direct.core.entity.keyword.service.validation.phrase.minusphrase.MinusPhraseConstraints.maxLengthKeywordsWithoutSpecSymbolsAndSpaces
import ru.yandex.direct.core.entity.keyword.service.validation.phrase.minusphrase.MinusPhraseValidator
import ru.yandex.direct.core.entity.keyword.service.validation.phrase.minusphrase.MinusPhraseValidator.minusKeywordIsValid
import ru.yandex.direct.core.entity.performancefilter.schema.compiled.PerformanceDefault
import ru.yandex.direct.core.entity.performancefilter.service.PerformanceFilterStorage
import ru.yandex.direct.core.entity.region.validation.RegionIdsValidator
import ru.yandex.direct.core.entity.retargeting.service.uc.UcRetargetingConditionService
import ru.yandex.direct.core.entity.retargeting.service.validation2.AddRetargetingConditionValidationService2
import ru.yandex.direct.core.entity.sspplatform.repository.SspPlatformsRepository
import ru.yandex.direct.core.entity.uac.UacCommonUtils.CREATIVE_ID_KEY
import ru.yandex.direct.core.entity.uac.UacCommonUtils.CREATIVE_TYPE_KEY
import ru.yandex.direct.core.entity.uac.converter.UacBidModifiersConverter
import ru.yandex.direct.core.entity.uac.grut.GrutTransactionProvider
import ru.yandex.direct.core.entity.uac.model.AdvType
import ru.yandex.direct.core.entity.uac.model.AppInfo
import ru.yandex.direct.core.entity.uac.model.CampaignStatuses
import ru.yandex.direct.core.entity.uac.model.Content
import ru.yandex.direct.core.entity.uac.model.CreativeType
import ru.yandex.direct.core.entity.uac.model.MediaType
import ru.yandex.direct.core.entity.uac.model.Status
import ru.yandex.direct.core.entity.uac.model.TrackingUrl
import ru.yandex.direct.core.entity.uac.model.UacStrategyName
import ru.yandex.direct.core.entity.uac.model.relevance_match.UacRelevanceMatchCategory
import ru.yandex.direct.core.entity.uac.repository.ydb.UacYdbUtils
import ru.yandex.direct.core.entity.uac.repository.ydb.UacYdbUtils.toIdString
import ru.yandex.direct.core.entity.uac.service.BaseUacCampaignService
import ru.yandex.direct.core.entity.uac.service.CpmBannerCampaignService
import ru.yandex.direct.core.entity.uac.service.GrutUacCampaignService
import ru.yandex.direct.core.entity.uac.service.RmpCampaignService
import ru.yandex.direct.core.entity.uac.service.UacDisabledDomainsService
import ru.yandex.direct.core.entity.uac.service.UacGeoService.getGeoForUacGroups
import ru.yandex.direct.core.entity.uac.validation.maxImageContentSize
import ru.yandex.direct.core.entity.uac.validation.maxVideoContentSize
import ru.yandex.direct.core.entity.user.model.User
import ru.yandex.direct.dbutil.model.ClientId
import ru.yandex.direct.dbutil.model.UidAndClientId
import ru.yandex.direct.dbutil.sharding.ShardHelper
import ru.yandex.direct.feature.FeatureName
import ru.yandex.direct.feature.FeatureName.DISABLE_VIDEO_CREATIVE
import ru.yandex.direct.feature.FeatureName.ECOM_UC_NEW_BACKEND_ENABLED
import ru.yandex.direct.feature.FeatureName.ENABLE_ALTERNATIVE_STORES_IN_UAC
import ru.yandex.direct.feature.FeatureName.RELEVANCE_MATCH_CATEGORIES_ALLOWED_IN_UC
import ru.yandex.direct.feature.FeatureName.SEARCH_LIFT
import ru.yandex.direct.feature.FeatureName.SEARCH_RETARGETING_ENABLED
import ru.yandex.direct.feature.FeatureName.UC_CUSTOM_AUDIENCE_ENABLED
import ru.yandex.direct.feature.FeatureName.UC_MULTIPLE_ADS_ENABLED
import ru.yandex.direct.grid.processing.service.campaign.uc.UcCampaignMutationService
import ru.yandex.direct.grid.processing.service.validation.presentation.SkipByDefaultMappingPathNodeConverter
import ru.yandex.direct.libs.mirrortools.utils.HostingsHandler
import ru.yandex.direct.operation.Applicability
import ru.yandex.direct.regions.GeoTree
import ru.yandex.direct.result.Result
import ru.yandex.direct.validation.builder.Constraint.fromPredicate
import ru.yandex.direct.validation.builder.When
import ru.yandex.direct.validation.constraint.CollectionConstraints.listSize
import ru.yandex.direct.validation.constraint.CollectionConstraints.minListSize
import ru.yandex.direct.validation.constraint.CollectionConstraints.notEmptyCollection
import ru.yandex.direct.validation.constraint.CommonConstraints.inSet
import ru.yandex.direct.validation.constraint.CommonConstraints.isNull
import ru.yandex.direct.validation.constraint.CommonConstraints.notNull
import ru.yandex.direct.validation.constraint.CommonConstraints.notTrue
import ru.yandex.direct.validation.constraint.CommonConstraints.validId
import ru.yandex.direct.validation.constraint.NumberConstraints.notLessThan
import ru.yandex.direct.validation.constraint.StringConstraints.notBlank
import ru.yandex.direct.validation.defect.CommonDefects
import ru.yandex.direct.validation.defect.CommonDefects.invalidValue
import ru.yandex.direct.validation.result.DefaultPathNodeConverterProvider
import ru.yandex.direct.validation.result.DefaultPathNodeConverterProvider.builder
import ru.yandex.direct.validation.result.Defect
import ru.yandex.direct.validation.result.MappingPathNodeConverter
import ru.yandex.direct.validation.result.ValidationResult
import ru.yandex.direct.validation.util.check
import ru.yandex.direct.validation.util.checkEach
import ru.yandex.direct.validation.util.listProperty
import ru.yandex.direct.validation.util.property
import ru.yandex.direct.validation.util.validateObject
import ru.yandex.direct.web.entity.uac.converter.UacCampaignConverter.toCpmBannerCampaign
import ru.yandex.direct.web.entity.uac.converter.UacMobileCampaignConverter.toMobileApp
import ru.yandex.direct.web.entity.uac.converter.UacMobileCampaignConverter.toMobileContentCampaign
import ru.yandex.direct.web.entity.uac.converter.UacTextCampaignConverter.getGoalIds
import ru.yandex.direct.web.entity.uac.converter.UacTextCampaignConverter.toGdAddUcCampaignInput
import ru.yandex.direct.web.entity.uac.model.CreateCampaignInternalRequest
import ru.yandex.direct.web.entity.uac.model.CreateCampaignRequest
import ru.yandex.direct.web.entity.uac.model.UacCampaign
import ru.yandex.direct.web.entity.uac.model.UacModifyCampaignDataContainer
import ru.yandex.direct.web.entity.uac.service.UacConstants.UC_IMAGE_MAX_LENGTH
import ru.yandex.direct.web.entity.uac.service.UacConstants.UC_IMAGE_MAX_LENGTH_BY_FEATURE
import ru.yandex.direct.web.entity.uac.service.UacConstants.UC_TEXTS_MAX_LENGTH
import ru.yandex.direct.web.entity.uac.service.UacConstants.UC_TEXTS_MAX_LENGTH_BY_FEATURE
import ru.yandex.direct.web.entity.uac.service.UacConstants.UC_TITLES_MAX_LENGTH
import ru.yandex.direct.web.entity.uac.service.UacConstants.UC_TITLES_MAX_LENGTH_BY_FEATURE
import ru.yandex.direct.web.entity.uac.service.UacConstants.UC_VIDEO_MAX_LENGTH
import ru.yandex.direct.web.entity.uac.service.UacConstants.UC_VIDEO_MAX_LENGTH_BY_FEATURE
import ru.yandex.direct.web.entity.uac.validation.CpmAssetValidator
import ru.yandex.direct.web.entity.uac.validation.CpmImpressionLimitsValidator
import ru.yandex.direct.web.entity.uac.validation.CpmRetargetingsConditionsValidator
import ru.yandex.direct.web.entity.uac.validation.CpmSocdemValidator
import ru.yandex.direct.web.entity.uac.validation.UacDisabledPlacesValidator
import ru.yandex.direct.web.entity.uac.validation.UacFeedFiltersValidator
import ru.yandex.direct.web.entity.uac.validation.UacGoalsValidator
import ru.yandex.direct.web.entity.uac.validation.UacSearchLiftValidator
import ru.yandex.direct.web.entity.uac.validation.UcRetargetingConditionValidator

abstract class BaseUacCampaignAddService(
    private val uacMobileAppService: UacMobileAppService,
    private val uacModifyCampaignDataContainerFactory: UacModifyCampaignDataContainerFactory,
    private val rmpCampaignService: RmpCampaignService,
    private val cpmBannerCampaignService: CpmBannerCampaignService,
    private val ucCampaignMutationService: UcCampaignMutationService,
    private val ucRetargetingConditionService: UcRetargetingConditionService,
    private val uacGoalsService: UacGoalsService,
    private val ucCampaignService: UcCampaignService,
    private val clientGeoService: ClientGeoService,
    private val uacCampaignValidationService: UacCampaignValidationService,
    private val featureService: FeatureService,
    private val bidModifierService: BidModifierService,
    private val uacPropertiesService: UacPropertiesService,
    private val clientService: ClientService,
    private val sspPlatformsRepository: SspPlatformsRepository,
    private val hostingsHandler: HostingsHandler,
    private val bannerImageFormatRepository: BannerImageFormatRepository,
    private val shardHelper: ShardHelper,
    private val cpmBannerService: UacCpmBannerService,
    private val clientLimitsService: ClientLimitsService,
    private val disableDomainValidationService: DisableDomainValidationService,
    private val uacAdjustmentsService: UacAdjustmentsService,
    private val rmpStrategyValidatorFactory: RmpStrategyValidatorFactory,
    private val uacDisabledDomainsService: UacDisabledDomainsService,
    private val retargetingConditionValidationService: AddRetargetingConditionValidationService2,
    private val baseUacCampaignService: BaseUacCampaignService,
    private val campaignService: CampaignService,
    private val grutTransactionProvider: GrutTransactionProvider,
    private val feedService: FeedService,
    private val filterSchemaStorage: PerformanceFilterStorage,
) {

    abstract fun getLogger(): Logger

    companion object {
        private val SKIP_CONVERTER = SkipByDefaultMappingPathNodeConverter.emptyConverter()
        private val CAMPAIGN_CONVERTER = MappingPathNodeConverter
            .builder("CampaignConverter")
            .skip(DbStrategyBase.STRATEGY.name())
            .replace(TextCampaign.NAME.name(), CreateCampaignRequest::displayName.name)
            .replace(TextCampaign.MEANINGFUL_GOALS.name(), CreateCampaignRequest::goals.name)
            .replace(TextCampaign.BUDGET_DISPLAY_FORMAT.name(), CreateCampaignRequest::weekLimit.name)
            .replace(TextCampaign.METRIKA_COUNTERS.name(), CreateCampaignRequest::counters.name)
            .replace(TextCampaign.HREF.name(), CreateCampaignRequest::href.name)
            .build()
        private val AD_GROUP_CONVERTER = SkipByDefaultMappingPathNodeConverter.builder()
            .replace(ComplexTextAdGroup.KEYWORDS.name(), CreateCampaignRequest::keywords.name)
            .replace(AdGroup.GEO.name(), CreateCampaignRequest::regions.name)
            .replace(AdGroup.HYPER_GEO_ID.name(), CreateCampaignRequest::hyperGeoId.name)
            .replace(AdGroup.MINUS_KEYWORDS.name(), CreateCampaignRequest::minusKeywords.name)
            .replace(AdGroup.TRACKING_PARAMS.name(), CreateCampaignRequest::trackingParams.name)
            .build()
        private val KEYWORD_CONVERTER = MappingPathNodeConverter
            .builder("KeywordConverter")
            .skip(Keyword.PHRASE.name())
            .build()
        private val BANNER_CONVERTER = MappingPathNodeConverter
            .builder("BannerConverter")
            .skip(ComplexBanner.SITELINK_SET.name())
            .replace(TextBanner.TITLE.name(), CreateCampaignRequest::titles.name)
            .replace(TextBanner.BODY.name(), CreateCampaignRequest::texts.name)
            .replace(TextBanner.IMAGE_HASH.name(), CreateCampaignRequest::contentIds.name)
            .replace(TextBanner.CREATIVE_ID.name(), CreateCampaignRequest::contentIds.name)
            .build()
        private val STRATEGY_CONVERTER = MappingPathNodeConverter
            .builder("StrategyConverter")
            .replace(StrategyData.SUM.name(), CreateCampaignRequest::weekLimit.name)
            .replace(StrategyData.AVG_CPI.name(), CreateCampaignRequest::cpa.name)
            .build()
        val UAC_PATH_NODE_CONVERTER_PROVIDER: DefaultPathNodeConverterProvider = builder()
            .register(TextCampaign::class.java, CAMPAIGN_CONVERTER)
            .register(MobileContentCampaign::class.java, CAMPAIGN_CONVERTER)
            .register(DbStrategy::class.java, SKIP_CONVERTER)
            .register(TextAdGroup::class.java, AD_GROUP_CONVERTER)
            .register(ComplexTextAdGroup::class.java, AD_GROUP_CONVERTER)
            .register(ComplexDynamicAdGroup::class.java, AD_GROUP_CONVERTER)
            .register(ComplexPerformanceAdGroup::class.java, AD_GROUP_CONVERTER)
            .register(ComplexMobileContentAdGroup::class.java, AD_GROUP_CONVERTER)
            .register(Keyword::class.java, KEYWORD_CONVERTER)
            .register(TextBanner::class.java, BANNER_CONVERTER)
            .register(StrategyData::class.java, STRATEGY_CONVERTER)
            .build()
    }

    fun addUacCampaign(
        operator: User,
        subjectUser: User,
        request: CreateCampaignInternalRequest,
    ): Result<UacCampaign> {
        val operation = UacCampaignAddOperation(
            operator = operator,
            subjectUser = subjectUser,
            request,
        )
        val validationResult = operation.prepare()
        if (validationResult.hasAnyErrors()) {
            return Result.broken(validationResult)
        }

        return operation.apply()
    }

    inner class UacCampaignAddOperation(
        private val operator: User,
        private val subjectUser: User,
        private val request: CreateCampaignInternalRequest,
    ) {
        private var prepared = false
        private var applied = false

        private var appInfo: AppInfo? = null
        private var trackingUrl: TrackingUrl? = null
        private var impressionUrl: TrackingUrl? = null
        private var contentById: Map<String, Content>? = null

        private lateinit var clientGeoTree: GeoTree

        fun prepare(): ValidationResult<CreateCampaignInternalRequest, Defect<*>> {
            check(!prepared) { "Operation was already prepared" }
            prepared = true

            val clientId = subjectUser.clientId
            val multipleAdsInUc = featureService.isEnabledForClientId(subjectUser.clientId, UC_MULTIPLE_ADS_ENABLED)
            val relevanceMatchCategoriesAreAllowed = featureService
                .isEnabledForClientId(subjectUser.clientId, RELEVANCE_MATCH_CATEGORIES_ALLOWED_IN_UC)
            val altAppStoresAreAllowed = featureService
                .isEnabledForClientId(subjectUser.clientId, ENABLE_ALTERNATIVE_STORES_IN_UAC)

            clientGeoTree = clientGeoService.getClientTranslocalGeoTree(subjectUser.clientId)

            val contentById = getContents(request.contentIds ?: emptyList())
                .associateBy { it.id }

            this.contentById = contentById

            return validateObject(request) {
                listProperty(CreateCampaignInternalRequest::contentIds) {
                    checkEach(CommonDefects.objectNotFound()) { contentId ->
                        val content = contentById[contentId]
                        content != null
                    }
                    checkEach(invalidValue()) { contentId ->
                        val content = contentById[contentId]
                        (request.advType != AdvType.MOBILE_CONTENT || content?.type != MediaType.SITELINK)
                            && (request.advType != AdvType.TEXT || content?.type != MediaType.HTML5)
                    }
                    checkEach(invalidValue()) { contentId ->
                        // todo отдельный дефект "видео не готово"?
                        val content = contentById[contentId]
                        val creativeId = content?.meta?.get(CREATIVE_ID_KEY)
                        content?.type != MediaType.VIDEO || creativeId != null
                    }
                }

                property(CreateCampaignInternalRequest::regions) {
                    checkBy(
                        { regions -> RegionIdsValidator().apply(regions, clientGeoTree) },
                        When.isTrue(request.regions != null && (request.regions.isNotEmpty() || request.advType != AdvType.MOBILE_CONTENT))
                    )
                }

                listProperty(CreateCampaignInternalRequest::titles) {
                    checkEachBy(uacCampaignValidationService.getTitleValidator())
                }
                listProperty(CreateCampaignInternalRequest::texts) {
                    checkEachBy(uacCampaignValidationService.getTextValidator(request.advType))
                }
                listProperty(CreateCampaignInternalRequest::adjustments) {
                    check(isNull(), When.isTrue(request.advType != AdvType.MOBILE_CONTENT))
                    checkEach(invalidValue()) { adjustment -> uacAdjustmentsService.isValidAdjustment(adjustment) }
                }
                if (request.advType == AdvType.MOBILE_CONTENT) {

                    appInfo = request.appId?.let { uacMobileAppService.getAppInfo(it) }
                    trackingUrl = uacMobileAppService.getTrackingUrl(request.trackingUrl, appInfo)
                    impressionUrl = uacMobileAppService.getImpressionUrl(request.impressionUrl, appInfo)

                    property(CreateCampaignInternalRequest::trackingUrl) {
                        check(notBlank())
                        check(
                            { trackingUrl -> uacMobileAppService.validateTrackingUrl(trackingUrl, appInfo) },
                            When.isValid()
                        )
                    }
                    property(CreateCampaignInternalRequest::impressionUrl) {
                        check(notBlank())
                        check(uacMobileAppService::validateImpressionUrl)
                    }
                } else {
                    property(CreateCampaignInternalRequest::trackingUrl) {
                        check(isNull())
                    }
                    property(CreateCampaignInternalRequest::impressionUrl) {
                        check(isNull())
                    }
                }

                if (request.advType == AdvType.CPM_BANNER) {
                    property(CreateCampaignInternalRequest::contentIds) {
                        check(notNull())
                        check(
                            fromPredicate(
                                { _ ->
                                    contentById.values.mapNotNull { it.meta[CREATIVE_TYPE_KEY] }
                                        .distinct().size == 1
                                },
                                CommonDefects.invalidValue()
                            )
                        )
                    }

                    property(CreateCampaignInternalRequest::titles) {
                        check(isNull())
                    }
                    property(CreateCampaignInternalRequest::texts) {
                        check(isNull())
                    }
                    property(CreateCampaignInternalRequest::videosAreNonSkippable) {
                        check(notNull())
                        check(notTrue(),
                            When.isFalse(contentById.values.mapNotNull { it.meta[CREATIVE_TYPE_KEY] }.any { it == CreativeType.NON_SKIPPABLE_CPM.id }))
                    }
                    property(CreateCampaignInternalRequest::socdem) {
                        check(notNull())
                        checkBy(CpmSocdemValidator(), When.isValid())
                    }
                    property(CreateCampaignInternalRequest::showsFrequencyLimit) {
                        checkBy(CpmImpressionLimitsValidator(), When.notNull())
                    }
                    property(CreateCampaignInternalRequest::retargetingCondition) {
                        checkBy(CpmRetargetingsConditionsValidator(), When.notNull())
                    }

                    val cpmAssetValidator = CpmAssetValidator(
                        uacCampaignValidationService,
                        bannerImageFormatRepository,
                        shardHelper,
                        subjectUser.clientId
                    )
                    property(CreateCampaignInternalRequest::cpmAssets) {
                        check(
                            fromPredicate({ cpmAssets ->
                                (contentById.keys - cpmAssets?.keys.orEmpty()).isEmpty()
                            }, invalidValue()),
                            When.notNull()
                        )

                        request.cpmAssets?.entries?.forEach {
                            item(it.value, it.key).checkBy(
                                cpmAssetValidator
                            )
                        }
                    }
                    val searchLiftEnabled = featureService
                        .isEnabledForClientId(subjectUser.clientId, SEARCH_LIFT)

                    property(CreateCampaignInternalRequest::searchLift) {
                        check(isNull(), When.isFalse(searchLiftEnabled))
                        checkBy(UacSearchLiftValidator(), When.notNullAnd(When.isTrue(searchLiftEnabled)))

                    }
                } else {
                    property(CreateCampaignInternalRequest::cpmAssets) {
                        check(isNull())
                    }
                    property(CreateCampaignInternalRequest::uacBrandsafety) {
                        check(isNull())
                    }
                }

                if (request.advType == AdvType.TEXT) {

                    val ucCustomAudienceEnabled = featureService
                        .isEnabledForClientId(subjectUser.clientId, UC_CUSTOM_AUDIENCE_ENABLED)

                    val ucRetargetingConditionValidator = UcRetargetingConditionValidator(
                        retargetingConditionValidationService,
                        subjectUser.clientId,
                    )

                    val maxTextsSize = if (multipleAdsInUc) UC_TEXTS_MAX_LENGTH_BY_FEATURE else UC_TEXTS_MAX_LENGTH
                    val maxTitlesSize = if (multipleAdsInUc) UC_TITLES_MAX_LENGTH_BY_FEATURE else UC_TITLES_MAX_LENGTH
                    val maxImagesSize = if (multipleAdsInUc) UC_IMAGE_MAX_LENGTH_BY_FEATURE else UC_IMAGE_MAX_LENGTH
                    val maxVideosSize = if (multipleAdsInUc) UC_VIDEO_MAX_LENGTH_BY_FEATURE else UC_VIDEO_MAX_LENGTH

                    val imageContentById = contentById.filterValues { it.type == MediaType.IMAGE }
                    val videoContentById = contentById.filterValues { it.type == MediaType.VIDEO }

                    property(CreateCampaignInternalRequest::texts) {
                        check(listSize(1, maxTextsSize))
                    }

                    property(CreateCampaignInternalRequest::titles) {
                        check(listSize(1, maxTitlesSize))
                    }

                    property(CreateCampaignInternalRequest::contentIds) {
                        check(maxImageContentSize(maxImagesSize)) {
                            imageContentById.size <= maxImagesSize
                        }
                        check(maxVideoContentSize(maxVideosSize)) {
                            videoContentById.size <= maxVideosSize
                        }
                    }
                    property(CreateCampaignInternalRequest::goals) {
                        checkBy(UacGoalsValidator(request.cpa, request.crr))
                    }
                    property(CreateCampaignInternalRequest::strategy) {
                        check(isNull())
                    }
                    property(CreateCampaignInternalRequest::retargetingCondition) {
                        check(isNull(), When.isFalse(ucCustomAudienceEnabled))
                        checkBy(ucRetargetingConditionValidator, When.notNullAnd(When.isTrue(ucCustomAudienceEnabled)))
                    }
                    property(CreateCampaignInternalRequest::uacDisabledPlaces) {
                        check(isNull())
                    }
                } else {
                    property(CreateCampaignInternalRequest::zenPublisherId) {
                        check(isNull())
                    }
                    val uacDisabledPlacesValidator = UacDisabledPlacesValidator(
                        clientLimitsService,
                        subjectUser.clientId,
                        hostingsHandler,
                        disableDomainValidationService,
                        sspPlatformsRepository
                    )
                    property(CreateCampaignInternalRequest::uacDisabledPlaces) {
                        checkBy(uacDisabledPlacesValidator, When.notNull())
                    }
                }

                if (request.advType == AdvType.MOBILE_CONTENT) {
                    property(CreateCampaignInternalRequest::texts) {
                        check(listSize(0, uacPropertiesService.maxTexts))
                    }

                    property(CreateCampaignInternalRequest::titles) {
                        check(listSize(0, uacPropertiesService.maxTitles))
                    }

                    listProperty(CreateCampaignInternalRequest::minusKeywords) {
                        checkBy(minusKeywordIsValid(MinusPhraseValidator.ValidationMode.ONE_ERROR_PER_TYPE))
                        check(maxLengthKeywordsWithoutSpecSymbolsAndSpaces(CAMPAIGN_MINUS_KEYWORDS_MAX_LENGTH))
                    }

                    property(CreateCampaignInternalRequest::appId) {
                        check(CommonDefects.objectNotFound()) {
                            appInfo != null
                        }
                    }

                    property(CreateCampaignInternalRequest::cpa) {
                        check(notNull(), When.isTrue(request.strategy == null))
                    }

                    property(CreateCampaignInternalRequest::adjustments) {
                        check(listSize(0, UacConstants.UAC_ADJUSTMENTS_MAX_LENGTH))
                    }

                    property(CreateCampaignInternalRequest::crr) {
                        check(isNull())
                    }

                    // РМП пока не поддерживает ecom-сценарии, так что запрещаем передавать соответствующее:
                    property(CreateCampaignInternalRequest::isEcom) {
                        check(isNull())
                    }

                    property(CreateCampaignInternalRequest::feedId) {
                        check(isNull())
                    }

                    property(CreateCampaignInternalRequest::feedFilters) {
                        check(isNull())
                    }

                    property(CreateCampaignInternalRequest::trackingParams) {
                        check(isNull())
                    }

                    val isSkadNetworkEnabled = request.skadNetworkEnabled ?: false
                    property(CreateCampaignInternalRequest::strategy) {
                        check(notNull(), When.isTrue(request.cpa == null))
                        checkBy(
                            rmpStrategyValidatorFactory.createRmpStrategyValidator(
                                subjectUser.clientId,
                                uacGoalsService.getAvailableStrategyGoalsForRmp(clientId, trackingUrl, appInfo, isSkadNetworkEnabled)
                            ), When.notNull()
                        )
                    }
                    property(CreateCampaignInternalRequest::targetId) {
                        val avl = uacGoalsService.getAvailableStrategyGoalsForRmp(clientId, trackingUrl, appInfo, isSkadNetworkEnabled)
                        check(fromPredicate({
                            val strategies: Set<UacStrategyName>? = avl.goals[it]
                            strategies?.contains(request.strategy?.uacStrategyName) ?: false
                        }, invalidValue()), When.isTrue(request.strategy != null))
                    }
                    property(CreateCampaignInternalRequest::retargetingCondition) {
                        check(fromPredicate({ it?.id != null }, invalidValue()), When.notNull())
                    }
                }

                if (request.advType != AdvType.CPM_BANNER) {
                    property(CreateCampaignInternalRequest::videosAreNonSkippable) {
                        check(isNull())
                    }
                    property(CreateCampaignInternalRequest::brandSurveyId) {
                        check(isNull())
                    }
                    property(CreateCampaignInternalRequest::brandSurveyName) {
                        check(isNull())
                    }

                    property(CreateCampaignInternalRequest::showsFrequencyLimit) {
                        check(isNull())
                    }
                    property(CreateCampaignInternalRequest::searchLift) {
                        check(isNull())
                    }
                }

                if (request.isEcom != null && request.isEcom) {
                    val counterIdsLongList = request.counters?.map { it.toLong() } ?: listOf()
                    val availableCounters =
                        uacGoalsService.getAvailableCounters(subjectUser.clientId, counterIdsLongList)
                    val goalsByIds =
                        uacGoalsService.getAvailableGoalsForEcomCampaign(operator.uid, subjectUser.clientId, request)

                    listProperty(CreateCampaignInternalRequest::counters) {
                        check(notNull())
                        check(notEmptyCollection())
                        check({ counters ->
                            if (availableCounters.intersect(counters).isEmpty()) {
                                CampaignDefects.metrikaCounterIsUnavailable()
                            } else null
                        }, When.isValid())
                        checkEach(notNull())
                    }

                    property(CreateCampaignInternalRequest::feedId) {
                        check(notNull())
                        check(validId())
                    }

                    if (request.feedId != null) {
                        val feed = feedService.getFeedsSimple(
                            shardHelper.getShardByClientIdStrictly(clientId), listOf(request.feedId)).getOrNull(0)
                        val filterSchema = feed
                            ?.let { filterSchemaStorage.getFilterSchema(it.businessType, it.feedType) }
                            ?: PerformanceDefault()

                        val uacFeedFiltersValidator = UacFeedFiltersValidator(filterSchema)

                        property(CreateCampaignInternalRequest::feedFilters) {
                            checkBy(uacFeedFiltersValidator, When.notNull())
                        }
                    }

                    property(CreateCampaignInternalRequest::goals) {
                        check(notNull())
                        check(minListSize(1))
                        check({ goals ->
                            val availableRequestedCounters = availableCounters.intersect(request.counters ?: setOf())

                            val counterIds = goals?.map { goalsByIds[it.goalId]?.counterId }
                            val counterId = counterIds?.firstOrNull()
                            if (counterIds?.any { it != counterId || !availableRequestedCounters.contains(it) } != false) {
                                invalidValue()
                            } else null
                        }, When.isValid())
                    }

                    // В случае Еком сценария недельный бюджет опционален, но если присутствует, то должен быть не меньше
                    // трёх (по числе создаваемых кампаний) минимальных сумм недельного бюджета для валюты клиента
                    // Эти расчёты не актуальны для товарной кампании на основе единственной кампании в Директе
                    val workCurrency = clientService.getWorkCurrency(subjectUser.clientId)
                    val minWeekLimit = if (ECOM_UC_NEW_BACKEND_ENABLED.enabled()) {
                        workCurrency.minAutobudget
                    } else {
                        workCurrency.minAutobudget.multiply(BigDecimal(3))
                    }
                    property(CreateCampaignInternalRequest::weekLimit) {
                        check(notLessThan(minWeekLimit), When.notNull())
                    }
                }

                if (request.source != null) {
                    property(CreateCampaignInternalRequest::source) {
                        check(inSet(setOf(CampaignSource.UAC, CampaignSource.WIDGET)))
                    }
                }

                property(CreateCampaignInternalRequest::relevanceMatch) {
                    check(
                        isNull(),
                        When.isTrue(request.advType != AdvType.TEXT || !relevanceMatchCategoriesAreAllowed)
                    )
                    check({ relevanceMatch ->
                        // Если категории не переданы - все включены. Если переданы - EXACT должен быть всегда включен
                        if (relevanceMatch != null
                            && relevanceMatch.active
                            && relevanceMatch.categories.isNotEmpty()
                            && !relevanceMatch.categories.contains(UacRelevanceMatchCategory.EXACT_MARK)
                        ) {
                            invalidValue()
                        } else null
                    }, When.isValid())
                }
                property(CreateCampaignInternalRequest::showTitleAndBody) {
                    check(
                        notTrue(),
                        When.isFalse(featureService.isEnabledForClientId(subjectUser.clientId, DISABLE_VIDEO_CREATIVE))
                    )
                }
                property(CreateCampaignInternalRequest::altAppStores) {
                    check(isNull(), When.isFalse(altAppStoresAreAllowed))
                }
            }
        }

        fun apply(): Result<UacCampaign> {
            check(prepared) { "Operation is not prepared" }
            check(!applied) { "Operation was already applied" }
            applied = true

            val campaignId = UacYdbUtils.generateUniqueRandomId()
            val createDataContainer = uacModifyCampaignDataContainerFactory.dataContainerFromCreateRequest(
                campaignId,
                request, appInfo, trackingUrl, impressionUrl,
                FeatureName.ADVANCED_GEOTARGETING.enabled()
            )
            val contents = contentById!!.values

            val campaign: Result<Long> = when (request.advType) {
                AdvType.TEXT -> addUcCampaign(
                    operator = operator,
                    subjectUser = subjectUser,
                    createDataContainer,
                    contents,
                    clientGeoTree
                )
                AdvType.MOBILE_CONTENT -> addMobileContentCampaign(
                    operator = operator,
                    subjectUser = subjectUser,
                    createDataContainer
                )
                AdvType.CPM_BANNER -> addCpmBannerCampaign(
                    operator = operator,
                    subjectUser = subjectUser,
                    createDataContainer,
                    contents
                )
            }

            if (!campaign.isSuccessful) {
                return Result.broken(campaign.validationResult)
            }
            val directCampaignId = campaign.result!!

            if (
                request.advType == AdvType.MOBILE_CONTENT
                && (!createDataContainer.adjustments.isNullOrEmpty() || createDataContainer.retargetingCondition != null)
            ) {
                val bidModifiers = UacBidModifiersConverter.toBidModifiers(
                    createDataContainer.adjustments,
                    createDataContainer.retargetingCondition?.id,
                    directCampaignId,
                    featureService.isEnabledForClientId(subjectUser.clientId, SEARCH_RETARGETING_ENABLED)
                )
                val result = bidModifierService.add(bidModifiers, subjectUser.clientId, operator.uid)
                if (!result.isSuccessful) {
                    return Result.broken(result.validationResult)
                }
            }

            val useGrut = baseUacCampaignService is GrutUacCampaignService
            val uacCampaign = try {
                grutTransactionProvider.runInTransactionIfNeeded(useGrut) {
                    saveBrief(
                        directCampaignId,
                        operator,
                        subjectUser,
                        createDataContainer,
                        contents,
                        CampaignStatuses(
                            status = Status.DRAFT,
                            targetStatus = createDataContainer.targetStatus,
                        ),
                    )
                }
            } catch (e: Throwable) {
                if (useGrut) revertOnError(subjectUser.clientId, directCampaignId, operator.uid)
                throw e
            }

            return Result.successful(uacCampaign)
        }
    }

    private fun addUcCampaign(
        operator: User,
        subjectUser: User,
        createDataContainer: UacModifyCampaignDataContainer,
        contents: Collection<Content>,
        clientGeoTree: GeoTree,
    ): Result<Long> {
        val currencyCode = clientService.getWorkCurrency(subjectUser.clientId).code
        val gdCampaign = toGdAddUcCampaignInput(createDataContainer, contents, currencyCode)
        val clientId = subjectUser.clientId
        val autoRetargeting = ucRetargetingConditionService.getAutoRetargetingCondition(
            clientId,
            createDataContainer.counters,
            getGoalIds(gdCampaign),
            null
        )

        val operatorUid = operator.uid
        val addingGroupVr = ucCampaignMutationService.validateAddingGroup(
            gdCampaign,
            clientGeoTree,
            operatorUid,
            clientId,
            autoRetargeting
        )
        if (addingGroupVr.hasAnyErrors()) {
            return Result.broken(addingGroupVr)
        }
        return ucCampaignMutationService.createCampaign(
            gdCampaign,
            operatorUid,
            subjectUser,
            createDataContainer.minusKeywords
        )
    }

    protected fun addMobileContentCampaign(
        operator: User,
        subjectUser: User,
        createDataContainer: UacModifyCampaignDataContainer,
    ): Result<Long> {
        val geo = getGeoForUacGroups(createDataContainer.regions, createDataContainer.minusRegions)
        val adGroupVr = uacCampaignValidationService.validateUacAdGroup(
            subjectUser.clientId, createDataContainer.keywords, listOf(), geo,
        )
        if (!adGroupVr.isSuccessful) {
            val vrErrors: String = adGroupVr.validationResult?.flattenErrors()!!.joinToString("; ")
            getLogger().error("error while validating groups: {}", vrErrors)
            return (adGroupVr as Result<Long>)
        }
        val mobileApp = toMobileApp(
            createDataContainer.appInfo!!,
            createDataContainer.trackingUrl,
            createDataContainer.impressionUrl,
        )
        val disabledDomains = uacDisabledDomainsService.getDisabledDomains(
            createDataContainer.uacDisabledPlaces?.disabledPlaces.orEmpty()
        )
        val enabledFeaturesForClient = featureService.getEnabledForClientId(subjectUser.clientId)

        val mobileContentCampaign = toMobileContentCampaign(
            createDataContainer,
            operator,
            enabledFeaturesForClient,
            disabledDomains
        )
        getLogger().info("Mobile content campaign: $mobileContentCampaign")

        val uidAndClientId = UidAndClientId.of(subjectUser.chiefUid, subjectUser.clientId)
        val result = rmpCampaignService.addRmpCampaign(
            operator,
            uidAndClientId,
            mobileContentCampaign,
            mobileApp
        )

        if (!result.validationResult?.flattenErrors().isNullOrEmpty()) {
            val vrErrors: String = result.validationResult?.flattenErrors()!!.joinToString("; ")
            getLogger().error("error while creating campaigns: {}", vrErrors)
        }
        if (!result.isSuccessful) {
            return Result.broken(result.validationResult)
        }
        return Result.successful(result.result.id)
    }

    private fun addCpmBannerCampaign(
        operator: User,
        subjectUser: User,
        createDataContainer: UacModifyCampaignDataContainer,
        contents: Collection<Content>
    ): Result<Long> {
        val uidAndClientId = UidAndClientId.of(subjectUser.chiefUid, subjectUser.clientId)

        val clientId = subjectUser.clientId
        clientGeoService.getClientTranslocalGeoTree(clientId)
        cpmBannerService.fillCampaign(clientId, createDataContainer)

        val addingGroupVr: ValidationResult<AdGroup, Defect<*>> =
            uacCampaignValidationService.validateAddingCpmGroup(createDataContainer, contents, clientId)

        if (addingGroupVr.hasAnyErrors()) {
            return Result.broken(addingGroupVr)
        }

        val cpmBannerCampaign =
            toCpmBannerCampaign(createDataContainer, subjectUser, sspPlatformsRepository, hostingsHandler)
        val result = cpmBannerCampaignService.addCpmBannerCampaign(operator, uidAndClientId, cpmBannerCampaign)

        if (!result.validationResult?.flattenErrors().isNullOrEmpty()) {
            val vrErrors: String = result.validationResult?.flattenErrors()!!.joinToString("; ")
            getLogger().warn("error while creating campaigns: {}", vrErrors)
        }

        if (!result.isSuccessful) {
            return Result.broken(result.validationResult)
        }

        return Result.successful(cpmBannerCampaign.id)
    }

    private fun revertOnError(clientId: ClientId, campaignId: Long, uid: Long) {

        val corrupted = baseUacCampaignService
            .getCampaignById(campaignId.toIdString()) == null

        if (corrupted) {
            val result = campaignService.deleteCampaigns(listOf(campaignId), uid, clientId, Applicability.FULL)
            if (result.isSuccessful) {
                getLogger().info("Corrupted campaign $campaignId was successfully deleted for client $clientId ")
            } else {
                getLogger().error("Fatal error! Failed to delete corrupted campaign $campaignId for client $clientId")
            }
        }
    }

    abstract fun saveBrief(
        directCampaignId: Long,
        operator: User,
        subjectUser: User,
        createDataContainer: UacModifyCampaignDataContainer,
        contents: Collection<Content>,
        campaignStatuses: CampaignStatuses,
    ): UacCampaign

    abstract fun getContents(
        contentIds: Collection<String>,
    ): List<Content>
}
