package tracing

import (
	"errors"
	"io"
	"net"
	"net/http"
	"strings"
	"sync"

	"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"
)

type wrappedHTTPTransport interface {
	agentTeamObservabilityHTTPTransport()
}

type httpClientTransport struct {
	base       http.RoundTripper
	tracer     oteltrace.Tracer
	propagator propagation.TextMapPropagator
	name       string
}

// WrapHTTPClient returns a shallow copy of client whose RoundTripper records
// outbound HTTP client spans and injects W3C propagation headers.
func WrapHTTPClient(client *http.Client, tracer oteltrace.Tracer, propagator propagation.TextMapPropagator, name string) *http.Client {
	if client == nil {
		client = &http.Client{}
	}
	wrapped := *client
	transport := wrapped.Transport
	if transport == nil {
		transport = http.DefaultTransport
	}
	if _, ok := transport.(wrappedHTTPTransport); ok {
		return &wrapped
	}
	if tracer == nil {
		tracer = noop.NewTracerProvider().Tracer(InstrumentationName)
	}
	if propagator == nil {
		propagator = NewPropagator()
	}
	wrapped.Transport = &httpClientTransport{
		base:       transport,
		tracer:     tracer,
		propagator: propagator,
		name:       strings.TrimSpace(name),
	}
	return &wrapped
}

func (transport *httpClientTransport) agentTeamObservabilityHTTPTransport() {}

func (transport *httpClientTransport) RoundTrip(request *http.Request) (*http.Response, error) {
	if request == nil {
		return transport.base.RoundTrip(request)
	}
	tracer := transport.tracer
	if tracer == nil {
		tracer = noop.NewTracerProvider().Tracer(InstrumentationName)
	}
	propagator := transport.propagator
	if propagator == nil {
		propagator = NewPropagator()
	}

	method := methodLabel(request)
	ctx, span := tracer.Start(
		request.Context(),
		clientSpanName(method),
		oteltrace.WithSpanKind(oteltrace.SpanKindClient),
	)
	span.SetAttributes(clientRequestAttributes(request, method, transport.name)...)

	outgoing := request.Clone(ctx)
	outgoing.Header = request.Header.Clone()
	propagator.Inject(ctx, propagation.HeaderCarrier(outgoing.Header))

	response, err := transport.base.RoundTrip(outgoing)
	if err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, err.Error())
		span.End()
		return response, err
	}
	if response == nil {
		span.End()
		return nil, nil
	}
	span.SetAttributes(attribute.Int("http.response.status_code", response.StatusCode))
	if response.StatusCode >= http.StatusInternalServerError {
		span.SetStatus(codes.Error, http.StatusText(response.StatusCode))
	}
	if response.Body == nil {
		span.End()
		return response, nil
	}

	response.Body = &spanEndingReadCloser{
		body: response.Body,
		span: span,
	}
	return response, nil
}

func clientSpanName(method string) string {
	return "HTTP " + method
}

func clientRequestAttributes(request *http.Request, method string, name string) []attribute.KeyValue {
	attributes := []attribute.KeyValue{
		attribute.String("http.request.method", method),
	}
	if name != "" {
		attributes = append(attributes, attribute.String("agentteam.http.client", name))
	}
	if request == nil || request.URL == nil {
		return attributes
	}
	if scheme := strings.TrimSpace(request.URL.Scheme); scheme != "" {
		attributes = append(attributes, attribute.String("url.scheme", scheme))
	}
	host := request.URL.Hostname()
	if host != "" {
		attributes = append(attributes, attribute.String("server.address", host))
	}
	if port := request.URL.Port(); port != "" {
		if parsedPort, err := net.LookupPort("tcp", port); err == nil {
			attributes = append(attributes, attribute.Int("server.port", parsedPort))
		}
	}
	return attributes
}

type spanEndingReadCloser struct {
	body io.ReadCloser
	span oteltrace.Span
	once sync.Once
}

func (body *spanEndingReadCloser) Read(p []byte) (int, error) {
	n, err := body.body.Read(p)
	if err != nil {
		body.end(err)
	}
	return n, err
}

func (body *spanEndingReadCloser) Close() error {
	err := body.body.Close()
	body.end(err)
	return err
}

func (body *spanEndingReadCloser) end(err error) {
	body.once.Do(func() {
		if err != nil && !errors.Is(err, io.EOF) {
			body.span.RecordError(err)
			body.span.SetStatus(codes.Error, err.Error())
		}
		body.span.End()
	})
}
