package resourcepermissions

import (
	"context"
	"errors"
	"fmt"
	"slices"
	"sort"
	"strings"

	"github.com/grafana/grafana/pkg/api/routing"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/licensing"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
	"github.com/grafana/grafana/pkg/services/team"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/setting"
)

var _ pluginaccesscontrol.ActionSetRegistry = (ActionSetService)(nil)

type Store interface {
	// SetUserResourcePermission sets permission for managed user role on a resource
	SetUserResourcePermission(
		ctx context.Context, orgID int64,
		user accesscontrol.User,
		cmd SetResourcePermissionCommand,
		hook UserResourceHookFunc,
	) (*accesscontrol.ResourcePermission, error)

	// SetTeamResourcePermission sets permission for managed team role on a resource
	SetTeamResourcePermission(
		ctx context.Context, orgID, teamID int64,
		cmd SetResourcePermissionCommand,
		hook TeamResourceHookFunc,
	) (*accesscontrol.ResourcePermission, error)

	// SetBuiltInResourcePermission sets permissions for managed builtin role on a resource
	SetBuiltInResourcePermission(
		ctx context.Context, orgID int64, builtinRole string,
		cmd SetResourcePermissionCommand,
		hook BuiltinResourceHookFunc,
	) (*accesscontrol.ResourcePermission, error)

	SetResourcePermissions(
		ctx context.Context, orgID int64,
		commands []SetResourcePermissionsCommand,
		hooks ResourceHooks,
	) ([]accesscontrol.ResourcePermission, error)

	// GetResourcePermissions will return all permission for supplied resource id
	GetResourcePermissions(ctx context.Context, orgID int64, query GetResourcePermissionsQuery) ([]accesscontrol.ResourcePermission, error)

	// DeleteResourcePermissions will delete all permissions for supplied resource id
	DeleteResourcePermissions(ctx context.Context, orgID int64, cmd *DeleteResourcePermissionsCmd) error
}

func New(cfg *setting.Cfg,
	options Options, features featuremgmt.FeatureToggles, router routing.RouteRegister, license licensing.Licensing,
	ac accesscontrol.AccessControl, service accesscontrol.Service, sqlStore db.DB,
	teamService team.Service, userService user.Service, actionSetService ActionSetService,
) (*Service, error) {
	permissions := make([]string, 0, len(options.PermissionsToActions))
	actionSet := make(map[string]struct{})
	for permission, actions := range options.PermissionsToActions {
		permissions = append(permissions, permission)
		for _, a := range actions {
			actionSet[a] = struct{}{}
		}
		actionSetService.StoreActionSet(GetActionSetName(options.Resource, permission), actions)
	}

	// Sort all permissions based on action length. Will be used when mapping between actions to permissions
	sort.Slice(permissions, func(i, j int) bool {
		return len(options.PermissionsToActions[permissions[i]]) > len(options.PermissionsToActions[permissions[j]])
	})

	actions := make([]string, 0, len(actionSet))
	for action := range actionSet {
		actions = append(actions, action)
	}

	s := &Service{
		ac:           ac,
		features:     features,
		store:        NewStore(cfg, sqlStore, features),
		options:      options,
		license:      license,
		log:          log.New("resourcepermissions"),
		permissions:  permissions,
		actions:      actions,
		sqlStore:     sqlStore,
		service:      service,
		teamService:  teamService,
		userService:  userService,
		actionSetSvc: actionSetService,
	}

	s.api = newApi(cfg, ac, router, s, features, s.options.RestConfigProvider)

	if err := s.declareFixedRoles(); err != nil {
		return nil, err
	}

	s.api.registerEndpoints()

	return s, nil
}

// Service is used to create access control sub system including api / and service for managed resource permission
type Service struct {
	ac       accesscontrol.AccessControl
	features featuremgmt.FeatureToggles
	service  accesscontrol.Service
	store    Store
	api      *api
	license  licensing.Licensing

	log          log.Logger
	options      Options
	permissions  []string
	actions      []string
	sqlStore     db.DB
	teamService  team.Service
	userService  user.Service
	actionSetSvc ActionSetService
}

