package accesscontrol

import (
	"context"
	"fmt"

	"golang.org/x/exp/maps"

	"github.com/grafana/grafana/pkg/apimachinery/identity"
	ac "github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/ngalert/models"
)

const (
	instancesRead   = ac.ActionAlertingInstanceRead
	instancesCreate = ac.ActionAlertingInstanceCreate
	instancesWrite  = ac.ActionAlertingInstanceUpdate
	silenceRead     = ac.ActionAlertingSilencesRead
	silenceCreate   = ac.ActionAlertingSilencesCreate
	silenceWrite    = ac.ActionAlertingSilencesWrite
)

var (
	// asserts full read-only access to silences
	readAllSilencesEvaluator = ac.EvalAny(ac.EvalPermission(instancesRead), ac.EvalPermission(silenceRead, dashboards.ScopeFoldersProvider.GetResourceAllScope()))
	// shortcut assertion that to check if user can read silences
	readSomeSilenceEvaluator = ac.EvalAny(ac.EvalPermission(instancesRead), ac.EvalPermission(silenceRead))
	// asserts whether user has read access to rules in a specific folder
	readRuleSilenceEvaluator = func(folderUID string) ac.Evaluator {
		return ac.EvalAny(
			ac.EvalPermission(instancesRead),
			ac.EvalPermission(silenceRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)),
		)
	}

	// shortcut assertion to check if user can create any silence
	createAnySilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesCreate), readAllSilencesEvaluator)
	// asserts that user has access to create general silences, the ones that can match alerts created by one or many rules
	createGeneralSilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesCreate), readSomeSilenceEvaluator)
	// shortcut assertion to check if user can create silences at all
	createSomeRuleSilenceEvaluator = ac.EvalAll(
		readSomeSilenceEvaluator,
		ac.EvalAny(
			ac.EvalPermission(instancesCreate),
			ac.EvalPermission(silenceCreate)),
	)
	// asserts that user has access to create silences in a specific folder
	createRuleSilenceEvaluator = func(uid string) ac.Evaluator {
		return ac.EvalAll(
			ac.EvalAny(
				ac.EvalPermission(instancesCreate),
				ac.EvalPermission(silenceCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)),
			),
			readRuleSilenceEvaluator(uid),
		)
	}

	// shortcut assertion to check if user can update any silence
	updateAnySilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesWrite), readAllSilencesEvaluator)
	// asserts that user has access to update general silences
	updateGeneralSilenceEvaluator = ac.EvalAll(ac.EvalPermission(instancesWrite), readSomeSilenceEvaluator)
	// asserts that user has access to update silences at all
	updateSomeRuleSilenceEvaluator = ac.EvalAll(
		readSomeSilenceEvaluator,
		ac.EvalAny(
			ac.EvalPermission(instancesWrite),
			ac.EvalPermission(silenceWrite)),
	)
	// asserts that user has access to create silences in a specific folder
	updateRuleSilenceEvaluator = func(uid string) ac.Evaluator {
		return ac.EvalAll(
			ac.EvalAny(
				ac.EvalPermission(instancesWrite),
				ac.EvalPermission(silenceWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)),
			),
			readRuleSilenceEvaluator(uid),
		)
	}
)

type RuleUIDToNamespaceStore interface {
	GetNamespacesByRuleUID(ctx context.Context, orgID int64, uids ...string) (map[string]string, error)
}

type SilenceService struct {
	genericService
	store RuleUIDToNamespaceStore
}

func NewSilenceService(ac ac.AccessControl, store RuleUIDToNamespaceStore) *SilenceService {
	return &SilenceService{
		genericService: genericService{
			ac: ac,
		},
		store: store,
	}
}

// silenceWithFolder is a helper struct that holds a silence and its associated rule and folder UIDs.
type silenceWithFolder struct {
	*models.Silence
	ruleUID   *string
	folderUID string
}

