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

package resource

import (
	"context"
	"strings"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/internal/resource"
	"github.com/hashicorp/consul/proto-public/pbresource"
)

func (s *Server) MutateAndValidate(ctx context.Context, req *pbresource.MutateAndValidateRequest) (*pbresource.MutateAndValidateResponse, error) {
	tenancyMarkedForDeletion, err := s.mutateAndValidate(ctx, req.Resource, false)
	if err != nil {
		return nil, err
	}

	if tenancyMarkedForDeletion {
		return nil, status.Errorf(
			codes.InvalidArgument,
			"tenancy marked for deletion: %s/%s",
			req.Resource.Id.Tenancy.Partition,
			req.Resource.Id.Tenancy.Namespace,
		)
	}
	return &pbresource.MutateAndValidateResponse{Resource: req.Resource}, nil
}

// private DRY impl that is used by both the Write and MutateAndValidate RPCs.
func (s *Server) mutateAndValidate(ctx context.Context, res *pbresource.Resource, enforceLicenseCheck bool) (tenancyMarkedForDeletion bool, err error) {
	reg, err := s.ensureResourceValid(res, enforceLicenseCheck)
	if err != nil {
		return false, err
	}

	v1EntMeta := v2TenancyToV1EntMeta(res.Id.Tenancy)
	authz, authzContext, err := s.getAuthorizer(tokenFromContext(ctx), v1EntMeta)
	if err != nil {
		return false, err
	}
	v1EntMetaToV2Tenancy(reg, v1EntMeta, res.Id.Tenancy)

	// Check the user sent the correct type of data.
	if res.Data != nil && !res.Data.MessageIs(reg.Proto) {
		got := strings.TrimPrefix(res.Data.TypeUrl, "type.googleapis.com/")

		return false, status.Errorf(
			codes.InvalidArgument,
			"resource.data is of wrong type (expected=%q, got=%q)",
			reg.Proto.ProtoReflect().Descriptor().FullName(),
			got,
		)
	}

	if err = reg.Mutate(res); err != nil {
		return false, status.Errorf(codes.Internal, "failed mutate hook: %v", err.Error())
	}

	if err = reg.Validate(res); err != nil {
		return false, status.Error(codes.InvalidArgument, err.Error())
	}

	// ACL check comes before tenancy existence checks to not leak tenancy "existence".
	err = reg.ACLs.Write(authz, authzContext, res)
	switch {
	case acl.IsErrPermissionDenied(err):
		return false, status.Error(codes.PermissionDenied, err.Error())
	case err != nil:
		return false, status.Errorf(codes.Internal, "failed write acl: %v", err)
	}

	// Check tenancy exists for the V2 resource
	if err = tenancyExists(reg, s.TenancyBridge, res.Id.Tenancy, codes.InvalidArgument); err != nil {
		return false, err
	}

	// This is used later in the "create" and "update" paths to block non-delete related writes
	// when a tenancy unit has been marked for deletion.
	tenancyMarkedForDeletion, err = isTenancyMarkedForDeletion(reg, s.TenancyBridge, res.Id.Tenancy)
	if err != nil {
		return false, status.Errorf(codes.Internal, "failed tenancy marked for deletion check: %v", err)
	}
	if tenancyMarkedForDeletion {
		return true, nil
	}
	return false, nil
}

func (s *Server) ensureResourceValid(res *pbresource.Resource, enforceLicenseCheck bool) (*resource.Registration, error) {
	var field string
	switch {
	case res == nil:
		field = "resource"
	case res.Id == nil:
		field = "resource.id"
	}

	if field != "" {
		return nil, status.Errorf(codes.InvalidArgument, "%s is required", field)
	}

	if err := validateId(res.Id, "resource.id"); err != nil {
		return nil, err
	}

	if res.Owner != nil {
		if err := validateId(res.Owner, "resource.owner"); err != nil {
			return nil, err
		}
	}

	// Check type exists.
	reg, err := s.resolveType(res.Id.Type)
	if err != nil {
		return nil, err
	}

	// Since this is shared by Write and MutateAndValidate, only fail the operation
	// if it's a write operation and the feature is not allowed by the license.
	if err = s.FeatureCheck(reg); err != nil && enforceLicenseCheck {
		return nil, err
	}

	if err = checkV2Tenancy(s.UseV2Tenancy, res.Id.Type); err != nil {
		return nil, err
	}

	// Check scope
	if reg.Scope == resource.ScopePartition && res.Id.Tenancy.Namespace != "" {
		return nil, status.Errorf(
			codes.InvalidArgument,
			"partition scoped resource %s cannot have a namespace. got: %s",
			resource.ToGVK(res.Id.Type),
			res.Id.Tenancy.Namespace,
		)
	}

	return reg, nil
}