func (s *Service) GetPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) {
	ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.GetPermissions")
	defer span.End()

	var inheritedScopes []string
	if s.options.InheritedScopesSolver != nil {
		var err error
		inheritedScopes, err = s.options.InheritedScopesSolver(ctx, user.GetOrgID(), resourceID)
		if err != nil {
			return nil, err
		}
	}

	actions := s.actions
	for _, action := range s.actions {
		actionSets := s.actionSetSvc.ResolveAction(action)
		for _, actionSet := range actionSets {
			if !slices.Contains(actions, actionSet) {
				actions = append(actions, actionSet)
			}
		}
	}

	resourcePermissions, err := s.store.GetResourcePermissions(ctx, user.GetOrgID(), GetResourcePermissionsQuery{
		User:                 user,
		Actions:              actions,
		Resource:             s.options.Resource,
		ResourceID:           resourceID,
		ResourceAttribute:    s.options.ResourceAttribute,
		InheritedScopes:      inheritedScopes,
		OnlyManaged:          s.options.OnlyManaged,
		EnforceAccessControl: s.license.FeatureEnabled("accesscontrol.enforcement"),
	})
	if err != nil {
		return nil, err
	}

	for i := range resourcePermissions {
		actions := resourcePermissions[i].Actions
		var expandedActions []string
		for _, action := range actions {
			if isFolderOrDashboardAction(action) {
				actionSetActions := s.actionSetSvc.ResolveActionSet(action)
				if len(actionSetActions) > 0 {
					// Add all actions for folder
					if s.options.Resource == dashboards.ScopeFoldersRoot {
						expandedActions = append(expandedActions, actionSetActions...)
						continue
					}
					// This check is needed for resolving inherited permissions - we don't want to include
					// actions that are not related to dashboards when expanding dashboard action sets
					for _, actionSetAction := range actionSetActions {
						if slices.Contains(s.actions, actionSetAction) {
							expandedActions = append(expandedActions, actionSetAction)
						}
					}
					continue
				}
			}
			expandedActions = append(expandedActions, action)
		}
		resourcePermissions[i].Actions = expandedActions
	}

	return resourcePermissions, nil
}

func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
	ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetUserPermission")
	defer span.End()

	actions, err := s.mapPermission(permission)
	if err != nil {
		return nil, err
	}

	if err := s.validateResource(ctx, orgID, resourceID); err != nil {
		return nil, err
	}

	if err := s.validateUser(ctx, orgID, user.ID); err != nil {
		return nil, err
	}

	return s.store.SetUserResourcePermission(ctx, orgID, user, SetResourcePermissionCommand{
		Actions:           actions,
		Permission:        permission,
		Resource:          s.options.Resource,
		ResourceID:        resourceID,
		ResourceAttribute: s.options.ResourceAttribute,
	}, s.options.OnSetUser)
}

func (s *Service) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
	ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetTeamPermission")
	defer span.End()

	actions, err := s.mapPermission(permission)
	if err != nil {
		return nil, err
	}

	if err := s.validateTeam(ctx, orgID, teamID); err != nil {
		return nil, err
	}

	if err := s.validateResource(ctx, orgID, resourceID); err != nil {
		return nil, err
	}

	return s.store.SetTeamResourcePermission(ctx, orgID, teamID, SetResourcePermissionCommand{
		Actions:           actions,
		Permission:        permission,
		Resource:          s.options.Resource,
		ResourceID:        resourceID,
		ResourceAttribute: s.options.ResourceAttribute,
	}, s.options.OnSetTeam)
}

func (s *Service) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
	ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetBuiltInRolePermission")
	defer span.End()

	actions, err := s.mapPermission(permission)
	if err != nil {
		return nil, err
	}

	if err := s.validateBuiltinRole(ctx, builtInRole); err != nil {
		return nil, err
	}

	if err := s.validateResource(ctx, orgID, resourceID); err != nil {
		return nil, err
	}

	return s.store.SetBuiltInResourcePermission(ctx, orgID, builtInRole, SetResourcePermissionCommand{
		Actions:           actions,
		Permission:        permission,
		Resource:          s.options.Resource,
		ResourceID:        resourceID,
		ResourceAttribute: s.options.ResourceAttribute,
	}, s.options.OnSetBuiltInRole)
}

