package server

import (
	"context"
	"fmt"
	"strings"

	"github.com/pkg/errors"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/chef/automate/lib/logger"
	"github.com/chef/automate/lib/projectassignment"
	"github.com/chef/automate/lib/validate"

	api "github.com/chef/automate/api/interservice/authz"
	constants "github.com/chef/automate/components/authz-service/constants"
	"github.com/chef/automate/components/authz-service/engine"
	"github.com/chef/automate/components/authz-service/storage"
	"github.com/chef/automate/components/authz-service/storage/memstore"
	"github.com/chef/automate/components/authz-service/storage/postgres"
)

// policyServer is the server state for policies
type policyServer struct {
	log             logger.Logger
	store           storage.Storage
	engine          engine.Writer
	policyRefresher PolicyRefresher
}

// PolicyServer is the server interface for policies: what we defined via
// protobuf + the update interceptor
type PolicyServer interface {
	api.PoliciesServiceServer

	EngineUpdateInterceptor() grpc.UnaryServerInterceptor
}

// NewMemstorePolicyServer returns an instance of server.Server
func NewMemstorePolicyServer(
	ctx context.Context,
	l logger.Logger,
	pr PolicyRefresher,
	e engine.Writer) (PolicyServer, error) {

	return NewPoliciesServer(ctx, l, pr, memstore.New(), e)
}

// NewPostgresPolicyServer instantiates a server.Server that connects to a postgres backend
func NewPostgresPolicyServer(
	ctx context.Context,
	l logger.Logger,
	pr PolicyRefresher,
	e engine.Writer) (PolicyServer, error) {

	s := postgres.GetInstance()
	if s == nil {
		return nil, errors.New("postgres singleton not yet initialized for policy server")
	}
	return NewPoliciesServer(ctx, l, pr, s, e)
}

// NewPoliciesServer returns a new IAM Policy server.
func NewPoliciesServer(
	ctx context.Context,
	l logger.Logger,
	pr PolicyRefresher,
	s storage.Storage,
	e engine.Writer) (PolicyServer, error) {

	srv := &policyServer{
		log:             l,
		store:           s,
		engine:          e,
		policyRefresher: pr,
	}

	if err := srv.updateEngineStore(ctx); err != nil {
		return nil, errors.Wrapf(err, "initialize engine storage")
	}

	return srv, nil
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * * POLICIES  * * * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

// CreatePolicy creates an IAM policy.
func (s *policyServer) CreatePolicy(
	ctx context.Context,
	req *api.CreatePolicyReq) (*api.Policy, error) {

	// API requests always create custom policies.
	err := requiredFields(req, "policy")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	pol, err := s.policyFromAPI(
		req.Id,
		req.Name,
		storage.Custom,
		req.Members,
		req.Statements,
		req.Projects)

	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument,
			"error parsing policy %q: %s", req.Id, err.Error())
	}

	returnPol, err := s.store.CreatePolicy(ctx, &pol, false)
	switch err {
	case nil: // continue
	case storage.ErrConflict:
		return nil, status.Errorf(codes.AlreadyExists,
			"policy with id %q already exists", req.Id)
	default:
		switch err.(type) {
		case *projectassignment.ProjectsMissingError:
			return nil, status.Error(codes.NotFound, err.Error())
		case *projectassignment.ProjectsUnauthorizedForAssignmentErr:
			return nil, status.Error(codes.PermissionDenied, err.Error())
		case *storage.ForeignKeyError:
			return nil, status.Error(codes.InvalidArgument, err.Error())
		default:
			return nil, status.Errorf(codes.Internal,
				"creating policy %q: %s", req.Id, err.Error())
		}
	}

	return policyFromInternal(returnPol)
}

// PurgeSubjectFromPolicies removes a given subject from all policies.
func (s *policyServer) PurgeSubjectFromPolicies(ctx context.Context,
	req *api.PurgeSubjectFromPoliciesReq) (*api.PurgeSubjectFromPoliciesResp, error) {
	affected, err := s.store.PurgeSubjectFromPolicies(ctx, req.Subject)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to purge subject %q: %s", req.Subject, err.Error())
	}
	return &api.PurgeSubjectFromPoliciesResp{Ids: affected}, nil
}

