package rbac

import (
	"github.com/google/uuid"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/coderd/rbac/policy"
)

// ResourceUserObject is a helper function to create a user object for authz checks.
func ResourceUserObject(userID uuid.UUID) Object {
	return ResourceUser.WithID(userID).WithOwner(userID.String())
}

// Object is used to create objects for authz checks when you have none in
// hand to run the check on.
// An example is if you want to list all workspaces, you can create a Object
// that represents the set of workspaces you are trying to get access too.
// Do not export this type, as it can be created from a resource type constant.
type Object struct {
	// ID is the resource's uuid
	ID    string `json:"id"`
	Owner string `json:"owner"`
	// OrgID specifies which org the object is a part of.
	OrgID string `json:"org_owner"`
	// AnyOrgOwner will disregard the org_owner when checking for permissions
	// Use this to ask, "Can the actor do this action on any org?" when
	// the exact organization is not important or known.
	// E.g: The UI should show a "create template" button if the user
	// can create a template in any org.
	AnyOrgOwner bool `json:"any_org"`

	// Type is "workspace", "project", "app", etc
	Type string `json:"type"`

	ACLUserList  map[string][]policy.Action ` json:"acl_user_list"`
	ACLGroupList map[string][]policy.Action ` json:"acl_group_list"`
}

// ValidAction checks if the action is valid for the given object type.
func (z Object) ValidAction(action policy.Action) error {
	perms, ok := policy.RBACPermissions[z.Type]
	if !ok {
		return xerrors.Errorf("invalid type %q", z.Type)
	}
	if _, ok := perms.Actions[action]; !ok {
		return xerrors.Errorf("invalid action %q for type %q", action, z.Type)
	}

	return nil
}

// AvailableActions returns all available actions for a given object.
// Wildcard is omitted.
func (z Object) AvailableActions() []policy.Action {
	perms, ok := policy.RBACPermissions[z.Type]
	if !ok {
		return []policy.Action{}
	}

	actions := make([]policy.Action, 0, len(perms.Actions))
	for action := range perms.Actions {
		actions = append(actions, action)
	}

	return actions
}

func (z Object) Equal(b Object) bool {
	if z.ID != b.ID {
		return false
	}
	if z.Owner != b.Owner {
		return false
	}
	if z.OrgID != b.OrgID {
		return false
	}
	if z.Type != b.Type {
		return false
	}

	if !equalACLLists(z.ACLUserList, b.ACLUserList) {
		return false
	}

	if !equalACLLists(z.ACLGroupList, b.ACLGroupList) {
		return false
	}

	return true
}

func equalACLLists(a, b map[string][]policy.Action) bool {
	if len(a) != len(b) {
		return false
	}

	for k, actions := range a {
		if len(actions) != len(b[k]) {
			return false
		}
		for i, a := range actions {
			if a != b[k][i] {
				return false
			}
		}
	}
	return true
}

func (z Object) RBACObject() Object {
	return z
}

// All returns an object matching all resources of the same type.
func (z Object) All() Object {
	return Object{
		Owner:        "",
		OrgID:        "",
		Type:         z.Type,
		ACLUserList:  map[string][]policy.Action{},
		ACLGroupList: map[string][]policy.Action{},
		AnyOrgOwner:  z.AnyOrgOwner,
	}
}

func (z Object) WithIDString(id string) Object {
	return Object{
		ID:           id,
		Owner:        z.Owner,
		OrgID:        z.OrgID,
		Type:         z.Type,
		ACLUserList:  z.ACLUserList,
		ACLGroupList: z.ACLGroupList,
		AnyOrgOwner:  z.AnyOrgOwner,
	}
}

func (z Object) WithID(id uuid.UUID) Object {
	return Object{
		ID:           id.String(),
		Owner:        z.Owner,
		OrgID:        z.OrgID,
		Type:         z.Type,
		ACLUserList:  z.ACLUserList,
		ACLGroupList: z.ACLGroupList,
		AnyOrgOwner:  z.AnyOrgOwner,
	}
}

// InOrg adds an org OwnerID to the resource
func (z Object) InOrg(orgID uuid.UUID) Object {
	return Object{
		ID:           z.ID,
		Owner:        z.Owner,
		OrgID:        orgID.String(),
		Type:         z.Type,
		ACLUserList:  z.ACLUserList,
		ACLGroupList: z.ACLGroupList,
		// InOrg implies AnyOrgOwner is false
		AnyOrgOwner: false,
	}
}

func (z Object) AnyOrganization() Object {
	return Object{
		ID:    z.ID,
		Owner: z.Owner,
		// AnyOrgOwner cannot have an org owner also set.
		OrgID:        "",
		Type:         z.Type,
		ACLUserList:  z.ACLUserList,
		ACLGroupList: z.ACLGroupList,
		AnyOrgOwner:  true,
	}
}

// WithOwner adds an OwnerID to the resource
func (z Object) WithOwner(ownerID string) Object {
	return Object{
		ID:           z.ID,
		Owner:        ownerID,
		OrgID:        z.OrgID,
		Type:         z.Type,
		ACLUserList:  z.ACLUserList,
		ACLGroupList: z.ACLGroupList,
		AnyOrgOwner:  z.AnyOrgOwner,
	}
}

// WithACLUserList adds an ACL list to a given object
func (z Object) WithACLUserList(acl map[string][]policy.Action) Object {
	return Object{
		ID:           z.ID,
		Owner:        z.Owner,
		OrgID:        z.OrgID,
		Type:         z.Type,
		ACLUserList:  acl,
		ACLGroupList: z.ACLGroupList,
		AnyOrgOwner:  z.AnyOrgOwner,
	}
}

func (z Object) WithGroupACL(groups map[string][]policy.Action) Object {
	return Object{
		ID:           z.ID,
		Owner:        z.Owner,
		OrgID:        z.OrgID,
		Type:         z.Type,
		ACLUserList:  z.ACLUserList,
		ACLGroupList: groups,
		AnyOrgOwner:  z.AnyOrgOwner,
	}
}
