package tracing

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/propagation"
	oteltrace "go.opentelemetry.io/otel/trace"
	"go.opentelemetry.io/otel/trace/noop"

	"github.com/tidbcloud/AgentTeam/internal/observability/metrics"
)

const (
	InstrumentationName = "github.com/tidbcloud/AgentTeam/internal/observability"
	unknownValue        = "<unknown>"
)

var errHTTPHandlerPanic = errors.New("http handler panic")

// HTTPServer creates inbound HTTP server spans for the Gin router.
type HTTPServer struct {
	tracer     oteltrace.Tracer
	propagator propagation.TextMapPropagator
}

// NewHTTPServer constructs inbound HTTP tracing middleware.
func NewHTTPServer(tracer oteltrace.Tracer, propagator propagation.TextMapPropagator) *HTTPServer {
	if tracer == nil {
		tracer = noop.NewTracerProvider().Tracer(InstrumentationName)
	}
	if propagator == nil {
		propagator = NewPropagator()
	}
	return &HTTPServer{
		tracer:     tracer,
		propagator: propagator,
	}
}

// NewPropagator returns AgentTeam's W3C trace context and baggage propagator.
func NewPropagator() propagation.TextMapPropagator {
	return propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})
}

// Handler returns Gin middleware that records one server span per request.
func (server *HTTPServer) Handler() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		if server == nil {
			ctx.Next()
			return
		}
		tracer := server.tracer
		if tracer == nil {
			tracer = noop.NewTracerProvider().Tracer(InstrumentationName)
		}
		propagator := server.propagator
		if propagator == nil {
			propagator = NewPropagator()
		}

		requestCtx := requestContext(ctx)
		if ctx.Request != nil {
			requestCtx = propagator.Extract(requestCtx, propagation.HeaderCarrier(ctx.Request.Header))
		}
		method := methodLabel(ctx.Request)
		spanCtx, span := tracer.Start(
			requestCtx,
			spanName(method, unknownValue),
			oteltrace.WithSpanKind(oteltrace.SpanKindServer),
		)
		if ctx.Request != nil {
			ctx.Request = ctx.Request.WithContext(spanCtx)
		}

		defer func() {
			recovered := recover()
			status := ctx.Writer.Status()
			if recovered != nil {
				status = http.StatusInternalServerError
			}
			route := metrics.RouteLabelForStatus(ctx.FullPath(), status)
			span.SetName(spanName(method, route))
			span.SetAttributes(
				attribute.String("http.request.method", method),
				attribute.String("http.route", route),
				attribute.Int("http.response.status_code", status),
			)
			if status >= 500 {
				span.SetStatus(codes.Error, http.StatusText(status))
			}
			if recovered != nil {
				span.RecordError(errHTTPHandlerPanic)
				span.End()
				panic(recovered)
			}
			span.End()
		}()

		ctx.Next()
	}
}

func spanName(method string, route string) string {
	return fmt.Sprintf("HTTP %s %s", method, route)
}

func methodLabel(request *http.Request) string {
	if request == nil {
		return unknownValue
	}
	switch strings.ToUpper(strings.TrimSpace(request.Method)) {
	case http.MethodGet:
		return http.MethodGet
	case http.MethodPost:
		return http.MethodPost
	case http.MethodPut:
		return http.MethodPut
	case http.MethodPatch:
		return http.MethodPatch
	case http.MethodDelete:
		return http.MethodDelete
	case http.MethodHead:
		return http.MethodHead
	case http.MethodOptions:
		return http.MethodOptions
	default:
		return unknownValue
	}
}

func requestContext(ctx *gin.Context) context.Context {
	if ctx != nil && ctx.Request != nil && ctx.Request.Context() != nil {
		return ctx.Request.Context()
	}
	return context.Background()
}
