package grpctool

import (
	"context"
	"strings"
	"sync"

	"go.opentelemetry.io/otel/attribute"
	otelmetric "go.opentelemetry.io/otel/metric"
	"google.golang.org/grpc/stats"
)

const (
	grpcServiceAttr attribute.Key = "grpc_service"
	grpcMethodAttr  attribute.Key = "grpc_method"
	namespace                     = "grpc_"
)

type rpcTagCtxKey struct{}

type RequestsInFlightStatsHandler struct {
	mu      sync.Mutex                         // protects addOpts only
	addOpts map[string]*[]otelmetric.AddOption // full method name -> add options
	counter otelmetric.Int64UpDownCounter
}

func NewClientRequestsInFlightStatsHandler(m otelmetric.Meter) (*RequestsInFlightStatsHandler, error) {
	return NewRequestsInFlightStatsHandler("client", m)
}

func NewServerRequestsInFlightStatsHandler(m otelmetric.Meter) (*RequestsInFlightStatsHandler, error) {
	return NewRequestsInFlightStatsHandler("server", m)
}

func NewRequestsInFlightStatsHandler(sub string, m otelmetric.Meter) (*RequestsInFlightStatsHandler, error) {
	c, err := m.Int64UpDownCounter(namespace+sub+"_requests_in_flight",
		otelmetric.WithDescription("Number of requests in flight"),
	)
	if err != nil {
		return nil, err
	}
	return &RequestsInFlightStatsHandler{
		counter: c,
		addOpts: make(map[string]*[]otelmetric.AddOption),
	}, nil
}

func (h *RequestsInFlightStatsHandler) TagRPC(ctx context.Context, inf *stats.RPCTagInfo) context.Context {
	h.mu.Lock()
	defer h.mu.Unlock()
	opts, ok := h.addOpts[inf.FullMethodName]
	if !ok {
		service, method := parseMethod(inf.FullMethodName)
		opts = &[]otelmetric.AddOption{
			otelmetric.WithAttributeSet(attribute.NewSet(
				grpcServiceAttr.String(service),
				grpcMethodAttr.String(method),
			)),
		}
		h.addOpts[inf.FullMethodName] = opts
	}
	return context.WithValue(ctx, rpcTagCtxKey{}, opts)
}

func (h *RequestsInFlightStatsHandler) TagConn(ctx context.Context, _ *stats.ConnTagInfo) context.Context {
	return ctx
}

func (h *RequestsInFlightStatsHandler) HandleConn(_ context.Context, _ stats.ConnStats) {
}

func (h *RequestsInFlightStatsHandler) HandleRPC(ctx context.Context, stat stats.RPCStats) {
	switch stat.(type) {
	case *stats.Begin:
		opts := ctx.Value(rpcTagCtxKey{}).(*[]otelmetric.AddOption)
		h.counter.Add(context.Background(), 1, *opts...) //nolint:contextcheck
	case *stats.End:
		opts := ctx.Value(rpcTagCtxKey{}).(*[]otelmetric.AddOption)
		h.counter.Add(context.Background(), -1, *opts...) //nolint:contextcheck
	}
}

func parseMethod(name string) (string, string) {
	if !strings.HasPrefix(name, "/") {
		return "unknown", "unknown"
	}
	name = name[1:]

	pos := strings.LastIndex(name, "/")
	if pos < 0 {
		return "unknown", "unknown"
	}
	return name[:pos], name[pos+1:]
}
