package ru.yandex.webmaster3.core.metrics;

import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.config.NamingConvention;
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
import io.micrometer.core.instrument.distribution.TimeWindowMax;
import io.micrometer.core.instrument.distribution.pause.PauseDetector;
import io.micrometer.core.instrument.util.MeterEquivalence;
import io.micrometer.core.instrument.util.TimeUtils;
import io.micrometer.core.lang.Nullable;
import io.micrometer.prometheus.PrometheusNamingConvention;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import ru.yandex.webmaster3.core.solomon.metric.SolomonCounter;
import ru.yandex.webmaster3.core.solomon.metric.SolomonGauge;
import ru.yandex.webmaster3.core.solomon.metric.SolomonKey;
import ru.yandex.webmaster3.core.solomon.metric.SolomonMetricRegistry;

import java.lang.ref.WeakReference;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.function.DoubleSupplier;
import java.util.function.ToDoubleFunction;
import java.util.function.ToLongFunction;
import java.util.stream.Collectors;

import static io.micrometer.core.instrument.Meter.Type.TIMER;

/**
 * @author leonidrom
 *
 * WMC-7779: минимально необходимая прослойка дабы слать нужные нам Micrometer метрики в Соломон
 */
@Slf4j
public class SolomonMicrometerMeterRegistry extends MeterRegistry {
    private final static NamingConvention namingConvention = new PrometheusNamingConvention() {
        @Override
        public String name(@NotNull String name, Meter.Type type, @Nullable String baseUnit) {
            if (type == TIMER) {
                // У PrometheusNamingConvention таймеры в секундах, мы хотим в миллисекундах
                String conventionName = NamingConvention.snakeCase.name(name, type, baseUnit);
                return conventionName + "_millis";
            } else {
                return super.name(name, type, baseUnit);
            }
        }
    };

    private final static String LABEL_INDICATOR = "micrometer_indicator";

    private final SolomonMetricRegistry solomonMetricRegistry;

    @Autowired
    public SolomonMicrometerMeterRegistry(SolomonMetricRegistry solomonMetricRegistry) {
        super(Clock.SYSTEM);
        this.solomonMetricRegistry = solomonMetricRegistry;
    }

    @NotNull
    @Override
    protected <T> Gauge newGauge(@NotNull Meter.Id id, T obj, @NotNull ToDoubleFunction<T> valueFunction) {
        final WeakReference<T> ref = new WeakReference<>(obj);
        DoubleSupplier valueSupplier = () -> {
            T obj2 = ref.get();
            if (obj2 != null) {
                return valueFunction.applyAsDouble(obj2);
            } else {
                return 0;
            }
        };

        var solomonKey = toSolomonKey(id);
        var solomonGauge = solomonMetricRegistry.createMicrometerGauge(solomonKey, valueSupplier);

        return new SolomonMicrometerGauge(id, solomonGauge);
    }

    @NotNull
    @Override
    protected Counter newCounter(@NotNull Meter.Id id) {
        var solomonKey = toSolomonKey(id);
        var solomonCounter = solomonMetricRegistry.createSimpleCounter(solomonKey, 1.0d);
        return new SolomonMicrometerCounter(id, solomonCounter);
    }

    @NotNull
    @Override
    protected Timer newTimer(@NotNull Meter.Id id, @NotNull DistributionStatisticConfig distributionStatisticConfig, @NotNull PauseDetector pauseDetector) {
        var solomonCountKey = toSolomonKey(id, "_count");
        var solomonCountCounter = solomonMetricRegistry.createSimpleCounter(solomonCountKey, 1.0d);

        var solomonSumKey = toSolomonKey(id, "_sum");
        var solomonSumCounter = solomonMetricRegistry.createSimpleCounter(solomonSumKey, 1.0d);

        return new SolomonMicrometerTimer(id, clock, distributionStatisticConfig, pauseDetector,
                solomonCountCounter, solomonSumCounter);
    }

    @NotNull
    @Override
    protected LongTaskTimer newLongTaskTimer(@NotNull Meter.Id id) {
        log.error("newLongTaskTimer is not implemented yet");
        throw new UnsupportedOperationException();
    }

    @NotNull
    @Override
    protected DistributionSummary newDistributionSummary(@NotNull Meter.Id id, @NotNull DistributionStatisticConfig distributionStatisticConfig, double scale) {
        log.error("newDistributionSummary is not implemented yet");
        throw new UnsupportedOperationException();
    }