// ListPolicies fetches a list of all IAM policies.
func (s *policyServer) ListPolicies(ctx context.Context,
	_ *api.ListPoliciesReq) (*api.ListPoliciesResp, error) {

	pols, err := s.store.ListPolicies(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "error listing policies: %s", err.Error())
	}
	resp := api.ListPoliciesResp{}
	for _, polInternal := range pols {
		pol, err := policyFromInternal(polInternal)
		if err != nil {
			return nil, status.Errorf(codes.Internal, "error converting policy %q: %s", polInternal.Name, err.Error())
		}
		resp.Policies = append(resp.Policies, pol)
	}
	return &resp, nil
}

// GetPolicy fetches an IAM policy.
func (s *policyServer) GetPolicy(
	ctx context.Context,
	req *api.GetPolicyReq) (*api.Policy, error) {

	err := validate.RequiredID(req, "policy")
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "cannot get policy")
	}
	polInternal, err := s.store.GetPolicy(ctx, req.Id)
	switch err {
	case nil: // continue
	case storage.ErrNotFound:
		return nil, status.Errorf(codes.NotFound, "no policy with ID %q found", req.Id)
	default:
		return nil, status.Errorf(codes.Internal, "error retrieving policy with ID %q: %s", req.Id, err.Error())
	}

	return policyFromInternal(polInternal)
}

// DeletePolicy removes an IAM policy from the data store.
func (s *policyServer) DeletePolicy(
	ctx context.Context,
	req *api.DeletePolicyReq) (*api.DeletePolicyResp, error) {

	err := validate.RequiredID(req, "policy")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	err = s.store.DeletePolicy(ctx, req.Id)
	switch err {
	case nil:
		return &api.DeletePolicyResp{}, nil
	case storage.ErrNotFound:
		return nil, status.Errorf(codes.NotFound, "no policy with ID %q found", req.Id)
	default: // some other error
		return nil, status.Errorf(codes.Internal, "error deleting policy with ID %q: %s", req.Id, err.Error())
	}
}

// UpdatePolicy modifies properties of an IAM policy.
// All properties must be supplied, whether changed or not.
func (s *policyServer) UpdatePolicy(
	ctx context.Context,
	req *api.UpdatePolicyReq) (*api.Policy, error) {

	err := requiredFields(req, "policy")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	statements := make([]storage.Statement, len(req.Statements))

	for i, statement := range req.Statements {
		statementInt, err := s.statementFromAPI(statement)
		if err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "parse statement: %s", err.Error())
		}
		statements[i] = statementInt
	}

	members, err := membersFromAPI(req.Members)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "parse member: %s", err.Error())
	}

	// Assume custom policy for API requests.
	storagePolicy, err := storage.NewPolicy(req.Id,
		req.Name, storage.Custom, members, statements, req.Projects)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "parse policy with ID %q: %s", req.Id, err.Error())
	}

	polInternal, err := s.store.UpdatePolicy(ctx, &storagePolicy)
	if err != nil {
		switch err {
		case storage.ErrConflict:
			return nil, status.Errorf(codes.AlreadyExists, "policy with name %q already exists", req.Name)
		case storage.ErrNotFound:
			return nil, status.Errorf(codes.NotFound, "no policy with ID %q found", req.Id)
		default:
			switch err.(type) {
			case *projectassignment.ProjectsMissingError:
				return nil, status.Error(codes.NotFound, err.Error())
			case *projectassignment.ProjectsUnauthorizedForAssignmentErr:
				return nil, status.Error(codes.PermissionDenied, err.Error())
			case *storage.ForeignKeyError:
				return nil, status.Error(codes.InvalidArgument, err.Error())
			default:
				return nil, status.Errorf(codes.Internal,
					"updating policy %q: %s", req.Id, err.Error())
			}
		}
	}

	return policyFromInternal(polInternal)
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * *  MEMBERS  * * * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

