package rbac

import (
	"fmt"

	"github.com/google/uuid"

	"golang.org/x/xerrors"
)

type WorkspaceAgentScopeParams struct {
	WorkspaceID uuid.UUID
	OwnerID     uuid.UUID
	TemplateID  uuid.UUID
	VersionID   uuid.UUID
}

// WorkspaceAgentScope returns a scope that is the same as ScopeAll but can only
// affect resources in the allow list. Only a scope is returned as the roles
// should come from the workspace owner.
func WorkspaceAgentScope(params WorkspaceAgentScopeParams) Scope {
	if params.WorkspaceID == uuid.Nil || params.OwnerID == uuid.Nil || params.TemplateID == uuid.Nil || params.VersionID == uuid.Nil {
		panic("all uuids must be non-nil, this is a developer error")
	}

	allScope, err := ScopeAll.Expand()
	if err != nil {
		panic("failed to expand scope all, this should never happen")
	}
	return Scope{
		// TODO: We want to limit the role too to be extra safe.
		// Even though the allowlist blocks anything else, it is still good
		// incase we change the behavior of the allowlist. The allowlist is new
		// and evolving.
		Role: allScope.Role,
		// This prevents the agent from being able to access any other resource.
		// Include the list of IDs of anything that is required for the
		// agent to function.
		AllowIDList: []string{
			params.WorkspaceID.String(),
			params.TemplateID.String(),
			params.VersionID.String(),
			params.OwnerID.String(),
		},
	}
}

const (
	ScopeAll                ScopeName = "all"
	ScopeApplicationConnect ScopeName = "application_connect"
)

// TODO: Support passing in scopeID list for allowlisting resources.
var builtinScopes = map[ScopeName]Scope{
	// ScopeAll is a special scope that allows access to all resources. During
	// authorize checks it is usually not used directly and skips scope checks.
	ScopeAll: {
		Role: Role{
			Name:        fmt.Sprintf("Scope_%s", ScopeAll),
			DisplayName: "All operations",
			Site: Permissions(map[string][]Action{
				ResourceWildcard.Type: {WildcardSymbol},
			}),
			Org:  map[string][]Permission{},
			User: []Permission{},
		},
		AllowIDList: []string{WildcardSymbol},
	},

	ScopeApplicationConnect: {
		Role: Role{
			Name:        fmt.Sprintf("Scope_%s", ScopeApplicationConnect),
			DisplayName: "Ability to connect to applications",
			Site: Permissions(map[string][]Action{
				ResourceWorkspaceApplicationConnect.Type: {ActionCreate},
			}),
			Org:  map[string][]Permission{},
			User: []Permission{},
		},
		AllowIDList: []string{WildcardSymbol},
	},
}

type ExpandableScope interface {
	Expand() (Scope, error)
	// Name is for logging and tracing purposes, we want to know the human
	// name of the scope.
	Name() string
}

type ScopeName string

func (name ScopeName) Expand() (Scope, error) {
	return ExpandScope(name)
}

func (name ScopeName) Name() string {
	return string(name)
}

// Scope acts the exact same as a Role with the addition that is can also
// apply an AllowIDList. Any resource being checked against a Scope will
// reject any resource that is not in the AllowIDList.
// To not use an AllowIDList to reject authorization, use a wildcard for the
// AllowIDList. Eg: 'AllowIDList: []string{WildcardSymbol}'
type Scope struct {
	Role
	AllowIDList []string `json:"allow_list"`
}

func (s Scope) Expand() (Scope, error) {
	return s, nil
}

func (s Scope) Name() string {
	return s.Role.Name
}

func ExpandScope(scope ScopeName) (Scope, error) {
	role, ok := builtinScopes[scope]
	if !ok {
		return Scope{}, xerrors.Errorf("no scope named %q", scope)
	}
	return role, nil
}
