package permissions

import (
	"bytes"
	"fmt"
	"slices"
	"strings"

	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/services/accesscontrol"
	"github.com/grafana/grafana/pkg/services/dashboards"
	"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
	"github.com/grafana/grafana/pkg/services/featuremgmt"
	"github.com/grafana/grafana/pkg/services/folder"
	"github.com/grafana/grafana/pkg/services/login"
	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
	"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
)

// maximum possible capacity for recursive queries array: one query for folder and one for dashboard actions
const maximumRecursiveQueries = 2

type clause struct {
	string
	params []any
}

type accessControlDashboardPermissionFilter struct {
	user                identity.Requester
	dashboardAction     string
	dashboardActionSets []string
	folderAction        string
	folderActionSets    []string
	features            featuremgmt.FeatureToggles

	where clause
	// any recursive CTE queries (if supported)
	recQueries                   []clause
	recursiveQueriesAreSupported bool

	dialect migrator.Dialect
}

type PermissionsFilter interface {
	LeftJoin() string
	With() (string, []any)
	Where() (string, []any)

	buildClauses(dialect migrator.Dialect)
	nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTable string, Col string, rightTableCol string, orgID int64) (string, []any)
}

// NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the dashboardaccess.PermissionType and query type
// The filter is configured to use the new permissions filter (without subqueries) if the feature flag is enabled
// The filter is configured to use the old permissions filter (with subqueries) if the feature flag is disabled
func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissionLevel dashboardaccess.PermissionType, queryType string, features featuremgmt.FeatureToggles, recursiveQueriesAreSupported bool, dialect migrator.Dialect) PermissionsFilter {
	needEdit := permissionLevel > dashboardaccess.PERMISSION_VIEW

	var folderAction string
	var dashboardAction string
	var folderActionSets []string
	var dashboardActionSets []string
	switch queryType {
	case searchstore.TypeFolder:
		folderAction = dashboards.ActionFoldersRead
		folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
		if needEdit {
			folderAction = dashboards.ActionDashboardsCreate
			folderActionSets = []string{"folders:edit", "folders:admin"}
		}
	case searchstore.TypeDashboard:
		dashboardAction = dashboards.ActionDashboardsRead
		folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
		dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
		if needEdit {
			dashboardAction = dashboards.ActionDashboardsWrite
			folderActionSets = []string{"folders:edit", "folders:admin"}
			dashboardActionSets = []string{"dashboards:edit", "dashboards:admin"}
		}
	case searchstore.TypeAlertFolder:
		folderAction = accesscontrol.ActionAlertingRuleRead
		folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
		if needEdit {
			folderAction = accesscontrol.ActionAlertingRuleCreate
			folderActionSets = []string{"folders:edit", "folders:admin"}
		}
	case searchstore.TypeAnnotation:
		dashboardAction = accesscontrol.ActionAnnotationsRead
		folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
		dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
	default:
		folderAction = dashboards.ActionFoldersRead
		dashboardAction = dashboards.ActionDashboardsRead
		folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"}
		dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"}
		if needEdit {
			folderAction = dashboards.ActionDashboardsCreate
			dashboardAction = dashboards.ActionDashboardsWrite
			folderActionSets = []string{"folders:edit", "folders:admin"}
			dashboardActionSets = []string{"dashboards:edit", "dashboards:admin"}
		}
	}

	var f PermissionsFilter
	//nolint:staticcheck // not yet migrated to OpenFeature
	if features.IsEnabledGlobally(featuremgmt.FlagPermissionsFilterRemoveSubquery) {
		f = &accessControlDashboardPermissionFilterNoFolderSubquery{
			accessControlDashboardPermissionFilter: accessControlDashboardPermissionFilter{
				user: user, folderAction: folderAction, folderActionSets: folderActionSets, dashboardAction: dashboardAction, dashboardActionSets: dashboardActionSets,
				features: features, recursiveQueriesAreSupported: recursiveQueriesAreSupported, dialect: dialect,
			},
		}
	} else {
		f = &accessControlDashboardPermissionFilter{user: user, folderAction: folderAction, folderActionSets: folderActionSets, dashboardAction: dashboardAction, dashboardActionSets: dashboardActionSets,
			features: features, recursiveQueriesAreSupported: recursiveQueriesAreSupported, dialect: dialect,
		}
	}
	f.buildClauses(dialect)
	return f
}

