package metrics

import (
	"errors"
	"net/http"
	"strconv"
	"time"
	"unicode/utf8"

	"github.com/gin-gonic/gin"
	gokitmetrics "github.com/go-kit/kit/metrics"
	gokitprom "github.com/go-kit/kit/metrics/prometheus"
	"github.com/prometheus/client_golang/prometheus"
)

func NewAPIMetricsGinMiddleware(serviceName string) gin.HandlerFunc {
	reqTotalCounter := gokitprom.NewCounterFrom(
		prometheus.CounterOpts{
			Namespace: "rest",
			Subsystem: "server",
			Name:      "requests_total",
			Help:      "How many HTTP requests processed on a service, partitioned by method, uri and status code.",
		}, []string{"service", "method", "uri"})
	reqInFlightGauge := gokitprom.NewGaugeFrom(
		prometheus.GaugeOpts{
			Namespace: "rest",
			Subsystem: "server",
			Name:      "requests_in_flight",
			Help:      "How many in flight HTTP requests exist on a service, partitioned by method and uri.",
		}, []string{"service", "method", "uri"})
	reqDurationHistogram := gokitprom.NewHistogramFrom(
		prometheus.HistogramOpts{
			Namespace: "rest",
			Subsystem: "server",
			Name:      "request_duration_seconds",
			Help:      "How long it took to process the request on a service, partitioned by method, uri, and status code.",
		}, []string{"service", "method", "uri", "code"})

	resp4XXCounter := gokitprom.NewCounterFrom(
		prometheus.CounterOpts{
			Namespace: "rest",
			Subsystem: "server",
			Name:      "responses_4xx",
			Help:      "How many HTTP response 4xx on a service, partitioned by method, uri and status code.",
		}, []string{"service", "method", "uri", "code"})
	resp5XXCounter := gokitprom.NewCounterFrom(
		prometheus.CounterOpts{
			Namespace: "rest",
			Subsystem: "server",
			Name:      "responses_5xx",
			Help:      "How many HTTP response 5xx on a service, partitioned by method, uri and status code.",
		}, []string{"service", "method", "uri", "code"})

	mw := &metricsMiddleware{
		reqTotalCounter:      reqTotalCounter,
		reqDurationHistogram: MustNewHistogramWithScale(reqDurationHistogram, time.Second),
		reqInFlightGauge:     reqInFlightGauge,
		resp4xxCounter:       resp4XXCounter,
		resp5xxCounter:       resp5XXCounter,
		baseLabels:           []string{"service", serviceName},
	}
	return mw.GinMiddleware()
}

type metricsMiddleware struct {
	reqTotalCounter      gokitmetrics.Counter
	reqInFlightGauge     gokitmetrics.Gauge
	reqDurationHistogram *HistogramWithScale
	resp4xxCounter       gokitmetrics.Counter
	resp5xxCounter       gokitmetrics.Counter
	baseLabels           []string
}

func (m *metricsMiddleware) GinMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		req := c.Request
		// TODO(shenjun): use pool for the labels []string
		labels := make([]string, 0, 6)
		labels = append(labels, m.baseLabels...)
		labels = append(labels, "method", getMethod(req), "uri", c.FullPath())
		defer func(begin time.Time) {
			labelsWithCode := append(labels, "code", strconv.Itoa(c.Writer.Status()))
			m.reqInFlightGauge.With(labels...).Add(-1)
			m.reqDurationHistogram.With(labelsWithCode...).ObserveFromStart(begin)
			if c.Writer.Status() >= http.StatusInternalServerError {
				m.resp5xxCounter.With(labelsWithCode...).Add(1)
			} else if c.Writer.Status() >= http.StatusBadRequest {
				m.resp4xxCounter.With(labelsWithCode...).Add(1)
			}
		}(time.Now())
		m.reqTotalCounter.With(labels...).Add(1)
		m.reqInFlightGauge.With(labels...).Add(1)
		c.Next()
	}
}

// Copy from traefik metrics package
// getMethod returns the request's method.
// It checks whether the method is a valid UTF-8 string.
// To restrict the (potentially infinite) number of accepted values for the method,
// and avoid unbounded memory issues,
// values that are not part of the set of HTTP verbs are replaced with EXTENSION_METHOD.
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
// https://datatracker.ietf.org/doc/html/rfc2616/#section-5.1.1.
func getMethod(r *http.Request) string {
	if !utf8.ValidString(r.Method) {
		return "NON_UTF8_HTTP_METHOD"
	}

	switch r.Method {
	case "HEAD", "GET", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", // https://datatracker.ietf.org/doc/html/rfc7231#section-4
		"PATCH", // https://datatracker.ietf.org/doc/html/rfc5789#section-2
		"PRI":   // https://datatracker.ietf.org/doc/html/rfc7540#section-11.6
		return r.Method
	default:
		return "EXTENSION_METHOD"
	}
}

// HistogramWithScale is a histogram that will convert its observed value to the specified unit.
type HistogramWithScale struct {
	histogram gokitmetrics.Histogram
	unit      time.Duration
}

// With implements ScalableHistogram.
func (s *HistogramWithScale) With(labelValues ...string) *HistogramWithScale {
	h, _ := NewHistogramWithScale(s.histogram.With(labelValues...), s.unit)
	return h
}

// ObserveFromStart implements ScalableHistogram.
func (s *HistogramWithScale) ObserveFromStart(start time.Time) {
	if s.unit <= 0 {
		return
	}

	d := float64(time.Since(start).Nanoseconds()) / float64(s.unit)
	if d < 0 {
		d = 0
	}
	s.histogram.Observe(d)
}

// Observe implements ScalableHistogram.
func (s *HistogramWithScale) Observe(v float64) {
	s.histogram.Observe(v)
}

// NewHistogramWithScale returns a ScalableHistogram. It returns an error if the given unit is <= 0.
func NewHistogramWithScale(histogram gokitmetrics.Histogram, unit time.Duration) (*HistogramWithScale, error) {
	if unit <= 0 {
		return nil, errors.New("invalid time unit")
	}
	return &HistogramWithScale{
		histogram: histogram,
		unit:      unit,
	}, nil
}

func MustNewHistogramWithScale(histogram gokitmetrics.Histogram, unit time.Duration) *HistogramWithScale {
	if unit <= 0 {
		panic("invalid time unit")
	}
	return &HistogramWithScale{
		histogram: histogram,
		unit:      unit,
	}
}
