package http

import (
	"fmt"
	"net"
	"net/http"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/propagation"
	semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
)

// observableTransport wraps an http.RoundTripper with tracing, metrics, and logging
type observableTransport struct {
	base    http.RoundTripper
	config  AdapterConfig
	metrics *metrics
}

// RoundTrip implements http.RoundTripper
func (t *observableTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	// Skip if disabled
	if t.config.Disabled {
		return t.base.RoundTrip(req)
	}

	ctx := req.Context()
	start := time.Now()

	method := req.Method
	host := outboundHostLabel(req)

	// Track active requests
	if t.metrics != nil {
		t.metrics.activeRequests.WithLabelValues(method, host).Inc()
		defer t.metrics.activeRequests.WithLabelValues(method, host).Dec()
	}

	// Start span
	spanName := fmt.Sprintf("HTTP %s %s", method, req.URL.Path)
	ctx, span := t.config.Tracer.Start(ctx, spanName,
		trace.WithSpanKind(trace.SpanKindClient),
		trace.WithAttributes(
			semconv.HTTPRequestMethodKey.String(method),
			semconv.URLFullKey.String(req.URL.String()),
			semconv.URLSchemeKey.String(req.URL.Scheme),
			semconv.URLPathKey.String(req.URL.EscapedPath()),
		),
	)
	if address, port := splitHostPort(req.URL.Host); address != "" {
		span.SetAttributes(semconv.ServerAddressKey.String(address))
		if port > 0 {
			span.SetAttributes(semconv.ServerPort(port))
		}
	}
	defer span.End()

	// Inject trace context into HTTP headers
	otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))

	if !t.config.DisableLogging && t.config.Logger != nil {
		t.config.Logger.Debug("HTTP request started",
			zap.String("method", method),
			zap.String("url", req.URL.String()),
			zap.String("host", host),
			zap.String("trace_id", span.SpanContext().TraceID().String()),
			zap.String("span_id", span.SpanContext().SpanID().String()),
		)
	}

	// Record request size
	if t.metrics != nil && req.ContentLength > 0 {
		t.metrics.requestSize.WithLabelValues(method, host).Observe(float64(req.ContentLength))
	}

	// Execute request
	req = req.WithContext(ctx)
	resp, err := t.base.RoundTrip(req)

	duration := time.Since(start)

	if err != nil {
		// Handle error
		span.RecordError(err)
		span.SetStatus(codes.Error, err.Error())

		if t.metrics != nil {
			t.metrics.requestsTotal.WithLabelValues(method, host, "error").Inc()
			t.metrics.requestDuration.WithLabelValues(method, host).Observe(duration.Seconds())
		}

		if !t.config.DisableLogging && t.config.Logger != nil {
			t.config.Logger.Error("HTTP request failed",
				zap.String("method", method),
				zap.String("url", req.URL.String()),
				zap.Duration("duration", duration),
				zap.Error(err),
				zap.String("trace_id", span.SpanContext().TraceID().String()),
			)
		}

		return nil, err
	}

	// Success case
	statusCode := resp.StatusCode
	statusClass := fmt.Sprintf("%dxx", statusCode/100)

	// Record span attributes
	span.SetAttributes(
		semconv.HTTPResponseStatusCode(statusCode),
	)
	if resp.ContentLength > 0 {
		span.SetAttributes(semconv.HTTPResponseBodySizeKey.Int64(resp.ContentLength))
	}
	if statusCode >= 400 {
		span.SetStatus(codes.Error, fmt.Sprintf("HTTP %d", statusCode))
	}

	// Record metrics
	if t.metrics != nil {
		t.metrics.requestsTotal.WithLabelValues(method, host, statusClass).Inc()
		t.metrics.requestDuration.WithLabelValues(method, host).Observe(duration.Seconds())
		if resp.ContentLength > 0 {
			t.metrics.responseSize.WithLabelValues(method, host).Observe(float64(resp.ContentLength))
		}
	}

	// Log response
	logLevel := zap.DebugLevel
	if statusCode >= 500 {
		logLevel = zap.ErrorLevel
	} else if statusCode >= 400 {
		logLevel = zap.WarnLevel
	}

	if !t.config.DisableLogging && t.config.Logger != nil {
		t.config.Logger.Log(logLevel, "HTTP request completed",
			zap.String("method", method),
			zap.String("url", req.URL.String()),
			zap.Int("status", statusCode),
			zap.Duration("duration", duration),
			zap.Int64("response_size", resp.ContentLength),
			zap.String("trace_id", span.SpanContext().TraceID().String()),
		)
	}

	return resp, nil
}

func outboundHostLabel(req *http.Request) string {
	if req == nil || req.URL == nil {
		return "unknown"
	}
	host := req.URL.Hostname()
	if host == "" {
		host = req.Host
	}
	if host == "" {
		return "unknown"
	}
	if parsed := net.ParseIP(host); parsed != nil {
		return "ip"
	}
	return host
}
