package app

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const (
	appNotificationEventAction       = "agentteam.app.notification.action"
	appNotificationExternalResource  = "app_notification"
	appNotificationCreatedVia        = "app_notification"
	appNotificationInstallationLimit = 600
	appNotificationRecipientLimit    = 120
	appNotificationScopeLimit        = 300
)

// CreateAppNotificationForRuntime applies one canonical notification command
// using a transport-independent runtime principal.
func (store *Store) CreateAppNotificationForRuntime(ctx context.Context, authenticated AppNotificationRuntimePrincipal, command AppNotificationCommand) (AppNotificationResult, error) {
	command.WorkspaceID = authenticated.WorkspaceID
	command.ServiceInstallationID = authenticated.ServiceInstallationID
	command.Ingress = AppNotificationIngressRuntime
	command.ExecutionActor = authenticated.executionActor()
	return store.CreateAppNotification(ctx, command)
}

// CreateAppNotificationForInternal applies one canonical notification command
// after enforcing the trusted service's server-side allowlist.
func (store *Store) CreateAppNotificationForInternal(ctx context.Context, principal AppNotificationInternalPrincipal, command AppNotificationCommand) (AppNotificationResult, error) {
	command.WorkspaceID = principal.WorkspaceID
	command.ServiceInstallationID = principal.ServiceInstallationID
	command.Ingress = AppNotificationIngressInternal
	command.ExecutionActor = AppNotificationExecutionActor{Type: appNotificationExecutionActorInternalService, ID: principal.IdentityID}
	if err := principal.authorize(command); err != nil {
		return AppNotificationResult{}, err
	}
	return store.CreateAppNotification(ctx, command)
}

// CreateAppNotificationForBuiltin resolves a AgentTeam-owned in-process publisher
// identity and applies the same canonical notification command without HTTP.
func (store *Store) CreateAppNotificationForBuiltin(ctx context.Context, identityID, workspaceID string, command AppNotificationCommand) (AppNotificationResult, error) {
	principal, err := store.AppNotificationBuiltinPrincipal(ctx, workspaceID, identityID)
	if err != nil {
		return AppNotificationResult{}, err
	}
	return store.CreateAppNotificationForInternal(ctx, principal, command)
}