// FilterByAccess filters the given list of silences based on the access control permissions of the user.
// Global silence (one that is not attached to a particular rule) is considered available to all users.
// For silences that are not attached to a rule, are checked against authorization.
// This method is more preferred when many silences need to be checked.
func (s SilenceService) FilterByAccess(ctx context.Context, user identity.Requester, silences ...*models.Silence) ([]*models.Silence, error) {
	canAll, err := s.authorizeReadSilencePreConditions(ctx, user)
	if err != nil {
		return nil, err
	}
	if canAll {
		return silences, nil
	}

	silencesWithFolders, err := s.withFolders(ctx, user.GetOrgID(), silences...)
	if err != nil {
		return nil, err
	}

	result := make([]*models.Silence, 0, len(silences))
	accessCacheByFolder := make(map[string]bool)
	for _, silWithFolder := range silencesWithFolders {
		hasAccess, ok := accessCacheByFolder[silWithFolder.folderUID]
		if !ok {
			hasAccess = s.authorizeReadSilence(ctx, user, silWithFolder) == nil

			// Cache non-empty namespaces to avoid repeated checks for the same folder.
			if silWithFolder.folderUID != "" {
				accessCacheByFolder[silWithFolder.folderUID] = hasAccess
			}
		}
		if hasAccess {
			result = append(result, silWithFolder.Silence)
		}
	}
	return result, nil
}

// AuthorizeReadSilence checks if user has access to read a silence.
func (s SilenceService) AuthorizeReadSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error {
	canAll, err := s.authorizeReadSilencePreConditions(ctx, user)
	if canAll || err != nil { // return early if user can either read all silences or there is error
		return err
	}

	silWithFolder, err := s.withFolders(ctx, user.GetOrgID(), silence)
	if err != nil || len(silWithFolder) != 1 {
		return fmt.Errorf("resolve rule UID to folder UID: %w", err)
	}

	return s.authorizeReadSilence(ctx, user, silWithFolder[0])
}

// authorizeReadSilencePreConditions checks necessary preconditions for reading silences. Returns true if user can
// read all silences. Returns error if user does not have access to read any silences.
func (s SilenceService) authorizeReadSilencePreConditions(ctx context.Context, user identity.Requester) (bool, error) {
	canAll, err := s.HasAccess(ctx, user, readAllSilencesEvaluator)
	if canAll || err != nil { // return early if user can either read all silences or there is error
		return canAll, err
	}

	can, err := s.HasAccess(ctx, user, readSomeSilenceEvaluator)
	if err != nil {
		return false, err
	}
	if !can { // User does not have silence permissions at all.
		return false, NewAuthorizationErrorWithPermissions("read any silences", readSomeSilenceEvaluator)
	}
	return false, nil
}

// authorizeReadSilence checks if user has access to read a silence given precondition checks have passed.
func (s SilenceService) authorizeReadSilence(ctx context.Context, user identity.Requester, silence *silenceWithFolder) error {
	if silence.ruleUID == nil {
		return nil // No rule metadata means that this is a general silence and at this point the user can read them
	}

	if silence.folderUID == "" { // if we did not find folder by rule UID then it does not exist.
		return NewAuthorizationErrorGeneric(fmt.Sprintf("read silence for rule %s", *silence.ruleUID))
	}
	return s.HasAccessOrError(ctx, user, readRuleSilenceEvaluator(silence.folderUID), func() string {
		return "read silence"
	})
}

// AuthorizeCreateSilence checks if user has access to create a silence. Returns ErrAuthorizationBase if user is not authorized
func (s SilenceService) AuthorizeCreateSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error {
	canAny, err := s.authorizeCreateSilencePreConditions(ctx, user)
	if canAny || err != nil { // return early if user can either create any silence or there is an error
		return err
	}

	silWithFolder, err := s.withFolders(ctx, user.GetOrgID(), silence)
	if err != nil || len(silWithFolder) != 1 {
		return fmt.Errorf("resolve rule UID to folder UID: %w", err)
	}

	return s.authorizeCreateSilence(ctx, user, silWithFolder[0])
}

