package folderimpl

import (
	"context"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
	"k8s.io/apimachinery/pkg/selection"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
	folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1"
	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/apimachinery/utils"
	"github.com/grafana/grafana/pkg/events"
	"github.com/grafana/grafana/pkg/infra/metrics"
	"github.com/grafana/grafana/pkg/infra/slugify"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
	dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
	"github.com/grafana/grafana/pkg/services/folder"
	"github.com/grafana/grafana/pkg/services/search/model"
	"github.com/grafana/grafana/pkg/services/store/entity"
	"github.com/grafana/grafana/pkg/storage/unified/resource"
	"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
	"github.com/grafana/grafana/pkg/util"
)

const folderSearchLimit = 100000
const folderListLimit = 100000

func (s *Service) getFoldersFromApiServer(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
	ctx, span := s.tracer.Start(ctx, "folder.getFoldersFromApiServer")
	defer span.End()

	if q.SignedInUser == nil {
		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
	}

	qry := folder.NewGetFoldersQuery(q)
	permissions := q.SignedInUser.GetPermissions()
	folderPermissions := permissions[dashboards.ActionFoldersRead]
	qry.AncestorUIDs = make([]string, 0, len(folderPermissions))
	if len(folderPermissions) == 0 && !q.SignedInUser.GetIsGrafanaAdmin() {
		return nil, nil
	}
	for _, p := range folderPermissions {
		if p == dashboards.ScopeFoldersAll {
			// no need to query for folders with permissions
			// the user has permission to access all folders
			qry.AncestorUIDs = nil
			break
		}
		if folderUid, found := strings.CutPrefix(p, dashboards.ScopeFoldersPrefix); found {
			if !slices.Contains(qry.AncestorUIDs, folderUid) {
				qry.AncestorUIDs = append(qry.AncestorUIDs, folderUid)
			}
		}
	}

	var dashFolders []*folder.Folder
	var err error

	ctx = identity.WithRequester(ctx, q.SignedInUser)
	dashFolders, err = s.unifiedStore.GetFolders(ctx, qry)
	if err != nil {
		return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
	}

	return dashFolders, nil
}

func (s *Service) getFromApiServer(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
	ctx, span := s.tracer.Start(ctx, "folder.getFromApiServer")
	defer span.End()

	if q.SignedInUser == nil {
		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
	}

	if q.UID != nil && *q.UID == accesscontrol.GeneralFolderUID {
		return folder.RootFolder, nil
	}

	if q.UID != nil && *q.UID == folder.SharedWithMeFolderUID {
		return folder.SharedWithMeFolder.WithURL(), nil
	}

	ctx = identity.WithRequester(ctx, q.SignedInUser)

	var dashFolder *folder.Folder
	var err error
	switch {
	case q.UID != nil && *q.UID != "":
		dashFolder, err = s.unifiedStore.Get(ctx, *q)
		if err != nil {
			return nil, toFolderError(err)
		}
	// nolint:staticcheck
	case q.ID != nil && *q.ID != 0:
		dashFolder, err = s.getFolderByIDFromApiServer(ctx, *q.ID, q.OrgID)
		if err != nil {
			return nil, toFolderError(err)
		}
	case q.Title != nil && *q.Title != "":
		dashFolder, err = s.getFolderByTitleFromApiServer(ctx, q.OrgID, *q.Title, q.ParentUID)
		if err != nil {
			return nil, toFolderError(err)
		}
	default:
		return &folder.GeneralFolder, nil
	}

	if dashFolder.IsGeneral() {
		return dashFolder, nil
	}

	evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashFolder.UID))
	if canView, err := s.accessControl.Evaluate(ctx, q.SignedInUser, evaluator); err != nil || !canView {
		if err != nil {
			return nil, toFolderError(err)
		}
		return nil, dashboards.ErrFolderAccessDenied
	}

	metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
	// nolint:staticcheck
	if q.ID != nil {
		q.ID = nil
		q.UID = &dashFolder.UID
	}

	f := dashFolder

	// always expose the dashboard store sequential ID
	metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
	// nolint:staticcheck
	f.ID = dashFolder.ID
	f.Version = dashFolder.Version

	if q.WithFullpath || q.WithFullpathUIDs {
		f, err = s.setFullpath(ctx, f, false)
		if err != nil {
			return nil, err
		}
	}

	return f, err
}