// CreateAppNotification is the shared Policy/Planner/Applier transaction used
// by both authenticated ingress adapters.
func (store *Store) CreateAppNotification(ctx context.Context, command AppNotificationCommand) (AppNotificationResult, error) {
	now := time.Now().UTC()
	normalized, payloadDigest, card, err := validateAndNormalizeAppNotificationCommand(command, now)
	if err != nil {
		return AppNotificationResult{}, err
	}
	command = normalized
	if command.CorrelationID == "" {
		command.CorrelationID, err = identifiers.NewEntityID("cor")
		if err != nil {
			return AppNotificationResult{}, fmt.Errorf("generate App notification correlation id: %w", err)
		}
	}

	var result AppNotificationResult
	var committedMessage postCommitMessage
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		txServiceActor := store.serviceActorStore().InTx(tx)
		installation, err := txServiceActor.LoadInstallationForUpdate(ctx, command.ServiceInstallationID)
		if err != nil {
			return err
		}
		if strings.TrimSpace(installation.WorkspaceID) != command.WorkspaceID || strings.TrimSpace(installation.ServiceKind) != serviceactor.KindApp {
			return storeerr.ErrNotFound
		}
		if strings.TrimSpace(installation.Status) != serviceactor.StatusActive {
			return forbidden("App notification installation is not active")
		}
		serviceMember, err := txServiceActor.LoadMemberByInstallation(ctx, installation.ID)
		if err != nil {
			return err
		}
		if strings.TrimSpace(serviceMember.Status) != serviceactor.StatusActive {
			return forbidden("App notification service member is not active")
		}
		runtimeCapability, err := requirePinnedRuntimeCapability(installation)
		if err != nil {
			return err
		}
		if err := requireAppInstallationScopes(ctx, tx, installation.ID, []string{AppRuntimeScopeMessageWrite, AppNotificationScopeWrite}); err != nil {
			return err
		}
		if err := authorizeAppNotificationManifest(runtimeCapability, command, card); err != nil {
			return err
		}
		if existing, found, err := store.FindAppNotificationByClientIDInTx(ctx, tx, command.WorkspaceID, installation.ID, command.ClientNotificationID, true); err != nil {
			return err
		} else if found {
			if strings.TrimSpace(existing.CreatePayloadDigest) != payloadDigest {
				return conflict("App notification client id was already used with another payload")
			}
			loaded, err := store.appNotificationResultInTx(ctx, tx, existing)
			if err != nil {
				return err
			}
			loaded.AlreadyCreated = true
			result = loaded
			return nil
		}
		if err := enforceAppNotificationCreateAdmission(ctx, tx, command, now); err != nil {
			return err
		}

		resolved, err := store.resolveAppNotificationTargetInTx(ctx, tx, installation, serviceMember, command, now)
		if err != nil {
			return err
		}
		if resolved.EffectiveExpiresAt != nil {
			command.ExpiresAt = resolved.EffectiveExpiresAt
		}
		notificationID := deterministicAppNotificationID(installation.ID, command.ClientNotificationID)
		messageID := deterministicAppNotificationMessageID(notificationID)
		runtimeInstallation, err := loadRuntimeAppInstallationFromQueryer(ctx, tx, installation.ID)
		if err != nil {
			return err
		}
		messageType := ""
		visibleContent := command.Content.Text
		if command.Content.Kind == AppNotificationContentInteractiveCard {
			messageType = message.MessageTypeAppInteractiveCard
			visibleContent = command.Content.FallbackText
		}
		metadata := message.CopyMessageMetadata(command.Content.Metadata)
		delivery, preparedMessage, err := store.appendAppServiceMessageWithOptions(ctx, tx, runtimeInstallation, resolved.ThreadAccess, visibleContent, metadata, false, appendMessageOptions{
			AttachmentIDs:              command.Content.AttachmentIDs,
			MessageType:                messageType,
			RoutingPrincipalMemberID:   serviceMember.MemberID,
			NotifyPrincipalMemberID:    serviceMember.MemberID,
			IdempotencySubjectMemberID: serviceMember.MemberID,
			ClientMessageID:            "app-notification:" + notificationID,
			ForceMessageID:             messageID,
			SuppressAgentRouting:       true,
			Source: message.ContributionSource{
				Kind:    message.ContributionSourceKindServiceDirectPost,
				RefType: message.ContributionSourceRefTypeMessage,
				RefID:   messageID,
				Label:   runtimeInstallation.AppName,
			},
		},
			appServiceMessageAppNotification,
			func(delivery *message.MessageDelivery) {
				if command.Target.Kind == AppNotificationTargetHumanDM {
					delivery.Conversation = appNotificationDMConversationProjection(runtimeInstallation, delivery.AppMessage)
				}
			},
		)
		if err != nil {
			return err
		}
		committedMessage = preparedMessage
		if delivery.IdempotentReplay && strings.TrimSpace(delivery.MessageID) != messageID {
			return conflict("App notification message idempotency replay does not match the canonical message")
		}
		record := appNotificationRecordForCreate(command, installation, serviceMember, resolved, notificationID, delivery.MessageID, payloadDigest, now)
		if err := store.InsertAppNotificationInTx(ctx, tx, record); err != nil {
			if existing, found, loadErr := store.FindAppNotificationByClientIDInTx(ctx, tx, command.WorkspaceID, installation.ID, command.ClientNotificationID, true); loadErr == nil && found {
				if existing.CreatePayloadDigest != payloadDigest {
					return conflict("App notification client id was already used with another payload")
				}
				loaded, loadErr := store.appNotificationResultInTx(ctx, tx, existing)
				if loadErr != nil {
					return loadErr
				}
				loaded.AlreadyCreated = true
				result = loaded
				return nil
			}
			return err
		}
		if card != nil {
			actionSchema, err := appNotificationDeterministicJSON(map[string]any{"actions": card.Actions})
			if err != nil {
				return err
			}
			if err := store.InsertAppNotificationCardInTx(ctx, tx, AppNotificationCardRecord{
				ScopeID: resolved.Target.DeliveryScopeID, MessageID: delivery.MessageID, AppNotificationID: notificationID,
				SchemaVersion: card.SchemaVersion, CardRevision: 1, RenderPayloadJSON: command.Content.CardJSON,
				ActionSchemaJSON: actionSchema, State: AppNotificationCardStateActive, ExpiresAt: command.ExpiresAt,
				CreatedAt: now, UpdatedAt: now,
			}); err != nil {
				return err
			}
			if hasAppNotificationCallbackAction(*card) {
				if err := requireAppInstallationScopes(ctx, tx, installation.ID, []string{AppNotificationActionScopeRead}); err != nil {
					return err
				}
				if _, err := store.ensureAppNotificationActionSubscriptionInTx(ctx, tx, installation, serviceMember, resolved, record, command, now); err != nil {
					return err
				}
			}
		}
		result, err = store.appNotificationResultInTx(ctx, tx, record)
		return err
	})
	if err != nil {
		return AppNotificationResult{}, err
	}
	store.emitPostCommitMessage(ctx, committedMessage)
	return result, nil
}