// ListPolicyMembers fetches an IAM policy's membership list.
func (s *policyServer) ListPolicyMembers(
	ctx context.Context,
	req *api.ListPolicyMembersReq) (*api.ListPolicyMembersResp, error) {

	err := validate.RequiredID(req, "policy")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	membersInternal, err := s.store.ListPolicyMembers(ctx, req.Id)
	switch err {
	case nil: // continue
	case storage.ErrNotFound:
		return nil, status.Errorf(codes.NotFound, "no policy with ID %q found", req.Id)
	default:
		return nil, status.Errorf(codes.Internal, "error retrieving policy with ID %q: %s", req.Id, err.Error())
	}

	return &api.ListPolicyMembersResp{
		Members: storage.MemberSliceToStringSlice(membersInternal),
	}, nil
}

// AddPolicyMembers takes in a list of members and adds them to the specified policy.
func (s *policyServer) AddPolicyMembers(
	ctx context.Context,
	req *api.AddPolicyMembersReq) (*api.AddPolicyMembersResp, error) {

	err := validate.RequiredID(req, "policy")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	members, err := membersFromAPI(req.Members)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "parse member: %s", err.Error())
	}

	resp, err := s.store.AddPolicyMembers(ctx, req.Id, members)
	switch err {
	case nil:
		return &api.AddPolicyMembersResp{
			Members: storage.MemberSliceToStringSlice(resp),
		}, nil
	case storage.ErrNotFound:
		return nil, status.Errorf(codes.NotFound, "no policy with ID %q found", req.Id)
	default: // some other error
		return nil, status.Errorf(codes.Internal,
			"error adding members to policy with ID %q: %s", req.Id, err.Error())
	}
}

// ReplacePolicyMembers takes in a new list of policy members and completely replaces
// all policy members for given policy with new list.
func (s *policyServer) ReplacePolicyMembers(
	ctx context.Context,
	req *api.ReplacePolicyMembersReq) (*api.ReplacePolicyMembersResp, error) {

	err := validate.RequiredID(req, "policy")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	members, err := membersFromAPI(req.Members)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "parse member: %s", err.Error())
	}

	resp, err := s.store.ReplacePolicyMembers(ctx, req.Id, members)
	switch err {
	case nil:
		return &api.ReplacePolicyMembersResp{
			Members: storage.MemberSliceToStringSlice(resp),
		}, nil
	case storage.ErrNotFound:
		return nil, status.Errorf(codes.NotFound, "no policy with ID %q found", req.Id)
	default: // some other error
		return nil, status.Errorf(codes.Internal,
			"error replacing members on policy with ID %q: %s", req.Id, err.Error())
	}
}

// RemovePolicyMembers takes in a list of members and removes any
// that are currently members of the policy.
func (s *policyServer) RemovePolicyMembers(ctx context.Context,
	req *api.RemovePolicyMembersReq) (*api.RemovePolicyMembersResp, error) {

	err := validate.RequiredID(req, "policy")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	members, err := membersFromAPI(req.Members)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "parse member: %s", err.Error())
	}

	// TODO replace this check with a policy once we've got RemovePolicyMember
	if req.Id == constants.AdminPolicyID {
		for _, member := range members {
			if member.Name == "team:local:admins" {
				return nil, status.Error(codes.PermissionDenied, `cannot remove local team:
				admins from Chef-managed policy: Admin`)
			}
		}
	}

	resp, err := s.store.RemovePolicyMembers(ctx, req.Id, members)
	switch err {
	case nil: // continue
	case storage.ErrNotFound:
		return nil, status.Errorf(codes.NotFound, "no policy with ID %q found", req.Id)
	default: // some other error
		return nil, status.Errorf(codes.Internal,
			"error removing members on policy with ID %q: %s", req.Id, err.Error())
	}

	return &api.RemovePolicyMembersResp{
		Members: storage.MemberSliceToStringSlice(resp),
	}, nil

}

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * *   ROLES   * * * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