// searchFoldesFromApiServer uses the search grpc connection to search folders and returns the hit list
func (s *Service) searchFoldersFromApiServer(ctx context.Context, query folder.SearchFoldersQuery) (model.HitList, error) {
	ctx, span := s.tracer.Start(ctx, "folder.searchFoldersFromApiServer")
	defer span.End()

	if query.OrgID == 0 {
		requester, err := identity.GetRequester(ctx)
		if err != nil {
			return nil, err
		}
		query.OrgID = requester.GetOrgID()
	}

	request := &resourcepb.ResourceSearchRequest{
		Options: &resourcepb.ListOptions{
			Key: &resourcepb.ResourceKey{
				Namespace: s.k8sclient.GetNamespace(query.OrgID),
				Group:     folderv1.FolderResourceInfo.GroupVersionResource().Group,
				Resource:  folderv1.FolderResourceInfo.GroupVersionResource().Resource,
			},
			Fields: []*resourcepb.Requirement{},
			Labels: []*resourcepb.Requirement{},
		},
		Limit: folderSearchLimit}

	if len(query.UIDs) > 0 {
		request.Options.Fields = []*resourcepb.Requirement{{
			Key:      resource.SEARCH_FIELD_NAME,
			Operator: string(selection.In),
			Values:   query.UIDs,
		}}
	} else if len(query.IDs) > 0 {
		values := make([]string, len(query.IDs))
		for i, id := range query.IDs {
			values[i] = strconv.FormatInt(id, 10)
		}

		request.Options.Labels = append(request.Options.Labels, &resourcepb.Requirement{
			Key:      utils.LabelKeyDeprecatedInternalID, // nolint:staticcheck
			Operator: string(selection.In),
			Values:   values,
		})
	}

	if query.Title != "" {
		// allow wildcard search
		request.Query = "*" + strings.ToLower(query.Title) + "*"
		// or perform exact match if requested
		if query.TitleExactMatch {
			request.Query = query.Title
		}

		// if using query, you need to specify the fields you want
		request.Fields = dashboardsearch.IncludeFields
	}

	if query.Limit > 0 {
		request.Limit = query.Limit
	}

	parsedResults, err := dashboardsearch.SearchAll(ctx, query.OrgID, request, s.k8sclient.Search)
	if err != nil {
		return nil, err
	}

	hitList := make([]*model.Hit, len(parsedResults.Hits))
	for i, item := range parsedResults.Hits {
		slug := slugify.Slugify(item.Title)
		hitList[i] = &model.Hit{
			ID:          item.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID),
			UID:         item.Name,
			OrgID:       query.OrgID,
			Title:       item.Title,
			URI:         "db/" + slug,
			URL:         dashboards.GetFolderURL(item.Name, slug),
			Type:        model.DashHitFolder,
			FolderUID:   item.Folder,
			Description: item.Description,
		}
	}

	return hitList, nil
}

func (s *Service) getFolderByIDFromApiServer(ctx context.Context, id int64, orgID int64) (*folder.Folder, error) {
	ctx, span := s.tracer.Start(ctx, "folder.getFolderByIDFromApiServer")
	defer span.End()

	if id == 0 {
		return &folder.GeneralFolder, nil
	}

	folderkey := &resourcepb.ResourceKey{
		Namespace: s.k8sclient.GetNamespace(orgID),
		Group:     folderv1.FolderResourceInfo.GroupVersionResource().Group,
		Resource:  folderv1.FolderResourceInfo.GroupVersionResource().Resource,
	}

	request := &resourcepb.ResourceSearchRequest{
		Options: &resourcepb.ListOptions{
			Key:    folderkey,
			Fields: []*resourcepb.Requirement{},
			Labels: []*resourcepb.Requirement{
				{
					Key:      utils.LabelKeyDeprecatedInternalID, // nolint:staticcheck
					Operator: string(selection.In),
					Values:   []string{fmt.Sprintf("%d", id)},
				},
			},
		},
		Limit: folderSearchLimit}

	res, err := s.k8sclient.Search(ctx, orgID, request)
	if err != nil {
		return nil, err
	}

	hits, err := dashboardsearch.ParseResults(res, 0)
	if err != nil {
		return nil, err
	}

	return s.returnFirstFolderSearchResult(ctx, orgID, hits)
}

