package app

import (
	"bytes"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"math/big"
	"net/url"
	"regexp"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

const (
	appNotificationMaxContentBytes        = 256 * 1024
	appNotificationMaxMessageContentBytes = 32 * 1024
	appNotificationMaxActions             = 20
	appNotificationMaxURLBytes            = 2048
)

var appNotificationActionKeyPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`)

var appNotificationReservedMetadataKeys = map[string]struct{}{
	message.AgentDeliveryMetadataKey:      {},
	message.AgentThreadSessionMetadataKey: {},
	message.ChannelMemberEventMetadataKey: {},
	message.MentionMetadataKey:            {},
	message.MessageRecallMetadataKey:      {},
	message.ReviewFeedbackMetadataKey:     {},
	message.SystemNoticeMetadataKey:       {},
	"attachments":                         {},
	"authorProfile":                       {},
	"card":                                {},
}

type appNotificationRecipientPolicy struct {
	Version                   int64    `json:"recipient_policy_version"`
	AllowedBasisKinds         []string `json:"allowed_recipient_basis_kinds"`
	AllowedBusinessNamespaces []string `json:"allowed_business_object_namespaces"`
	AllowedBusinessTypes      []string `json:"allowed_business_object_types"`
	AllowedContentKinds       []string `json:"allowed_human_dm_content_kinds"`
	OrganizationPolicyRef     string   `json:"organization_policy_ref"`
	PolicyDigest              string   `json:"policy_digest"`
}

type appNotificationCardDocument struct {
	SchemaVersion string                      `json:"schemaVersion"`
	Title         string                      `json:"title,omitempty"`
	Actions       []appNotificationCardAction `json:"actions,omitempty"`
}

type appNotificationCardAction struct {
	ActionKey    string          `json:"actionKey"`
	Type         string          `json:"type"`
	Label        string          `json:"label"`
	Semantics    string          `json:"semantics,omitempty"`
	URL          string          `json:"url,omitempty"`
	Confirmation json.RawMessage `json:"confirmation,omitempty"`
	InputSchema  json.RawMessage `json:"inputSchema,omitempty"`
	ActorPolicy  json.RawMessage `json:"actorPolicy,omitempty"`
	ExpiresAt    *time.Time      `json:"expiresAt,omitempty"`
}

type appNotificationDeterministicCreatePayload struct {
	Target         AppNotificationTarget           `json:"target"`
	Content        appNotificationDeterministicContent `json:"content"`
	BusinessObject AppNotificationBusinessObject   `json:"businessObject,omitempty"`
	RecipientBasis AppNotificationRecipientBasis   `json:"recipientBasis,omitempty"`
	CausedByActor  AppNotificationActorRef         `json:"causedByActor,omitempty"`
	ExpiresAt      *time.Time                      `json:"expiresAt,omitempty"`
}

type appNotificationDeterministicContent struct {
	Kind          string          `json:"kind"`
	Text          string          `json:"text,omitempty"`
	FallbackText  string          `json:"fallbackText,omitempty"`
	Card          json.RawMessage `json:"card,omitempty"`
	AttachmentIDs []string        `json:"attachmentIds,omitempty"`
	Metadata      map[string]any  `json:"metadata,omitempty"`
}

func validateAndNormalizeAppNotificationCommand(command AppNotificationCommand, now time.Time) (AppNotificationCommand, string, *appNotificationCardDocument, error) {
	command.WorkspaceID = strings.TrimSpace(command.WorkspaceID)
	command.ServiceInstallationID = strings.TrimSpace(command.ServiceInstallationID)
	command.Ingress = strings.TrimSpace(command.Ingress)
	command.ExecutionActor.Type = strings.TrimSpace(command.ExecutionActor.Type)
	command.ExecutionActor.ID = strings.TrimSpace(command.ExecutionActor.ID)
	command.ClientNotificationID = strings.TrimSpace(command.ClientNotificationID)
	command.Target.Kind = strings.TrimSpace(command.Target.Kind)
	command.Target.MemberID = strings.TrimSpace(command.Target.MemberID)
	command.Target.ScopeID = strings.TrimSpace(command.Target.ScopeID)
	command.BusinessObject = normalizeAppNotificationBusinessObject(command.BusinessObject)
	command.RecipientBasis.Kind = strings.TrimSpace(command.RecipientBasis.Kind)
	command.RecipientBasis.Ref = strings.TrimSpace(command.RecipientBasis.Ref)
	command.CausedByActor.Type = strings.TrimSpace(command.CausedByActor.Type)
	command.CausedByActor.ID = strings.TrimSpace(command.CausedByActor.ID)
	command.CorrelationID = strings.TrimSpace(command.CorrelationID)

	if command.WorkspaceID == "" || command.ServiceInstallationID == "" || command.ClientNotificationID == "" {
		return AppNotificationCommand{}, "", nil, badRequest("App notification workspace, installation, and client notification id are required")
	}
	if command.ExecutionActor.Type == "" || command.ExecutionActor.ID == "" {
		return AppNotificationCommand{}, "", nil, badRequest("App notification execution actor is required")
	}
	if command.Ingress != AppNotificationIngressRuntime && command.Ingress != AppNotificationIngressInternal {
		return AppNotificationCommand{}, "", nil, badRequest("App notification ingress is not supported")
	}
	if len(command.ClientNotificationID) > 255 || len(command.CorrelationID) > 255 || len(command.ExecutionActor.Type) > 32 || len(command.ExecutionActor.ID) > 255 {
		return AppNotificationCommand{}, "", nil, badRequest("App notification identifier is too long")
	}
	if (command.CausedByActor.Type == "") != (command.CausedByActor.ID == "") {
		return AppNotificationCommand{}, "", nil, badRequest("App notification caused-by actor type and id are required together")
	}
	if len(command.CausedByActor.Type) > 32 || len(command.CausedByActor.ID) > 255 {
		return AppNotificationCommand{}, "", nil, badRequest("App notification caused-by actor is too long")
	}
	switch command.Target.Kind {
	case AppNotificationTargetHumanDM:
		if command.Target.MemberID == "" || command.Target.ScopeID != "" || command.RecipientBasis.Kind == "" || command.RecipientBasis.Ref == "" {
			return AppNotificationCommand{}, "", nil, badRequest("Human DM notification target requires member and recipient basis")
		}
		if len(command.Target.MemberID) > 255 || len(command.RecipientBasis.Kind) > 64 || len(command.RecipientBasis.Ref) > 512 {
			return AppNotificationCommand{}, "", nil, badRequest("Human DM notification target is too long")
		}
	case AppNotificationTargetScope:
		if command.Target.ScopeID == "" || command.Target.MemberID != "" || command.RecipientBasis.Kind != "" || command.RecipientBasis.Ref != "" {
			return AppNotificationCommand{}, "", nil, badRequest("scope notification target requires only scopeId")
		}
		if len(command.Target.ScopeID) > 255 {
			return AppNotificationCommand{}, "", nil, badRequest("scope notification target is too long")
		}
	default:
		return AppNotificationCommand{}, "", nil, badRequest("App notification target kind is not supported")
	}
	if err := validateAppNotificationBusinessObject(command.BusinessObject); err != nil {
		return AppNotificationCommand{}, "", nil, err
	}
	if command.ExpiresAt != nil {
		expiresAt := command.ExpiresAt.UTC()
		command.ExpiresAt = &expiresAt
		if !expiresAt.After(now.UTC()) {
			return AppNotificationCommand{}, "", nil, badRequest("App notification expiry must be in the future")
		}
	}
	content, card, err := validateAndNormalizeAppNotificationContentAt(command.Content, now)
	if err != nil {
		return AppNotificationCommand{}, "", nil, err
	}
	command.Content = content
	digest, err := appNotificationDigest(appNotificationDeterministicCreatePayload{
		Target: command.Target,
		Content: appNotificationDeterministicContent{
			Kind: content.Kind, Text: content.Text, FallbackText: content.FallbackText,
			Card: content.CardJSON, AttachmentIDs: content.AttachmentIDs, Metadata: content.Metadata,
		},
		BusinessObject: command.BusinessObject,
		RecipientBasis: command.RecipientBasis,
		CausedByActor:  command.CausedByActor,
		ExpiresAt:      command.ExpiresAt,
	})
	if err != nil {
		return AppNotificationCommand{}, "", nil, err
	}
	return command, digest, card, nil
}

func validateAndNormalizeAppNotificationContent(content AppNotificationContent) (AppNotificationContent, *appNotificationCardDocument, error) {
	return validateAndNormalizeAppNotificationContentAt(content, time.Now().UTC())
}

func validateAndNormalizeAppNotificationContentAt(content AppNotificationContent, now time.Time) (AppNotificationContent, *appNotificationCardDocument, error) {
	content.Kind = strings.TrimSpace(content.Kind)
	content.Text = strings.TrimSpace(content.Text)
	content.FallbackText = strings.TrimSpace(content.FallbackText)
	content.AttachmentIDs = normalizedAppNotificationAttachmentIDs(content.AttachmentIDs)
	if content.Metadata == nil {
		content.Metadata = map[string]any{}
	}
	for key := range content.Metadata {
		if _, reserved := appNotificationReservedMetadataKeys[strings.TrimSpace(key)]; reserved {
			return AppNotificationContent{}, nil, badRequest("App notification metadata contains a reserved key")
		}
	}
	if _, err := appNotificationDeterministicJSON(content.Metadata); err != nil {
		return AppNotificationContent{}, nil, badRequest("App notification metadata must be JSON")
	}
	var card *appNotificationCardDocument
	switch content.Kind {
	case AppNotificationContentMessage:
		if content.Text == "" && len(content.AttachmentIDs) == 0 {
			return AppNotificationContent{}, nil, badRequest("ordinary App notification requires text or attachments")
		}
		if len(bytes.TrimSpace(content.CardJSON)) != 0 || content.FallbackText != "" {
			return AppNotificationContent{}, nil, badRequest("ordinary App notification does not accept card fields")
		}
		if len([]byte(content.Text)) > appNotificationMaxMessageContentBytes {
			return AppNotificationContent{}, nil, badRequest("App notification message content is too large")
		}
	case AppNotificationContentInteractiveCard:
		if content.FallbackText == "" {
			return AppNotificationContent{}, nil, badRequest("interactive App notification requires fallbackText")
		}
		if content.Text != "" {
			return AppNotificationContent{}, nil, badRequest("interactive App notification uses fallbackText as visible content")
		}
		if len([]byte(content.FallbackText)) > appNotificationMaxMessageContentBytes {
			return AppNotificationContent{}, nil, badRequest("App notification fallback content is too large")
		}
		document, deterministic, err := validateAppNotificationCardAt(content.CardJSON, now)
		if err != nil {
			return AppNotificationContent{}, nil, err
		}
		content.CardJSON = deterministic
		card = &document
	default:
		return AppNotificationContent{}, nil, badRequest("App notification content kind is not supported")
	}
	payload, err := appNotificationDeterministicJSON(content)
	if err != nil {
		return AppNotificationContent{}, nil, err
	}
	if len(payload) > appNotificationMaxContentBytes {
		return AppNotificationContent{}, nil, badRequest("App notification content is too large")
	}
	return content, card, nil
}

func validateAppNotificationCard(payload []byte) (appNotificationCardDocument, []byte, error) {
	return validateAppNotificationCardAt(payload, time.Now().UTC())
}

func validateAppNotificationCardAt(payload []byte, now time.Time) (appNotificationCardDocument, []byte, error) {
	if len(bytes.TrimSpace(payload)) == 0 {
		return appNotificationCardDocument{}, nil, badRequest("interactive App notification card is required")
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var generic map[string]any
	if err := decoder.Decode(&generic); err != nil || generic == nil {
		return appNotificationCardDocument{}, nil, badRequest("interactive App notification card must be a JSON object")
	}
	if err := ensureAppNotificationJSONEOF(decoder); err != nil {
		return appNotificationCardDocument{}, nil, err
	}
	deterministic, err := json.Marshal(generic)
	if err != nil {
		return appNotificationCardDocument{}, nil, badRequest("interactive App notification card must be JSON")
	}
	var document appNotificationCardDocument
	if err := json.Unmarshal(deterministic, &document); err != nil {
		return appNotificationCardDocument{}, nil, badRequest("interactive App notification card is invalid")
	}
	if document.SchemaVersion != strings.TrimSpace(document.SchemaVersion) || document.Title != strings.TrimSpace(document.Title) {
		return appNotificationCardDocument{}, nil, badRequest("interactive App notification card text fields must be normalized")
	}
	if document.SchemaVersion == "" {
		return appNotificationCardDocument{}, nil, badRequest("interactive App notification card schemaVersion is required")
	}
	if len(document.Actions) > appNotificationMaxActions {
		return appNotificationCardDocument{}, nil, badRequest("interactive App notification has too many actions")
	}
	seen := map[string]struct{}{}
	for index := range document.Actions {
		action := &document.Actions[index]
		if action.ActionKey != strings.TrimSpace(action.ActionKey) || action.Type != strings.TrimSpace(action.Type) ||
			action.Label != strings.TrimSpace(action.Label) || action.Semantics != strings.TrimSpace(action.Semantics) || action.URL != strings.TrimSpace(action.URL) {
			return appNotificationCardDocument{}, nil, badRequest("interactive App notification action fields must be normalized")
		}
		if !appNotificationActionKeyPattern.MatchString(action.ActionKey) || action.Label == "" {
			return appNotificationCardDocument{}, nil, badRequest("interactive App notification action key and label are invalid")
		}
		if _, duplicate := seen[action.ActionKey]; duplicate {
			return appNotificationCardDocument{}, nil, badRequest("interactive App notification action keys must be unique")
		}
		seen[action.ActionKey] = struct{}{}
		switch action.Type {
		case "callback":
			if action.URL != "" {
				return appNotificationCardDocument{}, nil, badRequest("callback App notification action does not accept url")
			}
			if err := validateAppNotificationInputSchema(action.InputSchema); err != nil {
				return appNotificationCardDocument{}, nil, err
			}
			if err := validateAppNotificationActorPolicy(action.ActorPolicy); err != nil {
				return appNotificationCardDocument{}, nil, err
			}
		case "open_url":
			if action.URL == "" || len(bytes.TrimSpace(action.InputSchema)) != 0 {
				return appNotificationCardDocument{}, nil, badRequest("open_url App notification action requires url and no input schema")
			}
			if err := validateAppNotificationURL(action.URL); err != nil {
				return appNotificationCardDocument{}, nil, err
			}
		default:
			return appNotificationCardDocument{}, nil, badRequest("interactive App notification action type is not supported")
		}
		if action.ExpiresAt != nil {
			expiresAt := action.ExpiresAt.UTC()
			action.ExpiresAt = &expiresAt
			if !expiresAt.After(now.UTC()) {
				return appNotificationCardDocument{}, nil, badRequest("interactive App notification action expiry must be in the future")
			}
		}
	}
	return document, deterministic, nil
}

func validateAppNotificationInputSchema(payload []byte) error {
	if len(bytes.TrimSpace(payload)) == 0 {
		return nil
	}
	var schema map[string]any
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	if err := decoder.Decode(&schema); err != nil || schema == nil {
		return badRequest("App notification action inputSchema must be a JSON object")
	}
	if schemaType, _ := schema["type"].(string); schemaType != "" && schemaType != "object" {
		return badRequest("App notification action inputSchema must describe an object")
	}
	return nil
}

func ensureAppNotificationJSONEOF(decoder *json.Decoder) error {
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return badRequest("App notification JSON contains trailing content")
	}
	return nil
}

func validateAppNotificationURL(raw string) error {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" || len([]byte(trimmed)) > appNotificationMaxURLBytes {
		return badRequest("App notification URL is invalid")
	}
	parsed, err := url.Parse(trimmed)
	if err != nil || parsed.User != nil {
		return badRequest("App notification URL is invalid")
	}
	if parsed.IsAbs() {
		if (parsed.Scheme != "https" && parsed.Scheme != "http") || parsed.Host == "" {
			return badRequest("App notification URL must use http or https")
		}
		return nil
	}
	if !strings.HasPrefix(parsed.Path, "/") || strings.HasPrefix(trimmed, "//") {
		return badRequest("App notification internal URL must be an absolute path")
	}
	return nil
}

func validateAppNotificationActorPolicy(payload []byte) error {
	if len(bytes.TrimSpace(payload)) == 0 {
		return nil
	}
	var policy struct {
		Kind      string   `json:"kind"`
		MemberIDs []string `json:"memberIds"`
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	if err := decoder.Decode(&policy); err != nil {
		return badRequest("App notification action actor policy is invalid")
	}
	if err := ensureAppNotificationJSONEOF(decoder); err != nil {
		return err
	}
	policy.Kind = strings.TrimSpace(policy.Kind)
	switch policy.Kind {
	case "", "readable_human":
		if len(policy.MemberIDs) != 0 {
			return badRequest("readable Human actor policy does not accept memberIds")
		}
	case "members":
		if len(policy.MemberIDs) == 0 || len(policy.MemberIDs) > 100 {
			return badRequest("member actor policy requires a bounded memberIds list")
		}
		seen := make(map[string]struct{}, len(policy.MemberIDs))
		for _, memberID := range policy.MemberIDs {
			memberID = strings.TrimSpace(memberID)
			if memberID == "" || len(memberID) > 255 {
				return badRequest("member actor policy contains an invalid memberId")
			}
			if _, duplicate := seen[memberID]; duplicate {
				return badRequest("member actor policy contains duplicate memberIds")
			}
			seen[memberID] = struct{}{}
		}
	default:
		return badRequest("App notification action actor policy is not supported")
	}
	return nil
}

func validateAppNotificationActionInput(schemaPayload []byte, input map[string]any) error {
	if input == nil {
		input = map[string]any{}
	}
	if len(bytes.TrimSpace(schemaPayload)) == 0 {
		if len(input) != 0 {
			return badRequest("App notification action does not accept input")
		}
		return nil
	}
	var schema struct {
		Required             []string                   `json:"required"`
		Properties           map[string]json.RawMessage `json:"properties"`
		AdditionalProperties *bool                      `json:"additionalProperties"`
	}
	if err := json.Unmarshal(schemaPayload, &schema); err != nil {
		return badRequest("App notification action input schema is invalid")
	}
	for _, required := range schema.Required {
		if _, ok := input[required]; !ok {
			return badRequest(fmt.Sprintf("App notification action input %q is required", required))
		}
	}
	allowAdditional := schema.AdditionalProperties == nil || *schema.AdditionalProperties
	for key, value := range input {
		property, ok := schema.Properties[key]
		if !ok {
			if !allowAdditional {
				return badRequest(fmt.Sprintf("App notification action input %q is not allowed", key))
			}
			continue
		}
		if err := validateAppNotificationInputProperty(key, property, value); err != nil {
			return err
		}
	}
	return nil
}

func validateAppNotificationInputProperty(key string, schemaPayload []byte, value any) error {
	var schema struct {
		Type      string `json:"type"`
		MinLength *int   `json:"minLength"`
		MaxLength *int   `json:"maxLength"`
		Enum      []any  `json:"enum"`
	}
	if err := json.Unmarshal(schemaPayload, &schema); err != nil {
		return badRequest("App notification action property schema is invalid")
	}
	validType := true
	switch schema.Type {
	case "", "any":
	case "string":
		text, ok := value.(string)
		validType = ok
		if ok && schema.MinLength != nil && len([]rune(text)) < *schema.MinLength {
			return badRequest(fmt.Sprintf("App notification action input %q is too short", key))
		}
		if ok && schema.MaxLength != nil && len([]rune(text)) > *schema.MaxLength {
			return badRequest(fmt.Sprintf("App notification action input %q is too long", key))
		}
	case "boolean":
		_, validType = value.(bool)
	case "number":
		switch value.(type) {
		case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, json.Number:
		default:
			validType = false
		}
	case "integer":
		validType = appNotificationIntegerValue(value)
	case "object":
		_, validType = value.(map[string]any)
	case "array":
		_, validType = value.([]any)
	default:
		return badRequest("App notification action property type is not supported")
	}
	if !validType {
		return badRequest(fmt.Sprintf("App notification action input %q has the wrong type", key))
	}
	if len(schema.Enum) > 0 {
		encodedValue, _ := json.Marshal(value)
		matched := false
		for _, candidate := range schema.Enum {
			encodedCandidate, _ := json.Marshal(candidate)
			if bytes.Equal(encodedValue, encodedCandidate) {
				matched = true
				break
			}
		}
		if !matched {
			return badRequest(fmt.Sprintf("App notification action input %q is not an allowed value", key))
		}
	}
	return nil
}

func appNotificationIntegerValue(value any) bool {
	switch typed := value.(type) {
	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
		return true
	case float64:
		return !math.IsNaN(typed) && !math.IsInf(typed, 0) && math.Trunc(typed) == typed
	case float32:
		value64 := float64(typed)
		return !math.IsNaN(value64) && !math.IsInf(value64, 0) && math.Trunc(value64) == value64
	case json.Number:
		rational, ok := new(big.Rat).SetString(strings.TrimSpace(typed.String()))
		return ok && rational.IsInt()
	default:
		return false
	}
}

func appNotificationActionByKey(card appNotificationCardDocument, actionKey string) (appNotificationCardAction, bool) {
	trimmed := strings.TrimSpace(actionKey)
	for _, action := range card.Actions {
		if strings.TrimSpace(action.ActionKey) == trimmed {
			return action, true
		}
	}
	return appNotificationCardAction{}, false
}

func normalizeAppNotificationBusinessObject(object AppNotificationBusinessObject) AppNotificationBusinessObject {
	object.Namespace = strings.TrimSpace(object.Namespace)
	object.Type = strings.TrimSpace(object.Type)
	object.ID = strings.TrimSpace(object.ID)
	object.URL = strings.TrimSpace(object.URL)
	return object
}

func validateAppNotificationBusinessObject(object AppNotificationBusinessObject) error {
	if object.IsZero() {
		return nil
	}
	if object.Namespace == "" || object.Type == "" || object.ID == "" || object.Version <= 0 {
		return badRequest("App notification business object namespace, type, id, and positive version are required together")
	}
	if len(object.Namespace) > 64 || len(object.Type) > 64 || len(object.ID) > 255 {
		return badRequest("App notification business object identifier is too long")
	}
	if object.URL != "" {
		if err := validateAppNotificationURL(object.URL); err != nil {
			return err
		}
	}
	return nil
}

func normalizedAppNotificationAttachmentIDs(values []string) []string {
	seen := map[string]struct{}{}
	result := make([]string, 0, len(values))
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		result = append(result, trimmed)
	}
	return result
}

func parseAppNotificationRecipientPolicy(config []byte, grantVersion int64, grantDigest string) (appNotificationRecipientPolicy, error) {
	var policy appNotificationRecipientPolicy
	if len(bytes.TrimSpace(config)) == 0 {
		return policy, forbidden("App notification Human DM recipient policy is not configured")
	}
	if err := json.Unmarshal(config, &policy); err != nil {
		return policy, fmt.Errorf("decode App notification recipient policy: %w", err)
	}
	grantDigest = strings.TrimSpace(grantDigest)
	policy.PolicyDigest = strings.TrimSpace(policy.PolicyDigest)
	if policy.Version <= 0 || len(policy.AllowedBasisKinds) == 0 || len(policy.AllowedContentKinds) == 0 {
		return policy, forbidden("App notification Human DM recipient policy is incomplete")
	}
	if policy.Version != grantVersion || grantVersion <= 0 {
		return policy, forbidden("App notification Human DM recipient policy version is stale")
	}
	if len(grantDigest) != sha256.Size*2 || policy.PolicyDigest != grantDigest {
		return policy, forbidden("App notification Human DM recipient policy digest is stale")
	}
	return policy, nil
}

func (policy appNotificationRecipientPolicy) authorize(command AppNotificationCommand) error {
	if !stringSliceContains(policy.AllowedBasisKinds, command.RecipientBasis.Kind) {
		return forbidden("App notification recipient basis is not allowed by the installation grant")
	}
	if !stringSliceContains(policy.AllowedContentKinds, command.Content.Kind) {
		return forbidden("App notification content is not allowed by the installation grant")
	}
	if namespace := command.BusinessObject.Namespace; namespace != "" && len(policy.AllowedBusinessNamespaces) > 0 && !stringSliceContains(policy.AllowedBusinessNamespaces, namespace) {
		return forbidden("App notification business namespace is not allowed by the installation grant")
	}
	if objectType := command.BusinessObject.Type; objectType != "" && len(policy.AllowedBusinessTypes) > 0 && !stringSliceContains(policy.AllowedBusinessTypes, objectType) {
		return forbidden("App notification business object type is not allowed by the installation grant")
	}
	return nil
}

func stringSliceContains(values []string, expected string) bool {
	expected = strings.TrimSpace(expected)
	for _, value := range values {
		if strings.TrimSpace(value) == expected {
			return true
		}
	}
	return false
}
