package datadog

import (
	"context"
	"fmt"
	"time"

	"google.golang.org/grpc/codes"

	microerr "go-micro.dev/v4/errors"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)

type tracker struct {
	startedAt time.Time

	profile          *StatsProfile
	span             ddtrace.Span
	startSpanOptions []ddtrace.StartSpanOption

	reqEndpoint string
	reqService  string
}

type requestDescriptor interface {
	Service() string
	Endpoint() string
}

type publicationDescriptor interface {
	Topic() string
}

// newRequestTracker creates a new tracker for an RPC request (client or server).
func newRequestTracker(req requestDescriptor, profile *StatsProfile) *tracker {
	return &tracker{
		profile:     profile,
		reqService:  req.Service(),
		reqEndpoint: req.Endpoint(),
	}
}

// newEventTracker creates a new tracker for a publication (client or server).
func newEventTracker(pub publicationDescriptor, profile *StatsProfile) *tracker {
	return &tracker{
		profile:     profile,
		reqService:  "micro.pubsub",
		reqEndpoint: pub.Topic(),
	}
}

// start monitoring a request. You can choose to let this method
// start a span for the request or attach one later.
func (t *tracker) StartSpanFromContext(ctx context.Context) context.Context {
	t.startedAt = time.Now()

	opts := []ddtrace.StartSpanOption{
		tracer.ResourceName(t.reqEndpoint),
		tracer.SpanType(ext.AppTypeRPC),
		tracer.StartTime(t.startedAt),
	}

	t.span, ctx = StartSpanFromContext(ctx, t.profile.Role, opts...)

	return ctx
}

// finishWithError end a request's monitoring session. If there is a span ongoing, it will
// be ended.
func (t *tracker) finishWithError(err error, noDebugStack bool) {
	if t.span == nil {
		return
	}

	statusCode := codes.OK
	finishOptions := []tracer.FinishOption{
		tracer.FinishTime(time.Now()),
	}

	microErr, ok := err.(*microerr.Error)

	if ok {
		finishOptions = append(finishOptions, tracer.WithError(
			fmt.Errorf("%s: %s", microErr.Id, microErr.Detail),
		))

		c, ok := microCodeToStatusCode[microErr.Code]
		if ok {
			statusCode = c
		} else {
			statusCode = codes.Unknown
		}
	}

	t.span.SetTag(tagStatus, statusCode.String())

	if noDebugStack {
		finishOptions = append(finishOptions, tracer.NoDebugStack())
	}

	t.span.Finish(finishOptions...)
	t.span = nil
}