    @NotNull
    @Override
    protected Meter newMeter(@NotNull Meter.Id id, @NotNull Meter.Type type, @NotNull Iterable<Measurement> measurements) {
        log.error("newMeter is not implemented yet");
        throw new UnsupportedOperationException();
    }

    @NotNull
    @Override
    protected <T> FunctionTimer newFunctionTimer(@NotNull Meter.Id id, @NotNull T obj, @NotNull ToLongFunction<T> countFunction, ToDoubleFunction<T> totalTimeFunction, TimeUnit totalTimeFunctionUnit) {
        log.error("newFunctionTimer is not implemented yet");
        throw new UnsupportedOperationException();
    }

    @NotNull
    @Override
    protected <T> FunctionCounter newFunctionCounter(@NotNull Meter.Id id, @NotNull T obj, @NotNull ToDoubleFunction<T> countFunction) {
        log.error("newFunctionCounter is not implemented yet");
        throw new UnsupportedOperationException();
    }

    @NotNull
    @Override
    protected TimeUnit getBaseTimeUnit() {
        return TimeUnit.MILLISECONDS;
    }

    @NotNull
    @Override
    protected DistributionStatisticConfig defaultHistogramConfig() {
        return DistributionStatisticConfig.builder()
                .expiry(Duration.ofMinutes(1))
                .build()
                .merge(DistributionStatisticConfig.DEFAULT);
    }

    public static class SolomonMicrometerCounter extends AbstractMeter implements Counter {
        private final SolomonCounter solomonCounter;

        SolomonMicrometerCounter(Meter.Id id, SolomonCounter solomonCounter) {
            super(id);
            this.solomonCounter = solomonCounter;
        }

        @Override
        public void increment(double amount) {
            // Интересующие нас метрики всегда целочисленные, поэтому тупо приводим
            solomonCounter.add((long)amount);
        }

        @Override
        public double count() {
            return solomonCounter.get();
        }

        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
        @Override
        public boolean equals(Object o) {
            return MeterEquivalence.equals(this, o);
        }

        @Override
        public int hashCode() {
            return MeterEquivalence.hashCode(this);
        }
    }

    public static class SolomonMicrometerGauge extends AbstractMeter implements Gauge {
        private final SolomonGauge solomonGauge;

        SolomonMicrometerGauge(Meter.Id id, SolomonGauge solomonGauge) {
            super(id);
            this.solomonGauge = solomonGauge;
        }

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

        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
        @Override
        public boolean equals(Object o) {
            return MeterEquivalence.equals(this, o);
        }

        @Override
        public int hashCode() {
            return MeterEquivalence.hashCode(this);
        }
    }

    public static class SolomonMicrometerTimer extends AbstractTimer {
        private final TimeWindowMax max;

        // сколько раз произошло событие за время жизни таймера
        private final SolomonCounter solomonCountCounter;

        // суммарное время потраченное на событие
        private final SolomonCounter solomonSumCounter;

        SolomonMicrometerTimer(
                Id id, Clock clock,
                DistributionStatisticConfig distributionStatisticConfig,
                PauseDetector pauseDetector,
                SolomonCounter solomonCountCounter,
                SolomonCounter solomonSumCounter) {
            super(id, clock, distributionStatisticConfig, pauseDetector, TimeUnit.MILLISECONDS, false);
            this.max = new TimeWindowMax(clock, distributionStatisticConfig);
            this.solomonSumCounter = solomonSumCounter;
            this.solomonCountCounter = solomonCountCounter;
        }

        @Override
        protected void recordNonNegative(long amount, @NotNull TimeUnit unit) {
            solomonCountCounter.update();
            long amountMillis = TimeUnit.MILLISECONDS.convert(amount, unit);
            solomonSumCounter.add(amountMillis);
            max.record(amountMillis, TimeUnit.MILLISECONDS);
        }

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

        @Override
        public double totalTime(@NotNull TimeUnit unit) {
            return TimeUtils.millisToUnit(solomonSumCounter.get(), unit);
        }

        @Override
        public double max(@NotNull TimeUnit unit) {
            return max.poll(unit);
        }
    }

    private static SolomonKey toSolomonKey(Meter.Id id, String indicatorSuffix) {
        var tags = id.getConventionTags(namingConvention);
        var solomonLabels = tags.stream().collect(Collectors.toMap(Tag::getKey, Tag::getValue));
        solomonLabels.put(LABEL_INDICATOR, id.getConventionName(namingConvention) + indicatorSuffix);

        return SolomonKey.create(solomonLabels);
    }

    private static SolomonKey toSolomonKey(Meter.Id id) {
        return toSolomonKey(id, "");
    }
}
