// package metrics provided metrics middlewares.
package metrics

import (
	"bufio"
	"errors"
	"flag"
	"fmt"
	"net"
	"net/http"
	"strconv"
	"time"

	"github.com/pingcap/log"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"go.uber.org/zap"
)

var (
	totalRequests = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "http_requests_total",
			Help: "Number of get requests.",
		},
		[]string{"method", "path", "status"},
	)

	httpDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
		Name: "http_response_time_seconds",
		Help: "Duration of HTTP requests.",
	}, []string{"method", "path", "status"})

	// metrics server don't have auth, this port should be only used for internal traffic
	metricsPort = flag.Int("metrics-port", 8081, "Metrics server port")
)

func init() {
	prometheus.MustRegister(totalRequests)
	prometheus.MustRegister(httpDuration)
}

// Middleware returns the metrics middleware.
func Middleware(handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		url := r.URL.EscapedPath()
		log.Info("Proxy request start",
			zap.String("method", r.Method),
			zap.String("URL", url))

		start := time.Now()
		wrapped := wrapResponseWriter(w)
		handler.ServeHTTP(wrapped, r)

		dur := time.Since(start)
		log.Info("Proxy request end",
			zap.String("method", r.Method),
			zap.String("URL", url),
			zap.Int("status", wrapped.status),
			zap.Duration("duration", dur))

		// TODO: get cluster_id from URL
		totalRequests.WithLabelValues(r.Method, url, strconv.Itoa(wrapped.status)).Inc()
		httpDuration.WithLabelValues(r.Method, url, strconv.Itoa(wrapped.status)).Observe(dur.Seconds())
	})
}

// StartMetricsServer starts and serves the metrics server.
func StartMetricServer() {
	if metricsPort != nil {
		mux := http.NewServeMux()
		mux.Handle("/metrics", promhttp.Handler())
		l, err := net.Listen("tcp", fmt.Sprintf(":%d", *metricsPort))
		if err != nil {
			panic(fmt.Sprintf("failed to listen to the port %d", *metricsPort))
		}
		go func() {
			err := http.Serve(l, mux)
			if err != nil {
				panic(fmt.Sprintf("metrics server died %+v", err))
			}
		}()
	}
}

type responseWriter struct {
	http.ResponseWriter
	status      int
	wroteHeader bool
}

func wrapResponseWriter(w http.ResponseWriter) *responseWriter {
	return &responseWriter{ResponseWriter: w}
}

func (rw *responseWriter) Status() int {
	return rw.status
}

func (rw *responseWriter) WriteHeader(code int) {
	if rw.wroteHeader {
		return
	}
	rw.status = code
	rw.ResponseWriter.WriteHeader(code)
	rw.wroteHeader = true
}

func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	if h, ok := rw.ResponseWriter.(http.Hijacker); ok {
		return h.Hijack()
	}
	return nil, nil, errors.New("the internal writer is not a http.Hijacker instance")
}