func (f *accessControlDashboardPermissionFilter) LeftJoin() string {
	return ""
}

// Where returns:
// - a where clause for filtering dashboards with expected permissions
// - an array with the query parameters
func (f *accessControlDashboardPermissionFilter) Where() (string, []any) {
	return f.where.string, f.where.params
}

// Check if user has no permissions required for search to skip expensive query
func (f *accessControlDashboardPermissionFilter) hasRequiredActions() bool {
	permissions := f.user.GetPermissions()
	requiredActions := []string{f.folderAction, f.dashboardAction}
	for _, action := range requiredActions {
		if _, ok := permissions[action]; ok {
			return true
		}
	}

	return false
}

func (f *accessControlDashboardPermissionFilter) buildClauses(dialect migrator.Dialect) {
	if f.user == nil || f.user.IsNil() || !f.hasRequiredActions() {
		f.where = clause{string: "(1 = 0)"}
		return
	}
	dashWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeDashboardsPrefix)
	folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix)

	var userID int64
	if id, err := identity.UserIdentifier(f.user.GetID()); err == nil {
		userID = id
	}

	orgID := f.user.GetOrgID()
	filter, params := accesscontrol.UserRolesFilter(orgID, userID, f.user.GetTeams(), accesscontrol.GetOrgRoles(f.user), dialect)
	rolesFilter := " AND role_id IN(SELECT id FROM role " + filter + ") "
	var args []any
	builder := strings.Builder{}
	builder.WriteRune('(')

	permSelector := strings.Builder{}
	var permSelectorArgs []any

	// useSelfContainedPermissions is true if the user's permissions are stored and set from the JWT token
	// currently it's used for the extended JWT module (when the user is authenticated via a JWT token generated by Grafana)
	useSelfContainedPermissions := f.user.IsAuthenticatedBy(login.ExtendedJWTModule)

	if f.dashboardAction != "" {
		toCheckDashboards := actionsToCheck(f.dashboardAction, f.dashboardActionSets, f.user.GetPermissions(), dashWildcards, folderWildcards)
		toCheckFolders := actionsToCheck(f.dashboardAction, f.folderActionSets, f.user.GetPermissions(), dashWildcards, folderWildcards)

		if len(toCheckDashboards) > 0 {
			if !useSelfContainedPermissions {
				builder.WriteString("(dashboard.uid IN (SELECT identifier FROM permission WHERE kind = 'dashboards' AND attribute = 'uid'")
				builder.WriteString(rolesFilter)
				args = append(args, params...)
				if len(toCheckDashboards) == 1 {
					builder.WriteString(" AND action = ?) AND NOT dashboard.is_folder)")
					args = append(args, toCheckDashboards[0])
				} else {
					builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheckDashboards)-1) + ")) AND NOT dashboard.is_folder)")
					args = append(args, toCheckDashboards...)
				}
			} else {
				args = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeDashboardsPrefix)

				// Only add the IN clause if we have any dashboards to check
				if len(args) > 0 {
					builder.WriteString("(dashboard.uid IN (?" + strings.Repeat(", ?", len(args)-1) + "")
					builder.WriteString(") AND NOT dashboard.is_folder)")
				} else {
					builder.WriteString("(1 = 0)")
				}
			}

			builder.WriteString(" OR ")

			if !useSelfContainedPermissions {
				permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'")
				permSelector.WriteString(rolesFilter)
				permSelectorArgs = append(permSelectorArgs, params...)
				if len(toCheckDashboards) == 1 {
					permSelector.WriteString(" AND action = ?")
					permSelectorArgs = append(permSelectorArgs, toCheckDashboards[0])
				} else {
					permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheckDashboards)-1) + ")")
					permSelectorArgs = append(permSelectorArgs, toCheckFolders...)
				}
			} else {
				permSelectorArgs = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeFoldersPrefix)

				// Only add the IN clause if we have any folders to check
				if len(permSelectorArgs) > 0 {
					permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "")
				} else {
					permSelector.WriteString("(")
				}
			}
			permSelector.WriteRune(')')

			if len(permSelectorArgs) > 0 {
				switch f.recursiveQueriesAreSupported {
				case true:
					builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
					recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
					f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
					builder.WriteString(fmt.Sprintf("WHERE d.org_id = ? AND d.uid IN (SELECT uid FROM %s)", recQueryName))
					args = append(args, orgID)
				default:
					nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "folder_id", "d.id", orgID)
					builder.WriteRune('(')
					builder.WriteString(nestedFoldersSelectors)
					args = append(args, nestedFoldersArgs...)
				}
			} else {
				builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ")
				builder.WriteString("WHERE 1 = 0")
			}
			builder.WriteString(") AND NOT dashboard.is_folder)")

			// Include all the dashboards under the root if the user has the required permissions on the root (used to be the General folder)
			if hasAccessToRoot(f.dashboardAction, f.user) {
				builder.WriteString(" OR (dashboard.folder_id = 0 AND NOT dashboard.is_folder)")
			}
		} else {
			builder.WriteString("NOT dashboard.is_folder")
		}
	}

	// recycle and reuse
	permSelector.Reset()
	permSelectorArgs = permSelectorArgs[:0]

	if f.folderAction != "" {
		if f.dashboardAction != "" {
			builder.WriteString(" OR ")
		}

		toCheck := actionsToCheck(f.folderAction, f.folderActionSets, f.user.GetPermissions(), folderWildcards)

		if len(toCheck) > 0 {
			if !useSelfContainedPermissions {
				permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'")
				permSelector.WriteString(rolesFilter)
				permSelectorArgs = append(permSelectorArgs, params...)
				if len(toCheck) == 1 {
					permSelector.WriteString(" AND action = ?")
					permSelectorArgs = append(permSelectorArgs, toCheck[0])
				} else {
					permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ")")
					permSelectorArgs = append(permSelectorArgs, toCheck...)
				}
			} else {
				permSelectorArgs = getAllowedUIDs(f.folderAction, f.user, dashboards.ScopeFoldersPrefix)

				if len(permSelectorArgs) > 0 {
					permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "")
				} else {
					permSelector.WriteString("(")
				}
			}

			permSelector.WriteRune(')')

			if len(permSelectorArgs) > 0 {
				switch f.recursiveQueriesAreSupported {
				case true:
					recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries))
					f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID)
					builder.WriteString("(dashboard.uid IN ")
					builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName))
				default:
					nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "uid", "d.uid", orgID)
					builder.WriteRune('(')
					builder.WriteString(nestedFoldersSelectors)
					builder.WriteRune(')')
					args = append(args, nestedFoldersArgs...)
				}
			} else {
				builder.WriteString("(1 = 0")
			}
			builder.WriteString(" AND dashboard.is_folder)")
		} else {
			builder.WriteString("dashboard.is_folder")
		}
	}

	builder.WriteRune(')')

	f.where = clause{string: builder.String(), params: args}
}

