package http

import (
	"context"
	"encoding/json"
	"net/http"

	"github.com/go-chi/chi"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	"github.com/influxdata/influxdb/v2/kit/prom"
	kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
	"github.com/influxdata/influxdb/v2/pprof"
	"github.com/prometheus/client_golang/prometheus"
	"go.uber.org/zap"
)

const (
	// MetricsPath exposes the prometheus metrics over /metrics.
	MetricsPath = "/metrics"
	// ReadyPath exposes the readiness of the service over /ready.
	ReadyPath = "/ready"
	// HealthPath exposes the health of the service over /health.
	HealthPath = "/health"
	// DebugPath exposes /debug/pprof for go debugging.
	DebugPath = "/debug"
)

// Handler provides basic handling of metrics, health and debug endpoints.
// All other requests are passed down to the sub handler.
type Handler struct {
	name string
	r    chi.Router

	requests   *prometheus.CounterVec
	requestDur *prometheus.HistogramVec

	// log logs all HTTP requests as they are served
	log *zap.Logger
}

type (
	handlerOpts struct {
		log           *zap.Logger
		apiHandler    http.Handler
		healthHandler http.Handler
		readyHandler  http.Handler
		pprofEnabled  bool

		// NOTE: Track the registry even if metricsExposed = false
		// so we can report HTTP metrics via telemetry.
		metricsRegistry *prom.Registry
		metricsExposed  bool
	}

	HandlerOptFn func(opts *handlerOpts)
)

func (o *handlerOpts) metricsHTTPHandler() http.Handler {
	if o.metricsRegistry != nil && o.metricsExposed {
		return o.metricsRegistry.HTTPHandler()
	}
	handlerFunc := func(rw http.ResponseWriter, r *http.Request) {
		kithttp.WriteErrorResponse(r.Context(), rw, errors.EForbidden, "metrics disabled")
	}
	return http.HandlerFunc(handlerFunc)
}

func WithLog(l *zap.Logger) HandlerOptFn {
	return func(opts *handlerOpts) {
		opts.log = l
	}
}

func WithAPIHandler(h http.Handler) HandlerOptFn {
	return func(opts *handlerOpts) {
		opts.apiHandler = h
	}
}

func WithPprofEnabled(enabled bool) HandlerOptFn {
	return func(opts *handlerOpts) {
		opts.pprofEnabled = enabled
	}
}

func WithMetrics(reg *prom.Registry, exposed bool) HandlerOptFn {
	return func(opts *handlerOpts) {
		opts.metricsRegistry = reg
		opts.metricsExposed = exposed
	}
}

type AddHeader struct {
	WriteHeader func(header http.Header)
}

// Middleware is a middleware that mutates the header of all responses
func (h *AddHeader) Middleware(next http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		h.WriteHeader(w.Header())
		next.ServeHTTP(w, r)
	}
	return http.HandlerFunc(fn)
}

// NewRootHandler creates a new handler with the given name and registers any root-level
// (non-API) routes enabled by the caller.
func NewRootHandler(name string, opts ...HandlerOptFn) *Handler {
	opt := handlerOpts{
		log:             zap.NewNop(),
		healthHandler:   http.HandlerFunc(HealthHandler),
		readyHandler:    ReadyHandler(),
		pprofEnabled:    false,
		metricsRegistry: nil,
		metricsExposed:  false,
	}
	for _, o := range opts {
		o(&opt)
	}

	h := &Handler{
		name: name,
		log:  opt.log,
	}
	h.initMetrics()

	r := chi.NewRouter()
	buildHeader := &AddHeader{
		WriteHeader: func(header http.Header) {
			header.Add("X-Influxdb-Build", "OSS")
			header.Add("X-Influxdb-Version", influxdb.GetBuildInfo().Version)
		},
	}
	r.Use(buildHeader.Middleware)
	// only gather metrics for system handlers
	r.Group(func(r chi.Router) {
		r.Use(
			kithttp.Metrics(name, h.requests, h.requestDur),
		)
		r.Mount(MetricsPath, opt.metricsHTTPHandler())
		r.Mount(ReadyPath, opt.readyHandler)
		r.Mount(HealthPath, opt.healthHandler)
		r.Mount(DebugPath, pprof.NewHTTPHandler(opt.pprofEnabled))
	})

	// gather metrics and traces for everything else
	r.Group(func(r chi.Router) {
		r.Use(
			kithttp.Trace(name),
			kithttp.Metrics(name, h.requests, h.requestDur),
		)
		r.Mount("/", opt.apiHandler)
	})

	h.r = r

	if opt.metricsRegistry != nil {
		opt.metricsRegistry.MustRegister(h.PrometheusCollectors()...)
	}
	return h
}

// ServeHTTP delegates a request to the appropriate subhandler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h.r.ServeHTTP(w, r)
}

// PrometheusCollectors satisfies prom.PrometheusCollector.
func (h *Handler) PrometheusCollectors() []prometheus.Collector {
	return []prometheus.Collector{
		h.requests,
		h.requestDur,
	}
}

func (h *Handler) initMetrics() {
	const namespace = "http"
	const handlerSubsystem = "api"

	labelNames := []string{"handler", "method", "path", "status", "user_agent", "response_code"}
	h.requests = prometheus.NewCounterVec(prometheus.CounterOpts{
		Namespace: namespace,
		Subsystem: handlerSubsystem,
		Name:      "requests_total",
		Help:      "Number of http requests received",
	}, labelNames)

	h.requestDur = prometheus.NewHistogramVec(prometheus.HistogramOpts{
		Namespace: namespace,
		Subsystem: handlerSubsystem,
		Name:      "request_duration_seconds",
		Help:      "Time taken to respond to HTTP request",
	}, labelNames)
}

func encodeResponse(ctx context.Context, w http.ResponseWriter, code int, res interface{}) error {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(code)

	return json.NewEncoder(w).Encode(res)
}

func logEncodingError(log *zap.Logger, r *http.Request, err error) {
	// If we encounter an error while encoding the response to an http request
	// the best thing we can do is log that error, as we may have already written
	// the headers for the http request in question.
	log.Error("Error encoding response",
		zap.String("path", r.URL.Path),
		zap.String("method", r.Method),
		zap.Error(err))
}
