package app

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"unicode"
	"unicode/utf8"

	"github.com/google/uuid"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/coordinator"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
)

const (
	serviceAccountPublisherMetadataKey           = "serviceAccountPublisher"
	serviceAccountPostMetadataKey                = "serviceAccountPost"
	serviceAccountMessageAuditType               = "service_account.message.created"
	serviceAccountMessageManagerAuditBasis       = "service_grant+service_account_manager"
	serviceAccountMessageWorkspaceRoleAuditBasis = "service_grant+workspace_role"
)

type serviceAccountPublisherAuthority struct {
	account             serviceaccount.Account
	membership          access.HumanWorkspaceMember
	managerAssignmentID string
	managerVersion      int64
	permissionBasisKind string
	permissionBasisRef  string
	grantID             string
	grantPolicyVersion  int64
	targetBindingID     string
	canonicalUserName   string
	canonicalDisplay    string
	canonicalAvatarURL  string
}

type preparedServiceAccountPublisherMessage struct {
	authority               serviceAccountPublisherAuthority
	target                  message.ThreadAccess
	trimmedContent          string
	normalizedAttachmentIDs []string
	normalizedMessageType   string
	metadata                map[string]any
	clientMessageID         string
	messageID               string
}

func (store *Store) messageScopeType(ctx context.Context, workspaceID string, scopeID string) (string, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	scopeID = strings.TrimSpace(scopeID)
	if workspaceID == "" || scopeID == "" {
		return "", nil
	}
	var scopeType string
	if err := store.db.QueryRow(ctx, `
SELECT type
FROM scopes
WHERE workspace_id = $1 AND id = $2
LIMIT 1
`, workspaceID, scopeID).Scan(&scopeType); err != nil {
		if coredb.IsNotFound(err) {
			return "", nil
		}
		return "", fmt.Errorf("detect message scope type: %w", err)
	}
	return strings.TrimSpace(scopeType), nil
}

func (store *Store) sendMessageForUserTargetAware(ctx context.Context, user model.User, workspaceID string, scopeID string, content string, attachmentIDs []string, options SendAppMessageOptions) (message.AppMessage, message.MessageDelivery, error) {
	scopeType, err := store.messageScopeType(ctx, workspaceID, scopeID)
	if err != nil {
		return message.AppMessage{}, message.MessageDelivery{}, err
	}
	switch scopeType {
	case "task":
		if message.NormalizeAppMessageType(options.MessageType) == message.MessageTypeServiceAccountPost {
			return message.AppMessage{}, message.MessageDelivery{}, badRequest("service_account_post is only allowed in service-account scopes")
		}
		return store.sendTaskScopeMessageForUser(ctx, user, workspaceID, scopeID, content, attachmentIDs, options)
	case serviceaccount.ServiceKind:
		return store.sendServiceAccountPublisherMessage(ctx, user, workspaceID, scopeID, content, attachmentIDs, options)
	default:
		if message.NormalizeAppMessageType(options.MessageType) == message.MessageTypeServiceAccountPost {
			return message.AppMessage{}, message.MessageDelivery{}, badRequest("service_account_post is only allowed in service-account scopes")
		}
		return store.sendAppMessageForUser(ctx, user, workspaceID, scopeID, content, attachmentIDs, options)
	}
}

func (store *Store) validateMessageForUserTargetAware(ctx context.Context, user model.User, workspaceID string, scopeID string, content string, options SendAppMessageOptions, auditModerationBlock bool) error {
	scopeType, err := store.messageScopeType(ctx, workspaceID, scopeID)
	if err != nil {
		return err
	}
	switch scopeType {
	case "task":
		if message.NormalizeAppMessageType(options.MessageType) == message.MessageTypeServiceAccountPost {
			return badRequest("service_account_post is only allowed in service-account scopes")
		}
		return store.validateTaskScopeMessageForUser(ctx, user, workspaceID, scopeID, content, options, auditModerationBlock)
	case serviceaccount.ServiceKind:
		return store.validateServiceAccountPublisherMessage(ctx, user, workspaceID, scopeID, content, options, auditModerationBlock)
	default:
		if message.NormalizeAppMessageType(options.MessageType) == message.MessageTypeServiceAccountPost {
			return badRequest("service_account_post is only allowed in service-account scopes")
		}
		return store.validateMessageForUser(ctx, user, workspaceID, scopeID, content, options, auditModerationBlock)
	}
}