func appNotificationDMConversationProjection(installation model.RuntimeAppInstallation, appMessage *message.AppMessage) *message.DeliveryConversationProjection {
	if appMessage == nil {
		return nil
	}
	scopeID := strings.TrimSpace(appMessage.ChannelID)
	if scopeID == "" {
		return nil
	}
	profile := appMessage.EffectiveAuthorProfile()
	title := firstNonEmpty(strings.TrimSpace(profile.DisplayName), strings.TrimSpace(installation.AppName), strings.TrimSpace(installation.Handle), scopeID)
	subtitle := firstNonEmpty(strings.TrimSpace(profile.ServiceSlug), strings.TrimSpace(installation.AppSlug), strings.TrimSpace(installation.Handle))
	peerID := firstNonEmpty(strings.TrimSpace(installation.InstallationID), strings.TrimSpace(installation.Handle), strings.TrimSpace(profile.PrincipalID))
	conversationID := "dm:" + scopeID
	projection := map[string]any{
		"id":                conversationID,
		"kind":              message.ConversationKindDM,
		"scopeId":           scopeID,
		"title":             title,
		"subtitle":          subtitle,
		"avatarUrl":         strings.TrimSpace(profile.AvatarURL),
		"peerType":          "service",
		"peerId":            peerID,
		"preview":           appNotificationConversationPreview(appMessage.Content),
		"lastAuthorName":    title,
		"lastMessageAt":     appMessage.CreatedAt,
		"lastActivityAt":    appMessage.CreatedAt,
		"subscriptionState": "active",
		"lastMessage":       *appMessage,
	}
	return &message.DeliveryConversationProjection{
		ConversationID: conversationID,
		ScopeID:        scopeID,
		Kind:           message.ConversationKindDM,
		SortAt:         appMessage.CreatedAt,
		Projection:     projection,
	}
}

func appNotificationConversationPreview(content string) string {
	const maxRunes = 140
	trimmed := strings.TrimSpace(content)
	runes := []rune(trimmed)
	if len(runes) <= maxRunes {
		return trimmed
	}
	return string(runes[:maxRunes]) + "..."
}

type resolvedAppNotificationTarget struct {
	Target                 ServiceEventTarget
	ThreadAccess           message.ThreadAccess
	Binding                serviceactor.TargetBinding
	RecipientMemberID      *string
	RecipientPolicyVersion *int64
	RecipientBasisKind     *string
	RecipientBasisRef      *string
	RecipientFactVersion   *int64
	EffectiveExpiresAt     *time.Time
}

