package ru.yandex.direct.web.entity.mobilecontent.service;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

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

import com.google.common.collect.Sets;
import one.util.streamex.StreamEx;
import org.apache.commons.lang3.tuple.Pair;

import ru.yandex.direct.utils.UrlUtils;
import ru.yandex.direct.utils.model.UrlParts;

import static java.util.Collections.emptyList;
import static ru.yandex.direct.utils.CommonUtils.nvl;

@ParametersAreNonnullByDefault
class PropagationUtils {
    private PropagationUtils() {
    }

    /**
     * Создаёт новый трекинговый урл из {@code updatedAppTrackingUrl}, добавляя к нему
     * дополнительные параметры из {@code bannerTrackingUrl}
     * <p>
     * Предполагается, что {@code bannerTrackingUrl} -- ссылка, созданая из {@code formerAppTrackingUrl}
     * путём добавления дополнительных параметров. Если это не так, то создание новой ссылки невозможно.
     *
     * @param formerAppTrackingUrl  старый трекинговый урл в приложении
     * @param updatedAppTrackingUrl новый трекинговый урл в приложении
     * @param bannerTrackingUrl     действительный трекинговый урл баннера, возможно созданный из {@code formerAppTrackingUrl}
     * @return новый трекинговый урл -- {@code updatedAppTrackingUrl} с дополнителными параметрами или Optional.empty()
     * если к баннеру не должна быть применена пропагация
     */
    static Optional<TrackingUrl> createNewBannerTrackingUrl(
            @Nullable String formerAppTrackingUrl, @Nullable String updatedAppTrackingUrl,
            @Nullable String bannerTrackingUrl) {
        if (formerAppTrackingUrl == null) {
            if (bannerTrackingUrl == null) {
                // изменение применимо
                return Optional.of(TrackingUrl.of(updatedAppTrackingUrl));
            } else {
                // на баннере задали трекинговый урл, значит его нельзя перезаписывать
                return Optional.empty();
            }
        }
        if (bannerTrackingUrl == null) {
            // на баннере стёрт трекинговый урл, хотя в приложении указан -- перезаписывать нельзя
            return Optional.empty();
        }

        UrlParts formerAppUrlParts = UrlUtils.laxParseUrl(formerAppTrackingUrl);
        UrlParts bannerUrlParts = UrlUtils.laxParseUrl(bannerTrackingUrl);

        return computeDiffOfParameters(bannerUrlParts, formerAppUrlParts)
                .map(diff -> applyDiffToTrackingUrl(updatedAppTrackingUrl, diff));
    }

    /**
     * Сделать новую ссылку на объявлении из новой ссылки на приложении и диффа, который вернула computeDiffOfParameters
     *
     * @param updatedAppTrackingUrl трекинговая ссылка на приложении
     * @param diffOfParameters      параметры, которые нужно добавить к трекинговой ссылке
     */
    private static TrackingUrl applyDiffToTrackingUrl(@Nullable String updatedAppTrackingUrl,
                                                      List<Pair<String, String>> diffOfParameters) {
        if (updatedAppTrackingUrl == null) {
            return TrackingUrl.absent();
        }
        UrlParts freshUrlParts = UrlUtils.laxParseUrl(updatedAppTrackingUrl);

        Set<Pair<String, String>> seen = new HashSet<>();
        List<Pair<String, String>> mergedParameters = StreamEx.of(nvl(freshUrlParts.getParameters(), emptyList()))
                .append(diffOfParameters)
                .remove(seen::contains)
                .peek(seen::add)
                .toList();

        UrlParts newTrackingUrl = freshUrlParts.toBuilder()
                .withParameters(mergedParameters)
                .build();
        return TrackingUrl.of(newTrackingUrl.toUrl());
    }

    /**
     * Вычислить разницу между старой ссылкой на приложении и старой ссылкой на объявлении
     */
    private static Optional<List<Pair<String, String>>> computeDiffOfParameters(UrlParts bannerUrlParts,
                                                                                UrlParts formerAppUrlParts) {
        if (!formerAppUrlParts.getProtocol().equals(bannerUrlParts.getProtocol())
                || !formerAppUrlParts.getDomain().equals(bannerUrlParts.getDomain())
                || !formerAppUrlParts.getPath().equals(bannerUrlParts.getPath())) {
            return Optional.empty();
        }

        Set<Pair<String, String>> formerParametersSet =
                new LinkedHashSet<>(nvl(formerAppUrlParts.getParameters(), emptyList()));
        List<Pair<String, String>> actualParameters = nvl(bannerUrlParts.getParameters(), emptyList());

        if (formerParametersSet.isEmpty()) {
            return Optional.of(actualParameters);
        }

        Set<Pair<String, String>> actualParametersSet = new HashSet<>(actualParameters);

        // Если в параметрах старой трекинговой ссылки есть параметры, которых нет в актуальной,
        // то считаем невозможным определить дифф
        if (!Sets.difference(formerParametersSet, actualParametersSet).isEmpty()) {
            return Optional.empty();
        }

        List<Pair<String, String>> diffOfParameters = StreamEx.of(actualParameters)
                .remove(formerParametersSet::contains)
                .collect(Collectors.toList());
        return Optional.of(diffOfParameters);
    }

    public static class TrackingUrl {
        @Nullable
        private final String trackingUrl;

        private TrackingUrl(@Nullable String trackingUrl) {
            this.trackingUrl = trackingUrl;
        }

        public static TrackingUrl absent() {
            return new TrackingUrl(null);
        }

        public static TrackingUrl of(@Nullable String url) {
            return new TrackingUrl(url);
        }

        @Nullable
        public String getTrackingUrl() {
            return trackingUrl;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            TrackingUrl that = (TrackingUrl) o;
            return Objects.equals(trackingUrl, that.trackingUrl);
        }

        @Override
        public int hashCode() {
            return Objects.hash(trackingUrl);
        }
    }
}