func (store *Store) validateServiceAccountPublisherMessage(ctx context.Context, user model.User, workspaceID string, scopeID string, content string, options SendAppMessageOptions, auditModerationBlock bool) error {
	messageID, err := identifiers.NewMessageID()
	if err != nil {
		return fmt.Errorf("generate service-account validation message id: %w", err)
	}
	tx, err := store.db.BeginTx(ctx, coredb.TxOptions{})
	if err != nil {
		return fmt.Errorf("begin service-account message validation tx: %w", err)
	}
	var moderationBlock *moderationBlockRecord
	validationErr := func() error {
		prepared, err := store.prepareServiceAccountPublisherMessageTx(ctx, tx, user, workspaceID, scopeID, content, nil, options, messageID)
		if err != nil {
			return err
		}
		block, blocked, err := store.buildModerationBlockForHumanMessage(ctx, prepared.moderationInput(), user.ID, workspaceID, options.SkipContentModeration)
		if err != nil {
			return err
		}
		if blocked {
			moderationBlock = &block
			return message.ContentModerationBlocked(contentModerationBlockedMessage)
		}
		return nil
	}()
	if rollbackErr := tx.Rollback(ctx); rollbackErr != nil {
		rollbackErr = fmt.Errorf("rollback service-account message validation tx: %w", rollbackErr)
		if validationErr != nil {
			return errors.Join(validationErr, rollbackErr)
		}
		return rollbackErr
	}
	if auditModerationBlock && errors.Is(validationErr, message.ErrContentModerationBlocked) && moderationBlock != nil {
		if insertErr := store.insertModerationBlock(ctx, store.db, *moderationBlock); insertErr != nil {
			return insertErr
		}
	}
	return validationErr
}