// CreateRole creates an IAM role.
func (s *policyServer) CreateRole(
	ctx context.Context,
	req *api.CreateRoleReq) (*api.Role, error) {
	err := requiredFields(req, "role")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	storageRole, err := storage.NewRole(req.Id, req.Name, storage.Custom, req.Actions, req.Projects)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "error parsing role %q: %s", req.Id, err.Error())
	}

	returnRole, err := s.store.CreateRole(ctx, storageRole, false)

	switch err {
	case nil:
	case storage.ErrConflict:
		return nil, status.Errorf(codes.AlreadyExists, "role with id %q already exists", req.Id)
	default:
		switch err.(type) {
		case *storage.ForeignKeyError:
			return nil, status.Error(codes.InvalidArgument, err.Error())
		case *projectassignment.ProjectsMissingError:
			return nil, status.Error(codes.NotFound, err.Error())
		case *projectassignment.ProjectsUnauthorizedForAssignmentErr:
			return nil, status.Error(codes.PermissionDenied, err.Error())
		}
		return nil, status.Errorf(codes.Internal, "creating role %q: %s", req.Id, err.Error())
	}

	return roleFromInternal(returnRole)
}

// ListRoles fetches a list of all IAM roles.
func (s *policyServer) ListRoles(ctx context.Context,
	_ *api.ListRolesReq) (*api.ListRolesResp, error) {

	internalRoles, err := s.store.ListRoles(ctx)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "error listing roles: %s", err.Error())
	}

	roles := make([]*api.Role, len(internalRoles))

	for index, internalRole := range internalRoles {
		role, err := roleFromInternal(internalRole)
		if err != nil {
			return nil, status.Errorf(codes.Internal, "error converting role %q: %s", internalRole.Name, err.Error())
		}
		roles[index] = role
	}

	resp := api.ListRolesResp{
		Roles: roles,
	}

	return &resp, nil
}

// GetRole fetches an IAM role.
func (s *policyServer) GetRole(
	ctx context.Context,
	req *api.GetRoleReq) (*api.Role, error) {

	err := validate.RequiredID(req, "role")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	roleInternal, err := s.store.GetRole(ctx, req.Id)
	switch err {
	case nil:
		return roleFromInternal(roleInternal)
	case storage.ErrNotFound:
		return nil, status.Errorf(codes.NotFound, "no role with ID %q found", req.Id)
	default:
		return nil, status.Errorf(codes.Internal, "error retrieving role with ID %q: %s", req.Id, err.Error())
	}
}

// DeleteRole removes an IAM role from the data store.
func (s *policyServer) DeleteRole(
	ctx context.Context,
	req *api.DeleteRoleReq) (*api.DeleteRoleResp, error) {

	err := validate.RequiredID(req, "role")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	err = s.store.DeleteRole(ctx, req.Id)
	switch err {
	case nil:
		return &api.DeleteRoleResp{}, nil
	case storage.ErrNotFound:
		return nil, status.Errorf(codes.NotFound, "no role with ID %q found", req.Id)
	default: // some other error
		return nil, status.Errorf(codes.Internal, "error deleting role with ID %q: %s", req.Id, err.Error())
	}
}

// UpdateRole modifies properties of an IAM role.
// All properties must be supplied, whether changed or not.
func (s *policyServer) UpdateRole(
	ctx context.Context,
	req *api.UpdateRoleReq) (*api.Role, error) {

	err := requiredFields(req, "role")
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}

	storageRole, err := storage.NewUpdateRole(req.Id, req.Name, req.Actions, req.Projects)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "parse role with ID %q: %s", req.Id, err.Error())
	}

	roleInternal, err := s.store.UpdateRole(ctx, storageRole)
	switch err {
	case nil:
	case storage.ErrConflict:
		return nil, status.Errorf(codes.AlreadyExists, "role with name %q already exists", req.Name)
	case storage.ErrNotFound:
		return nil, status.Errorf(codes.NotFound, "no role with ID %q found", req.Id)
	default:
		switch err.(type) {
		case *projectassignment.ProjectsMissingError:
			return nil, status.Error(codes.NotFound, err.Error())
		case *projectassignment.ProjectsUnauthorizedForAssignmentErr:
			return nil, status.Error(codes.PermissionDenied, err.Error())
		}
		return nil, status.Errorf(codes.Internal, "updating role %q: %s, projects: %s", req.Id, req.Projects, err.Error())
	}

	return roleFromInternal(roleInternal)
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * *   MIGRATION   * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

