// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package scopeids

import (
	"context"
	"fmt"

	"github.com/hashicorp/boundary/internal/boundary"
	"github.com/hashicorp/boundary/internal/daemon/controller/auth"
	"github.com/hashicorp/boundary/internal/daemon/controller/common"
	"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
	"github.com/hashicorp/boundary/internal/errors"
	"github.com/hashicorp/boundary/internal/iam"
	"github.com/hashicorp/boundary/internal/perms"
	"github.com/hashicorp/boundary/internal/types/action"
	"github.com/hashicorp/boundary/internal/types/resource"
	"github.com/hashicorp/boundary/internal/types/scope"
	"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
)

type authzProtectedEntityProvider interface {
	// Fetches basic resource info for the given scopes. Note that this is a
	// "where clause" style of argument: if the set of scopes is populated these
	// are the scopes to limit to (e.g. to put in a where clause). An empty set
	// of scopes means to look in *all* scopes, not none!
	FetchAuthzProtectedEntitiesByScope(ctx context.Context, projectIds []string) (map[string][]boundary.AuthzProtectedEntity, error)
}

// ResourceInfo contains information about a particular resource
type ResourceInfo struct {
	AuthorizedActions action.ActionSet
}

// ScopeInfoWithResourceIds contains information about a scope and the resources
// found within it
type ScopeInfoWithResourceIds struct {
	*scopes.ScopeInfo
	Resources map[string]ResourceInfo
}

// GetListingResourceInformationInput contains input parameters to the function
type GetListingResourceInformationInput struct {
	// An IAM repo function to use for a scope listing call
	IamRepoFn common.IamRepoFactory

	// The original auth results from the list command
	AuthResults auth.VerifyResults

	// The scope ID to use, or the starting point for a recursive search
	RootScopeId string

	// The type of resource being listed
	Type resource.Type

	// Whether the search is recursive
	Recursive bool

	// A repo to fetch resources
	AuthzProtectedEntityProvider authzProtectedEntityProvider

	// The available actions for the resource type
	ActionSet action.ActionSet
}

// GetListingResourceInformationOutput contains results from the function
type GetListingResourceInformationOutput struct {
	// The calculated list of relevant scope IDs
	ScopeIds []string

	// The specific resource IDs calculated to be authorized for listing
	ResourceIds []string

	// A map of scope ID to scope information and a map of resource IDs in that
	// scope and specific information about that resource, such as available
	// actions
	ScopeResourceMap map[string]*ScopeInfoWithResourceIds
}