func (store *Store) resolveAppNotificationTargetInTx(ctx context.Context, tx coredb.Tx, installation serviceactor.Installation, serviceMember serviceactor.Member, command AppNotificationCommand, now time.Time) (resolvedAppNotificationTarget, error) {
	switch command.Target.Kind {
	case AppNotificationTargetHumanDM:
		grant, err := store.LoadActiveGrantPolicyInTx(ctx, tx, installation.ID, AppNotificationScopeWrite, true)
		if err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		policy, err := parseAppNotificationRecipientPolicy(grant.Config, grant.PolicyVersion, grant.PolicyDigest)
		if err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		if err := policy.authorize(command); err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		resolver, ok := store.appNotificationRecipientResolver(command.RecipientBasis.Kind)
		if !ok {
			return resolvedAppNotificationTarget{}, forbidden("App notification recipient basis resolver is not registered")
		}
		fact, err := resolver.ResolveAppNotificationRecipient(ctx, tx, AppNotificationRecipientFactRequest{
			WorkspaceID: command.WorkspaceID, InstallationID: installation.ID, Basis: command.RecipientBasis,
			BusinessObject: command.BusinessObject, ExpectedMemberID: command.Target.MemberID, Now: now,
		})
		if err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		if !fact.Active || fact.Version <= 0 || strings.TrimSpace(fact.WorkspaceID) != command.WorkspaceID || strings.TrimSpace(fact.RecipientMemberID) != command.Target.MemberID || strings.TrimSpace(fact.Kind) != command.RecipientBasis.Kind || strings.TrimSpace(fact.Ref) != command.RecipientBasis.Ref {
			return resolvedAppNotificationTarget{}, forbidden("App notification recipient fact is not active or does not match the target")
		}
		if fact.ExpiresAt != nil && !fact.ExpiresAt.After(now) {
			return resolvedAppNotificationTarget{}, forbidden("App notification recipient fact is expired")
		}
		if grant.ExpiresAt != nil && command.ExpiresAt != nil && command.ExpiresAt.After(*grant.ExpiresAt) {
			return resolvedAppNotificationTarget{}, forbidden("App notification expiry exceeds its recipient grant")
		}
		if fact.ExpiresAt != nil && command.ExpiresAt != nil && command.ExpiresAt.After(*fact.ExpiresAt) {
			return resolvedAppNotificationTarget{}, forbidden("App notification expiry exceeds its recipient fact")
		}
		if err := requireActiveHumanAppNotificationRecipient(ctx, tx, command.WorkspaceID, command.Target.MemberID); err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		threadAccess, err := message.EnsureDMThread(ctx, tx, message.MemberActor{WorkspaceID: command.WorkspaceID, MemberID: serviceMember.MemberID}, command.Target.MemberID)
		if err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		target, err := ResolveServiceEventTargetForScope(ctx, tx, command.WorkspaceID, serviceMember.MemberID, message.ScopeIDForThreadAccess(threadAccess))
		if err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		binding, err := store.upsertAppNotificationTargetBindingInTx(ctx, tx, installation, serviceMember, target, command.ExecutionActor, now)
		if err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		recipientMemberID := strings.TrimSpace(fact.RecipientMemberID)
		basisKind := strings.TrimSpace(fact.Kind)
		basisRef := strings.TrimSpace(fact.Ref)
		policyVersion := policy.Version
		factVersion := fact.Version
		effectiveExpiresAt := command.ExpiresAt
		for _, candidate := range []*time.Time{grant.ExpiresAt, fact.ExpiresAt} {
			if candidate != nil && (effectiveExpiresAt == nil || candidate.Before(*effectiveExpiresAt)) {
				value := candidate.UTC()
				effectiveExpiresAt = &value
			}
		}
		return resolvedAppNotificationTarget{
			Target: target, ThreadAccess: threadAccess, Binding: binding,
			RecipientMemberID: &recipientMemberID, RecipientPolicyVersion: &policyVersion,
			RecipientBasisKind: &basisKind, RecipientBasisRef: &basisRef, RecipientFactVersion: &factVersion,
			EffectiveExpiresAt: effectiveExpiresAt,
		}, nil
	case AppNotificationTargetScope:
		target, err := ResolveServiceEventTargetForScope(ctx, tx, command.WorkspaceID, serviceMember.MemberID, command.Target.ScopeID)
		if err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		binding, found, err := store.serviceActorStore().InTx(tx).LoadActiveTargetBindingForUpdate(ctx, command.WorkspaceID, target.TargetKind, target.TargetRef, installation.ID)
		if err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		if !found || binding.ServiceMemberID != serviceMember.ID || binding.DeliveryScopeID != target.DeliveryScopeID || binding.PermissionCheckScopeID != target.PermissionCheckScopeID {
			return resolvedAppNotificationTarget{}, forbidden("App notification scope target is not actively bound")
		}
		threadAccess, err := loadServiceEventDeliveryTarget(ctx, tx, serviceEventDeliveryTargetBinding{
			ID: binding.ID, WorkspaceID: binding.WorkspaceID, TargetKind: binding.TargetKind, TargetRef: binding.TargetRef,
			DeliveryScopeID: binding.DeliveryScopeID, PermissionCheckScopeID: binding.PermissionCheckScopeID,
			ServiceDefinitionID: binding.ServiceDefinitionID, ServiceInstallationID: binding.ServiceInstallationID,
			ServiceMemberID: binding.ServiceMemberID, Status: binding.Status,
		})
		if err != nil {
			return resolvedAppNotificationTarget{}, err
		}
		return resolvedAppNotificationTarget{Target: target, ThreadAccess: threadAccess, Binding: binding}, nil
	default:
		return resolvedAppNotificationTarget{}, badRequest("App notification target kind is not supported")
	}
}

