package service

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/traefik/traefik/v3/pkg/metrics"
	"github.com/traefik/traefik/v3/pkg/middlewares/observability"
	"github.com/traefik/traefik/v3/pkg/tracing"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/metric"
	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
	"go.opentelemetry.io/otel/trace"
)

type wrapper struct {
	semConvMetricRegistry *metrics.SemConvMetricsRegistry
	rt                    http.RoundTripper
}

func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
	start := time.Now()
	var span trace.Span
	var tracingCtx context.Context
	var tracer *tracing.Tracer
	if tracer = tracing.TracerFromContext(req.Context()); tracer != nil {
		tracingCtx, span = tracer.Start(req.Context(), "ReverseProxy", trace.WithSpanKind(trace.SpanKindClient))
		defer span.End()

		req = req.WithContext(tracingCtx)

		tracer.CaptureClientRequest(span, req)
		tracing.InjectContextIntoCarrier(req)
	}

	var statusCode int
	var headers http.Header
	response, err := t.rt.RoundTrip(req)
	if err != nil {
		statusCode = computeStatusCode(err)
	}
	if response != nil {
		statusCode = response.StatusCode
		headers = response.Header
	}

	if tracer != nil {
		tracer.CaptureResponse(span, headers, statusCode, trace.SpanKindClient)
	}

	end := time.Now()

	// Ending the span as soon as the response is handled because we want to use the same end time for the trace and the metric.
	// If any errors happen earlier, this span will be close by the defer instruction.
	if span != nil {
		span.End(trace.WithTimestamp(end))
	}

	if t.semConvMetricRegistry != nil && t.semConvMetricRegistry.HTTPClientRequestDuration() != nil {
		var attrs []attribute.KeyValue

		if statusCode < 100 || statusCode >= 600 {
			attrs = append(attrs, attribute.Key("error.type").String(fmt.Sprintf("Invalid HTTP status code %d", statusCode)))
		} else if statusCode >= 400 {
			attrs = append(attrs, attribute.Key("error.type").String(strconv.Itoa(statusCode)))
		}

		attrs = append(attrs, semconv.HTTPRequestMethodKey.String(req.Method))
		attrs = append(attrs, semconv.HTTPResponseStatusCode(statusCode))
		attrs = append(attrs, semconv.NetworkProtocolName(strings.ToLower(req.Proto)))
		attrs = append(attrs, semconv.NetworkProtocolVersion(observability.Proto(req.Proto)))
		attrs = append(attrs, semconv.ServerAddress(req.URL.Host))

		_, port, err := net.SplitHostPort(req.URL.Host)
		if err != nil {
			switch req.URL.Scheme {
			case "http":
				attrs = append(attrs, semconv.ServerPort(80))
			case "https":
				attrs = append(attrs, semconv.ServerPort(443))
			}
		} else {
			intPort, _ := strconv.Atoi(port)
			attrs = append(attrs, semconv.ServerPort(intPort))
		}

		attrs = append(attrs, semconv.URLScheme(req.Header.Get("X-Forwarded-Proto")))

		t.semConvMetricRegistry.HTTPClientRequestDuration().Record(req.Context(), end.Sub(start).Seconds(), metric.WithAttributes(attrs...))
	}

	return response, err
}

func newObservabilityRoundTripper(semConvMetricRegistry *metrics.SemConvMetricsRegistry, rt http.RoundTripper) http.RoundTripper {
	return &wrapper{
		semConvMetricRegistry: semConvMetricRegistry,
		rt:                    rt,
	}
}