func (store *Store) sendServiceAccountPublisherMessage(ctx context.Context, user model.User, workspaceID string, scopeID string, content string, attachmentIDs []string, options SendAppMessageOptions) (message.AppMessage, message.MessageDelivery, error) {
	messageID, err := identifiers.NewMessageID()
	if err != nil {
		return message.AppMessage{}, message.MessageDelivery{}, fmt.Errorf("generate service-account message id: %w", err)
	}

	var appMessage message.AppMessage
	var delivery message.MessageDelivery
	var moderationBlock *moderationBlockRecord
	if err := coordinator.New(store.db).Run(ctx, "send service-account publisher message", func(tx coredb.Tx) error {
		prepared, err := store.prepareServiceAccountPublisherMessageTx(ctx, tx, user, workspaceID, scopeID, content, attachmentIDs, options, messageID)
		if err != nil {
			return err
		}
		block, blocked, err := store.buildModerationBlockForHumanMessage(ctx, prepared.moderationInput(), user.ID, workspaceID, options.SkipContentModeration)
		if err != nil {
			return err
		}
		if blocked {
			moderationBlock = &block
			return message.ContentModerationBlocked(contentModerationBlockedMessage)
		}

		emptyRoutingPlan := message.AgentRoutingPlan{}
		source := message.ContributionSource{
			Kind:    message.ContributionSourceKindServiceDirectPost,
			RefType: message.ContributionSourceRefTypeMessage,
			RefID:   prepared.messageID,
			Label:   prepared.authority.account.Name,
		}
		delivery, err = store.appendMessageToThread(ctx, tx,
			prepared.authority.account.AuthorMemberID,
			domain.MemberTypeService,
			prepared.target,
			prepared.trimmedContent,
			prepared.normalizedAttachmentIDs,
			appendMessageOptions{
				AllowWorkflowContextMetadata: options.AllowWorkflowContextMetadata,
				MentionHints:                 options.MentionHints,
				MessageType:                  prepared.normalizedMessageType,
				ExplicitRoutingPlan:          &emptyRoutingPlan,
				Metadata:                     prepared.metadata,
				Source:                       source,
				RoutingPrincipalMemberID:     prepared.authority.account.AuthorMemberID,
				NotifyPrincipalMemberID:      prepared.authority.account.AuthorMemberID,
				IdempotencySubjectMemberID:   prepared.authority.account.AuthorMemberID,
				ClientMessageID:              prepared.clientMessageID,
				OwnerQuoteSourceMessageIDs:   options.OwnerQuoteSourceMessageIDs,
				ForceScopeID:                 prepared.authority.account.MessageScopeID,
				ForceMessageID:               prepared.messageID,
				AllocateWorkspaceGlobalSeq:   true,
			},
			messageAppendCallerOwned,
		)
		if err != nil {
			return err
		}

		// A replay may have been requested with a freshly generated candidate id.
		// Always bind the presentation source to the authoritative stored message.
		source.RefID = delivery.MessageID
		delivery.Message.Source = source.Normalize()
		delivery.Message.ChannelType = serviceaccount.ServiceKind
		delivery.Message.ChannelName = prepared.authority.account.Name
		delivery.Message.ChannelDisplayName = prepared.authority.account.Name
		delivery.MentionContext.ChannelType = serviceaccount.ServiceKind

		appMessage = appMessageFromDelivery(prepared.authority.account.MessageScopeID, message.AuthorProfile{
			ActorType:   message.AuthorActorTypeService,
			PrincipalID: prepared.authority.account.PrincipalID,
			MemberID:    prepared.authority.account.AuthorMemberID,
			TargetID:    "service-account:" + prepared.authority.account.ID,
			DisplayName: prepared.authority.account.Name,
			ServiceKind: message.AuthorServiceKindServiceAccount,
			ServiceSlug: prepared.authority.account.ID,
		}, delivery)
		appMessage.ChannelType = serviceaccount.ServiceKind
		appMessage.Source = source.Normalize()
		messages := []message.AppMessage{appMessage}
		if err := attachServiceAuthorProfilePresentationWithQueryer(ctx, tx, workspaceID, messages); err != nil {
			return fmt.Errorf("attach service-account publisher message presentation: %w", err)
		}
		appMessage = messages[0]
		if err := attachResolvedMentionsForSentAppMessage(ctx, tx, workspaceID, &appMessage); err != nil {
			return err
		}
		attachAppMessageToDelivery(&delivery, &appMessage)
		if !delivery.IdempotentReplay {
			if err := insertServiceAccountMessageAudit(ctx, serviceaccount.New(store.db).InTx(tx), prepared, delivery); err != nil {
				return err
			}
			if err := store.persistScopeFanoutPayloads(ctx, tx, delivery); err != nil {
				return err
			}
		}
		return nil
	}); err != nil {
		if errors.Is(err, message.ErrContentModerationBlocked) && moderationBlock != nil {
			if insertErr := store.insertModerationBlock(ctx, store.db, *moderationBlock); insertErr != nil {
				return message.AppMessage{}, message.MessageDelivery{}, insertErr
			}
		}
		return message.AppMessage{}, message.MessageDelivery{}, mapMessagingError(err)
	}
	return appMessage, delivery, nil
}

