package io.quarkus.micrometer.runtime;

import java.lang.reflect.Method;
import java.util.concurrent.CompletionStage;

import javax.annotation.Priority;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;

import io.micrometer.core.annotation.Timed;
import io.micrometer.core.instrument.LongTaskTimer;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;

/**
 * Quarkus defined interceptor for types or methods annotated with {@link Timed @Timed}.
 *
 * @see Timed
 */
@Interceptor
@MicrometerTimed
@Priority(Interceptor.Priority.LIBRARY_BEFORE + 10)
public class MicrometerTimedInterceptor {
    public static final String DEFAULT_METRIC_NAME = "method.timed";

    private final MeterRegistry meterRegistry;

    public MicrometerTimedInterceptor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @AroundInvoke
    Object timedMethod(InvocationContext context) throws Exception {
        Method method = context.getMethod();
        Timed timed = method.getAnnotation(Timed.class);
        if (timed == null) {
            return context.proceed();
        }

        Tags commonTags = getCommonTags(method.getDeclaringClass().getName(), method.getName());
        final boolean stopWhenCompleted = CompletionStage.class.isAssignableFrom(method.getReturnType());

        return time(context, timed, commonTags, stopWhenCompleted);
    }

    Object time(InvocationContext context, Timed timed, Tags commonTags, boolean stopWhenCompleted) throws Exception {
        final String metricName = timed.value().isEmpty() ? DEFAULT_METRIC_NAME : timed.value();

        if (timed.longTask()) {
            return processWithLongTaskTimer(context, timed, commonTags, metricName, stopWhenCompleted);
        } else {
            return processWithTimer(context, timed, commonTags, metricName, stopWhenCompleted);
        }
    }

    private Object processWithTimer(InvocationContext context, Timed timed, Tags commonTags, String metricName,
            boolean stopWhenCompleted) throws Exception {

        Timer.Sample sample = Timer.start(meterRegistry)
                .tags(commonTags)
                .tags(timed.extraTags());

        if (stopWhenCompleted) {
            try {
                return ((CompletionStage<?>) context.proceed()).whenComplete((result, throwable) -> {
                    record(timed, metricName, sample, MicrometerRecorder.getExceptionTag(throwable));
                });
            } catch (Exception ex) {
                record(timed, metricName, sample, MicrometerRecorder.getExceptionTag(ex));
                throw ex;
            }
        }

        String exceptionClass = MicrometerRecorder.getExceptionTag(null);
        try {
            return context.proceed();
        } catch (Exception ex) {
            exceptionClass = MicrometerRecorder.getExceptionTag(ex);
            throw ex;
        } finally {
            record(timed, metricName, sample, exceptionClass);
        }
    }

    private void record(Timed timed, String metricName, Timer.Sample sample, String exceptionClass) {
        try {
            Timer.Builder builder = Timer.builder(metricName)
                    .description(timed.description().isEmpty() ? null : timed.description())
                    .tag("exception", exceptionClass)
                    .publishPercentileHistogram(timed.histogram())
                    .publishPercentiles(timed.percentiles().length == 0 ? null : timed.percentiles());

            sample.stop(meterRegistry, builder);
        } catch (Exception e) {
            // ignoring on purpose
        }
    }

    private Object processWithLongTaskTimer(InvocationContext context, Timed timed, Tags commonTags, String metricName,
            boolean stopWhenCompleted) throws Exception {
        LongTaskTimer.Sample sample = startLongTaskTimer(timed, commonTags, metricName);
        if (sample == null) {
            return context.proceed();
        }

        if (stopWhenCompleted) {
            try {
                return ((CompletionStage<?>) context.proceed()).whenComplete((result, throwable) -> stopLongTaskTimer(sample));
            } catch (Exception ex) {
                stopLongTaskTimer(sample);
                throw ex;
            }
        }

        try {
            return context.proceed();
        } finally {
            stopLongTaskTimer(sample);
        }
    }

    LongTaskTimer.Sample startLongTaskTimer(Timed timed, Tags commonTags, String metricName) {
        try {
            // This will throw if the annotation is incorrect.
            // Errors are checked for at build time, but ...
            return LongTaskTimer.builder(metricName)
                    .description(timed.description().isEmpty() ? null : timed.description())
                    .tags(commonTags)
                    .tags(timed.extraTags())
                    .register(meterRegistry)
                    .start();
        } catch (Exception e) {
            return null;
        }
    }

    private void stopLongTaskTimer(LongTaskTimer.Sample sample) {
        try {
            sample.stop();
        } catch (Exception e) {
            // ignoring on purpose
        }
    }

    private Tags getCommonTags(String className, String methodName) {
        return Tags.of("class", className, "method", methodName);
    }
}

@@ -268,16 +268,50 @@
 ntext;%0A%0A
+import org.jboss.logging.Logger;%0A%0A
 import i
@@ -776,16 +776,106 @@
 eptor %7B%0A
+    private static final Logger log = Logger.getLogger(MicrometerTimedInterceptor.class);%0A
     publ
@@ -3845,32 +3845,249 @@
 oring on purpose
+: possible meter registration error should not interrupt main code flow.%0A            log.warnf(e, %22Unable to record observed timer value for %25s with exceptionClass %25s%22,%0A                    metricName, exceptionClass);
 %0A        %7D%0A    %7D
@@ -4540,32 +4540,57 @@
 ntext.proceed())
+%0A                        
 .whenComplete((r
@@ -4624,24 +4624,36 @@
 ngTaskTimer(
+metricName, 
 sample));%0A  
@@ -4713,32 +4713,44 @@
 opLongTaskTimer(
+metricName, 
 sample);%0A       
@@ -4895,16 +4895,28 @@
 skTimer(
+metricName, 
 sample);
@@ -5494,32 +5494,222 @@
 (Exception e) %7B%0A
+            // ignoring on purpose: possible meter registration error should not interrupt main code flow.%0A            log.warnf(e, %22Unable to create long task timer named %25s%22, metricName);%0A
             retu
@@ -5769,16 +5769,35 @@
 skTimer(
+String metricName, 
 LongTask
@@ -5927,16 +5927,99 @@
 purpose%0A
+            log.warnf(e, %22Unable to update long task timer named %25s%22, metricName);%0A
         

debug for timed exception cases