// authorizeCreateSilencePreConditions checks necessary preconditions for creating silences. Returns true if user can
// create any silence. Returns error if user does not have access to create any silences at all.
func (s SilenceService) authorizeCreateSilencePreConditions(ctx context.Context, user identity.Requester) (bool, error) {
	canAny, err := s.HasAccess(ctx, user, createAnySilenceEvaluator)
	if err != nil || canAny {
		// return early if user can either create any silence or there is an error
		return canAny, err
	}

	// pre-check whether a user has at least some basic permissions before hit the store
	if err := s.HasAccessOrError(ctx, user, createSomeRuleSilenceEvaluator, func() string { return "create any silences" }); err != nil {
		return false, err
	}

	return false, nil
}

// authorizeCreateSilence checks if user has access to create a silence given precondition checks have passed.
func (s SilenceService) authorizeCreateSilence(ctx context.Context, user identity.Requester, silence *silenceWithFolder) error {
	if silence.ruleUID == nil {
		return s.HasAccessOrError(ctx, user, createGeneralSilenceEvaluator, func() string {
			return "create a general silence"
		})
	}

	if silence.folderUID == "" { // if we did not find folder by rule UID then it does not exist.
		return NewAuthorizationErrorGeneric(fmt.Sprintf("create silence for rule %s", *silence.ruleUID))
	}
	return s.HasAccessOrError(ctx, user, createRuleSilenceEvaluator(silence.folderUID), func() string {
		return fmt.Sprintf("create silence for rule %s", *silence.ruleUID)
	})
}

// AuthorizeUpdateSilence checks if user has access to update\expire a silence. Returns ErrAuthorizationBase if user is not authorized
func (s SilenceService) AuthorizeUpdateSilence(ctx context.Context, user identity.Requester, silence *models.Silence) error {
	canAny, err := s.authorizeUpdateSilencePreConditions(ctx, user)
	if canAny || err != nil { // return early if user can either update any silence or there is an error
		return err
	}

	silWithFolder, err := s.withFolders(ctx, user.GetOrgID(), silence)
	if err != nil || len(silWithFolder) != 1 {
		return fmt.Errorf("resolve rule UID to folder UID: %w", err)
	}

	return s.authorizeUpdateSilence(ctx, user, silWithFolder[0])
}

// authorizeUpdateSilencePreConditions checks necessary preconditions for updating silences. Returns true if user can
// update any silence. Returns error if user does not have access to update any silences at all.
func (s SilenceService) authorizeUpdateSilencePreConditions(ctx context.Context, user identity.Requester) (bool, error) {
	canAny, err := s.HasAccess(ctx, user, updateAnySilenceEvaluator)
	if err != nil || canAny {
		// return early if user can either update any silence or there is an error
		return canAny, err
	}

	// pre-check whether a user has at least some basic permissions before hit the store
	if err := s.HasAccessOrError(ctx, user, updateSomeRuleSilenceEvaluator, func() string { return "update some silences" }); err != nil {
		return false, err
	}

	return false, nil
}

// authorizeUpdateSilence checks if user has access to update a silence given precondition checks have passed.
func (s SilenceService) authorizeUpdateSilence(ctx context.Context, user identity.Requester, silence *silenceWithFolder) error {
	if silence.ruleUID == nil {
		return s.HasAccessOrError(ctx, user, updateGeneralSilenceEvaluator, func() string {
			return "update a general silence"
		})
	}

	if silence.folderUID == "" { // if we did not find folder by rule UID then it does not exist.
		return NewAuthorizationErrorGeneric(fmt.Sprintf("create update for rule %s", *silence.ruleUID))
	}
	return s.HasAccessOrError(ctx, user, updateRuleSilenceEvaluator(silence.folderUID), func() string {
		return fmt.Sprintf("update silence for rule %s", *silence.ruleUID)
	})
}

