// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package resource

import (
	"context"
	"errors"
	"strings"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"

	"github.com/hashicorp/go-hclog"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/acl/resolver"
	"github.com/hashicorp/consul/internal/resource"
	"github.com/hashicorp/consul/internal/storage"
	"github.com/hashicorp/consul/lib/retry"
	"github.com/hashicorp/consul/proto-public/pbresource"
)

type Server struct {
	Config
}

type Config struct {
	Logger   hclog.Logger
	Registry Registry

	// Backend is the storage backend that will be used for resource persistence.
	Backend     Backend
	ACLResolver ACLResolver
	// TenancyBridge temporarily allows us to use V1 implementations of
	// partitions and namespaces until V2 implementations are available.
	TenancyBridge TenancyBridge

	// UseV2Tenancy is true if the "v2tenancy" experiement is active, false otherwise.
	// Attempts to create v2 tenancy resources (partition or namespace) will fail when the
	// flag is false.
	UseV2Tenancy bool
}

//go:generate mockery --name Registry --inpackage
type Registry interface {
	resource.Registry
}

//go:generate mockery --name Backend --inpackage
type Backend interface {
	storage.Backend
}

//go:generate mockery --name ACLResolver --inpackage
type ACLResolver interface {
	ResolveTokenAndDefaultMeta(string, *acl.EnterpriseMeta, *acl.AuthorizerContext) (resolver.Result, error)
}

//go:generate mockery --name TenancyBridge --inpackage
type TenancyBridge interface {
	PartitionExists(partition string) (bool, error)
	IsPartitionMarkedForDeletion(partition string) (bool, error)
	NamespaceExists(partition, namespace string) (bool, error)
	IsNamespaceMarkedForDeletion(partition, namespace string) (bool, error)
}

func NewServer(cfg Config) *Server {
	return &Server{cfg}
}

var _ pbresource.ResourceServiceServer = (*Server)(nil)

func (s *Server) Register(registrar grpc.ServiceRegistrar) {
	pbresource.RegisterResourceServiceServer(registrar, s)
}

// Get token from grpc metadata or AnonymounsTokenId if not found
func tokenFromContext(ctx context.Context) string {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return acl.AnonymousTokenID
	}

	vals := md.Get("x-consul-token")
	if len(vals) == 0 {
		return acl.AnonymousTokenID
	}
	return vals[0]
}

func (s *Server) resolveType(typ *pbresource.Type) (*resource.Registration, error) {
	v, ok := s.Registry.Resolve(typ)
	if ok {
		return &v, nil
	}
	return nil, status.Errorf(
		codes.InvalidArgument,
		"resource type %s not registered", resource.ToGVK(typ),
	)
}

func readConsistencyFrom(ctx context.Context) storage.ReadConsistency {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return storage.EventualConsistency
	}

	vals := md.Get("x-consul-consistency-mode")
	if len(vals) == 0 {
		return storage.EventualConsistency
	}

	if vals[0] == "consistent" {
		return storage.StrongConsistency
	}
	return storage.EventualConsistency
}

func (s *Server) getAuthorizer(token string, entMeta *acl.EnterpriseMeta) (acl.Authorizer, *acl.AuthorizerContext, error) {
	authzContext := &acl.AuthorizerContext{}
	authz, err := s.ACLResolver.ResolveTokenAndDefaultMeta(token, entMeta, authzContext)
	if err != nil {
		return nil, nil, status.Errorf(codes.Internal, "failed getting authorizer: %v", err)
	}
	return authz, authzContext, nil
}

func isGRPCStatusError(err error) bool {
	if err == nil {
		return false
	}
	_, ok := status.FromError(err)
	return ok
}

func validateId(id *pbresource.ID, errorPrefix string) error {
	if id.Type == nil {
		return status.Errorf(codes.InvalidArgument, "%s.type is required", errorPrefix)
	}

	if err := resource.ValidateName(id.Name); err != nil {
		return status.Errorf(codes.InvalidArgument, "%s.name invalid: %v", errorPrefix, err)
	}

	// Better UX: Allow callers to pass in nil tenancy.  Defaulting and inheritance of tenancy
	// from the request token will take place further down in the call flow.
	if id.Tenancy == nil {
		id.Tenancy = &pbresource.Tenancy{
			Partition: "",
			Namespace: "",
		}
	}

	if id.Tenancy.Partition != "" {
		if err := resource.ValidateName(id.Tenancy.Partition); err != nil {
			return status.Errorf(codes.InvalidArgument, "%s.tenancy.partition invalid: %v", errorPrefix, err)
		}
	}
	if id.Tenancy.Namespace != "" {
		if err := resource.ValidateName(id.Tenancy.Namespace); err != nil {
			return status.Errorf(codes.InvalidArgument, "%s.tenancy.namespace invalid: %v", errorPrefix, err)
		}
	}

	return nil
}

func validateRef(ref *pbresource.Reference, errorPrefix string) error {
	if ref.Type == nil {
		return status.Errorf(codes.InvalidArgument, "%s.type is required", errorPrefix)
	}
	if err := resource.ValidateName(ref.Name); err != nil {
		return status.Errorf(codes.InvalidArgument, "%s.name invalid: %v", errorPrefix, err)
	}
	if err := resource.ValidateName(ref.Tenancy.Partition); err != nil {
		return status.Errorf(codes.InvalidArgument, "%s.tenancy.partition invalid: %v", errorPrefix, err)
	}
	if err := resource.ValidateName(ref.Tenancy.Namespace); err != nil {
		return status.Errorf(codes.InvalidArgument, "%s.tenancy.namespace invalid: %v", errorPrefix, err)
	}
	return nil
}