func (store *Store) upsertAppNotificationTargetBindingInTx(ctx context.Context, tx coredb.Tx, installation serviceactor.Installation, serviceMember serviceactor.Member, target ServiceEventTarget, execution AppNotificationExecutionActor, now time.Time) (serviceactor.TargetBinding, error) {
	snapshot, err := model.MarshalJSONObject("App notification target snapshot", target.ScopeSnapshot)
	if err != nil {
		return serviceactor.TargetBinding{}, err
	}
	metadata, err := model.MarshalJSONObject("App notification target metadata", map[string]any{
		"event_type": appNotificationEventAction, "subscription_kind": serviceactor.SubscriptionKindAppNotificationAction,
		"source": "app_notification",
	})
	if err != nil {
		return serviceactor.TargetBinding{}, err
	}
	actors := store.serviceActorStore().InTx(tx)
	activeBindingKey := appNotificationBindingKey(installation.WorkspaceID, installation.ID, target.TargetKind, target.TargetRef)
	return actors.UpsertTargetBinding(ctx, serviceactor.TargetBindingUpsert{
		WorkspaceID: installation.WorkspaceID, ServiceDefinitionID: installation.ServiceDefinitionID,
		ServiceInstallationID: installation.ID, ServiceMemberID: serviceMember.ID, ServiceKind: serviceactor.KindApp,
		TargetKind: target.TargetKind, TargetRef: target.TargetRef, DeliveryScopeID: target.DeliveryScopeID,
		PermissionCheckScopeID: target.PermissionCheckScopeID, AuthoritySubjectMemberID: serviceMember.MemberID,
		DeliveryContract:  serviceEventDeliveryContractJSON(ServiceEventActor{ActorType: execution.Type, ActorID: execution.ID, MemberID: serviceMember.MemberID}, target),
		CreatedByMemberID: serviceMember.MemberID, CreatedByActorType: execution.Type, CreatedByActorID: execution.ID,
		CreatedVia: appNotificationCreatedVia, ScopeSnapshot: snapshot, Metadata: metadata,
		ActiveBindingKey: activeBindingKey, Now: now,
	})
}