// GetListingResourceInformation, given common parameters for List calls,
// returns useful information: the set of scope IDs in which to search for
// resources; the IDs of the resources known to be authorized for that user; and
// a memoized map of the scopes to their info for populating returned values.
func GetListingResourceInformation(
	// The context to use when listing in the DB, if required
	ctx context.Context,
	// The input struct
	input GetListingResourceInformationInput,
) (*GetListingResourceInformationOutput, error) {
	const op = "scopeids.GetListingResourceInformation"

	output := new(GetListingResourceInformationOutput)

	// Validation
	switch {
	case input.Type == resource.Unknown:
		return nil, errors.New(ctx, errors.InvalidParameter, op, "unknown resource")
	case input.IamRepoFn == nil:
		return nil, errors.New(ctx, errors.InvalidParameter, op, "nil iam repo")
	case input.RootScopeId == "":
		return nil, errors.New(ctx, errors.InvalidParameter, op, "missing root scope id")
	case input.AuthResults.Scope == nil:
		return nil, errors.New(ctx, errors.InvalidParameter, op, "nil scope in auth results")
	case !input.Recursive && input.AuthResults.Scope.Id != input.RootScopeId:
		return nil, errors.New(ctx, errors.InvalidParameter, op, "non-recursive search but auth results scope does not match root scope")
	}

	// This will be used to memoize scope info so we can put the right scope
	// info for each returned value
	output.ScopeResourceMap = map[string]*ScopeInfoWithResourceIds{}

	// Base case: if not recursive, return the scope we were given and the
	// already-looked-up info
	if !input.Recursive {
		output.ScopeResourceMap[input.AuthResults.Scope.Id] = &ScopeInfoWithResourceIds{ScopeInfo: input.AuthResults.Scope}
		// If we don't have information do to the resource lookup ourselves,
		// return what we have
		if input.AuthzProtectedEntityProvider == nil {
			output.ScopeIds = []string{input.AuthResults.Scope.Id}
			return output, nil
		}
		// Otherwise filter on this one scope and return
		if err := filterAuthorizedResourceIds(ctx, input, output); err != nil {
			return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error filtering to only authorized resources"))
		}
		return output, nil
	}

	repo, err := input.IamRepoFn()
	if err != nil {
		return nil, err
	}
	// Get all scopes recursively. Start at global because we need to take into
	// account permissions in parent scopes even if they want to scale back the
	// returned information to a child scope and its children.
	scps, err := repo.ListScopesRecursively(ctx, scope.Global.String())
	if err != nil {
		return nil, err
	}

	res := perms.Resource{
		Type: input.Type,
	}
	// For each scope, see if we have permission to list that type in that
	// scope
	var deferredScopes []*iam.Scope
	// Store whether global has list permission
	var globalHasList bool
	for _, scp := range scps {
		scpId := scp.GetPublicId()
		res.ScopeId = scpId
		res.ParentScopeId = scp.GetParentId()
		aSet := input.AuthResults.FetchActionSetForType(ctx,
			// This is overridden by WithResource
			resource.Unknown,
			action.NewActionSet(action.List),
			auth.WithResource(&res),
		)
		switch len(aSet) {
		case 0:
			// Defer until we've read all scopes. We do this because if the
			// ordering coming back isn't in parent-first ordering our map
			// lookup might fail.
			deferredScopes = append(deferredScopes, scp)
		case 1:
			if !aSet.HasAction(action.List) {
				return nil, errors.New(ctx, errors.Internal, op, "unexpected action in set")
			}
			if output.ScopeResourceMap[scpId] == nil {
				scopeInfo := &scopes.ScopeInfo{
					Id:            scp.GetPublicId(),
					Type:          scp.GetType(),
					Name:          scp.GetName(),
					Description:   scp.GetDescription(),
					ParentScopeId: scp.GetParentId(),
				}
				output.ScopeResourceMap[scpId] = &ScopeInfoWithResourceIds{ScopeInfo: scopeInfo}
			}
			if scpId == scope.Global.String() {
				globalHasList = true
			}
		default:
			return nil, errors.New(ctx, errors.Internal, op, "unexpected number of actions back in set")
		}
	}

	// Now go through these and see if a parent matches
	for _, scp := range deferredScopes {
		// If they had list on global scope anything else is automatically
		// included; otherwise if they had list on the parent scope, this
		// scope is included in the map and is sufficient here.
		if globalHasList || output.ScopeResourceMap[scp.GetParentId()] != nil {
			scpId := scp.GetPublicId()
			if output.ScopeResourceMap[scpId] == nil {
				scopeInfo := &scopes.ScopeInfo{
					Id:            scp.GetPublicId(),
					Type:          scp.GetType(),
					Name:          scp.GetName(),
					Description:   scp.GetDescription(),
					ParentScopeId: scp.GetParentId(),
				}
				output.ScopeResourceMap[scpId] = &ScopeInfoWithResourceIds{ScopeInfo: scopeInfo}
			}
		}
	}

	// Now elide out any that aren't under the root scope ID
	elideScopes := make([]string, 0, len(output.ScopeResourceMap))
	for scpId, scp := range output.ScopeResourceMap {
		switch input.RootScopeId {
		// If the root is global, it matches
		case scope.Global.String():
		// If the current scope matches the root, it matches
		case scpId:
		// Or if the parent of this scope is the root (for orgs that would mean
		// a root scope ID which is covered in the case above, so this is really
		// projects matching an org used as the root)
		case scp.GetParentScopeId():
		default:
			elideScopes = append(elideScopes, scpId)
		}
	}

	for _, scpId := range elideScopes {
		delete(output.ScopeResourceMap, scpId)
	}

	// If we have nothing in scopeInfoMap at this point, we aren't authorized
	// anywhere so return 403.
	if len(output.ScopeResourceMap) == 0 {
		return nil, handlers.ForbiddenError()
	}

	if input.AuthzProtectedEntityProvider == nil {
		output.populateScopeIdsFromScopeResourceMap()
		return output, nil
	}

	if err := filterAuthorizedResourceIds(ctx, input, output); err != nil {
		return nil, errors.Wrap(ctx, err, op, errors.WithMsg("error filtering to only authorized resources"))
	}

	return output, nil
}