// TODO Delete
// GetPolicyVersion returns the status of the data store.
func (s *policyServer) GetPolicyVersion(ctx context.Context,
	req *api.GetPolicyVersionReq) (*api.GetPolicyVersionResp, error) {
	return &api.GetPolicyVersionResp{
		Version: &api.Version{
			Major: api.Version_V2,
			Minor: api.Version_V1,
		},
	}, nil
}

// EngineUpdateInterceptor is a middleware for updating the engine when a
// certain set of methods has been executed successfully.
func (s *policyServer) EngineUpdateInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context,
		req interface{},
		info *grpc.UnaryServerInfo,
		handler grpc.UnaryHandler) (interface{}, error) {
		resp, err := handler(ctx, req)
		if err != nil {
			return nil, err
		}

		// ignore anything not related to the OPA store.
		if !strings.HasPrefix(info.FullMethod, "/chef.automate.domain.authz.PoliciesService/") &&
			!strings.HasPrefix(info.FullMethod, "/chef.automate.domain.authz.ProjectsService/") {
			return resp, nil
		}

		switch info.FullMethod {
		// Important! Any new endpoint that requires refreshing the OPA cache must be added here.
		case "/chef.automate.domain.authz.PoliciesService/ReplacePolicyMembers",
			"/chef.automate.domain.authz.PoliciesService/CreatePolicy",
			"/chef.automate.domain.authz.PoliciesService/DeletePolicy",
			"/chef.automate.domain.authz.PoliciesService/UpdatePolicy",
			"/chef.automate.domain.authz.PoliciesService/CreateRole",
			"/chef.automate.domain.authz.PoliciesService/DeleteRole",
			"/chef.automate.domain.authz.PoliciesService/UpdateRole",
			"/chef.automate.domain.authz.PoliciesService/RemovePolicyMembers",
			"/chef.automate.domain.authz.PoliciesService/AddPolicyMembers",
			"/chef.automate.domain.authz.PoliciesService/PurgeSubjectFromPolicies":
			if err := s.updateEngineStore(ctx); err != nil {
				return nil, status.Errorf(codes.Internal, "error updating engine store: %s", err.Error())
			}
		default:
			// do nothing
		}

		return resp, nil
	}
}

