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

package resource

import (
	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/proto-public/pbresource"
	"google.golang.org/protobuf/proto"
)

// DecodedValidationHook is the function signature needed for usage with the DecodeAndValidate function
type DecodedValidationHook[T proto.Message] func(*DecodedResource[T]) error

// DecodeAndValidate will generate a validation hook function that decodes the specified type and
// passes it off to another validation hook. This is mainly a convenience to avoid many other
// validation hooks needing to attempt decoding the data and erroring in a consistent manner.
func DecodeAndValidate[T proto.Message](fn DecodedValidationHook[T]) ValidationHook {
	return func(res *pbresource.Resource) error {
		decoded, err := Decode[T](res)
		if err != nil {
			return err
		}

		return fn(decoded)
	}
}

// DecodedMutationHook is the function signature needed for usage with the DecodeAndMutate function
// The boolean return value indicates whether the Data field within the DecodedResource was modified.
// When true, the DecodeAndMutate hook function will automatically re-encode the Any data and store
// it on the internal Resource's Data field.
type DecodedMutationHook[T proto.Message] func(*DecodedResource[T]) (bool, error)

// DecodeAndMutate will generate a MutationHook that decodes the specified type and passes it
// off to another mutation hook. This is mainly a convenience to avoid other mutation hooks
// needing to decode and potentially reencode the Any data. When the inner mutation hook returns
// no error and that the Data was modified (true for the boolean return value), the generated
// hook will reencode the Any data back into the Resource wrapper
func DecodeAndMutate[T proto.Message](fn DecodedMutationHook[T]) MutationHook {
	return func(res *pbresource.Resource) error {
		decoded, err := Decode[T](res)
		if err != nil {
			return err
		}

		modified, err := fn(decoded)
		if err != nil {
			return err
		}

		if modified {
			return decoded.Resource.Data.MarshalFrom(decoded.Data)
		}
		return nil
	}
}

// DecodedAuthorizationHook is the function signature needed for usage with the DecodeAndAuthorizeWrite
// and DecodeAndAuthorizeRead functions.
type DecodedAuthorizationHook[T proto.Message] func(acl.Authorizer, *acl.AuthorizerContext, *DecodedResource[T]) error

// DecodeAndAuthorizeWrite will generate an ACLAuthorizeWriteHook that decodes the specified type and passes
// it off to another authorization hook. This is mainly a convenience to avoid many other write authorization
// hooks needing to attempt decoding the data and erroring in a consistent manner.
func DecodeAndAuthorizeWrite[T proto.Message](fn DecodedAuthorizationHook[T]) ACLAuthorizeWriteHook {
	return func(authz acl.Authorizer, ctx *acl.AuthorizerContext, res *pbresource.Resource) error {
		decoded, err := Decode[T](res)
		if err != nil {
			return err
		}

		return fn(authz, ctx, decoded)
	}
}

// DecodeAndAuthorizeRead will generate an ACLAuthorizeReadHook that decodes the specified type and passes
// it off to another authorization hook. This is mainly a convenience to avoid many other read authorization
// hooks needing to attempt decoding the data and erroring in a consistent manner.
func DecodeAndAuthorizeRead[T proto.Message](fn DecodedAuthorizationHook[T]) ACLAuthorizeReadHook {
	return func(authz acl.Authorizer, ctx *acl.AuthorizerContext, _ *pbresource.ID, res *pbresource.Resource) error {
		if res == nil {
			return ErrNeedResource
		}

		decoded, err := Decode[T](res)
		if err != nil {
			return err
		}

		return fn(authz, ctx, decoded)
	}
}

type ReadAuthorizationWithResourceHook func(acl.Authorizer, *acl.AuthorizerContext, *pbresource.Resource) error

// AuthorizeReadWithResource is a small wrapper to ensure that the authorization function is
// invoked with the full resource being read instead of just an id.
func AuthorizeReadWithResource(fn ReadAuthorizationWithResourceHook) ACLAuthorizeReadHook {
	return func(authz acl.Authorizer, ctx *acl.AuthorizerContext, id *pbresource.ID, res *pbresource.Resource) error {
		if res == nil {
			return ErrNeedResource
		}

		return fn(authz, ctx, res)
	}
}
