package middleware

import (
	"fmt"
	"net/http"

	"github.com/gobwas/glob"
	"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/services/contexthandler"
	contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/util"
)

var (
	errInvalidAllowedURL = func(url string) error {
		return fmt.Errorf("action URL '%s' is invalid", url)
	}
)

type errorWithStatus struct {
	Underlying error
	HTTPStatus int
}

func (e errorWithStatus) Error() string {
	return e.Underlying.Error()
}

func (e errorWithStatus) Unwrap() error {
	return e.Underlying
}

func ValidateActionUrl(cfg *setting.Cfg, logger log.Logger) func(http.Handler) http.Handler {
	// get the urls allowed from server config
	allGlobs, globErr := cacheGlobs(cfg.ActionsAllowPostURL)
	if globErr != nil {
		logger.Error("invalid glob settings in config section [security] actions_allow_post_url", "url", cfg.ActionsAllowPostURL)
	}

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := contexthandler.FromContext(r.Context())
			// if no header
			// return nil
			// check if action header exists
			action := ctx.Req.Header.Get("X-Grafana-Action")
			if action == "" {
				// header not found, this is not an action request
				next.ServeHTTP(w, r)
				return
			}
			if globErr != nil {
				http.Error(w, "check server logs for glob configuration failure", http.StatusInternalServerError)
				return
			}
			matchErr := check(ctx, allGlobs, logger)
			if matchErr != nil {
				http.Error(w, matchErr.Error(), http.StatusMethodNotAllowed)
				return
			}
			// no errors fall through
			next.ServeHTTP(w, r)
		})
	}
}

// check
// Detects header for action urls and compares to globbed pattern list
// returns true if allowed
func check(ctx *contextmodel.ReqContext, allGlobs *[]glob.Glob, logger log.Logger) *errorWithStatus {
	// only process POST and PUT
	if ctx.Req.Method != http.MethodPost && ctx.Req.Method != http.MethodPut {
		return &errorWithStatus{
			Underlying: fmt.Errorf("method not allowed for path %s", ctx.Req.URL),
			HTTPStatus: http.StatusMethodNotAllowed,
		}
	}
	// for each split config
	// if matches glob
	// return nil
	urlToCheck := ctx.Req.URL
	if matchesAllowedPath(allGlobs, urlToCheck.Path) {
		return nil
	}
	logger.Warn("POST/PUT to path not allowed", "url", urlToCheck)
	// return some error
	return &errorWithStatus{
		Underlying: fmt.Errorf("method POST/PUT not allowed for path %s", urlToCheck),
		HTTPStatus: http.StatusMethodNotAllowed,
	}
}

func matchesAllowedPath(allGlobs *[]glob.Glob, pathToCheck string) bool {
	logger.Debug("Checking url", "actions", pathToCheck)
	for _, rule := range *allGlobs {
		logger.Debug("Checking match", "actions", rule)
		if rule.Match(pathToCheck) {
			// allowed
			logger.Debug("POST/PUT call matches allow configuration settings")
			return true
		}
	}
	return false
}

func cacheGlobs(actionsAllowPostURL string) (*[]glob.Glob, error) {
	allowedUrls := util.SplitString(actionsAllowPostURL)
	allGlobs := make([]glob.Glob, 0)
	for _, i := range allowedUrls {
		g, err := glob.Compile(i)
		if err != nil {
			return nil, errInvalidAllowedURL(err.Error())
		}
		allGlobs = append(allGlobs, g)
	}
	return &allGlobs, nil
}
