package permreg

import (
	"strings"

	"github.com/grafana/grafana/pkg/apimachinery/errutil"
	"github.com/grafana/grafana/pkg/infra/log"
)

var (
	// ErrInvalidScope is returned when the scope is not valid for the action
	ErrInvalidScopeTplt = "invalid scope: {{.Public.Scope}}, for action: {{.Public.Action}}, expected prefixes are {{.Public.ValidScopesFormat}}"
	ErrBaseInvalidScope = errutil.BadRequest("permreg.invalid-scope").MustTemplate(ErrInvalidScopeTplt, errutil.WithPublic(ErrInvalidScopeTplt))

	ErrUnknownActionTplt = "unknown action: {{.Public.Action}}, was not found in the list of valid actions"
	ErrBaseUnknownAction = errutil.BadRequest("permreg.unknown-action").MustTemplate(ErrUnknownActionTplt, errutil.WithPublic(ErrUnknownActionTplt))

	ErrBaseUnknownKind = errutil.BadRequest("permreg.unknown-kind").MustTemplate("unknown kind: {{.Public.Kind}}")
)

func ErrInvalidScope(scope string, action string, validScopePrefixes PrefixSet) error {
	if len(validScopePrefixes) == 0 {
		return ErrBaseInvalidScope.Build(errutil.TemplateData{Public: map[string]any{"Scope": scope, "Action": action, "ValidScopesFormat": "[none]"}})
	}
	formats := generateValidScopeFormats(validScopePrefixes)
	return ErrBaseInvalidScope.Build(errutil.TemplateData{Public: map[string]any{"Scope": scope, "Action": action, "ValidScopesFormat": formats}})
}

func ErrUnknownAction(action string) error {
	return ErrBaseUnknownAction.Build(errutil.TemplateData{Public: map[string]any{"Action": action}})
}

func ErrUnknownKind(kind string) error {
	return ErrBaseUnknownKind.Build(errutil.TemplateData{Public: map[string]any{"Kind": kind}})
}

func generateValidScopeFormats(acceptedScopePrefixes PrefixSet) []string {
	if len(acceptedScopePrefixes) == 0 {
		return []string{}
	}
	acceptedPrefixesList := make([]string, 0, 10)
	acceptedPrefixesList = append(acceptedPrefixesList, "*")
	for prefix := range acceptedScopePrefixes {
		parts := strings.Split(prefix, ":")
		// If the prefix has an attribute part add the intermediate format kind:*
		if len(parts) > 2 {
			acceptedPrefixesList = append(acceptedPrefixesList, parts[0]+":*")
		}
		// Add the most specific format kind:attribute:*
		acceptedPrefixesList = append(acceptedPrefixesList, prefix+"*")
	}
	return acceptedPrefixesList
}

type PermissionRegistry interface {
	RegisterPluginScope(scope string)
	RegisterPermission(action, scope string) error
	IsPermissionValid(action, scope string) error
	GetScopePrefixes(action string) (PrefixSet, bool)
}

type PrefixSet map[string]bool

var _ PermissionRegistry = &permissionRegistry{}

type permissionRegistry struct {
	actionScopePrefixes map[string]PrefixSet // TODO use thread safe map
	kindScopePrefix     map[string]string
	logger              log.Logger
}

func ProvidePermissionRegistry() PermissionRegistry {
	return newPermissionRegistry()
}

func newPermissionRegistry() *permissionRegistry {
	// defaultKindScopes maps the most specific accepted scope prefix for a given kind (folders, dashboards, etc)
	defaultKindScopes := map[string]string{
		"teams":           "teams:id:",
		"users":           "users:id:",
		"datasources":     "datasources:uid:",
		"dashboards":      "dashboards:uid:",
		"folders":         "folders:uid:",
		"annotations":     "annotations:type:",
		"apikeys":         "apikeys:id:",
		"orgs":            "orgs:id:",
		"plugins":         "plugins:id:",
		"provisioners":    "provisioners:",
		"reports":         "reports:id:",
		"permissions":     "permissions:type:",
		"serviceaccounts": "serviceaccounts:id:",
		"settings":        "settings:",
		"global.users":    "global.users:id:",
		"roles":           "roles:uid:",
		"services":        "services:",
		"receivers":       "receivers:uid:",
	}
	return &permissionRegistry{
		actionScopePrefixes: make(map[string]PrefixSet, 200),
		kindScopePrefix:     defaultKindScopes,
		logger:              log.New("accesscontrol.permreg"),
	}
}

func (pr *permissionRegistry) RegisterPluginScope(scope string) {
	if scope == "" {
		return
	}

	scopeParts := strings.Split(scope, ":")
	kind := scopeParts[0]

	// If the scope is already registered, return
	if _, found := pr.kindScopePrefix[kind]; found {
		return
	}

	// If the scope contains an attribute part, register the kind and attribute
	if len(scopeParts) > 2 {
		attr := scopeParts[1]
		pr.kindScopePrefix[kind] = kind + ":" + attr + ":"
		pr.logger.Debug("registered scope prefix", "kind", kind, "scope_prefix", kind+":"+attr+":")
		return
	}

	pr.logger.Debug("registered scope prefix", "kind", kind, "scope_prefix", kind+":")
	pr.kindScopePrefix[kind] = kind + ":"
}

func (pr *permissionRegistry) RegisterPermission(action, scope string) error {
	if _, ok := pr.actionScopePrefixes[action]; !ok {
		pr.actionScopePrefixes[action] = PrefixSet{}
	}

	if scope == "" {
		// scopeless action
		return nil
	}

	kind := strings.Split(scope, ":")[0]
	scopePrefix, ok := pr.kindScopePrefix[kind]
	if !ok {
		pr.logger.Error("unknown kind: please update `kindScopePrefix` with the correct scope prefix", "kind", kind)
		return ErrUnknownKind(kind)
	}

	// Add a new entry in case the scope is not empty
	pr.actionScopePrefixes[action][scopePrefix] = true
	return nil
}

func (pr *permissionRegistry) IsPermissionValid(action, scope string) error {
	validScopePrefixes, ok := pr.actionScopePrefixes[action]
	if !ok {
		return ErrUnknownAction(action)
	}

	if ok && len(validScopePrefixes) == 0 {
		// Expecting action without any scope
		if scope != "" {
			return ErrInvalidScope(scope, action, nil)
		}
		return nil
	}

	if !isScopeValid(scope, validScopePrefixes) {
		return ErrInvalidScope(scope, action, validScopePrefixes)
	}
	return nil
}

func isScopeValid(scope string, validScopePrefixes PrefixSet) bool {
	// Super wildcard scope
	if scope == "*" {
		return true
	}
	for scopePrefix := range validScopePrefixes {
		// Correct scope prefix
		if strings.HasPrefix(scope, scopePrefix) {
			return true
		}
		// Scope is wildcard of the correct prefix
		if strings.HasSuffix(scope, ":*") && strings.HasPrefix(scopePrefix, scope[:len(scope)-2]) {
			return true
		}
	}
	return false
}

func (pr *permissionRegistry) GetScopePrefixes(action string) (PrefixSet, bool) {
	set, ok := pr.actionScopePrefixes[action]
	return set, ok
}
