package ru.yandex.direct.api.v5.entity.campaigns.validation;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

import com.yandex.direct.api.v5.campaigns.AddRequest;
import com.yandex.direct.api.v5.campaigns.CampaignAddItem;
import one.util.streamex.EntryStream;

import ru.yandex.direct.api.v5.entity.campaigns.CampaignDefectTypes;
import ru.yandex.direct.api.v5.validation.DefectType;
import ru.yandex.direct.core.entity.campaign.model.BaseCampaign;
import ru.yandex.direct.core.entity.timetarget.model.GeoTimezone;
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.result.ValidationResult;

import static ru.yandex.direct.api.v5.entity.campaigns.CampaignDefectTypes.maxCampaignsPerAddRequest;
import static ru.yandex.direct.api.v5.entity.campaigns.CampaignDefectTypes.networkDefaultLimitPercentNotInRange;
import static ru.yandex.direct.api.v5.entity.campaigns.CampaignDefectTypes.networkDefaultLimitPercentNotMultiple;
import static ru.yandex.direct.api.v5.entity.campaigns.CampaignDefectTypes.networkStrategyHasMoreThanOneStructureWithSettings;
import static ru.yandex.direct.api.v5.entity.campaigns.CampaignDefectTypes.networkStrategyMustContainStructureWithSettings;
import static ru.yandex.direct.api.v5.entity.campaigns.CampaignDefectTypes.searchStrategyHasMoreThanOneStructureWithSettings;
import static ru.yandex.direct.api.v5.entity.campaigns.CampaignDefectTypes.searchStrategyMustContainStructureWithSettings;
import static ru.yandex.direct.api.v5.entity.campaigns.CampaignDefectTypes.structureForNetworkStrategyDoesNotMatchStrategyName;
import static ru.yandex.direct.api.v5.entity.campaigns.CampaignDefectTypes.structureForSearchStrategyDoesNotMatchStrategyName;
import static ru.yandex.direct.api.v5.validation.DefectTypes.possibleOnlyOneField;
import static ru.yandex.direct.api.v5.validation.DefectTypes.requiredAtLeastOneOfFields;
import static ru.yandex.direct.core.entity.campaign.model.CampaignTypeKinds.API5_EDIT_CAMPAIGNS;

@ParametersAreNonnullByDefault
public class CampaignsRequestValidator {

    private static final String API5_EDIT_CAMPAIGNS_STRING = API5_EDIT_CAMPAIGNS.stream()
            .map(Object::toString)
            .collect(Collectors.joining(", "));

    private static final Integer ADD_IDS_LIMIT = 10;

    private CampaignsRequestValidator() {
        // no instantiation
    }

    public static ValidationResult<AddRequest, DefectType> validateExternalRequest(AddRequest externalRequest) {
        ItemValidationBuilder<AddRequest, DefectType> vb = ItemValidationBuilder.of(externalRequest);

        vb.item(externalRequest.getCampaigns(), "campaigns")
                .check(campaignsCountNotMoreThanMax());

        if (vb.getResult().hasAnyErrors()) {
            return vb.getResult();
        }
        return null;
    }

    private static Constraint<List<CampaignAddItem>, DefectType> campaignsCountNotMoreThanMax() {
        return campaigns -> campaigns.size() > ADD_IDS_LIMIT ?
                maxCampaignsPerAddRequest(ADD_IDS_LIMIT) : null;
    }

    /**
     * Валидация опирается на специальные значения из {@link CampaignsApiValidationSignals},
     * проставляемые конвертером, таким образом он передаёт информацию об ошибках в исходном запросе.
     */
    public static ValidationResult<List<BaseCampaign>, DefectType> validateInternalRequest(
            List<BaseCampaign> internalRequest) {
        var realModels = CampaignsApiValidationSignals.getCampaigns(internalRequest);
        return ListValidationBuilder.<BaseCampaign, DefectType>of(realModels)
                .checkEachBy((index, campaign) -> CampaignsApiValidationSignals.getExternalRequestDefects(
                        internalRequest.get(index)))
                .getResult();
    }

    /**
     * Аналог перлового sub _validate_campaign_type
     */
    public static Constraint<CampaignAddItem, DefectType> validCampaignType() {
        return campaignAddItem -> {
            long campaignsCount = Stream.of(
                            campaignAddItem.getTextCampaign(),
                            campaignAddItem.getCpmBannerCampaign(),
                            campaignAddItem.getDynamicTextCampaign(),
                            campaignAddItem.getMobileAppCampaign(),
                            campaignAddItem.getSmartCampaign())
                    .filter(Objects::nonNull)
                    .count();
            if (campaignsCount > 1) {
                return possibleOnlyOneField(API5_EDIT_CAMPAIGNS_STRING);
            }
            if (campaignsCount == 0) {
                return requiredAtLeastOneOfFields(API5_EDIT_CAMPAIGNS_STRING);
            }
            return null;
        };
    }


    /**
     * Проверяет соответствует ли название стратегии названию структуры с настройками
     */
    @Nullable
    public static <S, T extends Enum<T>> DefectType validateStrategyConsistent(
            S strategy,
            T strategyType,
            Map<T, Function<S, Object>> structureGetterByStrategyType,
            Set<T> strategyTypesWithOptionalStructure,
            boolean isSearch) {

        List<T> structureNames = EntryStream.of(structureGetterByStrategyType)
                .filterValues(getter -> getter.apply(strategy) != null)
                .keys()
                .toList();
        boolean needStructure = structureGetterByStrategyType.containsKey(strategyType)
                && !strategyTypesWithOptionalStructure.contains(strategyType);

        if (structureNames.size() > 1) {
            return strategyHasMoreThanOneStructureWithSettings(isSearch);

        } else if ((structureNames.size() == 1) && (structureNames.get(0) != strategyType)) {
            return structureForStrategyDoesNotMatchStrategyName(isSearch);

        } else if (structureNames.isEmpty() && needStructure) {
            return strategyMustContainStructureWithSettings(isSearch, strategyType.name());
        }
        return null;
    }

    private static DefectType strategyHasMoreThanOneStructureWithSettings(boolean isSearch) {
        return isSearch ? searchStrategyHasMoreThanOneStructureWithSettings()
                : networkStrategyHasMoreThanOneStructureWithSettings();
    }

    private static DefectType structureForStrategyDoesNotMatchStrategyName(boolean isSearch) {
        return isSearch ? structureForSearchStrategyDoesNotMatchStrategyName()
                : structureForNetworkStrategyDoesNotMatchStrategyName();
    }

    private static DefectType strategyMustContainStructureWithSettings(boolean isSearch, String strategyType) {
        return isSearch ? searchStrategyMustContainStructureWithSettings(strategyType)
                : networkStrategyMustContainStructureWithSettings(strategyType);
    }

    @Nullable
    public static DefectType validateNetworkDefaultLimitPercent(@Nullable Integer limitPercent) {
        if (limitPercent != null) {
            if (limitPercent < 10 || limitPercent > 100) {
                return networkDefaultLimitPercentNotInRange();
            }
            int multiplicity = 10;
            if (limitPercent % multiplicity != 0) {
                return networkDefaultLimitPercentNotMultiple(multiplicity);
            }
        }
        return null;
    }

    public static DefectType timeZoneDoesntExist(CampaignAddItem item,
                                                 Map<String, GeoTimezone> timeZones) {
        String timeZone = item.getTimeZone();
        return timeZone != null && !timeZones.containsKey(timeZone) ?
                CampaignDefectTypes.timeZoneDoesntExist() :
                null;
    }

}