func (s *Service) SetPermissions(
	ctx context.Context, orgID int64, resourceID string,
	commands ...accesscontrol.SetResourcePermissionCommand,
) ([]accesscontrol.ResourcePermission, error) {
	ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetPermissions")
	defer span.End()

	if err := s.validateResource(ctx, orgID, resourceID); err != nil {
		return nil, err
	}

	dbCommands := make([]SetResourcePermissionsCommand, 0, len(commands))
	for _, cmd := range commands {
		if cmd.UserID != 0 {
			if err := s.validateUser(ctx, orgID, cmd.UserID); err != nil {
				return nil, err
			}
		} else if cmd.TeamID != 0 {
			if err := s.validateTeam(ctx, orgID, cmd.TeamID); err != nil {
				return nil, err
			}
		} else {
			if err := s.validateBuiltinRole(ctx, cmd.BuiltinRole); err != nil {
				return nil, err
			}
		}

		actions, err := s.mapPermission(cmd.Permission)
		if err != nil {
			return nil, err
		}

		dbCommands = append(dbCommands, SetResourcePermissionsCommand{
			User:        accesscontrol.User{ID: cmd.UserID},
			TeamID:      cmd.TeamID,
			BuiltinRole: cmd.BuiltinRole,
			SetResourcePermissionCommand: SetResourcePermissionCommand{
				Actions:           actions,
				Resource:          s.options.Resource,
				ResourceID:        resourceID,
				ResourceAttribute: s.options.ResourceAttribute,
				Permission:        cmd.Permission,
			},
		})
	}

	return s.store.SetResourcePermissions(ctx, orgID, dbCommands, ResourceHooks{
		User:        s.options.OnSetUser,
		Team:        s.options.OnSetTeam,
		BuiltInRole: s.options.OnSetBuiltInRole,
	})
}

func (s *Service) MapActions(permission accesscontrol.ResourcePermission) string {
	for _, p := range s.permissions {
		if permission.Contains(s.options.PermissionsToActions[p]) {
			return p
		}
	}
	return ""
}

func (s *Service) DeleteResourcePermissions(ctx context.Context, orgID int64, resourceID string) error {
	return s.store.DeleteResourcePermissions(ctx, orgID, &DeleteResourcePermissionsCmd{
		Resource:          s.options.Resource,
		ResourceAttribute: s.options.ResourceAttribute,
		ResourceID:        resourceID,
	})
}

func (s *Service) mapPermission(permission string) ([]string, error) {
	if permission == "" {
		return []string{}, nil
	}

	var actions []string

	// Write action sets for folders and dashboards
	if s.options.Resource == dashboards.ScopeFoldersRoot || s.options.Resource == dashboards.ScopeDashboardsRoot {
		actions = append(actions, GetActionSetName(s.options.Resource, permission))

		// If we only want to store action sets, return now
		//nolint:staticcheck // not yet migrated to OpenFeature
		if s.features.IsEnabledGlobally(featuremgmt.FlagOnlyStoreActionSets) {
			return actions, nil
		}
	}

	for k, v := range s.options.PermissionsToActions {
		if permission == k {
			actions = append(actions, v...)
			return actions, nil
		}
	}
	return nil, ErrInvalidPermission.Build(ErrInvalidPermissionData(permission))
}

func (s *Service) validateResource(ctx context.Context, orgID int64, resourceID string) error {
	ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateResource")
	defer span.End()

	if s.options.ResourceValidator != nil {
		return s.options.ResourceValidator(ctx, orgID, resourceID)
	}
	return nil
}

func (s *Service) validateUser(ctx context.Context, orgID, userID int64) error {
	ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateUser")
	defer span.End()

	if !s.options.Assignments.Users {
		return ErrInvalidAssignment.Build(ErrInvalidAssignmentData("users"))
	}

	_, err := s.userService.GetSignedInUser(ctx, &user.GetSignedInUserQuery{OrgID: orgID, UserID: userID})
	switch {
	case errors.Is(err, user.ErrUserNotFound):
		return accesscontrol.ErrAssignmentEntityNotFound.Build(accesscontrol.ErrAssignmentEntityNotFoundData("user"))
	default:
		return err
	}
}