func (s *Service) returnFirstFolderSearchResult(ctx context.Context, orgID int64, hits v0alpha1.SearchResults) (*folder.Folder, error) {
	if len(hits.Hits) == 0 {
		return nil, dashboards.ErrFolderNotFound
	}

	uid := hits.Hits[0].Name
	user, err := identity.GetRequester(ctx)
	if err != nil {
		return nil, err
	}

	f, err := s.Get(ctx, &folder.GetFolderQuery{UID: &uid, SignedInUser: user, OrgID: orgID})
	if err != nil {
		return nil, err
	}

	return f, nil
}

func (s *Service) getFolderByTitleFromApiServer(ctx context.Context, orgID int64, title string, parentUID *string) (*folder.Folder, error) {
	ctx, span := s.tracer.Start(ctx, "folder.getFolderByTitleFromApiServer")
	defer span.End()

	if title == "" {
		return nil, dashboards.ErrFolderTitleEmpty
	}

	folderkey := &resourcepb.ResourceKey{
		Namespace: s.k8sclient.GetNamespace(orgID),
		Group:     folderv1.FolderResourceInfo.GroupVersionResource().Group,
		Resource:  folderv1.FolderResourceInfo.GroupVersionResource().Resource,
	}

	request := &resourcepb.ResourceSearchRequest{
		Options: &resourcepb.ListOptions{
			Key: folderkey,
			Fields: []*resourcepb.Requirement{
				{
					Key:      resource.SEARCH_FIELD_TITLE_PHRASE, // nolint:staticcheck
					Operator: string(selection.Equals),
					Values:   []string{title},
				},
			},
			Labels: []*resourcepb.Requirement{},
		},
		Limit: folderSearchLimit}

	if parentUID != nil {
		req := []*resourcepb.Requirement{{
			Key:      resource.SEARCH_FIELD_FOLDER,
			Operator: string(selection.In),
			Values:   []string{*parentUID},
		}}
		request.Options.Fields = append(request.Options.Fields, req...)
	}

	res, err := s.k8sclient.Search(ctx, orgID, request)
	if err != nil {
		return nil, err
	}

	hits, err := dashboardsearch.ParseResults(res, 0)
	if err != nil {
		return nil, err
	}

	// If we're searching for top-level folders (parentUID == nil), and the first result is not in the root folder, remove it from the results.
	for parentUID == nil && len(hits.Hits) > 0 && hits.Hits[0].Folder != "" {
		hits.Hits = hits.Hits[1:]
	}

	return s.returnFirstFolderSearchResult(ctx, orgID, hits)
}

func (s *Service) getChildrenFromApiServer(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) {
	ctx, span := s.tracer.Start(ctx, "folder.getChildrenFromApiServer")
	defer span.End()
	defer func(t time.Time) {
		parent := q.UID
		if q.UID != folder.SharedWithMeFolderUID {
			parent = "folder"
		}
		s.metrics.foldersGetChildrenRequestsDuration.WithLabelValues(parent).Observe(time.Since(t).Seconds())
	}(time.Now())

	if q.SignedInUser == nil {
		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
	}

	if q.UID == folder.SharedWithMeFolderUID {
		return s.GetSharedWithMe(ctx, q, false)
	}

	if q.UID == "" {
		return s.getRootFoldersFromApiServer(ctx, q)
	}

	// we only need to check access to the folder
	// if the parent is accessible then the subfolders are accessible as well (due to inheritance)
	evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersRead, dashboards.ScopeFoldersProvider.GetResourceScopeUID(q.UID))
	if q.Permission == dashboardaccess.PERMISSION_EDIT {
		evaluator = accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(q.UID))
	}
	if hasAccess, err := s.accessControl.Evaluate(ctx, q.SignedInUser, evaluator); err != nil || !hasAccess {
		if err != nil {
			return nil, err
		}
		return nil, dashboards.ErrFolderAccessDenied
	}

	children, err := s.unifiedStore.GetChildren(ctx, *q)
	if err != nil {
		return nil, err
	}

	return children, nil
}