func (store *Store) prepareServiceAccountPublisherMessageTx(ctx context.Context, tx coredb.Tx, user model.User, workspaceID string, scopeID string, content string, attachmentIDs []string, options SendAppMessageOptions, messageID string) (preparedServiceAccountPublisherMessage, error) {
	if err := message.ValidateWorkflowContextMetadata(options.Metadata, options.AllowWorkflowContextMetadata); err != nil {
		return preparedServiceAccountPublisherMessage{}, badRequest(err.Error())
	}
	workspaceID = strings.TrimSpace(workspaceID)
	scopeID = strings.TrimSpace(scopeID)
	if workspaceID == "" || scopeID == "" {
		return preparedServiceAccountPublisherMessage{}, badRequest("workspaceId and scopeId are required")
	}
	if options.StartAgentThread {
		return preparedServiceAccountPublisherMessage{}, badRequest("service-account messages cannot start agent threads")
	}
	if strings.TrimSpace(options.InterruptPolicy) != "" {
		return preparedServiceAccountPublisherMessage{}, badRequest("service-account messages do not support interruptPolicy")
	}
	if !options.Source.IsZero() {
		return preparedServiceAccountPublisherMessage{}, badRequest("service-account contribution source is server-owned")
	}
	clientMessageID, err := normalizeServiceAccountClientMessageID(options.ClientMessageID)
	if err != nil {
		return preparedServiceAccountPublisherMessage{}, err
	}
	normalizedAttachmentIDs, err := normalizeAttachmentIDs(attachmentIDs)
	if err != nil {
		return preparedServiceAccountPublisherMessage{}, err
	}
	trimmedContent := strings.TrimSpace(content)
	if trimmedContent == "" && len(normalizedAttachmentIDs) == 0 {
		return preparedServiceAccountPublisherMessage{}, badRequest("content is required")
	}
	normalizedMessageType := message.NormalizeAppMessageType(options.MessageType)
	if strings.TrimSpace(options.MessageType) != "" && normalizedMessageType == "" {
		return preparedServiceAccountPublisherMessage{}, badRequest("unsupported messageType")
	}
	if normalizedMessageType != message.MessageTypeServiceAccountPost {
		return preparedServiceAccountPublisherMessage{}, badRequest("service-account publishing requires messageType service_account_post")
	}
	postMetadata, err := normalizeServiceAccountPostMetadata(options.Metadata)
	if err != nil {
		return preparedServiceAccountPublisherMessage{}, err
	}
	semantics := message.ClassifyMessageSemantics(normalizedMessageType, options.Metadata)
	if !semantics.CurrentWriteAllowed() {
		return preparedServiceAccountPublisherMessage{}, badRequest("message type is not allowed for active writes")
	}
	if semantics.RenderKind == message.MessageRenderSticker {
		if trimmedContent != "" {
			return preparedServiceAccountPublisherMessage{}, badRequest("sticker messages do not support text")
		}
		if len(normalizedAttachmentIDs) == 0 {
			return preparedServiceAccountPublisherMessage{}, badRequest("sticker messages require attachments")
		}
	}

	membership, err := store.loadAuthorizedHumanWorkspaceMember(ctx, tx, user.ID, workspaceID, workspaceAccessAction)
	if err != nil {
		return preparedServiceAccountPublisherMessage{}, err
	}
	authority, err := loadServiceAccountPublisherAuthority(ctx, tx, membership, workspaceID, scopeID)
	if err != nil {
		return preparedServiceAccountPublisherMessage{}, err
	}
	metadata, err := serviceAccountPublisherMetadata(postMetadata, authority, user.ID)
	if err != nil {
		return preparedServiceAccountPublisherMessage{}, err
	}
	target := message.ThreadAccess{
		ThreadID:    authority.account.MessageScopeID,
		ThreadType:  domain.ThreadTypeChannel,
		ChannelID:   authority.account.MessageScopeID,
		ChannelName: authority.account.Name,
	}
	if semantics.RenderKind == message.MessageRenderOwnerQuote {
		if err := store.validateOwnerQuoteMessageForTarget(ctx, tx, workspaceID, target, trimmedContent); err != nil {
			return preparedServiceAccountPublisherMessage{}, err
		}
	}
	return preparedServiceAccountPublisherMessage{
		authority:               authority,
		target:                  target,
		trimmedContent:          trimmedContent,
		normalizedAttachmentIDs: normalizedAttachmentIDs,
		normalizedMessageType:   normalizedMessageType,
		metadata:                metadata,
		clientMessageID:         clientMessageID,
		messageID:               strings.TrimSpace(messageID),
	}, nil
}