// With returns:
// - a with clause for fetching folders with inherited permissions if nested folders are enabled or an empty string
func (f *accessControlDashboardPermissionFilter) With() (string, []any) {
	var sb bytes.Buffer
	var params []any
	if len(f.recQueries) > 0 {
		sb.WriteString("WITH RECURSIVE ")
		sb.WriteString(f.recQueries[0].string)
		params = append(params, f.recQueries[0].params...)
		for _, r := range f.recQueries[1:] {
			sb.WriteRune(',')
			sb.WriteString(r.string)
			params = append(params, r.params...)
		}
	}
	return sb.String(), params
}

func (f *accessControlDashboardPermissionFilter) addRecQry(queryName string, whereUIDSelect string, whereParams []any, orgID int64) {
	forceIndex := ""
	if f.dialect.DriverName() == migrator.MySQL {
		forceIndex = " FORCE INDEX (IDX_folder_org_id_parent_uid) "
	}

	if f.recQueries == nil {
		f.recQueries = make([]clause, 0, maximumRecursiveQueries)
	}
	c := make([]any, len(whereParams))
	copy(c, whereParams)
	c = append([]any{orgID}, c...)
	f.recQueries = append(f.recQueries, clause{
		// covered by UQE_folder_org_id_uid and UQE_folder_org_id_parent_uid_title
		string: fmt.Sprintf(`%s AS (
			SELECT uid, parent_uid, org_id FROM folder WHERE org_id = ? AND uid IN %s
			UNION ALL SELECT f.uid, f.parent_uid, f.org_id FROM folder f %s INNER JOIN %s r ON f.parent_uid = r.uid and f.org_id = r.org_id
		)`, queryName, whereUIDSelect, forceIndex, queryName),
		params: c,
	})
}

