// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package resource

import (
	"context"
	"errors"
	"fmt"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/oklog/ulid/v2"

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

func (s *Server) WriteStatus(ctx context.Context, req *pbresource.WriteStatusRequest) (*pbresource.WriteStatusResponse, error) {
	authz, err := s.getAuthorizer(tokenFromContext(ctx))
	if err != nil {
		return nil, err
	}

	// check acls
	err = authz.ToAllowAuthorizer().OperatorWriteAllowed(&acl.AuthorizerContext{})
	switch {
	case acl.IsErrPermissionDenied(err):
		return nil, status.Error(codes.PermissionDenied, err.Error())
	case err != nil:
		return nil, status.Errorf(codes.Internal, "failed operator:write allowed acl: %v", err)
	}

	if err := validateWriteStatusRequest(req); err != nil {
		return nil, err
	}

	_, err = s.resolveType(req.Id.Type)
	if err != nil {
		return nil, err
	}

	// At the storage backend layer, all writes are CAS operations.
	//
	// See comment in write.go for more information.
	//
	// Most controllers *won't* do an explicit CAS write of the status because it
	// doesn't provide much value, and conflicts are fairly likely in the flurry
	// of activity after a resource is updated.
	//
	// Here's why that's okay:
	//
	//	- Controllers should only update their own status (identified by its key)
	//	  and updating separate statuses is commutative.
	//
	//	- Controllers that make writes should be leader-elected singletons (i.e.
	//	  there should only be one instance of the controller running) so we don't
	//	  need to worry about multiple instances racing with each other.
	//
	//	- Only controllers are supposed to write statuses, so you should never be
	//	  racing with a user's write of the same status.
	var result *pbresource.Resource
	err = s.retryCAS(ctx, req.Version, func() error {
		resource, err := s.Backend.Read(ctx, storage.EventualConsistency, req.Id)
		if err != nil {
			return err
		}

		if req.Version != "" && req.Version != resource.Version {
			return storage.ErrCASFailure
		}

		resource = clone(resource)
		if resource.Status == nil {
			resource.Status = make(map[string]*pbresource.Status)
		}

		status := clone(req.Status)
		status.UpdatedAt = timestamppb.Now()
		resource.Status[req.Key] = status

		result, err = s.Backend.WriteCAS(ctx, resource)
		return err
	})

	switch {
	case errors.Is(err, storage.ErrNotFound):
		return nil, status.Error(codes.NotFound, err.Error())
	case errors.Is(err, storage.ErrCASFailure):
		return nil, status.Error(codes.Aborted, err.Error())
	case err != nil:
		return nil, status.Errorf(codes.Internal, "failed to write resource: %v", err.Error())
	}

	return &pbresource.WriteStatusResponse{Resource: result}, nil
}

func validateWriteStatusRequest(req *pbresource.WriteStatusRequest) error {
	var field string
	switch {
	case req.Id == nil:
		field = "id"
	case req.Id.Type == nil:
		field = "id.type"
	case req.Id.Tenancy == nil:
		field = "id.tenancy"
	case req.Id.Name == "":
		field = "id.name"
	case req.Id.Uid == "":
		// We require Uid because only controllers should write statuses and
		// controllers should *always* refer to a specific incarnation of a
		// resource using its Uid.
		field = "id.uid"
	case req.Key == "":
		field = "key"
	case req.Status == nil:
		field = "status"
	case req.Status.ObservedGeneration == "":
		field = "status.observed_generation"
	default:
		for i, condition := range req.Status.Conditions {
			if condition.Type == "" {
				field = fmt.Sprintf("status.conditions[%d].type", i)
				break
			}

			if condition.Resource != nil {
				switch {
				case condition.Resource.Type == nil:
					field = fmt.Sprintf("status.conditions[%d].resource.type", i)
					break
				case condition.Resource.Tenancy == nil:
					field = fmt.Sprintf("status.conditions[%d].resource.tenancy", i)
					break
				case condition.Resource.Name == "":
					field = fmt.Sprintf("status.conditions[%d].resource.name", i)
					break
				}
			}
		}
	}
	if field != "" {
		return status.Errorf(codes.InvalidArgument, "%s is required", field)
	}

	if req.Status.UpdatedAt != nil {
		return status.Error(codes.InvalidArgument, "status.updated_at is automatically set and cannot be provided")
	}

	if _, err := ulid.ParseStrict(req.Status.ObservedGeneration); err != nil {
		return status.Error(codes.InvalidArgument, "status.observed_generation is not valid")
	}

	return nil
}