func normalizeServiceAccountPostMetadata(clientMetadata map[string]any) (map[string]any, error) {
	metadata := message.CopyMessageMetadata(clientMetadata)
	raw, ok := metadata[serviceAccountPostMetadataKey]
	if !ok {
		return nil, badRequest(serviceAccountPostMetadataKey + ".title is required")
	}
	post, ok := raw.(map[string]any)
	if !ok {
		return nil, badRequest(serviceAccountPostMetadataKey + " metadata must be an object")
	}
	title, ok := post["title"].(string)
	title = strings.TrimSpace(title)
	if !ok || title == "" || utf8.RuneCountInString(title) > 120 {
		return nil, badRequest(serviceAccountPostMetadataKey + ".title must be between 1 and 120 characters")
	}
	for _, r := range title {
		if unicode.IsControl(r) {
			return nil, badRequest(serviceAccountPostMetadataKey + ".title cannot contain control characters")
		}
	}
	metadata[serviceAccountPostMetadataKey] = map[string]any{"title": title}
	return metadata, nil
}

func (prepared preparedServiceAccountPublisherMessage) moderationInput() preparedUserAppMessage {
	return preparedUserAppMessage{
		membership:              prepared.authority.membership,
		target:                  prepared.target,
		trimmedContent:          prepared.trimmedContent,
		normalizedAttachmentIDs: prepared.normalizedAttachmentIDs,
		normalizedMessageType:   prepared.normalizedMessageType,
		metadata:                prepared.metadata,
	}
}

func normalizeServiceAccountClientMessageID(raw string) (string, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return "", badRequest("clientMessageId is required for service-account messages")
	}
	parsed, err := uuid.Parse(trimmed)
	if err != nil {
		return "", badRequest("clientMessageId must be a UUID")
	}
	return parsed.String(), nil
}