func (s *Service) validateTeam(ctx context.Context, orgID, teamID int64) error {
	ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateTeam")
	defer span.End()

	if !s.options.Assignments.Teams {
		return ErrInvalidAssignment.Build(ErrInvalidAssignmentData("teams"))
	}

	if _, err := s.teamService.GetTeamByID(ctx, &team.GetTeamByIDQuery{OrgID: orgID, ID: teamID}); err != nil {
		switch {
		case errors.Is(err, team.ErrTeamNotFound):
			return accesscontrol.ErrAssignmentEntityNotFound.Build(accesscontrol.ErrAssignmentEntityNotFoundData("team"))
		default:
			return err
		}
	}
	return nil
}

func (s *Service) validateBuiltinRole(ctx context.Context, builtinRole string) error {
	_, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateBuiltinRole")
	defer span.End()

	if !s.options.Assignments.BuiltInRoles {
		return ErrInvalidAssignment.Build(ErrInvalidAssignmentData("builtInRoles"))
	}

	if err := accesscontrol.ValidateBuiltInRoles([]string{builtinRole}); err != nil {
		return err
	}
	return nil
}

func (s *Service) declareFixedRoles() error {
	scopeAll := accesscontrol.Scope(s.options.Resource, "*")
	readerRole := accesscontrol.RoleRegistration{
		Role: accesscontrol.RoleDTO{
			Name:        fmt.Sprintf("fixed:%s.permissions:reader", s.options.Resource),
			DisplayName: s.options.ReaderRoleName,
			Group:       s.options.RoleGroup,
			Permissions: []accesscontrol.Permission{
				{Action: fmt.Sprintf("%s.permissions:read", s.options.Resource), Scope: scopeAll},
			},
		},
		Grants: []string{string(org.RoleAdmin)},
	}

	writerRole := accesscontrol.RoleRegistration{
		Role: accesscontrol.RoleDTO{
			Name:        fmt.Sprintf("fixed:%s.permissions:writer", s.options.Resource),
			DisplayName: s.options.WriterRoleName,
			Group:       s.options.RoleGroup,
			Permissions: accesscontrol.ConcatPermissions(readerRole.Role.Permissions, []accesscontrol.Permission{
				{Action: fmt.Sprintf("%s.permissions:write", s.options.Resource), Scope: scopeAll},
			}),
		},
		Grants: []string{string(org.RoleAdmin)},
	}

	return s.service.DeclareFixedRoles(readerRole, writerRole)
}

type ActionSetService interface {
	// ActionResolver defines method for expanding permissions from permissions with action sets to fine-grained permissions.
	// We use an ActionResolver interface to avoid circular dependencies
	accesscontrol.ActionResolver

	// ResolveAction returns all the action sets that the action belongs to.
	ResolveAction(action string) []string
	// ResolveActionSet resolves an action set to a list of corresponding actions.
	ResolveActionSet(actionSet string) []string
	// StoreActionSet stores action set. If a set with the given name has already been stored, the new actions will be appended to the existing actions.
	StoreActionSet(name string, actions []string)

	pluginaccesscontrol.ActionSetRegistry
}

// ActionSet is a struct that represents a set of actions that can be performed on a resource.
// An example of an action set is "folders:edit" which represents the set of RBAC actions that are granted by edit access to a folder.
type ActionSet struct {
	Action  string   `json:"action"`
	Actions []string `json:"actions"`
}

type ActionSetStore interface {
	// StoreActionSet stores action set. If a set with the given name has already been stored, the new actions will be appended to the existing actions.
	StoreActionSet(name string, actions []string)
	// ResolveActionSet resolves an action set to a list of corresponding actions.
	ResolveActionSet(actionSet string) []string
	// ResolveAction returns all the action sets that the action belongs to.
	ResolveAction(action string) []string
	// ResolveActionPrefix returns all action sets that include at least one action with the specified prefix
	ResolveActionPrefix(prefix string) []string
	// ExpandActionSetsWithFilter takes a set of permissions that might include some action set permissions, and returns a set of permissions with action sets expanded into underlying permissions.
	// When action sets are expanded into the underlying permissions only those permissions whose action is matched by actionMatcher are included.
	ExpandActionSetsWithFilter(permissions []accesscontrol.Permission, actionMatcher func(action string) bool) []accesscontrol.Permission
}

type ActionSetSvc struct {
	store ActionSetStore
}