// SilenceAccess returns the permission sets for a slice of silences. The permission set includes read, write, and
// create which corresponds the given user being able to read, write, and create each given silence, respectively.
func (s SilenceService) SilenceAccess(ctx context.Context, user identity.Requester, silences []*models.Silence) (map[*models.Silence]models.SilencePermissionSet, error) {
	basePerms := make(models.SilencePermissionSet, 3)
	canReadAll, err := s.authorizeReadSilencePreConditions(ctx, user)
	if err != nil || canReadAll {
		basePerms[models.SilencePermissionRead] = err == nil
	}
	canUpdateAny, err := s.authorizeUpdateSilencePreConditions(ctx, user)
	if err != nil || canUpdateAny {
		basePerms[models.SilencePermissionWrite] = err == nil
	}
	canCreateAny, err := s.authorizeCreateSilencePreConditions(ctx, user)
	if err != nil || canCreateAny {
		basePerms[models.SilencePermissionCreate] = err == nil
	}

	if basePerms.AllSet() {
		// Shortcut for the case when all permissions are known based on preconditions. We don't need to hit the database to find folder UIDs.
		return withPermissionSet(silences, basePerms), nil
	}

	silencesWithFolders, err := s.withFolders(ctx, user.GetOrgID(), silences...)
	if err != nil {
		return nil, err
	}

	result := make(map[*models.Silence]models.SilencePermissionSet, len(silences))
	accessCacheByFolder := make(map[string]models.SilencePermissionSet)
	for _, silWithFolder := range silencesWithFolders {
		if perms, ok := accessCacheByFolder[silWithFolder.folderUID]; ok {
			result[silWithFolder.Silence] = perms.Clone()
			continue
		}

		permSet := basePerms.Clone()
		if _, ok := permSet[models.SilencePermissionRead]; !ok {
			err := s.authorizeReadSilence(ctx, user, silWithFolder)
			permSet[models.SilencePermissionRead] = err == nil
		}

		if _, ok := permSet[models.SilencePermissionWrite]; !ok {
			err := s.authorizeUpdateSilence(ctx, user, silWithFolder)
			permSet[models.SilencePermissionWrite] = err == nil
		}

		if _, ok := permSet[models.SilencePermissionCreate]; !ok {
			err := s.authorizeCreateSilence(ctx, user, silWithFolder)
			permSet[models.SilencePermissionCreate] = err == nil
		}

		result[silWithFolder.Silence] = permSet
		// Cache non-empty namespaces to avoid repeated checks for the same folder.
		if silWithFolder.folderUID != "" {
			accessCacheByFolder[silWithFolder.folderUID] = permSet
		}
	}
	return result, nil
}

// withFolders resolves rule UIDs to folder UIDs for rule-specific silences and returns a list of silenceWithFolder
// that includes rule information, if available.
func (s SilenceService) withFolders(ctx context.Context, orgID int64, silences ...*models.Silence) ([]*silenceWithFolder, error) {
	result := make([]*silenceWithFolder, 0, len(silences))
	ruleUIDs := make(map[string]struct{})
	for _, silence := range silences {
		silWithFolder := silenceWithFolder{Silence: silence, ruleUID: silence.GetRuleUID()}
		if silWithFolder.ruleUID != nil {
			ruleUIDs[*silWithFolder.ruleUID] = struct{}{}
		}
		result = append(result, &silWithFolder)
	}

	if len(ruleUIDs) == 0 {
		return result, nil
	}

	namespaceByRuleUID, err := s.store.GetNamespacesByRuleUID(ctx, orgID, maps.Keys(ruleUIDs)...)
	if err != nil {
		return nil, err
	}

	for _, silWithFolder := range result {
		if silWithFolder.ruleUID != nil {
			silWithFolder.folderUID = namespaceByRuleUID[*silWithFolder.ruleUID]
		}
	}
	return result, nil
}

func withPermissionSet(silences []*models.Silence, perms models.SilencePermissionSet) map[*models.Silence]models.SilencePermissionSet {
	result := make(map[*models.Silence]models.SilencePermissionSet, len(silences))
	for _, silence := range silences {
		result[silence] = perms.Clone()
	}
	return result
}