func (s *Service) getRootFoldersFromApiServer(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) {
	ctx, span := s.tracer.Start(ctx, "folder.getRootFoldersFromApiServer")
	defer span.End()
	permissions := q.SignedInUser.GetPermissions()
	var folderPermissions []string
	if q.Permission == dashboardaccess.PERMISSION_EDIT {
		folderPermissions = permissions[dashboards.ActionFoldersWrite]
	} else {
		folderPermissions = permissions[dashboards.ActionFoldersRead]
	}

	if len(folderPermissions) == 0 && !q.SignedInUser.GetIsGrafanaAdmin() {
		return nil, nil
	}

	q.FolderUIDs = make([]string, 0, len(folderPermissions))
	for _, p := range folderPermissions {
		if p == dashboards.ScopeFoldersAll {
			// no need to query for folders with permissions
			// the user has permission to access all folders
			q.FolderUIDs = nil
			break
		}
		if folderUid, found := strings.CutPrefix(p, dashboards.ScopeFoldersPrefix); found {
			if !slices.Contains(q.FolderUIDs, folderUid) {
				q.FolderUIDs = append(q.FolderUIDs, folderUid)
			}
		}
	}

	children, err := s.unifiedStore.GetChildren(ctx, *q)
	if err != nil {
		return nil, err
	}

	// add "shared with me" folder on the 1st page
	if (q.Page == 0 || q.Page == 1) && len(q.FolderUIDs) != 0 {
		children = append([]*folder.FolderReference{folder.SharedWithMeFolder.ToFolderReference()}, children...)
	}

	return children, nil
}

func (s *Service) getParentsFromApiServer(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
	ctx, span := s.tracer.Start(ctx, "folder.getParentsFromApiServer")
	defer span.End()

	if q.UID == accesscontrol.GeneralFolderUID {
		return nil, nil
	}
	if q.UID == folder.SharedWithMeFolderUID {
		return []*folder.Folder{&folder.SharedWithMeFolder}, nil
	}

	return s.unifiedStore.GetParents(ctx, q)
}

func (s *Service) createOnApiServer(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
	ctx, span := s.tracer.Start(ctx, "folder.createOnApiServer")
	defer span.End()

	if cmd.SignedInUser == nil || cmd.SignedInUser.IsNil() {
		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
	}

	if cmd.ParentUID != "" {
		// Check that the user is allowed to create a subfolder in this folder
		parentUIDScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.ParentUID)
		legacyEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, parentUIDScope)
		newEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersCreate, parentUIDScope)
		evaluator := accesscontrol.EvalAny(legacyEvaluator, newEvaluator)
		hasAccess, evalErr := s.accessControl.Evaluate(ctx, cmd.SignedInUser, evaluator)
		if evalErr != nil {
			return nil, evalErr
		}
		if !hasAccess {
			return nil, dashboards.ErrFolderCreationAccessDenied.Errorf("user is missing the permission with action either folders:create or folders:write and scope %s or any of the parent folder scopes", parentUIDScope)
		}
	} else {
		evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID))
		hasAccess, evalErr := s.accessControl.Evaluate(ctx, cmd.SignedInUser, evaluator)
		if evalErr != nil {
			return nil, evalErr
		}
		if !hasAccess {
			return nil, dashboards.ErrFolderCreationAccessDenied.Errorf("user is missing the permission with action folders:create and scope folders:uid:general, which is required to create a folder under the root level")
		}
	}

	if cmd.UID == folder.SharedWithMeFolderUID {
		return nil, folder.ErrBadRequest.Errorf("cannot create folder with UID %s", folder.SharedWithMeFolderUID)
	}

	trimmedUID := strings.TrimSpace(cmd.UID)
	if trimmedUID == accesscontrol.GeneralFolderUID {
		return nil, dashboards.ErrFolderInvalidUID
	}

	cmd = &folder.CreateFolderCommand{
		// TODO: Today, if a UID isn't specified, the dashboard store
		// generates a new UID. The new folder store will need to do this as
		// well, but for now we take the UID from the newly created folder.
		UID:          trimmedUID,
		OrgID:        cmd.OrgID,
		Title:        cmd.Title,
		Description:  cmd.Description,
		ParentUID:    cmd.ParentUID,
		SignedInUser: cmd.SignedInUser,
		// pass along provisioning details
		ManagerKindClassicFP: cmd.ManagerKindClassicFP, // nolint:staticcheck
	}

	f, err := s.unifiedStore.Create(ctx, *cmd)
	if err != nil {
		return nil, err
	}

	return f, nil
}