func (store *Store) ensureAppNotificationActionSubscriptionInTx(ctx context.Context, tx coredb.Tx, installation serviceactor.Installation, serviceMember serviceactor.Member, resolved resolvedAppNotificationTarget, notification AppNotificationRecord, command AppNotificationCommand, now time.Time) (serviceactor.EventSubscription, error) {
	deliveryPolicy, connectionRef, err := store.appNotificationActionRoute(command)
	if err != nil {
		return serviceactor.EventSubscription{}, err
	}
	ownerMemberID := serviceMember.MemberID
	if resolved.RecipientMemberID != nil {
		ownerMemberID = strings.TrimSpace(*resolved.RecipientMemberID)
	}
	filters, err := model.MarshalJSONObject("App notification action filters", map[string]any{"events": []string{appNotificationEventAction}})
	if err != nil {
		return serviceactor.EventSubscription{}, err
	}
	metadata, err := model.MarshalJSONObject("App notification action subscription metadata", map[string]any{
		"event_type": appNotificationEventAction, "notification_id": notification.ID,
	})
	if err != nil {
		return serviceactor.EventSubscription{}, err
	}
	subscription, _, err := store.serviceActorStore().InTx(tx).UpsertEventSubscription(ctx, serviceactor.EventSubscriptionUpsert{
		WorkspaceID: notification.WorkspaceID, ServiceDefinitionID: notification.ServiceDefinitionID,
		ServiceInstallationID: notification.ServiceInstallationID, ServiceMemberID: notification.ServiceMemberID,
		ServiceConnectionRef: connectionRef, ServiceTargetBindingID: notification.ServiceTargetBindingID,
		ServiceKind: serviceactor.KindApp, SubscriptionKind: serviceactor.SubscriptionKindAppNotificationAction,
		TargetKind: resolved.Target.TargetKind, TargetRef: resolved.Target.TargetRef,
		DeliveryScopeID: resolved.Target.DeliveryScopeID, PermissionCheckScopeID: resolved.Target.PermissionCheckScopeID,
		OwnerMemberID: ownerMemberID, AuthorizerMemberID: ownerMemberID,
		CreatedByActorType: command.ExecutionActor.Type, CreatedByActorID: command.ExecutionActor.ID,
		CreatedVia: appNotificationCreatedVia, ExternalResourceType: appNotificationExternalResource,
		ExternalResourceKey: notification.ID, ExternalResourceRef: notification.ID,
		EventFilterKey: appNotificationEventFilterKey(notification.ID), EventFilters: filters,
		DeliveryPolicy: deliveryPolicy, NotifyPrincipal: false,
		Status:                serviceactor.EventSubscriptionStatusActive,
		ActiveSubscriptionKey: appNotificationSubscriptionKey(notification.WorkspaceID, notification.ServiceInstallationID, notification.ID),
		Metadata:              metadata, Now: now,
	})
	return subscription, err
}

func (store *Store) appNotificationActionRoute(command AppNotificationCommand) (string, string, error) {
	if strings.TrimSpace(command.Ingress) == AppNotificationIngressInternal {
		identityID := strings.TrimSpace(command.ExecutionActor.ID)
		store.appNotificationRegistryMu.RLock()
		service, ok := store.appNotificationInternalServices[identityID]
		store.appNotificationRegistryMu.RUnlock()
		if ok && strings.TrimSpace(service.ActionHandlerKey) != "" {
			connectionRef, err := serviceactor.InternalHandlerConnectionRef(service.ActionHandlerKey)
			if err != nil {
				return "", "", err
			}
			return serviceactor.AppRuntimeDeliveryPolicyInternalHandler, connectionRef, nil
		}
	}
	return serviceactor.AppRuntimeDeliveryPolicyExternal, serviceactor.AppRuntimeConnectionRefExternal, nil
}