func (s *policyServer) updateEngineStore(ctx context.Context) error {
	return s.policyRefresher.Refresh(ctx)
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * *  CONVERTERS   * * * * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

func (s *policyServer) policyFromAPI(
	ID, name string,
	typeVal storage.Type,
	membersToAttach []string,
	statementsToAttach []*api.Statement,
	inProjects []string) (storage.Policy, error) {

	statements := make([]storage.Statement, len(statementsToAttach))
	for i, statement := range statementsToAttach {
		statementInt, err := s.statementFromAPI(statement)
		if err != nil {
			return storage.Policy{}, errors.Wrap(err, "parse statement")
		}
		statements[i] = statementInt
	}

	members := make([]storage.Member, len(membersToAttach))
	for i, member := range membersToAttach {
		memberInt, err := storage.NewMember(member)
		if err != nil {
			return storage.Policy{}, errors.Wrap(err, "parse statement")
		}
		members[i] = memberInt
	}

	return storage.NewPolicy(ID, name, typeVal, members, statements, inProjects)
}

func policyFromInternal(pol *storage.Policy) (*api.Policy, error) {
	resp := &api.Policy{
		Id:         pol.ID,
		Name:       pol.Name,
		Type:       typeFromInternal(pol.Type),
		Members:    storage.MemberSliceToStringSlice(pol.Members),
		Statements: statementsFromInternal(pol.Statements),
		Projects:   pol.Projects,
	}

	return resp, nil
}

func typeFromInternal(t storage.Type) api.Type {
	var ret api.Type
	switch t {
	case storage.Custom:
		ret = api.Type_CUSTOM
	case storage.ChefManaged:
		ret = api.Type_CHEF_MANAGED
	}

	return ret
}

func statementsFromInternal(internal []storage.Statement) []*api.Statement {
	resp := make([]*api.Statement, len(internal))
	for i, statement := range internal {
		projects := make([]string, len(statement.Projects))
		for i, project := range statement.Projects {
			if project == constants.AllProjectsID {
				projects[i] = constants.AllProjectsExternalID
			} else {
				projects[i] = project
			}
		}

		resp[i] = &api.Statement{
			Effect:    effectFromInternal(statement.Effect),
			Role:      statement.Role,
			Projects:  projects,
			Actions:   statement.Actions,
			Resources: statement.Resources,
		}
	}

	return resp
}

func effectFromInternal(internal storage.Effect) api.Statement_Effect {
	switch internal {
	case storage.Allow:
		return api.Statement_ALLOW
	case storage.Deny:
		return api.Statement_DENY
	default:
		panic("effect must always be one of allow or deny")
	}
}

func roleFromInternal(role *storage.Role) (*api.Role, error) {
	resp := &api.Role{
		Id:       role.ID,
		Name:     role.Name,
		Type:     typeFromInternal(role.Type),
		Actions:  role.Actions,
		Projects: role.Projects,
	}

	return resp, nil
}

func membersFromAPI(apiMembers []string) ([]storage.Member, error) {
	members := make([]storage.Member, len(apiMembers))
	for i, member := range apiMembers {
		memberInternal, err := storage.NewMember(member)
		if err != nil {
			return nil, errors.Wrap(err, "format member")
		}
		members[i] = memberInternal
	}

	return members, nil
}

func (s *policyServer) statementFromAPI(statement *api.Statement) (storage.Statement, error) {
	effect, err := effectFromAPI(statement.Effect)
	if err != nil {
		return storage.Statement{}, err
	}

	// set resources to wildcard if not provided
	if len(statement.Resources) == 0 {
		statement.Resources = []string{"*"}
	}

	projects := make([]string, len(statement.Projects))

	if len(statement.Projects) == 0 {
		return storage.Statement{}, errors.New("policy statements must include projects")
	}

	// map external representation of "all projects" to actual ID for that meta-project
	for i, project := range statement.Projects {
		if project == constants.AllProjectsExternalID {
			projects[i] = constants.AllProjectsID
		} else {
			projects[i] = project
		}
	}

	return storage.NewStatement(effect, statement.Role, projects, statement.Resources, statement.Actions)
}

func effectFromAPI(eff api.Statement_Effect) (storage.Effect, error) {
	switch eff {
	case api.Statement_ALLOW:
		return storage.Allow, nil
	case api.Statement_DENY:
		return storage.Deny, nil
	default:
		return storage.Allow, errors.New("effect must always be one of allow or deny")
	}
}

func (s *policyServer) logPolicies(policies []*storage.Policy) {
	kv := logger.KV{}
	for _, p := range policies {
		kv[p.ID] = logger.KV{
			"name":       p.Name,
			"subjects":   p.Members,
			"statements": p.Statements,
		}
	}
	s.log.WithFields(kv).Info("Policy definition")
}

// requiredFields verifies that all the required inputs are not empty
func requiredFields(obj interface {
	GetId() string
	GetName() string
	GetProjects() []string
}, resourceName string) error {
	err := validate.RequiredIDandName(obj, resourceName)
	if err != nil {
		return err
	}
	return requiredProjects(obj.GetProjects())
}

// requiredProjects verifies that the projects are not empty and do not include '(unassigned)'
func requiredProjects(projects []string) error {
	for _, project := range projects {
		if validate.EmptyOrWhitespaceOnlyRE.MatchString(project) {
			e := fmt.Sprintf("projects must contain at least one non-whitespace character")
			return errors.New(e)
		}
		if project == constants.UnassignedProjectID {
			return errors.Errorf("%q cannot explicitly be set; pass an empty projects array instead.",
				constants.UnassignedProjectID)
		}
	}
	return nil
}