func actionsToCheck(action string, actionSets []string, permissions map[string][]string, wildcards ...accesscontrol.Wildcards) []any {
	for _, scope := range permissions[action] {
		for _, w := range wildcards {
			if w.Contains(scope) {
				return []any{}
			}
		}
	}

	toCheck := []any{action}
	for _, a := range actionSets {
		toCheck = append(toCheck, a)
	}

	return toCheck
}

func (f *accessControlDashboardPermissionFilter) nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTable string, leftCol string, rightTableCol string, orgID int64) (string, []any) {
	wheres := make([]string, 0, folder.MaxNestedFolderDepth+1)
	args := make([]any, 0, len(permSelectorArgs)*(folder.MaxNestedFolderDepth+1))

	joins := make([]string, 0, folder.MaxNestedFolderDepth+2)

	// covered by UQE_folder_org_id_uid
	tmpl := "INNER JOIN folder %s ON %s.%s = %s.uid AND %s.org_id = %s.org_id "

	prev := "d"
	onCol := "uid"
	for i := 1; i <= folder.MaxNestedFolderDepth+2; i++ {
		t := fmt.Sprintf("f%d", i)
		s := fmt.Sprintf(tmpl, t, prev, onCol, t, prev, t)
		joins = append(joins, s)

		// covered by UQE_folder_org_id_uid
		wheres = append(wheres, fmt.Sprintf("(%s.org_id = ? AND %s.%s IN (SELECT %s FROM dashboard d %s WHERE %s.org_id = ? AND %s.uid IN %s)", leftTable, leftTable, leftCol, rightTableCol, strings.Join(joins, " "), t, t, permSelector))
		args = append(args, orgID, orgID)
		args = append(args, permSelectorArgs...)

		prev = t
		onCol = "parent_uid"
	}

	return strings.Join(wheres, ") OR "), args
}

func getAllowedUIDs(action string, user identity.Requester, scopePrefix string) []any {
	uidScopes := user.GetPermissions()[action]

	args := make([]any, 0, len(uidScopes))
	for _, uidScope := range uidScopes {
		if !strings.HasPrefix(uidScope, scopePrefix) {
			continue
		}
		uid := strings.TrimPrefix(uidScope, scopePrefix)
		args = append(args, uid)
	}

	return args
}

// Checks if the user has the required permissions on the root (used to be the General folder)
func hasAccessToRoot(actionToCheck string, user identity.Requester) bool {
	generalFolderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)
	return slices.Contains(user.GetPermissions()[actionToCheck], generalFolderScope)
}