func loadServiceAccountPublisherAuthority(ctx context.Context, tx coredb.Tx, membership access.HumanWorkspaceMember, workspaceID string, scopeID string) (serviceAccountPublisherAuthority, error) {
	var authority serviceAccountPublisherAuthority
	authority.membership = membership
	row := tx.QueryRow(ctx, `
SELECT
  account.id, account.principal_id, account.workspace_id,
  account.management_kind, COALESCE(account.platform_service_account_id, ''),
  account.service_installation_id, account.service_member_id,
  account.author_member_id, account.message_scope_id,
  account.name, account.description,
  COALESCE(account.avatar_asset_id, ''),
  account.audience_policy_version, account.effective_audience_version,
  account.audience_state, account.version,
  COALESCE(manager.id, ''), COALESCE(manager.version, 0),
  publisher_member.workspace_role,
  service_grant.id, service_grant.policy_version,
  service_binding.id,
  COALESCE(u.name, ''), COALESCE(u.display_name, ''), COALESCE(u.avatar_url, '')
FROM scopes s
JOIN service_accounts account
  ON account.workspace_id = s.workspace_id
 AND account.message_scope_id = s.id
 AND account.management_kind = 'workspace'
 AND account.audience_state = 'ready'
 AND account.effective_audience_version = account.audience_policy_version
JOIN scope_memberships audience
  ON audience.scope_id = s.id
 AND audience.member_id = $3
 AND audience.visibility = 'public'
 AND audience.effective_to_seq IS NULL
 AND audience.removed_at IS NULL
 AND audience.archived_at IS NULL
JOIN members publisher_member
  ON publisher_member.workspace_id = account.workspace_id
 AND publisher_member.id = $3
 AND publisher_member.user_id = $4
 AND publisher_member.type = 'human'
 AND publisher_member.status = 'active'
JOIN service_installations installation
  ON installation.id = account.service_installation_id
 AND installation.workspace_id = account.workspace_id
 AND installation.service_definition_id = 'svcdef_service_account'
 AND installation.service_kind = 'service_account'
 AND installation.status = 'active'
JOIN service_members service_member
  ON service_member.id = account.service_member_id
 AND service_member.workspace_id = account.workspace_id
 AND service_member.service_definition_id = 'svcdef_service_account'
 AND service_member.service_installation_id = installation.id
 AND service_member.member_id = account.author_member_id
 AND service_member.service_kind = 'service_account'
 AND service_member.status = 'active'
JOIN members durable_author
  ON durable_author.id = account.author_member_id
 AND durable_author.workspace_id = account.workspace_id
 AND durable_author.type = 'service'
 AND durable_author.status = 'active'
JOIN service_grants service_grant
  ON service_grant.workspace_id = account.workspace_id
 AND service_grant.service_definition_id = 'svcdef_service_account'
 AND service_grant.service_installation_id = installation.id
 AND service_grant.service_kind = 'service_account'
 AND service_grant.scope = 'message.write'
 AND service_grant.revoked_at IS NULL
 AND (service_grant.expires_at IS NULL OR service_grant.expires_at > CURRENT_TIMESTAMP)
JOIN service_target_bindings service_binding
  ON service_binding.workspace_id = account.workspace_id
 AND service_binding.service_definition_id = 'svcdef_service_account'
 AND service_binding.service_installation_id = installation.id
 AND service_binding.service_member_id = account.service_member_id
 AND service_binding.service_kind = 'service_account'
 AND service_binding.target_kind = 'service_account'
 AND service_binding.target_ref = CONCAT('service-account:', account.id)
 AND service_binding.delivery_scope_id = s.id
 AND service_binding.permission_check_scope_id = s.id
 AND service_binding.authority_subject_member_id = account.author_member_id
 AND service_binding.status = 'active'
 AND service_binding.removed_at IS NULL
LEFT JOIN service_account_managers manager
  ON manager.workspace_id = account.workspace_id
 AND manager.service_account_id = account.id
 AND manager.member_id = publisher_member.id
 AND manager.status = 'active'
 AND manager.ended_at IS NULL
JOIN users u ON u.id = publisher_member.user_id
WHERE s.workspace_id = $1
  AND s.id = $2
  AND s.type = 'service_account'
  AND s.status = 'active'
  AND s.archived_at IS NULL
  AND account.management_kind = 'workspace'
  AND (
    manager.id IS NOT NULL
    OR publisher_member.workspace_role IN ('owner', 'admin')
  )
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID), membership.Member.ID, valueOrEmpty(membership.Member.UserID)).
		Scan(
			&authority.account.ID, &authority.account.PrincipalID, &authority.account.WorkspaceID,
			&authority.account.ManagementKind, &authority.account.PlatformServiceAccountID,
			&authority.account.ServiceInstallationID, &authority.account.ServiceMemberID,
			&authority.account.AuthorMemberID, &authority.account.MessageScopeID,
			&authority.account.Name, &authority.account.Description,
			&authority.account.AvatarAssetID,
			&authority.account.AudiencePolicyVersion, &authority.account.EffectiveAudienceVersion,
			&authority.account.AudienceState, &authority.account.Version,
			&authority.managerAssignmentID, &authority.managerVersion,
			&authority.membership.WorkspaceRole,
			&authority.grantID, &authority.grantPolicyVersion,
			&authority.targetBindingID,
			&authority.canonicalUserName, &authority.canonicalDisplay, &authority.canonicalAvatarURL,
		)
	if err := row; err != nil {
		if coredb.IsNotFound(err) {
			return serviceAccountPublisherAuthority{}, ErrForbidden
		}
		return serviceAccountPublisherAuthority{}, fmt.Errorf("resolve service-account publisher authority: %w", err)
	}
	if access.HasWorkspaceAdminRole(domain.WorkspaceRole(authority.membership.WorkspaceRole)) {
		authority.permissionBasisKind = serviceAccountMessageWorkspaceRoleAuditBasis
		authority.permissionBasisRef = authority.membership.WorkspaceRole
	} else {
		authority.permissionBasisKind = serviceAccountMessageManagerAuditBasis
		authority.permissionBasisRef = authority.managerAssignmentID
	}
	return authority, nil
}

func valueOrEmpty(value *string) string {
	if value == nil {
		return ""
	}
	return strings.TrimSpace(*value)
}

func serviceAccountPublisherMetadata(clientMetadata map[string]any, authority serviceAccountPublisherAuthority, authenticatedUserID string) (map[string]any, error) {
	metadata := message.CopyMessageMetadata(clientMetadata)
	if _, exists := metadata[serviceAccountPublisherMetadataKey]; exists {
		return nil, badRequest(serviceAccountPublisherMetadataKey + " metadata is server-owned")
	}
	if metadata == nil {
		metadata = make(map[string]any, 1)
	}
	actorRef := map[string]any{
		"actorType":   message.AuthorActorTypeHuman,
		"principalId": strings.TrimSpace(authenticatedUserID),
		"memberId":    authority.membership.Member.ID,
	}
	permissionBasis := map[string]any{
		"kind":    authority.permissionBasisKind,
		"grantId": authority.grantID,
	}
	if authority.permissionBasisKind == serviceAccountMessageManagerAuditBasis {
		permissionBasis["managerAssignmentId"] = authority.managerAssignmentID
	} else if authority.permissionBasisKind == serviceAccountMessageWorkspaceRoleAuditBasis {
		permissionBasis["workspaceRole"] = authority.permissionBasisRef
	}
	metadata[serviceAccountPublisherMetadataKey] = map[string]any{
		"bylineActorRef":  actorRef,
		"causedByActor":   actorRef,
		"executionActor":  actorRef,
		"permissionBasis": permissionBasis,
	}
	return metadata, nil
}

func insertServiceAccountMessageAudit(ctx context.Context, accountTx *serviceaccount.TxStore, prepared preparedServiceAccountPublisherMessage, delivery message.MessageDelivery) error {
	authority := prepared.authority
	metadata, err := json.Marshal(map[string]any{
		"durableAuthorMemberId":      authority.account.AuthorMemberID,
		"serviceInstallationId":      authority.account.ServiceInstallationID,
		"serviceMemberId":            authority.account.ServiceMemberID,
		"routingPrincipalMemberId":   authority.account.AuthorMemberID,
		"notifyPrincipalMemberId":    authority.account.AuthorMemberID,
		"idempotencySubjectMemberId": authority.account.AuthorMemberID,
		"managerMemberId":            authority.membership.Member.ID,
		"managerDisplayName":         preferredDisplayName(authority.canonicalDisplay, authority.membership.Member.DisplayName, authority.canonicalUserName, authority.membership.Member.ID),
		"managerAvatarUrl":           authority.canonicalAvatarURL,
		"managerVersion":             authority.managerVersion,
		"serviceGrantPolicyVersion":  authority.grantPolicyVersion,
		"serviceTargetBindingId":     authority.targetBindingID,
		"clientMessageId":            prepared.clientMessageID,
		"workspaceGlobalSeq":         delivery.GlobalSeq,
		"sourceKind":                 message.ContributionSourceKindServiceDirectPost,
		"bylineActorRef": map[string]any{
			"actorType":   message.AuthorActorTypeHuman,
			"principalId": valueOrEmpty(authority.membership.Member.UserID),
			"memberId":    authority.membership.Member.ID,
		},
	})
	if err != nil {
		return fmt.Errorf("encode service-account message audit metadata: %w", err)
	}
	return accountTx.InsertAuditEvent(ctx, serviceaccount.AuditEvent{
		ID:                  serviceAccountMessageAuditID(delivery.MessageID),
		WorkspaceID:         authority.account.WorkspaceID,
		ServiceAccountID:    authority.account.ID,
		EventType:           serviceAccountMessageAuditType,
		SubjectType:         "message",
		SubjectID:           delivery.MessageID,
		SubjectVersion:      1,
		ActorType:           message.AuthorActorTypeService,
		ActorID:             authority.account.PrincipalID,
		CausedByActorType:   message.AuthorActorTypeHuman,
		CausedByActorID:     authority.membership.Member.ID,
		ExecutionActorType:  message.AuthorActorTypeHuman,
		ExecutionActorID:    authority.membership.Member.ID,
		PermissionBasisKind: authority.permissionBasisKind,
		PermissionBasisRef:  authority.grantID + ":" + authority.permissionBasisRef,
		CorrelationID:       prepared.clientMessageID,
		Metadata:            metadata,
		CreatedAt:           delivery.Message.Timestamp.UTC(),
	})
}

func serviceAccountMessageAuditID(messageID string) string {
	digest := sha256.Sum256([]byte("service-account-message-audit:" + strings.TrimSpace(messageID)))
	return "saaudit_" + hex.EncodeToString(digest[:16])
}
