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

import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

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

import com.yandex.direct.api.v5.general.IdsCriteria;
import com.yandex.direct.api.v5.sitelinks.GetRequest;
import com.yandex.direct.api.v5.sitelinks.GetResponse;
import com.yandex.direct.api.v5.sitelinks.ObjectFactory;
import com.yandex.direct.api.v5.sitelinks.SitelinkFieldEnum;
import com.yandex.direct.api.v5.sitelinks.SitelinkGetItem;
import com.yandex.direct.api.v5.sitelinks.SitelinksSetFieldEnum;
import com.yandex.direct.api.v5.sitelinks.SitelinksSetGetItem;
import one.util.streamex.StreamEx;
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.common.validation.GetRequestGeneralValidator;
import ru.yandex.direct.api.v5.converter.ResultConverter;
import ru.yandex.direct.api.v5.entity.GenericGetRequest;
import ru.yandex.direct.api.v5.entity.GetApiServiceDelegate;
import ru.yandex.direct.api.v5.entity.sitelinks.SitelinksEndpoint;
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.client.service.checker.ClientAccessCheckerTypeSupportFacade;
import ru.yandex.direct.core.entity.sitelink.model.SitelinkSet;
import ru.yandex.direct.core.entity.sitelink.service.SitelinkSetService;
import ru.yandex.direct.dbutil.model.ClientId;
import ru.yandex.direct.validation.builder.Constraint;
import ru.yandex.direct.validation.builder.ItemValidationBuilder;
import ru.yandex.direct.validation.builder.When;
import ru.yandex.direct.validation.result.ValidationResult;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toCollection;
import static ru.yandex.direct.api.v5.common.constants.GetRequestCommonConstants.DEFAULT_MAX_IDS_COUNT;
import static ru.yandex.direct.api.v5.validation.DefectTypes.maxIdsInSelection;
import static ru.yandex.direct.api.v5.validation.DefectTypes.mutuallyExclusiveParameters;
import static ru.yandex.direct.api.v5.validation.DefectTypes.requiredAtLeastOneOfFields;
import static ru.yandex.direct.api.v5.validation.constraints.Constraints.maxListSize;
import static ru.yandex.direct.utils.FunctionalUtils.mapList;
import static ru.yandex.direct.validation.result.PathHelper.field;
import static ru.yandex.direct.validation.result.PathHelper.path;

/**
 * Получить наборы быстрых ссылок
 * <p>
 * https://tech.yandex.ru/direct/doc/ref-v5/sitelinks/get-docpage/
 */