func appNotificationRecordForCreate(command AppNotificationCommand, installation serviceactor.Installation, serviceMember serviceactor.Member, resolved resolvedAppNotificationTarget, notificationID, messageID, digest string, now time.Time) AppNotificationRecord {
	record := AppNotificationRecord{
		ID: notificationID, WorkspaceID: command.WorkspaceID, ServiceDefinitionID: installation.ServiceDefinitionID,
		ServiceInstallationID: installation.ID, ServiceMemberID: serviceMember.ID,
		ServiceTargetBindingID: resolved.Binding.ID, TargetKind: command.Target.Kind,
		TargetRef: firstNonEmpty(command.Target.MemberID, command.Target.ScopeID), ScopeID: resolved.Target.DeliveryScopeID,
		RecipientMemberID: resolved.RecipientMemberID, RecipientPolicyVersion: resolved.RecipientPolicyVersion,
		RecipientBasisKind: resolved.RecipientBasisKind, RecipientBasisRef: resolved.RecipientBasisRef,
		RecipientFactVersion: resolved.RecipientFactVersion, MessageID: messageID, ContentKind: command.Content.Kind,
		ClientNotificationID: command.ClientNotificationID, CreatePayloadDigest: digest,
		Status: AppNotificationStatusActive, Revision: 1,
		CausedByActorType:  firstNonEmpty(command.CausedByActor.Type, "service"),
		CausedByActorID:    firstNonEmpty(command.CausedByActor.ID, serviceMember.ID),
		ExecutionActorType: command.ExecutionActor.Type, ExecutionActorID: command.ExecutionActor.ID,
		CorrelationID: command.CorrelationID, ExpiresAt: command.ExpiresAt, CreatedAt: now, UpdatedAt: now,
	}
	if !command.BusinessObject.IsZero() {
		record.BusinessObjectNamespace = stringPointer(command.BusinessObject.Namespace)
		record.BusinessObjectType = stringPointer(command.BusinessObject.Type)
		record.BusinessObjectID = stringPointer(command.BusinessObject.ID)
		record.BusinessObjectVersion = int64Pointer(command.BusinessObject.Version)
		if command.BusinessObject.URL != "" {
			record.BusinessObjectURL = stringPointer(command.BusinessObject.URL)
		}
	}
	return record
}

func (store *Store) appNotificationResultInTx(ctx context.Context, queryer coredb.QueryRower, notification AppNotificationRecord) (AppNotificationResult, error) {
	var seq int64
	if err := queryer.QueryRow(ctx, `
SELECT seq
FROM scope_messages
WHERE workspace_id = $1 AND scope_id = $2 AND id = $3
`, notification.WorkspaceID, notification.ScopeID, notification.MessageID).Scan(&seq); err != nil {
		return AppNotificationResult{}, fmt.Errorf("load App notification message sequence: %w", err)
	}
	return AppNotificationResult{
		Accepted: true, WorkspaceID: notification.WorkspaceID, NotificationID: notification.ID, NotificationRevision: notification.Revision,
		MessageID: notification.MessageID, ScopeID: notification.ScopeID, MessageSeq: seq,
		Status: notification.Status, ContentKind: notification.ContentKind, CorrelationID: notification.CorrelationID,
		ExpiresAt: notification.ExpiresAt, ResolvedAt: notification.ResolvedAt, WithdrawnAt: notification.WithdrawnAt,
		CreatedAt: notification.CreatedAt, UpdatedAt: notification.UpdatedAt,
	}, nil
}