// NewActionSetService returns a new instance of InMemoryActionSetService.
func NewActionSetService() ActionSetService {
	return &ActionSetSvc{
		store: NewInMemoryActionSetStore(),
	}
}

// ResolveAction returns all the action sets that the action belongs to.
func (a *ActionSetSvc) ResolveAction(action string) []string {
	sets := a.store.ResolveAction(action)
	filteredSets := make([]string, 0, len(sets))
	for _, set := range sets {
		// Only use action sets for folders and dashboards for now
		// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
		if !isFolderOrDashboardAction(set) {
			continue
		}
		filteredSets = append(filteredSets, set)
	}

	return filteredSets
}

// ResolveActionPrefix returns all action sets that include at least one action with the specified prefix
func (a *ActionSetSvc) ResolveActionPrefix(actionPrefix string) []string {
	sets := a.store.ResolveActionPrefix(actionPrefix)
	filteredSets := make([]string, 0, len(sets))
	for _, set := range sets {
		// Only use action sets for folders and dashboards for now
		// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
		if !isFolderOrDashboardAction(set) {
			continue
		}
		filteredSets = append(filteredSets, set)
	}

	return filteredSets
}

// ResolveActionSet resolves an action set to a list of corresponding actions.
func (a *ActionSetSvc) ResolveActionSet(actionSet string) []string {
	// Only use action sets for folders and dashboards for now
	// We need to verify that action sets for other resources do not share names with actions (eg, `datasources:read`)
	if !isFolderOrDashboardAction(actionSet) {
		return nil
	}
	return a.store.ResolveActionSet(actionSet)
}

// StoreActionSet stores action set. If a set with the given name has already been stored, the new actions will be appended to the existing actions.
func (a *ActionSetSvc) StoreActionSet(name string, actions []string) {
	// To avoid backwards incompatible changes, we don't want to store these actions in the DB
	// Once action sets are fully enabled, we can include dashboards.ActionFoldersCreate in the list of other folder edit/admin actions
	// Tracked in https://github.com/grafana/identity-access-team/issues/794
	if name == "folders:edit" || name == "folders:admin" {
		if !slices.Contains(a.ResolveActionSet(name), dashboards.ActionFoldersCreate) {
			actions = append(actions, dashboards.ActionFoldersCreate)
		}
	}

	a.store.StoreActionSet(name, actions)
}

// ExpandActionSets takes a set of permissions that might include some action set permissions, and returns a set of permissions with action sets expanded into underlying permissions
func (a *ActionSetSvc) ExpandActionSets(permissions []accesscontrol.Permission) []accesscontrol.Permission {
	actionMatcher := func(_ string) bool {
		return true
	}
	return a.ExpandActionSetsWithFilter(permissions, actionMatcher)
}

// ExpandActionSetsWithFilter works like ExpandActionSets, but it also takes a function for action filtering. When action sets are expanded into the underlying permissions,
// only those permissions whose action is matched by actionMatcher are included.
func (a *ActionSetSvc) ExpandActionSetsWithFilter(permissions []accesscontrol.Permission, actionMatcher func(action string) bool) []accesscontrol.Permission {
	return a.store.ExpandActionSetsWithFilter(permissions, actionMatcher)
}

// RegisterActionSets allow the caller to expand the existing action sets with additional permissions
// This is intended to be used by plugins, and currently supports extending folder and dashboard action sets
func (a *ActionSetSvc) RegisterActionSets(ctx context.Context, pluginID string, registrations []plugins.ActionSet) error {
	_, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.RegisterActionSets")
	defer span.End()

	for _, reg := range registrations {
		if err := pluginutils.ValidatePluginActionSet(pluginID, reg); err != nil {
			return err
		}
		a.StoreActionSet(reg.Action, reg.Actions)
	}
	return nil
}

func isFolderOrDashboardAction(action string) bool {
	return strings.HasPrefix(action, dashboards.ScopeDashboardsRoot) || strings.HasPrefix(action, dashboards.ScopeFoldersRoot)
}

// GetActionSetName function creates an action set from a list of actions and stores it inmemory.
func GetActionSetName(resource, permission string) string {
	// lower cased
	resource = strings.ToLower(resource)
	permission = strings.ToLower(permission)
	return fmt.Sprintf("%s:%s", resource, permission)
}
