package telemetry

import (
	"fmt"
	"net/http"
	"regexp"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/propagation"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
	"go.opentelemetry.io/otel/trace"
)

// Middleware returns an HTTP middleware that traces requests using OpenTelemetry.
// It creates a span for each request with standard HTTP attributes.
func Middleware(serviceName string) func(http.Handler) http.Handler {
	tracer := otel.Tracer(serviceName)

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Extract trace context from incoming request headers
			ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))

			// Determine the route pattern (for chi router)
			routePattern := getRoutePattern(r)
			if routePattern == "" {
				routePattern = r.URL.Path
			}

			// Create span name: "HTTP METHOD /path"
			spanName := fmt.Sprintf("%s %s", r.Method, routePattern)

			// Start span
			ctx, span := tracer.Start(ctx, spanName,
				trace.WithSpanKind(trace.SpanKindServer),
				trace.WithAttributes(
					semconv.HTTPRequestMethodKey.String(r.Method),
					semconv.URLPath(r.URL.Path),
					semconv.URLScheme(getScheme(r)),
					semconv.ServerAddress(r.Host),
					semconv.UserAgentOriginal(r.UserAgent()),
					semconv.HTTPRoute(routePattern),
				),
			)
			defer span.End()

			// Add request ID if available (from chi middleware)
			if reqID := middleware.GetReqID(ctx); reqID != "" {
				span.SetAttributes(attribute.String("request.id", reqID))
			}

			// Add client IP
			if clientIP := r.Header.Get("X-Real-IP"); clientIP != "" {
				span.SetAttributes(semconv.ClientAddress(clientIP))
			} else if clientIP := r.Header.Get("X-Forwarded-For"); clientIP != "" {
				span.SetAttributes(semconv.ClientAddress(clientIP))
			} else {
				span.SetAttributes(semconv.ClientAddress(r.RemoteAddr))
			}

			// Wrap response writer to capture status code
			rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

			// Continue with the next handler
			next.ServeHTTP(rw, r.WithContext(ctx))

			// Record response attributes
			span.SetAttributes(semconv.HTTPResponseStatusCode(rw.statusCode))

			// Mark span as error if status >= 400
			if rw.statusCode >= 400 {
				span.SetAttributes(attribute.Bool("error", true))
			}

			// Add response size if available
			if rw.bytesWritten > 0 {
				span.SetAttributes(attribute.Int64("http.response.body.size", rw.bytesWritten))
			}
		})
	}
}

// responseWriter wraps http.ResponseWriter to capture status code and bytes written.
type responseWriter struct {
	http.ResponseWriter
	statusCode   int
	bytesWritten int64
}

func (rw *responseWriter) WriteHeader(code int) {
	rw.statusCode = code
	rw.ResponseWriter.WriteHeader(code)
}

func (rw *responseWriter) Write(b []byte) (int, error) {
	n, err := rw.ResponseWriter.Write(b)
	rw.bytesWritten += int64(n)
	return n, err
}

// Unwrap returns the underlying ResponseWriter for middleware that needs it.
func (rw *responseWriter) Unwrap() http.ResponseWriter {
	return rw.ResponseWriter
}

// getRoutePattern attempts to get the chi route pattern for the request.
// Falls back to a normalized path if no pattern is available.
func getRoutePattern(r *http.Request) string {
	// Try to get chi's route pattern
	rctx := chi.RouteContext(r.Context())
	if rctx != nil && rctx.RoutePattern() != "" {
		return rctx.RoutePattern()
	}

	// Fall back to normalizing the path to avoid cardinality explosion
	return normalizePath(r.URL.Path)
}

// pathNormalizers contains patterns to normalize variable path segments.
var pathNormalizers = []struct {
	pattern *regexp.Regexp
	replace string
}{
	// /keys/uuid -> /keys/{id}
	{regexp.MustCompile(`^/keys/[^/]+$`), "/keys/{id}"},
	// /projects/{id}/claude-config/{type}/{name}
	{regexp.MustCompile(`^/projects/[^/]+/claude-config/(commands|skills|agents)/[^/]+$`), "/projects/{id}/claude-config/$1/{name}"},
	// /projects/{id}/... (any sub-path)
	{regexp.MustCompile(`^/projects/[^/]+(/.*)?$`), "/projects/{id}$1"},
}

// normalizePath normalizes the URL path for consistent span names.
// Replaces variable path segments with placeholders to prevent cardinality explosion.
func normalizePath(path string) string {
	for _, n := range pathNormalizers {
		if n.pattern.MatchString(path) {
			return n.pattern.ReplaceAllString(path, n.replace)
		}
	}
	return path
}

// getScheme determines the request scheme (http or https).
func getScheme(r *http.Request) string {
	if r.TLS != nil {
		return "https"
	}
	if scheme := r.Header.Get("X-Forwarded-Proto"); scheme != "" {
		return scheme
	}
	return "http"
}

// SpanFromRequest extracts the current span from a request context.
// Useful for adding attributes or events to the span in handlers.
func SpanFromRequest(r *http.Request) trace.Span {
	return trace.SpanFromContext(r.Context())
}