func (s *Service) updateOnApiServer(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
	ctx, span := s.tracer.Start(ctx, "folder.updateOnApiServer")
	defer span.End()

	if cmd.SignedInUser == nil {
		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
	}

	if cmd.NewTitle != nil && *cmd.NewTitle != "" {
		title := strings.TrimSpace(*cmd.NewTitle)
		cmd.NewTitle = &title

		if strings.EqualFold(*cmd.NewTitle, dashboards.RootFolderName) {
			return nil, dashboards.ErrDashboardFolderNameExists
		}
	}

	if !util.IsValidShortUID(cmd.UID) {
		return nil, dashboards.ErrDashboardInvalidUid
	} else if util.IsShortUIDTooLong(cmd.UID) {
		return nil, dashboards.ErrDashboardUidTooLong
	}

	cmd.UID = strings.TrimSpace(cmd.UID)

	if cmd.NewTitle != nil && *cmd.NewTitle == "" {
		return nil, dashboards.ErrDashboardTitleEmpty
	}

	evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.UID))
	if hasAccess, err := s.accessControl.Evaluate(ctx, cmd.SignedInUser, evaluator); err != nil || !hasAccess {
		if err != nil {
			return nil, err
		}
		return nil, toFolderError(dashboards.ErrDashboardUpdateAccessDenied)
	}

	user := cmd.SignedInUser

	folder, err := s.unifiedStore.Update(ctx, folder.UpdateFolderCommand{
		UID:                  cmd.UID,
		OrgID:                cmd.OrgID,
		NewTitle:             cmd.NewTitle,
		NewDescription:       cmd.NewDescription,
		SignedInUser:         user,
		Overwrite:            cmd.Overwrite,
		Version:              cmd.Version,
		ManagerKindClassicFP: cmd.ManagerKindClassicFP, // nolint:staticcheck
	})

	if err != nil {
		return nil, err
	}

	if cmd.NewTitle != nil {
		metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()

		if err := s.publishFolderFullPathUpdatedEventViaApiServer(ctx, folder.Updated, cmd.OrgID, cmd.UID); err != nil {
			return nil, err
		}
	}

	// always expose the dashboard store sequential ID
	metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()

	return folder, nil
}

func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
	ctx, span := s.tracer.Start(ctx, "folder.deleteFromApiServer")
	defer span.End()

	if cmd.SignedInUser == nil {
		return folder.ErrBadRequest.Errorf("missing signed in user")
	}
	if cmd.UID == "" {
		return folder.ErrBadRequest.Errorf("missing UID")
	}
	if cmd.OrgID < 1 {
		return folder.ErrBadRequest.Errorf("invalid orgID")
	}

	evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.UID))
	if hasAccess, err := s.accessControl.Evaluate(ctx, cmd.SignedInUser, evaluator); err != nil || !hasAccess {
		if err != nil {
			return toFolderError(err)
		}
		return dashboards.ErrFolderAccessDenied
	}

	descFolders, err := s.unifiedStore.GetDescendants(ctx, cmd.OrgID, cmd.UID)
	if err != nil {
		return err
	}
	descFolders = folder.SortByPostorder(descFolders)

	folders := []string{}
	for _, f := range descFolders {
		folders = append(folders, f.UID)
	}
	// must delete children first, then the parent folder
	s.log.InfoContext(ctx, "deleting folder with descendants", "org_id", cmd.OrgID, "uid", cmd.UID, "folderUIDs", strings.Join(folders, ","))
	folders = append(folders, cmd.UID)

	if cmd.ForceDeleteRules {
		if err := s.deleteChildrenInFolder(ctx, cmd.OrgID, folders, cmd.SignedInUser); err != nil {
			return err
		}
	} else {
		alertRuleSrv, ok := s.registry[entity.StandardKindAlertRule]
		if !ok {
			return folder.ErrInternal.Errorf("no alert rule service found in registry")
		}
		alertRulesInFolder, err := alertRuleSrv.CountInFolders(ctx, cmd.OrgID, folders, cmd.SignedInUser)
		if err != nil {
			s.log.Error("failed to count alert rules in folder", "error", err)
			return err
		}
		if alertRulesInFolder > 0 {
			return folder.ErrFolderNotEmpty.Errorf("folder contains %d alert rules", alertRulesInFolder)
		}

		libraryPanelSrv, ok := s.registry[entity.StandardKindLibraryPanel]
		if !ok {
			return folder.ErrInternal.Errorf("no library panel service found in registry")
		}
		//	/* TODO: after a decision regarding folder deletion permissions has been made
		//	(https://github.com/grafana/grafana-enterprise/issues/5144),
		//	remove the following call to DeleteInFolders
		//	and remove "user" from the signature of DeleteInFolder in the folder RegistryService.
		//	Context: https://github.com/grafana/grafana/pull/69149#discussion_r1235057903
		//	*/
		// Obs: DeleteInFolders only deletes dangling library panels (not linked to any dashboard) and throws errors if there are connections
		if err := libraryPanelSrv.DeleteInFolders(ctx, cmd.OrgID, folders, cmd.SignedInUser); err != nil {
			s.log.Error("failed to delete dangling library panels in folders", "error", err, "folders", strings.Join(folders, ","))
			return err
		}
		// We need a list of dashboard uids inside the folder to delete related dashboards & public dashboards -
		// we cannot use the dashboard service directly due to circular dependencies, so use the search client to get the dashboards
		request := &resourcepb.ResourceSearchRequest{
			Options: &resourcepb.ListOptions{
				Labels: []*resourcepb.Requirement{},
				Fields: []*resourcepb.Requirement{
					{
						Key:      resource.SEARCH_FIELD_FOLDER,
						Operator: string(selection.In),
						Values:   folders,
					},
				},
			},
			Limit: folderSearchLimit}

		hits, err := dashboardsearch.SearchAll(ctx, cmd.OrgID, request, s.dashboardK8sClient.Search)
		if err != nil {
			return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
		}

		dashboardUIDs := make([]string, len(hits.Hits))
		for i, dashboard := range hits.Hits {
			dashboardUIDs[i] = dashboard.Name
			err = s.dashboardK8sClient.Delete(ctx, dashboard.Name, cmd.OrgID, metav1.DeleteOptions{})
			if err != nil {
				return folder.ErrInternal.Errorf("failed to delete child dashboard: %w", err)
			}
		}
		// Delete all public dashboards in the folders
		err = s.publicDashboardService.DeleteByDashboardUIDs(ctx, cmd.OrgID, dashboardUIDs)
		if err != nil {
			return folder.ErrInternal.Errorf("failed to delete public dashboards: %w", err)
		}
	}

	err = s.unifiedStore.Delete(ctx, folders, cmd.OrgID)
	if err != nil {
		return err
	}

	return nil
}