func validateWildcardTenancy(tenancy *pbresource.Tenancy, namePrefix string) error {
	// Partition has to be a valid name if not wildcard or empty
	if tenancy.Partition != "" && tenancy.Partition != "*" {
		if err := resource.ValidateName(tenancy.Partition); err != nil {
			return status.Errorf(codes.InvalidArgument, "tenancy.partition invalid: %v", err)
		}
	}

	// Namespace has to be a valid name if not wildcard or empty
	if tenancy.Namespace != "" && tenancy.Namespace != "*" {
		if err := resource.ValidateName(tenancy.Namespace); err != nil {
			return status.Errorf(codes.InvalidArgument, "tenancy.namespace invalid: %v", err)
		}
	}

	// Not doing a strict resource name validation here because the prefix can be
	// something like "foo-" which is a valid prefix but not valid resource name.
	// relax validation to just check for lowercasing
	if namePrefix != strings.ToLower(namePrefix) {
		return status.Errorf(codes.InvalidArgument, "name_prefix invalid: must be lowercase alphanumeric, got: %v", namePrefix)
	}

	return nil
}

// tenancyExists return an error with the passed in gRPC status code when tenancy partition or namespace do not exist.
func tenancyExists(reg *resource.Registration, tenancyBridge TenancyBridge, tenancy *pbresource.Tenancy, errCode codes.Code) error {
	if reg.Scope == resource.ScopePartition || reg.Scope == resource.ScopeNamespace {
		exists, err := tenancyBridge.PartitionExists(tenancy.Partition)
		switch {
		case err != nil:
			return err
		case !exists:
			return status.Errorf(errCode, "partition not found: %v", tenancy.Partition)
		}
	}

	if reg.Scope == resource.ScopeNamespace {
		exists, err := tenancyBridge.NamespaceExists(tenancy.Partition, tenancy.Namespace)
		switch {
		case err != nil:
			return err
		case !exists:
			return status.Errorf(errCode, "namespace not found: %v", tenancy.Namespace)
		}
	}
	return nil
}

func validateScopedTenancy(scope resource.Scope, resourceType *pbresource.Type, tenancy *pbresource.Tenancy, allowWildcards bool) error {
	if scope == resource.ScopePartition && tenancy.Namespace != "" && (!allowWildcards || tenancy.Namespace != storage.Wildcard) {
		return status.Errorf(
			codes.InvalidArgument,
			"partition scoped resource %s cannot have a namespace. got: %s",
			resource.ToGVK(resourceType),
			tenancy.Namespace,
		)
	}

	if scope == resource.ScopeCluster {
		if tenancy.Partition != "" && (!allowWildcards || tenancy.Partition != storage.Wildcard) {
			return status.Errorf(
				codes.InvalidArgument,
				"cluster scoped resource %s cannot have a partition: %s",
				resource.ToGVK(resourceType),
				tenancy.Partition,
			)
		}
		if tenancy.Namespace != "" && (!allowWildcards || tenancy.Namespace != storage.Wildcard) {
			return status.Errorf(
				codes.InvalidArgument,
				"cluster scoped resource %s cannot have a namespace: %s",
				resource.ToGVK(resourceType),
				tenancy.Namespace,
			)
		}
	}
	return nil
}

func isTenancyMarkedForDeletion(reg *resource.Registration, tenancyBridge TenancyBridge, tenancy *pbresource.Tenancy) (bool, error) {
	if reg.Scope == resource.ScopePartition || reg.Scope == resource.ScopeNamespace {
		marked, err := tenancyBridge.IsPartitionMarkedForDeletion(tenancy.Partition)
		if err != nil {
			return false, err
		}
		if marked {
			return marked, nil
		}
	}

	if reg.Scope == resource.ScopeNamespace {
		marked, err := tenancyBridge.IsNamespaceMarkedForDeletion(tenancy.Partition, tenancy.Namespace)
		if err != nil {
			return false, err
		}
		return marked, nil
	}

	// Cluster scope has no tenancy so always return false
	return false, nil
}

// retryCAS retries the given operation with exponential backoff if the user
// didn't provide a version. This is intended to hide failures when the user
// isn't intentionally performing a CAS operation (all writes are, by design,
// CAS operations at the storage backend layer).
func (s *Server) retryCAS(ctx context.Context, vsn string, cas func() error) error {
	if vsn != "" {
		return cas()
	}

	const maxAttempts = 5

	// These parameters are fairly arbitrary, so if you find better ones then go
	// ahead and swap them out! In general, we want to wait long enough to smooth
	// over small amounts of storage replication lag, but not so long that we make
	// matters worse by holding onto load.
	backoff := &retry.Waiter{
		MinWait: 50 * time.Millisecond,
		MaxWait: 1 * time.Second,
		Jitter:  retry.NewJitter(50),
		Factor:  75 * time.Millisecond,
	}

	var err error
	for i := 1; i <= maxAttempts; i++ {
		if err = cas(); !errors.Is(err, storage.ErrCASFailure) {
			break
		}
		if backoff.Wait(ctx) != nil {
			break
		}
		s.Logger.Trace("retrying failed CAS operation", "failure_count", i)
	}
	return err
}

func clone[T proto.Message](v T) T { return proto.Clone(v).(T) }
