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

package resource

import (
	"context"
	"errors"

	"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/internal/storage"
	"github.com/hashicorp/consul/proto-public/pbresource"
)

func (s *Server) Read(ctx context.Context, req *pbresource.ReadRequest) (*pbresource.ReadResponse, error) {
	// Light first pass validation based on what user passed in and not much more.
	reg, err := s.ensureReadRequestValid(req)
	if err != nil {
		return nil, err
	}

	// acl.EnterpriseMeta acl.AuthorizerContext follow rules for V1 resources since they integrate with the V1 acl subsystem.
	// pbresource.Tenancy follows rules for V2 resources and the Resource service.
	// Example:
	//
	//    A CE namespace scoped resource:
	//      V1: EnterpriseMeta{}
	//      V2: Tenancy {Partition: "default", Namespace: "default"}
	//
	//   An ENT namespace scoped resource:
	//      V1: EnterpriseMeta{Partition: "default", Namespace: "default"}
	//      V2: Tenancy {Partition: "default", Namespace: "default"}
	//
	// It is necessary to convert back and forth depending on which component supports which version, V1 or V2.
	entMeta := v2TenancyToV1EntMeta(req.Id.Tenancy)
	authz, authzContext, err := s.getAuthorizer(tokenFromContext(ctx), entMeta)
	if err != nil {
		return nil, err
	}

	v1EntMetaToV2Tenancy(reg, entMeta, req.Id.Tenancy)

	// ACL check usually comes before tenancy existence checks to not leak
	// tenancy "existence", unless the ACL check requires the data payload
	// to function.
	authzNeedsData := false
	err = reg.ACLs.Read(authz, authzContext, req.Id, nil)
	switch {
	case errors.Is(err, resource.ErrNeedResource):
		authzNeedsData = true
		err = nil
	case acl.IsErrPermissionDenied(err):
		return nil, status.Error(codes.PermissionDenied, err.Error())
	case err != nil:
		return nil, status.Errorf(codes.Internal, "failed read acl: %v", err)
	}

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

	resource, err := s.Backend.Read(ctx, readConsistencyFrom(ctx), req.Id)
	switch {
	case errors.Is(err, storage.ErrNotFound):
		return nil, status.Error(codes.NotFound, err.Error())
	case errors.As(err, &storage.GroupVersionMismatchError{}):
		return nil, status.Error(codes.InvalidArgument, err.Error())
	case err != nil:
		return nil, status.Errorf(codes.Internal, "failed read: %v", err)
	}

	if authzNeedsData {
		err = reg.ACLs.Read(authz, authzContext, req.Id, resource)
		switch {
		case acl.IsErrPermissionDenied(err):
			return nil, status.Error(codes.PermissionDenied, err.Error())
		case err != nil:
			return nil, status.Errorf(codes.Internal, "failed read acl: %v", err)
		}
	}

	return &pbresource.ReadResponse{Resource: resource}, nil
}

func (s *Server) ensureReadRequestValid(req *pbresource.ReadRequest) (*resource.Registration, error) {
	if req.Id == nil {
		return nil, status.Errorf(codes.InvalidArgument, "id is required")
	}

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

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

	// Ignore return value since read ops are allowed but will log a warning if the feature is
	// not enabled in the license.
	_ = s.FeatureCheck(reg)

	// Check scope
	if err = validateScopedTenancy(reg.Scope, req.Id.Type, req.Id.Tenancy, false); err != nil {
		return nil, err
	}

	return reg, nil
}