// filterAuthorizedResourceIds calls the passed in function to get IDs for
// resources in the given scopes and then figures out which ones are actually
// authorized for listing by the user.
//
// It also populates the scope IDs in the output
func filterAuthorizedResourceIds(
	// The context to use when listing in the DB, if required
	ctx context.Context,
	// The input struct
	input GetListingResourceInformationInput,
	// The scope information to fill out
	output *GetListingResourceInformationOutput,
) error {
	const op = "scopeids.filterAuthorizedResources"

	// Populate scopeIds and determine if we found global
	output.populateScopeIdsFromScopeResourceMap()

	// The calling function is giving us a complete set with any recursive
	// lookup already performed (that's the point of the function).
	scopedResourceInfo, err := input.AuthzProtectedEntityProvider.FetchAuthzProtectedEntitiesByScope(ctx, output.ScopeIds)
	if err != nil {
		return errors.Wrap(ctx, err, op)
	}

	res := perms.Resource{
		Type: input.Type,
	}

	// Now run authorization checks against each so we know if there is a point
	// in fetching the full resource, and cache the authorized actions
	for scopeId, resourceInfos := range scopedResourceInfo {
		for _, resourceInfo := range resourceInfos {
			res.Id = resourceInfo.GetPublicId()
			res.ScopeId = scopeId
			authorizedActions := input.AuthResults.FetchActionSetForId(ctx, resourceInfo.GetPublicId(), input.ActionSet, auth.WithResource(&res))
			if len(authorizedActions) == 0 {
				continue
			}

			if resourceInfo.GetUserId() != "" {
				if authorizedActions.OnlySelf() && resourceInfo.GetUserId() != input.AuthResults.UserId {
					continue
				}
			}

			if output.ScopeResourceMap[scopeId] == nil {
				return errors.New(ctx, errors.Internal, op, fmt.Sprintf("scope id %s returned from fetching authz protected entities not found in scope resource map", scopeId))
			}
			if output.ScopeResourceMap[scopeId].Resources == nil {
				output.ScopeResourceMap[scopeId].Resources = make(map[string]ResourceInfo)
			}

			output.ScopeResourceMap[scopeId].Resources[resourceInfo.GetPublicId()] = ResourceInfo{AuthorizedActions: authorizedActions}
			output.ResourceIds = append(output.ResourceIds, resourceInfo.GetPublicId())
		}
	}

	return nil
}

// populateScopeIdsFromScopeResourceMap populates the ScopeIds field and returns
// whether global scope was found
func (i *GetListingResourceInformationOutput) populateScopeIdsFromScopeResourceMap() {
	for k := range i.ScopeResourceMap {
		i.ScopeIds = append(i.ScopeIds, k)
	}
}

// GetListingScopeIds is provided for backwards compatibility with existing
// services; services should eventually migrate to
// GetListingResourceInformation.
func GetListingScopeIds(
	// The context to use when listing in the DB, if required
	ctx context.Context,
	// An IAM repo function to use for a listing call, if required
	repoFn common.IamRepoFactory,
	// The original auth results from the list command
	authResults auth.VerifyResults,
	// The scope ID to use, or to use as the starting point for a recursive
	// search
	rootScopeId string,
	// The type of resource we are listing
	typ resource.Type,
	// Whether or not the search should be recursive
	recursive bool,
) ([]string, map[string]*scopes.ScopeInfo, error) {
	const op = "scopeids.GetListingScopeIds"
	scopeResourceInfo, err := GetListingResourceInformation(ctx,
		GetListingResourceInformationInput{
			IamRepoFn:   repoFn,
			AuthResults: authResults,
			RootScopeId: rootScopeId,
			Type:        typ,
			Recursive:   recursive,
		},
	)
	if err != nil {
		if err == handlers.ForbiddenError() {
			return nil, nil, err
		}
		return nil, nil, errors.Wrap(ctx, err, op)
	}
	if len(scopeResourceInfo.ScopeIds) == 0 {
		// This should have already happened in the other function, but...
		return nil, nil, handlers.ForbiddenError()
	}
	scopeMap := make(map[string]*scopes.ScopeInfo, len(scopeResourceInfo.ScopeResourceMap))
	for k, v := range scopeResourceInfo.ScopeResourceMap {
		scopeMap[k] = v.ScopeInfo
	}
	return scopeResourceInfo.ScopeIds, scopeMap, nil
}