func requireActiveHumanAppNotificationRecipient(ctx context.Context, queryer coredb.QueryRower, workspaceID, memberID string) error {
	var marker int
	if err := queryer.QueryRow(ctx, `
SELECT 1 FROM members
WHERE workspace_id = $1 AND id = $2 AND type = 'human' AND status = 'active'
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(memberID)).Scan(&marker); err != nil {
		if coredb.IsNotFound(err) {
			return forbidden("App notification Human recipient is not active")
		}
		return fmt.Errorf("load App notification Human recipient: %w", err)
	}
	return nil
}

func enforceAppNotificationCreateAdmission(ctx context.Context, queryer coredb.QueryRower, command AppNotificationCommand, now time.Time) error {
	windowStart := now.Add(-time.Minute)
	var installationCount int
	if err := queryer.QueryRow(ctx, `
SELECT COUNT(*) FROM service_notifications
WHERE workspace_id = $1 AND service_installation_id = $2 AND created_at >= $3
`, command.WorkspaceID, command.ServiceInstallationID, windowStart).Scan(&installationCount); err != nil {
		return fmt.Errorf("count App notification installation admission: %w", err)
	}
	if installationCount >= appNotificationInstallationLimit {
		return storeerr.Conflict("app_notification_rate_limited")
	}
	var targetCount int
	switch command.Target.Kind {
	case AppNotificationTargetHumanDM:
		if err := queryer.QueryRow(ctx, `
SELECT COUNT(*) FROM service_notifications
WHERE workspace_id = $1 AND recipient_member_id = $2 AND created_at >= $3
`, command.WorkspaceID, command.Target.MemberID, windowStart).Scan(&targetCount); err != nil {
			return fmt.Errorf("count App notification recipient admission: %w", err)
		}
		if targetCount >= appNotificationRecipientLimit {
			return storeerr.Conflict("app_notification_recipient_rate_limited")
		}
	case AppNotificationTargetScope:
		if err := queryer.QueryRow(ctx, `
SELECT COUNT(*) FROM service_notifications
WHERE workspace_id = $1 AND scope_id = $2 AND created_at >= $3
`, command.WorkspaceID, command.Target.ScopeID, windowStart).Scan(&targetCount); err != nil {
			return fmt.Errorf("count App notification scope admission: %w", err)
		}
		if targetCount >= appNotificationScopeLimit {
			return storeerr.Conflict("app_notification_scope_rate_limited")
		}
	}
	return nil
}

func authorizeAppNotificationManifest(capability registry.StoredAppRuntimeCapability, command AppNotificationCommand, card *appNotificationCardDocument) error {
	if !capability.Supported || !capability.Notifications.Supported {
		return forbidden("App manifest does not declare proactive notifications")
	}
	manifestTarget := command.Target.Kind
	if manifestTarget == AppNotificationTargetScope {
		manifestTarget = "bound_scope"
	}
	if !stringSliceContains(capability.Notifications.Targets, manifestTarget) || !stringSliceContains(capability.Notifications.Content, command.Content.Kind) {
		return forbidden("App manifest does not allow this notification target or content kind")
	}
	if card != nil && hasAppNotificationCallbackAction(*card) && !stringSliceContains(capability.Events, registry.AppEventNotificationAction) {
		return forbidden("App manifest does not declare notification action events")
	}
	return nil
}

func hasAppNotificationCallbackAction(card appNotificationCardDocument) bool {
	for _, action := range card.Actions {
		if strings.TrimSpace(action.Type) == "callback" {
			return true
		}
	}
	return false
}

func appNotificationBindingKey(workspaceID, installationID, targetKind, targetRef string) string {
	return "app-target:" + appNotificationStableKey(workspaceID, installationID, targetKind, targetRef)
}

func appNotificationSubscriptionKey(workspaceID, installationID, notificationID string) string {
	return "app-notification-action:" + appNotificationStableKey(workspaceID, installationID, notificationID)
}

func appNotificationEventFilterKey(notificationID string) string {
	return "app-action:" + appNotificationStableKey(notificationID, appNotificationEventAction)
}

func appNotificationStableKey(parts ...string) string {
	joined := make([]string, 0, len(parts))
	for _, part := range parts {
		joined = append(joined, strings.TrimSpace(part))
	}
	digest := sha256.Sum256([]byte(strings.Join(joined, "\x00")))
	return hex.EncodeToString(digest[:16])
}

func stringPointer(value string) *string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return nil
	}
	return &trimmed
}

func int64Pointer(value int64) *int64 {
	return &value
}

func appNotificationBusinessObjectFromRecord(record AppNotificationRecord) AppNotificationBusinessObject {
	object := AppNotificationBusinessObject{}
	if record.BusinessObjectNamespace != nil {
		object.Namespace = *record.BusinessObjectNamespace
	}
	if record.BusinessObjectType != nil {
		object.Type = *record.BusinessObjectType
	}
	if record.BusinessObjectID != nil {
		object.ID = *record.BusinessObjectID
	}
	if record.BusinessObjectVersion != nil {
		object.Version = *record.BusinessObjectVersion
	}
	if record.BusinessObjectURL != nil {
		object.URL = *record.BusinessObjectURL
	}
	return normalizeAppNotificationBusinessObject(object)
}

func appNotificationRecipientBasisFromRecord(record AppNotificationRecord) AppNotificationRecipientBasis {
	basis := AppNotificationRecipientBasis{}
	if record.RecipientBasisKind != nil {
		basis.Kind = strings.TrimSpace(*record.RecipientBasisKind)
	}
	if record.RecipientBasisRef != nil {
		basis.Ref = strings.TrimSpace(*record.RecipientBasisRef)
	}
	return basis
}