func (s *Service) moveOnApiServer(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
	ctx, span := s.tracer.Start(ctx, "folder.moveOnApiServer")
	defer span.End()

	if cmd.SignedInUser == nil {
		return nil, folder.ErrBadRequest.Errorf("missing signed in user")
	}

	// k6-specific check to prevent folder move for a k6-app folder and its children
	if cmd.UID == accesscontrol.K6FolderUID {
		return nil, folder.ErrBadRequest.Errorf("k6 project may not be moved")
	}

	// Check that the user is allowed to move the folder to the destination folder
	hasAccess, evalErr := s.canMoveViaApiServer(ctx, cmd)
	if evalErr != nil {
		return nil, evalErr
	}
	if !hasAccess {
		return nil, dashboards.ErrFolderAccessDenied
	}

	f, err := s.unifiedStore.Update(ctx, folder.UpdateFolderCommand{
		UID:          cmd.UID,
		OrgID:        cmd.OrgID,
		NewParentUID: &cmd.NewParentUID,
		SignedInUser: cmd.SignedInUser,
	})
	if err != nil {
		return nil, folder.ErrInternal.Errorf("failed to move folder: %w", err)
	}

	if err := s.publishFolderFullPathUpdatedEventViaApiServer(ctx, f.Updated, cmd.OrgID, cmd.UID); err != nil {
		return nil, err
	}

	return f, nil
}

func (s *Service) publishFolderFullPathUpdatedEventViaApiServer(ctx context.Context, timestamp time.Time, orgID int64, folderUID string) error {
	ctx, span := s.tracer.Start(ctx, "folder.publishFolderFullPathUpdatedEventViaApiServer")
	defer span.End()

	descFolders, err := s.unifiedStore.GetDescendants(ctx, orgID, folderUID)
	if err != nil {
		s.log.ErrorContext(ctx, "Failed to get descendants of the folder", "folderUID", folderUID, "orgID", orgID, "error", err)
		return err
	}

	uids := make([]string, 0, len(descFolders)+1)
	uids = append(uids, folderUID)
	for _, f := range descFolders {
		uids = append(uids, f.UID)
	}
	span.AddEvent("found folder descendants", trace.WithAttributes(
		attribute.Int64("folders", int64(len(uids))),
	))

	if err := s.bus.Publish(ctx, &events.FolderFullPathUpdated{
		Timestamp: timestamp,
		UIDs:      uids,
		OrgID:     orgID,
	}); err != nil {
		s.log.ErrorContext(ctx, "Failed to publish FolderFullPathUpdated event", "folderUID", folderUID, "orgID", orgID, "descendantsUIDs", uids, "error", err)
		return err
	}

	return nil
}

