package ru.yandex.webmaster3.worker.digest.notificationsettings;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import ru.yandex.webmaster3.core.solomon.metric.SolomonCounter;
import ru.yandex.webmaster3.core.solomon.metric.SolomonCounterImpl;
import ru.yandex.webmaster3.core.solomon.metric.SolomonGroupingUtil;
import ru.yandex.webmaster3.core.solomon.metric.SolomonHistogram;
import ru.yandex.webmaster3.core.solomon.metric.SolomonHistogramImpl;
import ru.yandex.webmaster3.core.solomon.metric.SolomonKey;
import ru.yandex.webmaster3.storage.notifications.NotificationChannel;
import ru.yandex.webmaster3.storage.user.notification.NotificationType;
import ru.yandex.webmaster3.worker.notifications.info.UserHostsChannelsInfo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author avhaliullin
 */
public class UserNotificationSettingsMetricsAcc {
    private static final List<Integer> buckets = new ArrayList<>(Arrays.asList(1, 2, 5, 10, 25, 50, 100, 500, 1000, 2000));

    private static final String SOLOMON_INDICATOR_USERS = "users_by_notifications";
    private static final String SOLOMON_INDICATOR_CHANNELS = "user_notification_channels";
    private static final String SOLOMON_INDICATOR_EMAILS = "user_emails";

    private static final String SOLOMON_LABEL_TYPE = "notification_type";
    private static final String SOLOMON_LABEL_CHANNEL = "notification_channel";
    private static final String SOLOMON_LABEL_HOSTS_COUNT = "hosts_count";
    private static final Collection<Set<String>> GROUPINGS = SolomonGroupingUtil.generateAllGroupings(
            Arrays.asList(SOLOMON_LABEL_TYPE, SOLOMON_LABEL_CHANNEL, SOLOMON_LABEL_HOSTS_COUNT),
            false,
            true
    );

    private final Map<SolomonKey, SolomonCounterImpl> channelSensors;
    private final Map<SolomonKey, IntersectingCounter> userSensors;
    private final SolomonCounterImpl usersWithEmails;
    private final Map<Triple<NotificationType, NotificationChannel, String>, SolomonHistogram<Integer>> metrics;
    private final boolean threadSafe;

    public UserNotificationSettingsMetricsAcc(boolean threadSafe) {
        this.channelSensors = threadSafe ? new ConcurrentHashMap<>() : new HashMap<>();
        this.userSensors = threadSafe ? new ConcurrentHashMap<>() : new HashMap<>();
        this.threadSafe = threadSafe;
        metrics = threadSafe ? new ConcurrentHashMap<>() : new HashMap<>();
        usersWithEmails = SolomonCounterImpl.create(threadSafe);
        channelSensors.put(SolomonKey.create(SolomonKey.LABEL_INDICATOR, SOLOMON_INDICATOR_EMAILS), usersWithEmails);
    }

    private <C extends SolomonCounter> SolomonHistogram<Integer> createHistogram(
            NotificationType type,
            NotificationChannel channel,
            String indicator,
            Function<SolomonKey, C> basicCounterSupplier,
            Map<SolomonKey, C> acc) {

        return metrics.computeIfAbsent(Triple.of(type, channel, indicator),
                ign -> SolomonHistogramImpl.create(bucket ->
                                SolomonGroupingUtil.registerCounterWithGroupings(
                                        acc,
                                        indicator,
                                        GROUPINGS,
                                        SolomonKey.create(SOLOMON_LABEL_TYPE, type.name())
                                                .withLabel(SOLOMON_LABEL_CHANNEL, channel.name())
                                                .withLabel(SOLOMON_LABEL_HOSTS_COUNT, "<=" + bucket),
                                        basicCounterSupplier
                                ),
                        buckets
                )
        );
    }

    public void accountUserStats(UserHostsChannelsInfo info) {
        Map<Pair<NotificationType, NotificationChannel>, Long> userSettings2HostsCount = info.getHost2Settings().values().stream()
                .flatMap(hostSettings ->
                        hostSettings.entrySet().stream()
                                .flatMap(notificationTypeSettings -> {
                                    NotificationType notificationType = notificationTypeSettings.getKey();
                                    return notificationTypeSettings.getValue().stream().map(channel -> Pair.of(notificationType, channel));
                                })
                ).collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

        // обновляем статистику по каналам
        userSettings2HostsCount.forEach((key, value) -> {
                    int intValue = Math.toIntExact(value);
                    createHistogram(key.getLeft(), key.getRight(), SOLOMON_INDICATOR_CHANNELS, ign -> SolomonCounterImpl.create(threadSafe), channelSensors)
                            .update(intValue);
                    createHistogram(key.getLeft(), key.getRight(), SOLOMON_INDICATOR_USERS, ign -> createIntersectingCounter(), userSensors)
                            .update(intValue);
                }
        );
        userSensors.values().forEach(IntersectingCounter::flush);

        // обновляем статистику по email'ам
        if (StringUtils.isNotEmpty(info.getEmail())) {
            usersWithEmails.update();
        }
    }

    Stream<Pair<SolomonKey, Long>> getSensorValues() {
        return Stream.concat(
                userSensors.entrySet().stream().map(e -> Pair.of(e.getKey(), e.getValue().get())),
                channelSensors.entrySet().stream().map(e -> Pair.of(e.getKey(), e.getValue().getAsLong()))
        );
    }

    private IntersectingCounter createIntersectingCounter() {
        return threadSafe ? new IntersectingTSCounter() : new IntersectingNonTSCounter();
    }

    private interface IntersectingCounter extends SolomonCounter {
        void flush();
    }

    private static class IntersectingNonTSCounter implements IntersectingCounter {
        private long value = 0L;
        private long current = 0L;

        @Override
        public void add(long value) {
            current = value;
        }

        public void flush() {
            value += current;
            current = 0L;
        }

        public long get() {
            return value;
        }
    }

    private static class IntersectingTSCounter implements IntersectingCounter {
        private AtomicLong value = new AtomicLong(0L);
        private ThreadLocal<Long> current = new ThreadLocal<>();

        @Override
        public void flush() {
            Long curVal = current.get();
            if (curVal != null) {
                current.remove();
                value.addAndGet(curVal);
            }
        }

        @Override
        public long get() {
            return value.get();
        }

        @Override
        public void add(long value) {
            current.set(value);
        }
    }
}
