// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package iam supports the resource-specific operations of Google Cloud
// IAM (Identity and Access Management) for the Google Cloud Libraries.
// See https://cloud.google.com/iam for more about IAM.
//
// Users of the Google Cloud Libraries will typically not use this package
// directly. Instead they will begin with some resource that supports IAM, like
// a pubsub topic, and call its IAM method to get a Handle for that resource.
package iam

import (
	"context"
	"fmt"
	"time"

	gax "github.com/googleapis/gax-go/v2"
	pb "google.golang.org/genproto/googleapis/iam/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
)

// client abstracts the IAMPolicy API to allow multiple implementations.
type client interface {
	Get(ctx context.Context, resource string) (*pb.Policy, error)
	Set(ctx context.Context, resource string, p *pb.Policy) error
	Test(ctx context.Context, resource string, perms []string) ([]string, error)
}

// grpcClient implements client for the standard gRPC-based IAMPolicy service.
type grpcClient struct {
	c pb.IAMPolicyClient
}

var withRetry = gax.WithRetry(func() gax.Retryer {
	return gax.OnCodes([]codes.Code{
		codes.DeadlineExceeded,
		codes.Unavailable,
	}, gax.Backoff{
		Initial:    100 * time.Millisecond,
		Max:        60 * time.Second,
		Multiplier: 1.3,
	})
})

func (g *grpcClient) Get(ctx context.Context, resource string) (*pb.Policy, error) {
	var proto *pb.Policy
	md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
	ctx = insertMetadata(ctx, md)

	err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
		var err error
		proto, err = g.c.GetIamPolicy(ctx, &pb.GetIamPolicyRequest{Resource: resource})
		return err
	}, withRetry)
	if err != nil {
		return nil, err
	}
	return proto, nil
}

func (g *grpcClient) Set(ctx context.Context, resource string, p *pb.Policy) error {
	md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
	ctx = insertMetadata(ctx, md)

	return gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
		_, err := g.c.SetIamPolicy(ctx, &pb.SetIamPolicyRequest{
			Resource: resource,
			Policy:   p,
		})
		return err
	}, withRetry)
}

func (g *grpcClient) Test(ctx context.Context, resource string, perms []string) ([]string, error) {
	var res *pb.TestIamPermissionsResponse
	md := metadata.Pairs("x-goog-request-params", fmt.Sprintf("%s=%v", "resource", resource))
	ctx = insertMetadata(ctx, md)

	err := gax.Invoke(ctx, func(ctx context.Context, _ gax.CallSettings) error {
		var err error
		res, err = g.c.TestIamPermissions(ctx, &pb.TestIamPermissionsRequest{
			Resource:    resource,
			Permissions: perms,
		})
		return err
	}, withRetry)
	if err != nil {
		return nil, err
	}
	return res.Permissions, nil
}

// A Handle provides IAM operations for a resource.
type Handle struct {
	c        client
	resource string
}

// InternalNewHandle is for use by the Google Cloud Libraries only.
//
// InternalNewHandle returns a Handle for resource.
// The conn parameter refers to a server that must support the IAMPolicy service.
func InternalNewHandle(conn *grpc.ClientConn, resource string) *Handle {
	return InternalNewHandleGRPCClient(pb.NewIAMPolicyClient(conn), resource)
}

// InternalNewHandleGRPCClient is for use by the Google Cloud Libraries only.
//
// InternalNewHandleClient returns a Handle for resource using the given
// grpc service that implements IAM as a mixin
func InternalNewHandleGRPCClient(c pb.IAMPolicyClient, resource string) *Handle {
	return InternalNewHandleClient(&grpcClient{c: c}, resource)
}

// InternalNewHandleClient is for use by the Google Cloud Libraries only.
//
// InternalNewHandleClient returns a Handle for resource using the given
// client implementation.
func InternalNewHandleClient(c client, resource string) *Handle {
	return &Handle{
		c:        c,
		resource: resource,
	}
}

// Policy retrieves the IAM policy for the resource.
func (h *Handle) Policy(ctx context.Context) (*Policy, error) {
	proto, err := h.c.Get(ctx, h.resource)
	if err != nil {
		return nil, err
	}
	return &Policy{InternalProto: proto}, nil
}

// SetPolicy replaces the resource's current policy with the supplied Policy.
//
// If policy was created from a prior call to Get, then the modification will
// only succeed if the policy has not changed since the Get.
func (h *Handle) SetPolicy(ctx context.Context, policy *Policy) error {
	return h.c.Set(ctx, h.resource, policy.InternalProto)
}