func (s *Service) canMoveViaApiServer(ctx context.Context, cmd *folder.MoveFolderCommand) (bool, error) {
	ctx, span := s.tracer.Start(ctx, "folder.canMoveViaApiServer")
	defer span.End()

	// Check that the user is allowed to move the folder to the destination folder
	var evaluator accesscontrol.Evaluator
	parentUID := cmd.NewParentUID
	if parentUID != "" {
		legacyEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.NewParentUID))
		newEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.NewParentUID))
		evaluator = accesscontrol.EvalAny(legacyEvaluator, newEvaluator)
	} else {
		// Evaluate folder creation permission when moving folder to the root level
		evaluator = accesscontrol.EvalPermission(dashboards.ActionFoldersCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID))
		parentUID = folder.GeneralFolderUID
	}
	if hasAccess, err := s.accessControl.Evaluate(ctx, cmd.SignedInUser, evaluator); err != nil {
		return false, err
	} else if !hasAccess {
		return false, dashboards.ErrMoveAccessDenied.Errorf("user does not have permissions to move a folder to folder with UID %s", parentUID)
	}

	// Check that the user would not be elevating their permissions by moving a folder to the destination folder
	// This is needed for plugins, as different folders can have different plugin configs
	// We do this by checking that there are no permissions that user has on the destination parent folder but not on the source folder
	// We also need to look at the folder tree for the destination folder, as folder permissions are inherited
	newFolderAndParentUIDs, err := s.getFolderAndParentUIDScopesViaApiServer(ctx, parentUID, cmd.OrgID)
	if err != nil {
		return false, err
	}

	permissions := cmd.SignedInUser.GetPermissions()
	var evaluators []accesscontrol.Evaluator
	currentFolderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.UID)
	for action, scopes := range permissions {
		for _, scope := range newFolderAndParentUIDs {
			if slices.Contains(scopes, scope) {
				evaluators = append(evaluators, accesscontrol.EvalPermission(action, currentFolderScope))
				break
			}
		}
	}

	if hasAccess, err := s.accessControl.Evaluate(ctx, cmd.SignedInUser, accesscontrol.EvalAll(evaluators...)); err != nil {
		return false, err
	} else if !hasAccess {
		return false, dashboards.ErrFolderAccessEscalation.Errorf("user cannot move a folder to another folder where they have higher permissions")
	}
	return true, nil
}

func (s *Service) getFolderAndParentUIDScopesViaApiServer(ctx context.Context, folderUID string, orgID int64) ([]string, error) {
	ctx, span := s.tracer.Start(ctx, "folder.getFolderAndParentUIDScopesViaApiServer")
	defer span.End()

	folderAndParentUIDScopes := []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)}
	if folderUID == folder.GeneralFolderUID {
		return folderAndParentUIDScopes, nil
	}
	folderParents, err := s.unifiedStore.GetParents(ctx, folder.GetParentsQuery{UID: folderUID, OrgID: orgID})
	if err != nil {
		return nil, err
	}
	for _, newParent := range folderParents {
		scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(newParent.UID)
		folderAndParentUIDScopes = append(folderAndParentUIDScopes, scope)
	}
	return folderAndParentUIDScopes, nil
}

func (s *Service) getDescendantCountsFromApiServer(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
	ctx, span := s.tracer.Start(ctx, "folder.getDescendantCountsFromApiServer")
	defer span.End()

	if q.SignedInUser == nil {
		return nil, folder.ErrBadRequest.Errorf("missing signed-in user")
	}
	if q.UID == nil || *q.UID == "" {
		return nil, folder.ErrBadRequest.Errorf("missing UID")
	}
	if q.OrgID < 1 {
		return nil, folder.ErrBadRequest.Errorf("invalid orgID")
	}

	return s.unifiedStore.(*FolderUnifiedStoreImpl).CountFolderContent(ctx, q.OrgID, *q.UID)
}
