package pprof

import (
	"fmt"
	"io"
	"net/http"
	httppprof "net/http/pprof"
	"strconv"
	"time"

	"github.com/go-chi/chi"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	ihttp "github.com/influxdata/influxdb/v2/kit/transport/http"
)

type Handler struct {
	chi.Router
}

func NewHTTPHandler(profilingEnabled bool) *Handler {
	r := chi.NewRouter()
	r.Route("/pprof", func(r chi.Router) {
		if !profilingEnabled {
			r.NotFound(profilingDisabledHandler)
			return
		}
		r.Get("/cmdline", httppprof.Cmdline)
		r.Get("/profile", httppprof.Profile)
		r.Get("/symbol", httppprof.Symbol)
		r.Get("/trace", httppprof.Trace)
		r.Get("/all", archiveProfilesHandler)
		r.Mount("/", http.HandlerFunc(httppprof.Index))
	})

	return &Handler{r}
}

func profilingDisabledHandler(w http.ResponseWriter, r *http.Request) {
	ihttp.WriteErrorResponse(r.Context(), w, errors.EForbidden, "profiling disabled")
}

func archiveProfilesHandler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	// We parse the form here so that we can use the http.Request.Form map.
	//
	// Otherwise we'd have to use r.FormValue() which makes it impossible to
	// distinguish between a form value that exists and has no value and one that
	// does not exist at all.
	if err := r.ParseForm(); err != nil {
		ihttp.WriteErrorResponse(ctx, w, errors.EInternal, err.Error())
		return
	}

	// In the following two blocks, we check if the request should include cpu
	// profiles and a trace log.
	//
	// Since the submitted form can contain multiple version of a variable like:
	//
	//   http://localhost:8086?cpu=1s&cpu=30s&trace=3s&cpu=5s
	//
	// the question arises: which value should we use?  We choose to use the LAST
	// value supplied.
	//
	// This is an edge case but if for some reason, for example, a url is
	// programmatically built and multiple values are supplied, this will do what
	// is expected.
	//
	var traceDuration, cpuDuration time.Duration

	// last() returns either the last item from a slice of strings or an empty
	// string if the supplied slice is empty or nil.
	last := func(s []string) string {
		if len(s) == 0 {
			return ""
		}
		return s[len(s)-1]
	}

	// If trace exists as a form value, add it to the profiles slice with the
	// decoded duration.
	//
	// Requests for a trace should look like:
	//
	//  ?trace=10s
	//
	if vals, exists := r.Form["trace"]; exists {
		// parse the duration encoded in the last "trace" value supplied.
		val := last(vals)
		duration, err := time.ParseDuration(val)

		// If we can't parse the duration or if the user supplies a negative
		// number, return an appropriate error status and message.
		//
		// In this case it is a StatusBadRequest (400) since the problem is in the
		// supplied form data.
		if duration < 0 {
			ihttp.WriteErrorResponse(ctx, w, errors.EInvalid, "negative trace durations not allowed")
			return
		}

		if err != nil {
			ihttp.WriteErrorResponse(ctx, w, errors.EInvalid, fmt.Sprintf("could not parse supplied duration for trace %q", val))
			return
		}

		// Trace files can get big. Lets clamp the maximum trace duration to 45s.
		if duration > 45*time.Second {
			ihttp.WriteErrorResponse(ctx, w, errors.EInvalid, "cannot trace for longer than 45s")
			return
		}

		traceDuration = duration
	}

	// Capturing CPU profiles is a little trickier. The preferred way to send the
	// cpu profile duration is via the supplied "cpu" variable's value.
	//
	// The duration should be encoded as a Go duration that can be parsed by
	// time.ParseDuration().
	//
	// In the past users were encouraged to assign any value to cpu and provide
	// the duration in a separate "seconds" value.
	//
	// The code below handles both -- first it attempts to use the old method
	// which would look like:
	//
	//    ?cpu=foobar&seconds=10
	//
	// Then it attempts to ascertain the duration provided with:
	//
	//    ?cpu=10s
	//
	// This preserves backwards compatibility with any tools that have been
	// written to gather profiles.
	//
	if vals, exists := r.Form["cpu"]; exists {
		duration := time.Second * 30
		val := last(vals)

		// getDuration is a small function literal that encapsulates the logic
		// for getting the duration from either the "seconds" form value or from
		// the value assigned to "cpu".
		getDuration := func() (time.Duration, error) {
			if seconds, exists := r.Form["seconds"]; exists {
				s, err := strconv.ParseInt(last(seconds), 10, 64)
				if err != nil {
					return 0, err
				}
				return time.Second * time.Duration(s), nil
			}
			// see if the value of cpu is a duration like:  cpu=10s
			return time.ParseDuration(val)
		}

		duration, err := getDuration()
		if err != nil {
			ihttp.WriteErrorResponse(ctx, w, errors.EInvalid, fmt.Sprintf("could not parse supplied duration for cpu profile %q", val))
			return
		}

		cpuDuration = duration
	}

	tarstream, err := collectAllProfiles(ctx, traceDuration, cpuDuration)
	if err != nil {
		ihttp.WriteErrorResponse(ctx, w, errors.EInternal, err.Error())
		return
	}
	_, _ = io.Copy(w, tarstream)
}