// TestPermissions returns the subset of permissions that the caller has on the resource.
func (h *Handle) TestPermissions(ctx context.Context, permissions []string) ([]string, error) {
	return h.c.Test(ctx, h.resource, permissions)
}

// A RoleName is a name representing a collection of permissions.
type RoleName string

// Common role names.
const (
	Owner  RoleName = "roles/owner"
	Editor RoleName = "roles/editor"
	Viewer RoleName = "roles/viewer"
)

const (
	// AllUsers is a special member that denotes all users, even unauthenticated ones.
	AllUsers = "allUsers"

	// AllAuthenticatedUsers is a special member that denotes all authenticated users.
	AllAuthenticatedUsers = "allAuthenticatedUsers"
)

// A Policy is a list of Bindings representing roles
// granted to members.
//
// The zero Policy is a valid policy with no bindings.
type Policy struct {
	// TODO(jba): when type aliases are available, put Policy into an internal package
	// and provide an exported alias here.

	// This field is exported for use by the Google Cloud Libraries only.
	// It may become unexported in a future release.
	InternalProto *pb.Policy
}

// Members returns the list of members with the supplied role.
// The return value should not be modified. Use Add and Remove
// to modify the members of a role.
func (p *Policy) Members(r RoleName) []string {
	b := p.binding(r)
	if b == nil {
		return nil
	}
	return b.Members
}

// HasRole reports whether member has role r.
func (p *Policy) HasRole(member string, r RoleName) bool {
	return memberIndex(member, p.binding(r)) >= 0
}

// Add adds member member to role r if it is not already present.
// A new binding is created if there is no binding for the role.
func (p *Policy) Add(member string, r RoleName) {
	b := p.binding(r)
	if b == nil {
		if p.InternalProto == nil {
			p.InternalProto = &pb.Policy{}
		}
		p.InternalProto.Bindings = append(p.InternalProto.Bindings, &pb.Binding{
			Role:    string(r),
			Members: []string{member},
		})
		return
	}
	if memberIndex(member, b) < 0 {
		b.Members = append(b.Members, member)
		return
	}
}

// Remove removes member from role r if it is present.
func (p *Policy) Remove(member string, r RoleName) {
	bi := p.bindingIndex(r)
	if bi < 0 {
		return
	}
	bindings := p.InternalProto.Bindings
	b := bindings[bi]
	mi := memberIndex(member, b)
	if mi < 0 {
		return
	}
	// Order doesn't matter for bindings or members, so to remove, move the last item
	// into the removed spot and shrink the slice.
	if len(b.Members) == 1 {
		// Remove binding.
		last := len(bindings) - 1
		bindings[bi] = bindings[last]
		bindings[last] = nil
		p.InternalProto.Bindings = bindings[:last]
		return
	}
	// Remove member.
	// TODO(jba): worry about multiple copies of m?
	last := len(b.Members) - 1
	b.Members[mi] = b.Members[last]
	b.Members[last] = ""
	b.Members = b.Members[:last]
}

// Roles returns the names of all the roles that appear in the Policy.
func (p *Policy) Roles() []RoleName {
	if p.InternalProto == nil {
		return nil
	}
	var rns []RoleName
	for _, b := range p.InternalProto.Bindings {
		rns = append(rns, RoleName(b.Role))
	}
	return rns
}

// binding returns the Binding for the suppied role, or nil if there isn't one.
func (p *Policy) binding(r RoleName) *pb.Binding {
	i := p.bindingIndex(r)
	if i < 0 {
		return nil
	}
	return p.InternalProto.Bindings[i]
}

func (p *Policy) bindingIndex(r RoleName) int {
	if p.InternalProto == nil {
		return -1
	}
	for i, b := range p.InternalProto.Bindings {
		if b.Role == string(r) {
			return i
		}
	}
	return -1
}

// memberIndex returns the index of m in b's Members, or -1 if not found.
func memberIndex(m string, b *pb.Binding) int {
	if b == nil {
		return -1
	}
	for i, mm := range b.Members {
		if mm == m {
			return i
		}
	}
	return -1
}

// insertMetadata inserts metadata into the given context
func insertMetadata(ctx context.Context, mds ...metadata.MD) context.Context {
	out, _ := metadata.FromOutgoingContext(ctx)
	out = out.Copy()
	for _, md := range mds {
		for k, v := range md {
			out[k] = append(out[k], v...)
		}
	}
	return metadata.NewOutgoingContext(ctx, out)
}
