package metrics

import (
	"errors"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus"
)

const (
	routeNotFound         = "<not_found>"
	routeMethodNotAllowed = "<method_not_allowed>"
	routeUnknown          = "<unknown>"
	labelUnknown          = "<unknown>"
	labelInvalid          = "<invalid>"
)

type HTTPMiddleware struct {
	requests *prometheus.CounterVec
	duration *prometheus.HistogramVec
	inflight prometheus.Gauge
	panics   *prometheus.CounterVec
}

func RegisterHTTP(reg prometheus.Registerer) (*HTTPMiddleware, error) {
	if reg == nil {
		return nil, errors.New("prometheus registerer is nil")
	}
	middleware := &HTTPMiddleware{
		requests: prometheus.NewCounterVec(
			prometheus.CounterOpts{
				Name: "agentteam_http_requests_total",
				Help: "Total number of HTTP requests.",
			},
			[]string{"method", "route", "status"},
		),
		duration: prometheus.NewHistogramVec(
			prometheus.HistogramOpts{
				Name:    "agentteam_http_request_duration_seconds",
				Help:    "HTTP request duration in seconds.",
				Buckets: BucketsLatencySeconds,
			},
			[]string{"method", "route", "status_class"},
		),
		inflight: prometheus.NewGauge(
			prometheus.GaugeOpts{
				Name: "agentteam_http_inflight",
				Help: "Current number of HTTP requests in flight.",
			},
		),
		panics: prometheus.NewCounterVec(
			prometheus.CounterOpts{
				Name: "agentteam_http_panics_total",
				Help: "Total number of recovered HTTP request panics.",
			},
			[]string{"method", "route"},
		),
	}
	for _, collector := range []prometheus.Collector{
		middleware.requests,
		middleware.duration,
		middleware.inflight,
		middleware.panics,
	} {
		if err := reg.Register(collector); err != nil {
			return nil, err
		}
	}
	return middleware, nil
}

func (middleware *HTTPMiddleware) Handler(skipPaths ...string) gin.HandlerFunc {
	skipped := make(map[string]struct{}, len(skipPaths))
	for _, path := range skipPaths {
		if trimmed := strings.TrimSpace(path); trimmed != "" {
			skipped[trimmed] = struct{}{}
		}
	}
	return func(ctx *gin.Context) {
		if middleware == nil || isSkippedPath(ctx, skipped) {
			ctx.Next()
			return
		}

		startedAt := time.Now()
		middleware.inflight.Inc()
		defer func() {
			recovered := recover()
			status := ctx.Writer.Status()
			if recovered != nil {
				status = http.StatusInternalServerError
			}
			method := methodLabel(ctx.Request.Method)
			route := routeLabel(ctx, status)
			middleware.requests.WithLabelValues(method, route, statusCodeLabel(status)).Inc()
			middleware.duration.WithLabelValues(method, route, statusClassLabel(status)).Observe(time.Since(startedAt).Seconds())
			middleware.inflight.Dec()
			if recovered != nil {
				panic(recovered)
			}
		}()

		ctx.Next()
	}
}

func (middleware *HTTPMiddleware) RecordPanic(method string, route string) {
	if middleware == nil {
		return
	}
	middleware.panics.WithLabelValues(methodLabel(method), routeLabelValue(route)).Inc()
}

func isSkippedPath(ctx *gin.Context, skipped map[string]struct{}) bool {
	if ctx == nil || ctx.Request == nil || ctx.Request.URL == nil {
		return false
	}
	_, ok := skipped[ctx.Request.URL.Path]
	return ok
}

func methodLabel(method string) string {
	switch strings.ToUpper(strings.TrimSpace(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 labelUnknown
	}
}

func routeLabel(ctx *gin.Context, status int) string {
	if ctx == nil {
		return routeUnknown
	}
	return routeLabelForStatus(ctx.FullPath(), status)
}

func routeLabelForStatus(route string, status int) string {
	if trimmed := strings.TrimSpace(route); trimmed != "" {
		return trimmed
	}
	switch status {
	case http.StatusNotFound:
		return routeNotFound
	case http.StatusMethodNotAllowed:
		return routeMethodNotAllowed
	default:
		return routeUnknown
	}
}

// RouteLabelForStatus returns the canonical low-cardinality route label for an HTTP status.
func RouteLabelForStatus(route string, status int) string {
	return routeLabelForStatus(route, status)
}

func routeLabelValue(route string) string {
	if trimmed := strings.TrimSpace(route); trimmed != "" {
		return trimmed
	}
	return routeUnknown
}

func statusCodeLabel(status int) string {
	if status < 100 || status > 599 {
		return labelInvalid
	}
	return strconv.Itoa(status)
}

func statusClassLabel(status int) string {
	if status < 100 || status > 599 {
		return labelInvalid
	}
	return strconv.Itoa(status/100) + "xx"
}