@Component
@ParametersAreNonnullByDefault
public class GetSitelinksDelegate
        extends GetApiServiceDelegate<GetRequest, GetResponse, SitelinkAnyFieldEnum, List<Long>, SitelinkSet> {

    private static final ObjectFactory FACTORY = new ObjectFactory();
    private final SitelinkSetService service;
    private final EnumPropertyFilter<SitelinkAnyFieldEnum> propertyFilter;
    private final EnumPropertyFilter<SitelinkFieldEnum> sitelinkPropertyFilter;
    private final ResultConverter resultConverter;
    private final ClientAccessCheckerTypeSupportFacade clientAccessCheckerTypeSupportFacade;

    @Autowired
    public GetSitelinksDelegate(ApiAuthenticationSource auth,
                                SitelinkSetService service,
                                PropertyFilter propertyFilter,
                                PropertyFilter sitelinkPropertyFilter,
                                ResultConverter resultConverter,
                                ClientAccessCheckerTypeSupportFacade clientAccessCheckerTypeSupportFacade) {
        super(SitelinksEndpoint.PATH_CONVERTER, auth);

        this.service = service;
        this.resultConverter = resultConverter;
        this.sitelinkPropertyFilter = EnumPropertyFilter.from(SitelinkFieldEnum.class, sitelinkPropertyFilter);
        this.propertyFilter = EnumPropertyFilter.from(SitelinkAnyFieldEnum.class, propertyFilter);
        this.clientAccessCheckerTypeSupportFacade = clientAccessCheckerTypeSupportFacade;
    }

    @Override
    public ValidationResult<GetRequest, DefectType> validateRequest(GetRequest externalRequest) {
        ItemValidationBuilder<GetRequest, DefectType> vb = ItemValidationBuilder.of(externalRequest);

        vb.checkBy(GetRequestGeneralValidator::validateRequestWithDefectTypes);
        vb.check(doesNotContainSitelinkFieldsWhenContainsSitelinks());
        vb.check(doesContainFieldNamesOrSitelinkFieldNames());

        vb.item(externalRequest.getSelectionCriteria(), "SelectionCriteria")
                .checkBy(this::validateSelectionCriteria, When.notNull());

        return vb.getResult();
    }

    private Constraint<GetRequest, DefectType> doesNotContainSitelinkFieldsWhenContainsSitelinks() {
        return request -> {
            if (request.getFieldNames() != null
                    && request.getSitelinkFieldNames() != null
                    && request.getFieldNames().contains(SitelinksSetFieldEnum.SITELINKS)
                    && !request.getSitelinkFieldNames().isEmpty()
            ) {
                return mutuallyExclusiveParameters(
                        path(field("SitelinkFieldNames")),
                        path(field("FieldNames")),

                        SitelinksSetFieldEnum.SITELINKS.value()
                );
            } else {
                return null;
            }
        };
    }

    private Constraint<GetRequest, DefectType> doesContainFieldNamesOrSitelinkFieldNames() {
        return request -> {
            if (
                    (request.getFieldNames() == null || request.getFieldNames().isEmpty())
                            &&
                            (request.getSitelinkFieldNames() == null || request.getSitelinkFieldNames().isEmpty())
            ) {
                return requiredAtLeastOneOfFields("FieldNames, SitelinkFieldNames");
            } else {
                return null;
            }
        };
    }

    private ValidationResult<IdsCriteria, DefectType> validateSelectionCriteria(IdsCriteria selectonCriteria) {
        ItemValidationBuilder<IdsCriteria, DefectType> vb = ItemValidationBuilder.of(selectonCriteria);

        vb.item(selectonCriteria.getIds(), "Ids")
                .check(maxListSize(DEFAULT_MAX_IDS_COUNT), maxIdsInSelection());

        return vb.getResult();
    }

    @Override
    public List<Long> extractSelectionCriteria(GetRequest externalRequest) {
        IdsCriteria selectionCriteria = externalRequest.getSelectionCriteria();
        return selectionCriteria != null && !selectionCriteria.getIds().isEmpty()
                ? selectionCriteria.getIds() : null;
    }

    @Override
    public Set<SitelinkAnyFieldEnum> extractFieldNames(GetRequest externalRequest) {
        return StreamEx.of(
                externalRequest.getFieldNames().stream().map(SitelinkAnyFieldEnum::fromSitelinksSetFieldEnum),
                externalRequest.getSitelinkFieldNames().stream()
                        .map(SitelinkAnyFieldEnum::fromSitelinkFieldEnum)
        ).flatMap(identity()).toSet();
    }

    @Override
    public List<SitelinkSet> get(GenericGetRequest<SitelinkAnyFieldEnum, List<Long>> getRequest) {
        ClientId clientId = auth.getChiefSubclient().getClientId();
        Collection<Long> ids = getRequest.getSelectionCriteria();
        if (ids != null && !ids.isEmpty()) {
            ids =
                    clientAccessCheckerTypeSupportFacade.sendToCheck(Map.of(SitelinkSet.class, ids),
                            clientId)
                            .getOrDefault(SitelinkSet.class, Collections.emptySet());
            if (ids.isEmpty()) {
                return Collections.emptyList();
            }

        }
        return service.getSitelinkSets(ids, clientId, getRequest.getLimitOffset());
    }

    @Override
    public GetResponse convertGetResponse(List<SitelinkSet> result, Set<SitelinkAnyFieldEnum> requestedFields,
                                          @Nullable Long limitedBy) {
        List<SitelinksSetGetItem> getItems = mapList(result, this::convertSet);

        filterProperties(getItems, requestedFields);

        return new GetResponse()
                .withSitelinksSets(getItems)
                .withLimitedBy(limitedBy);
    }

    private void filterProperties(List<SitelinksSetGetItem> getItems, Set<SitelinkAnyFieldEnum> requestedFields) {
        Map<? extends Class<?>, List<SitelinkAnyFieldEnum>> requestedFieldsByType =
                requestedFields.stream().collect(Collectors.groupingBy(SitelinkAnyFieldEnum::getEnumClass));

        List<SitelinkGetItem> sitelinks = StreamEx.of(getItems)
                .map(SitelinksSetGetItem::getSitelinks)
                .toFlatList(Function.identity());
        if (!sitelinks.isEmpty()) {
            EnumSet<SitelinkFieldEnum> sitelinkFields =
                    getRequestedFieldsByType(requestedFieldsByType.get(SitelinkFieldEnum.class),
                            SitelinkFieldEnum.class);
            if (sitelinkFields.isEmpty()) {
                //Если поля не заданы явным образом - отдаем стандартный набор полей (без TURBO_PAGE_ID)
                sitelinkFields.addAll(StreamEx.of(SitelinkFieldEnum.values())
                        .filter(entry -> !entry.equals(SitelinkFieldEnum.TURBO_PAGE_ID)).toList());
            } else {
                //Если поля сайтлинков запрошены явным образом - нужно вернуть и сайтлинки
                requestedFields.add(SitelinkAnyFieldEnum.SITELINKS);
            }
            sitelinkPropertyFilter.filterProperties(sitelinks, sitelinkFields);
            propertyFilter.filterProperties(getItems, requestedFields);
        }
    }

    private <T extends Enum<T>> EnumSet<T> getRequestedFieldsByType(List<SitelinkAnyFieldEnum> adAnyFieldEnums,
                                                                    Class<T> clazz) {
        return adAnyFieldEnums == null ? EnumSet.noneOf(clazz) :
                adAnyFieldEnums.stream().map(e -> clazz.cast(e.getValue()))
                        .collect(toCollection(() -> EnumSet.noneOf(clazz)));
    }

    private SitelinksSetGetItem convertSet(SitelinkSet set) {
        return new SitelinksSetGetItem()
                .withId(set.getId())
                .withSitelinks(mapList(set.getSitelinks(), item ->
                        new SitelinkGetItem()
                                .withTitle(item.getTitle())
                                .withDescription(FACTORY.createSitelinkGetItemDescription(item.getDescription()))
                                .withHref(FACTORY.createSitelinkGetItemHref(item.getHref()))
                                .withTurboPageId(FACTORY.createSitelinkGetItemTurboPageId(item.getTurboLandingId()))));
    }
}
