package metric

import (
	"context"
	"time"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	otelmetric "go.opentelemetry.io/otel/metric"
	"go.opentelemetry.io/otel/trace"
)

const (
	rateLimiterBlockDurationName               = "limiter_block_duration"
	rateLimiterLimitName                       = "limiter_limit"
	allowedAttr                  attribute.Key = "allowed"
	limiterNameAttr              attribute.Key = "limiter_name"
	limiterLimitUnitAttr         attribute.Key = "unit"
)

type LimiterWrapper struct {
	tr          trace.Tracer
	hist        otelmetric.Float64Histogram
	allowedOpts []otelmetric.RecordOption
	deniedOpts  []otelmetric.RecordOption
}

func NewLimiterWrapper(limiterName string, limit float64, limitUnit string, m otelmetric.Meter, tr trace.Tracer) (*LimiterWrapper, error) {
	limitGauge, err := m.Float64Gauge(rateLimiterLimitName, otelmetric.WithDescription("Limit for the rate limiter"))
	if err != nil {
		return nil, err
	}
	nameAttr := limiterNameAttr.String(limiterName)
	limitGauge.Record(context.Background(), limit, otelmetric.WithAttributeSet(attribute.NewSet(
		nameAttr,
		limiterLimitUnitAttr.String(limitUnit),
	)))
	hist, err := m.Float64Histogram(
		rateLimiterBlockDurationName,
		otelmetric.WithUnit("s"),
		otelmetric.WithDescription("Duration the rate limiter blocked for deciding to allow/block the call"),
		otelmetric.WithExplicitBucketBoundaries(0.001, 0.004, 0.016, 0.064, 0.256, 1.024, 4.096, 16.384),
	)
	if err != nil {
		return nil, err
	}
	return &LimiterWrapper{
		tr:   tr,
		hist: hist,
		allowedOpts: []otelmetric.RecordOption{
			otelmetric.WithAttributeSet(attribute.NewSet(
				nameAttr,
				allowedAttr.Bool(true),
			)),
		}, // allocate slice once
		deniedOpts: []otelmetric.RecordOption{
			otelmetric.WithAttributeSet(attribute.NewSet(
				nameAttr,
				allowedAttr.Bool(false),
			)),
		}, // allocate slice once
	}, nil
}

func (w *LimiterWrapper) Start(ctx context.Context) (context.Context, func(allowed bool)) {
	start := time.Now()
	ctx, span := w.tr.Start(ctx, "limiter", trace.WithSpanKind(trace.SpanKindInternal)) //nolint:spancheck
	return ctx, func(allowed bool) {                                                    //nolint:spancheck
		duration := float64(time.Since(start)) / float64(time.Second)
		opts := w.allowedOpts
		if !allowed {
			opts = w.deniedOpts
		}
		// Pass background context because we always want to record the duration.
		w.hist.Record(context.Background(), duration, opts...)
		code := codes.Ok
		if !allowed {
			code = codes.Error
		}
		span.SetStatus(code, "")
		span.End()
	}
}
