package approvalapp

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/service/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	serviceaccountstore "github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
)

type NotificationStore interface {
	RegisterAppNotificationBuiltinService(app.AppNotificationBuiltinServiceRegistration) error
	RegisterAppNotificationRecipientResolver(string, app.AppNotificationRecipientResolver) error
	CreateAppNotificationForBuiltin(context.Context, string, string, app.AppNotificationCommand) (app.AppNotificationResult, error)
	GetAppNotificationForBuiltin(context.Context, string, string, string) (app.AppNotificationResult, error)
	LoadAppNotification(context.Context, string, string) (app.AppNotificationRecord, error)
	LoadAppNotificationCardInTx(context.Context, coredb.QueryRower, string, bool) (app.AppNotificationCardRecord, error)
	FindAppNotificationByClientID(context.Context, string, string, string) (app.AppNotificationRecord, bool, error)
	MutateAppNotificationForBuiltin(context.Context, string, string, app.AppNotificationMutationCommand) (app.AppNotificationResult, error)
	MutateAppNotificationForBuiltinDelivery(context.Context, string, string, string, string, app.AppNotificationMutationCommand) (app.AppNotificationResult, error)
	ListAppNotificationsByBusinessObject(context.Context, string, string, string, string, string, int) ([]app.AppNotificationRecord, error)
	ListActiveAppNotificationCardsByBusinessObject(context.Context, string, string, string, string, string, string, int) ([]app.AppNotificationRecord, error)
	GetAppInstallationRequest(context.Context, string, string) (app.AppInstallationRequestView, error)
	SettlePrivilegedAppInstallationRequest(context.Context, string, string) (app.AppInstallationRequestView, bool, error)
}

type Projector struct {
	notifications NotificationStore
	provisioner   *Provisioner
	now           func() time.Time
}

const maxAppInstallationReviewers = 200

const maxAppInstallationProjectionReconcileLimit = 100

// One request can legitimately have maxAppInstallationReviewers actionable
// cards plus an applicant receipt/result and bounded reviewer generations.
// Every App projection read uses the store's 500-row ceiling so the applicant
// receipt cannot displace the 200th reviewer during terminal convergence.
const maxAppInstallationProjectionNotifications = 500

const (
	appInstallationChangeCannotBeAppliedText    = "此变更已无法应用。请拒绝此申请，并让申请人根据当前状态重新提交。"
	appInstallationChangeCannotBeAppliedTextKey = "app_change_cannot_be_applied"
	appInstallationSetupPatchingRetryText       = "GitHub 配置尚未完成。请重试通过；此阶段不能拒绝或更换安装目标。"
	appInstallationSetupPatchingRetryTextKey    = "github_setup_patching_retry"
)

func NewProjector(notifications NotificationStore, provisioner *Provisioner) *Projector {
	return &Projector{notifications: notifications, provisioner: provisioner, now: time.Now}
}

type skillDeleteResolutionProjection struct {
	notificationID       string
	notificationRevision int64
	recipientBasisRef    string
	alreadyResolved      bool
	resolved             app.AppNotificationResult
}

func (handler *skillDeleteSubjectHandler) requireSkillDeleteProjector() error {
	if handler == nil || handler.projector == nil ||
		handler.projector.notifications == nil || handler.projector.provisioner == nil {
		return fmt.Errorf("Skill delete Approval projector is not configured")
	}
	return nil
}

func (handler *skillDeleteSubjectHandler) PresentPending(
	ctx context.Context,
	result SubmitResult,
) (app.AppNotificationResult, error) {
	if err := handler.requireSkillDeleteProjector(); err != nil {
		return app.AppNotificationResult{}, err
	}
	instance, assignment, payload, err := handler.pendingProjection(result)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if _, err := handler.projector.provisioner.EnsureWorkspace(
		ctx, instance.WorkspaceID, time.Now().UTC(),
	); err != nil {
		return app.AppNotificationResult{}, err
	}
	content, err := skillDeleteCardContent(
		instance, assignment.ID, assignment.ApproverMemberID,
		payload, approval.StatePending,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	return handler.projector.notifications.CreateAppNotificationForBuiltin(
		ctx, IdentityID, instance.WorkspaceID, app.AppNotificationCommand{
			ClientNotificationID: skillDeleteReviewClientNotificationID(instance.ID),
			Target: app.AppNotificationTarget{
				Kind:     app.AppNotificationTargetHumanDM,
				MemberID: assignment.ApproverMemberID,
			},
			Content: content,
			BusinessObject: app.AppNotificationBusinessObject{
				Namespace: BusinessNamespace, Type: BusinessTypeInstance,
				ID: instance.ID, Version: instance.Version,
			},
			RecipientBasis: app.AppNotificationRecipientBasis{
				Kind: RecipientBasisAssignment, Ref: assignment.ID,
			},
			CausedByActor: app.AppNotificationActorRef{
				Type: "agent", ID: payload.ApplicantAgentID,
			},
			CorrelationID: instance.CorrelationID,
		},
	)
}

func (handler *skillDeleteSubjectHandler) PresentResolved(
	ctx context.Context,
	result DecisionResult,
	presentation PresentationContext,
) (app.AppNotificationResult, error) {
	if handler == nil || handler.approvals == nil {
		return app.AppNotificationResult{}, fmt.Errorf(
			"Skill delete Approval projector is not configured",
		)
	}
	if err := handler.requireSkillDeleteProjector(); err != nil {
		return app.AppNotificationResult{}, err
	}
	instance := result.Instance
	notificationID, err := handler.resolutionNotificationID(instance, presentation)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	projection, err := handler.projector.prepareSkillDeleteResolution(
		ctx, instance, notificationID,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if projection.alreadyResolved {
		return projection.resolved, nil
	}
	assignment, err := handler.resolvedAssignment(
		ctx, instance, projection.recipientBasisRef,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	payload, err := decodeSkillDeletePayload(instance)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	return handler.projector.resolveSkillDeleteProjection(
		ctx, instance, assignment, payload, presentation, projection,
	)
}

func (projector *Projector) prepareSkillDeleteResolution(
	ctx context.Context,
	instance approval.Instance,
	notificationID string,
) (skillDeleteResolutionProjection, error) {
	provisioned, err := projector.provisioner.EnsureWorkspace(
		ctx, instance.WorkspaceID, time.Now().UTC(),
	)
	if err != nil {
		return skillDeleteResolutionProjection{}, err
	}
	stable, found, err := projector.notifications.FindAppNotificationByClientID(
		ctx, instance.WorkspaceID, provisioned.Installation.ID,
		skillDeleteReviewClientNotificationID(instance.ID),
	)
	if err != nil {
		return skillDeleteResolutionProjection{}, err
	}
	if !found || stable.ID != notificationID {
		return skillDeleteResolutionProjection{}, fmt.Errorf(
			"Skill delete action notification does not match",
		)
	}
	notification, err := projector.notifications.LoadAppNotification(
		ctx, instance.WorkspaceID, notificationID,
	)
	if err != nil {
		return skillDeleteResolutionProjection{}, err
	}
	if notification.Status == app.AppNotificationStatusResolved {
		resolved, err := projector.notifications.GetAppNotificationForBuiltin(
			ctx, IdentityID, instance.WorkspaceID, notification.ID,
		)
		return skillDeleteResolutionProjection{
			alreadyResolved: true,
			resolved:        resolved,
		}, err
	}
	if notification.Status != app.AppNotificationStatusActive ||
		notification.BusinessObjectID == nil ||
		*notification.BusinessObjectID != instance.ID ||
		notification.RecipientBasisRef == nil {
		return skillDeleteResolutionProjection{}, fmt.Errorf(
			"Skill delete action notification authority does not match",
		)
	}
	return skillDeleteResolutionProjection{
		notificationID:       notification.ID,
		notificationRevision: notification.Revision,
		recipientBasisRef:    *notification.RecipientBasisRef,
	}, nil
}

func (projector *Projector) resolveSkillDeleteProjection(
	ctx context.Context,
	instance approval.Instance,
	assignment approval.Assignment,
	payload skillDeletePayload,
	presentation PresentationContext,
	projection skillDeleteResolutionProjection,
) (app.AppNotificationResult, error) {
	content, err := skillDeleteCardContent(
		instance, assignment.ID, "", payload, instance.State,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	mutationID := strings.TrimSpace(presentation.MutationID)
	if mutationID == "" {
		return app.AppNotificationResult{}, fmt.Errorf(
			"Skill delete action mutation id is required",
		)
	}
	version := instance.Version
	command := app.AppNotificationMutationCommand{
		NotificationID:        projection.notificationID,
		Kind:                  app.AppNotificationMutationResolve,
		ExpectedRevision:      projection.notificationRevision,
		ClientMutationID:      "skill-delete-action:" + mutationID + ":" + projection.notificationID,
		Content:               &content,
		BusinessObjectVersion: &version,
		CausedByActor: app.AppNotificationActorRef{
			Type: "human", ID: instance.DecidedByMemberID,
		},
		CorrelationID: instance.CorrelationID,
	}
	if presentation.DeliveryID != "" || presentation.AttemptID != "" {
		if presentation.DeliveryID == "" || presentation.AttemptID == "" {
			return app.AppNotificationResult{}, fmt.Errorf(
				"Skill delete delivery and attempt must be provided together",
			)
		}
		return projector.notifications.MutateAppNotificationForBuiltinDelivery(
			ctx, IdentityID, instance.WorkspaceID,
			presentation.DeliveryID, presentation.AttemptID, command,
		)
	}
	return projector.notifications.MutateAppNotificationForBuiltin(
		ctx, IdentityID, instance.WorkspaceID, command,
	)
}

func skillDeleteCardContent(
	instance approval.Instance,
	assignmentID string,
	approverMemberID string,
	payload skillDeletePayload,
	status string,
) (app.AppNotificationContent, error) {
	assignmentID = strings.TrimSpace(assignmentID)
	approverMemberID = strings.TrimSpace(approverMemberID)
	if instance.ID == "" || instance.Version <= 0 || assignmentID == "" {
		return app.AppNotificationContent{}, fmt.Errorf(
			"Skill delete card identity is required",
		)
	}
	if status != approval.StatePending &&
		status != approval.StateApproved &&
		status != approval.StateRejected {
		return app.AppNotificationContent{}, fmt.Errorf(
			"Skill delete card status is invalid",
		)
	}
	if status == approval.StatePending && approverMemberID == "" {
		return app.AppNotificationContent{}, fmt.Errorf(
			"Skill delete pending approver is required",
		)
	}
	titleKey := "skill_delete_review"
	if status == approval.StateApproved {
		titleKey = "skill_delete_approved"
	} else if status == approval.StateRejected {
		titleKey = "skill_delete_rejected"
	}
	card := map[string]any{
		"schemaVersion": "1",
		"template":      "approval_action",
		"title":         "审批 Skill 删除请求",
		"titleKey":      titleKey,
		"status":        status,
		"proposal": map[string]any{
			"action": "delete",
			"summary": map[string]any{
				"skillId": payload.SkillID,
				"name":    payload.SkillName,
			},
		},
		"approval": map[string]any{
			"id": instance.ID, "version": instance.Version,
			"assignmentId": assignmentID,
		},
		"actions": []map[string]any{},
	}
	if status == approval.StatePending {
		actorPolicy := map[string]any{
			"kind": "members", "memberIds": []string{approverMemberID},
		}
		card["actions"] = []map[string]any{
			{
				"actionKey": ActionApprove, "type": "callback", "label": "通过",
				"semantics": ActionSemanticsCommit, "actorPolicy": actorPolicy,
			},
			{
				"actionKey": ActionReject, "type": "callback", "label": "暂不通过",
				"semantics": ActionSemanticsRevise, "actorPolicy": actorPolicy,
			},
		}
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, err
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: "Skill 删除请求：" + payload.SkillName,
		CardJSON:     encoded,
	}, nil
}

// Register installs the process-local builtin publisher and Approval-owned
// recipient resolvers. It must run before the internal handler registry seals.
func (projector *Projector) Register() error {
	if projector == nil || projector.notifications == nil || projector.provisioner == nil {
		return fmt.Errorf("approval App projector is not configured")
	}
	registration := Registration()
	if err := projector.notifications.RegisterAppNotificationBuiltinService(app.AppNotificationBuiltinServiceRegistration{
		IdentityID: registration.IdentityID, ServiceDefinitionSlug: registration.ServiceDefinitionSlug,
		ActionHandlerKey:          registration.ActionHandlerKey,
		AllowedBusinessNamespaces: registration.AllowedBusinessNamespaces,
		AllowedTargetKinds:        registration.AllowedTargetKinds,
		AllowedContentKinds:       registration.AllowedContentKinds,
	}); err != nil {
		return err
	}
	resolver := Resolver{}
	if err := projector.notifications.RegisterAppNotificationRecipientResolver(RecipientBasisAssignment, resolver); err != nil {
		return err
	}
	if err := projector.notifications.RegisterAppNotificationRecipientResolver(RecipientBasisApplicant, resolver); err != nil {
		return err
	}
	if err := projector.notifications.RegisterAppNotificationRecipientResolver(RecipientBasisAppInstallationReviewer, resolver); err != nil {
		return err
	}
	if err := projector.notifications.RegisterAppNotificationRecipientResolver(RecipientBasisAppInstallationApplicant, resolver); err != nil {
		return err
	}
	if err := projector.notifications.RegisterAppNotificationRecipientResolver(RecipientBasisWikiGenerationRequester, resolver); err != nil {
		return err
	}
	return projector.notifications.RegisterAppNotificationRecipientResolver(RecipientBasisWorkflowRunAssignee, resolver)
}

// ProjectPendingRequest creates one stable interactive card per assignment and
// one read-only pending receipt for the applicant. The returned slice contains
// only actionable reviewer cards so the API's notificationsProjected count
// continues to describe the approver fan-out.
func (projector *Projector) ProjectPendingRequest(
	ctx context.Context,
	result serviceaccount.WorkspaceCreateRequestResult,
) ([]app.AppNotificationResult, error) {
	if result.Approval == nil || result.Request.ApprovalInstanceID == "" {
		return nil, nil
	}
	deterministic, settled, err := serviceaccount.New(projector.provisioner.db).SettlePrivilegedWorkspaceCreateRequest(
		ctx,
		result.Request.WorkspaceID,
		result.Request.ID,
		time.Now().UTC(),
	)
	if err != nil {
		return nil, err
	}
	if settled {
		return []app.AppNotificationResult{}, nil
	}
	result = deterministic
	if _, err := projector.provisioner.EnsureWorkspace(ctx, result.Request.WorkspaceID, time.Now().UTC()); err != nil {
		return nil, err
	}
	payload, err := decodeWorkspacePayload(result.Request)
	if err != nil {
		return nil, err
	}
	applicantName, err := projector.memberDisplayName(
		ctx,
		result.Request.WorkspaceID,
		result.Request.RequestedByMemberID,
		"",
	)
	if err != nil {
		return nil, err
	}
	currentProcessorNames, err := projector.assignmentProcessorNames(
		ctx, result.Request.WorkspaceID, result.Assignments,
	)
	if err != nil {
		return nil, err
	}
	projected := make([]app.AppNotificationResult, 0, len(result.Assignments))
	for _, assignment := range result.Assignments {
		content, err := pendingCardContent(
			result.Request,
			*result.Approval,
			assignment,
			payload,
			applicantName,
			currentProcessorNames,
		)
		if err != nil {
			return nil, err
		}
		created, err := projector.notifications.CreateAppNotificationForBuiltin(ctx, IdentityID, result.Request.WorkspaceID, app.AppNotificationCommand{
			ClientNotificationID: assignmentClientNotificationID(result.Request.ID, assignment.ApproverMemberID),
			Target:               app.AppNotificationTarget{Kind: app.AppNotificationTargetHumanDM, MemberID: assignment.ApproverMemberID},
			Content:              content,
			BusinessObject: app.AppNotificationBusinessObject{
				Namespace: BusinessNamespace, Type: BusinessTypeInstance,
				ID: result.Approval.ID, Version: result.Approval.Version,
				URL: "/service-account/create-requests/" + result.Request.ID,
			},
			RecipientBasis: app.AppNotificationRecipientBasis{Kind: RecipientBasisAssignment, Ref: assignment.ID},
			CausedByActor:  app.AppNotificationActorRef{Type: "human", ID: result.Request.RequestedByMemberID},
			CorrelationID:  result.Approval.CorrelationID,
		})
		if err != nil {
			return nil, err
		}
		projected = append(projected, created)
	}
	if _, err := projector.projectPendingApplicantReceipt(
		ctx,
		result.Request,
		*result.Approval,
		payload,
		applicantName,
		currentProcessorNames,
	); err != nil {
		return nil, err
	}
	// Close the commit-to-projection role-change window. A promotion that wins
	// while cards are being created withdraws those just-created projections;
	// later promotions are picked up by the bounded Service Account catalog
	// reconciliation scan.
	if _, settled, err := serviceaccount.New(projector.provisioner.db).SettlePrivilegedWorkspaceCreateRequest(
		ctx,
		result.Request.WorkspaceID,
		result.Request.ID,
		time.Now().UTC(),
	); err != nil {
		return nil, err
	} else if settled {
		return []app.AppNotificationResult{}, nil
	}
	return projected, nil
}

// ProjectPendingAppInstallationRequest creates one stable Approval card
// for every active workspace owner/admin plus one read-only, detail-minimized
// submission receipt for the applicant. The returned slice contains only
// actionable reviewer cards, so notificationsProjected continues to describe
// approver fan-out rather than the applicant acknowledgement.
func (projector *Projector) ProjectPendingAppInstallationRequest(
	ctx context.Context,
	request app.AppInstallationRequestView,
) ([]app.AppNotificationResult, error) {
	if strings.TrimSpace(request.ID) == "" || strings.TrimSpace(request.WorkspaceID) == "" {
		return nil, fmt.Errorf("App installation request identity is required")
	}
	settledRequest, settled, err := projector.notifications.SettlePrivilegedAppInstallationRequest(
		ctx,
		request.WorkspaceID,
		request.ID,
	)
	if err != nil {
		return nil, err
	}
	request = settledRequest
	if settled || app.IsPrivilegedAppRequestWithdrawalDecision(request) {
		return []app.AppNotificationResult{}, nil
	}
	if !appInstallationRequestReviewActive(request.Status, request.ApprovalRequired) {
		return nil, fmt.Errorf("App installation request is not review-active")
	}
	if _, err := projector.provisioner.EnsureWorkspace(ctx, request.WorkspaceID, time.Now().UTC()); err != nil {
		return nil, err
	}
	applicantName, err := projector.memberDisplayName(
		ctx,
		request.WorkspaceID,
		request.RequestedByMemberID,
		request.RequestedByDisplayName,
	)
	if err != nil {
		return nil, err
	}
	request.RequestedByDisplayName = applicantName
	reviewers, err := projector.listAppInstallationReviewers(ctx, request.WorkspaceID)
	if err != nil {
		if _, receiptErr := projector.projectPendingAppInstallationApplicantReceipt(ctx, request); receiptErr != nil {
			return nil, errors.Join(err, receiptErr)
		}
		return nil, err
	}
	if len(reviewers) == 0 {
		if _, err := projector.projectPendingAppInstallationApplicantReceipt(ctx, request); err != nil {
			return nil, err
		}
		return nil, fmt.Errorf("App installation request has no active workspace reviewer")
	}
	request.CurrentProcessorNames, err = projector.memberDisplayNames(
		ctx, request.WorkspaceID, reviewers,
	)
	if err != nil {
		return nil, err
	}
	if _, err := projector.projectPendingAppInstallationApplicantReceipt(ctx, request); err != nil {
		return nil, err
	}
	request, stopped, err := projector.convergeAppInstallationRequestAfterPendingProjection(ctx, request)
	if err != nil {
		return nil, err
	}
	if stopped {
		return []app.AppNotificationResult{}, nil
	}
	if strings.TrimSpace(request.Status) == app.AppInstallationRequestStatusSetupPatching {
		if err := projector.refreshAppInstallationSetupPatchingCards(ctx, request); err != nil {
			return nil, err
		}
	}
	existing, err := projector.notifications.ListAppNotificationsByBusinessObject(
		ctx,
		request.WorkspaceID,
		IdentityID,
		BusinessNamespace,
		BusinessTypeAppInstallationRequest,
		request.ID,
		maxAppInstallationProjectionNotifications,
	)
	if err != nil {
		return nil, err
	}
	if request.ReviewState == app.AppInstallationRequestReviewStateCannotBeApplied {
		activeReviewers, err := projector.activeAppInstallationReviewerNotifications(ctx, request)
		if err != nil {
			return nil, err
		}
		existing = mergeAppNotificationRecords(existing, activeReviewers)
	}
	content, err := pendingAppInstallationReviewContent(request)
	if err != nil {
		return nil, err
	}
	projected := make([]app.AppNotificationResult, 0, len(reviewers))
	for _, reviewerMemberID := range reviewers {
		active, found, historyCount := activeAppInstallationReviewerProjection(existing, reviewerMemberID)
		if found {
			current, err := projector.notifications.GetAppNotificationForBuiltin(
				ctx,
				IdentityID,
				request.WorkspaceID,
				active.ID,
			)
			if err != nil {
				return nil, err
			}
			current.AlreadyCreated = true
			projected = append(projected, current)
			continue
		}
		clientNotificationID := appInstallationReviewerClientNotificationID(request.ID, reviewerMemberID)
		if historyCount > 0 {
			// App notifications are terminal once resolved/withdrawn/expired and
			// their client id is immutable. A still-pending request therefore
			// needs a new deterministic generation rather than replaying the
			// terminal row and falsely reporting convergence. Concurrent
			// reconcilers observe the same history count and race on the same
			// unique client id, preserving idempotency.
			clientNotificationID = fmt.Sprintf("%s:retry:%d", clientNotificationID, historyCount+1)
		}
		created, err := projector.notifications.CreateAppNotificationForBuiltin(ctx, IdentityID, request.WorkspaceID, app.AppNotificationCommand{
			ClientNotificationID: clientNotificationID,
			Target:               app.AppNotificationTarget{Kind: app.AppNotificationTargetHumanDM, MemberID: reviewerMemberID},
			Content:              content,
			BusinessObject: app.AppNotificationBusinessObject{
				Namespace: BusinessNamespace, Type: BusinessTypeAppInstallationRequest,
				ID: request.ID, Version: appInstallationRequestBusinessVersion(request),
				URL: appInstallationDetailURL(request),
			},
			RecipientBasis: app.AppNotificationRecipientBasis{Kind: RecipientBasisAppInstallationReviewer, Ref: reviewerMemberID},
			CausedByActor:  app.AppNotificationActorRef{Type: "human", ID: request.RequestedByMemberID},
			CorrelationID:  request.ID,
		})
		if err != nil {
			if _, stopped, convergenceErr := projector.convergeAppInstallationRequestAfterPendingProjection(ctx, request); stopped && convergenceErr == nil {
				return []app.AppNotificationResult{}, nil
			} else if convergenceErr != nil {
				return nil, errors.Join(err, convergenceErr)
			}
			return nil, err
		}
		projected = append(projected, created)
	}
	converged, stopped, err := projector.convergeAppInstallationRequestAfterPendingProjection(ctx, request)
	if err != nil {
		return nil, err
	}
	if stopped {
		return []app.AppNotificationResult{}, nil
	}
	if converged.ReviewState == app.AppInstallationRequestReviewStateCannotBeApplied {
		if _, err := projector.markAppInstallationChangeCannotBeApplied(
			ctx, converged, "", "", "", 0,
		); err != nil {
			return nil, err
		}
	}
	return projected, nil
}

// convergeAppInstallationRequestAfterPendingProjection closes both sides of
// the request-state race. If a decision or requester promotion commits while a
// pending notification transaction is in flight, the now-authoritative terminal
// projection immediately resolves/withdraws every card created by the loser.
// If this check observes pending, a later decision projector necessarily runs
// after the notification commit and observes the projection itself.
func (projector *Projector) convergeAppInstallationRequestAfterPendingProjection(
	ctx context.Context,
	request app.AppInstallationRequestView,
) (app.AppInstallationRequestView, bool, error) {
	deterministic, settled, err := projector.notifications.SettlePrivilegedAppInstallationRequest(
		ctx,
		request.WorkspaceID,
		request.ID,
	)
	if err != nil {
		return app.AppInstallationRequestView{}, false, err
	}
	if settled || app.IsPrivilegedAppRequestWithdrawalDecision(deterministic) {
		return deterministic, true, nil
	}
	if !deterministic.ApprovalRequired {
		return app.AppInstallationRequestView{}, false, fmt.Errorf("App installation request is not human-reviewable")
	}
	switch strings.TrimSpace(deterministic.Status) {
	case app.AppInstallationRequestStatusPending, app.AppInstallationRequestStatusSetupPatching:
		return deterministic, false, nil
	case app.AppInstallationRequestStatusApproved, app.AppInstallationRequestStatusRejected:
		if _, err := projector.ProjectAppInstallationRequestDecision(ctx, deterministic); err != nil {
			return app.AppInstallationRequestView{}, false, err
		}
		return deterministic, true, nil
	default:
		return app.AppInstallationRequestView{}, false, fmt.Errorf("unsupported App installation request status %q", deterministic.Status)
	}
}

func activeAppInstallationReviewerProjection(
	notifications []app.AppNotificationRecord,
	reviewerMemberID string,
) (app.AppNotificationRecord, bool, int) {
	reviewerMemberID = strings.TrimSpace(reviewerMemberID)
	historyCount := 0
	for _, notification := range notifications {
		if notification.RecipientBasisKind == nil || notification.RecipientBasisRef == nil ||
			strings.TrimSpace(*notification.RecipientBasisKind) != RecipientBasisAppInstallationReviewer ||
			strings.TrimSpace(*notification.RecipientBasisRef) != reviewerMemberID {
			continue
		}
		historyCount++
		if notification.ContentKind == app.AppNotificationContentInteractiveCard &&
			notification.Status == app.AppNotificationStatusActive &&
			notification.BusinessObjectVersion != nil &&
			*notification.BusinessObjectVersion == 1 {
			return notification, true, historyCount
		}
	}
	return app.AppNotificationRecord{}, false, historyCount
}

func (projector *Projector) activeAppInstallationReviewerNotifications(
	ctx context.Context,
	request app.AppInstallationRequestView,
) ([]app.AppNotificationRecord, error) {
	notifications, err := projector.notifications.ListActiveAppNotificationCardsByBusinessObject(
		ctx,
		request.WorkspaceID,
		IdentityID,
		BusinessNamespace,
		BusinessTypeAppInstallationRequest,
		request.ID,
		RecipientBasisAppInstallationReviewer,
		maxAppInstallationReviewers+1,
	)
	if err != nil {
		return nil, err
	}
	if len(notifications) > maxAppInstallationReviewers {
		return nil, fmt.Errorf("App installation request active reviewer projection set exceeds %d notifications", maxAppInstallationReviewers)
	}
	return notifications, nil
}

func mergeAppNotificationRecords(
	base []app.AppNotificationRecord,
	extra []app.AppNotificationRecord,
) []app.AppNotificationRecord {
	if len(extra) == 0 {
		return base
	}
	seen := make(map[string]struct{}, len(base)+len(extra))
	merged := make([]app.AppNotificationRecord, 0, len(base)+len(extra))
	for _, notification := range base {
		seen[notification.ID] = struct{}{}
		merged = append(merged, notification)
	}
	for _, notification := range extra {
		if _, found := seen[notification.ID]; found {
			continue
		}
		seen[notification.ID] = struct{}{}
		merged = append(merged, notification)
	}
	return merged
}

// pendingAppInstallationReviewContent derives every reviewer generation from
// the authoritative request-row fence, never from a bounded card-history scan.
func pendingAppInstallationReviewContent(request app.AppInstallationRequestView) (app.AppNotificationContent, error) {
	if strings.TrimSpace(request.Status) == app.AppInstallationRequestStatusSetupPatching {
		return pendingAppInstallationSetupPatchingCardContent(request)
	}
	if request.ReviewState == app.AppInstallationRequestReviewStateCannotBeApplied {
		return pendingAppInstallationCardContentWithChangeCannotBeApplied(request)
	}
	return pendingAppInstallationCardContent(request)
}

// ProjectAppInstallationRequestDecision converges all reviewer cards and
// creates the applicant's stable terminal result notification after the
// existing App installation authority commits a decision.
func (projector *Projector) ProjectAppInstallationRequestDecision(
	ctx context.Context,
	request app.AppInstallationRequestView,
) (app.AppNotificationResult, error) {
	if !request.ApprovalRequired {
		return app.AppNotificationResult{}, fmt.Errorf("App installation request is not human-reviewable")
	}
	if app.IsPrivilegedAppRequestWithdrawalDecision(request) {
		// Settlement is also the idempotent cleanup boundary for a v1 card
		// whose notification transaction committed after the original role-
		// promotion withdrawal pass.
		_, _, err := projector.notifications.SettlePrivilegedAppInstallationRequest(
			ctx,
			request.WorkspaceID,
			request.ID,
		)
		return app.AppNotificationResult{}, err
	}
	if appInstallationRequestBusinessVersion(request) <= 1 {
		return app.AppNotificationResult{}, fmt.Errorf("App installation request is not decided")
	}
	if _, err := projector.resolveAppInstallationCards(ctx, request, "", "", "", ""); err != nil {
		return app.AppNotificationResult{}, err
	}
	return projector.projectAppInstallationApplicantResult(ctx, request)
}

// ReconcileAppInstallationRequestProjections treats the request rows as the
// durable retry ledger for their Approval projection. It only loads rows
// with a detectable projection gap, then replays the same idempotent pending or
// terminal projection used by the write path. This deliberately avoids a
// second generic outbox while still allowing a transient post-commit failure
// (or a process restart immediately after the commit) to converge later.
//
// Review-active gaps include a missing reviewer card, a missing submission
// receipt for an active applicant, and every pending request whose author is
// now owner/admin (even inactive), so reconciliation can withdraw
// already-projected cards. A marked setup_patching request remains v1 and is
// converged to an approve-only recovery card. Internal provider sagas are
// excluded by the durable approval_required trust-boundary marker.
// Terminal gaps include an active reviewer card, a stale active applicant
// receipt, or a missing exact result for an active applicant. Receipt and
// result client ids are deliberately distinguished so the former can never
// falsely satisfy the latter. System-withdrawn privileged requests are scanned
// only while a late active v1 card still needs idempotent withdrawal and never
// receive an applicant result. Successfully reconciled rows fall out of the
// next scan, so an oldest-first bounded scan eventually reaches every gap.
func (projector *Projector) ReconcileAppInstallationRequestProjections(
	ctx context.Context,
	workspaceID string,
	limit int,
) (int, error) {
	if projector == nil || projector.notifications == nil || projector.provisioner == nil {
		return 0, fmt.Errorf("approval App projector is not configured")
	}
	workspaceID = strings.TrimSpace(workspaceID)
	if workspaceID == "" {
		return 0, fmt.Errorf("App installation projection workspace is required")
	}
	if limit <= 0 {
		limit = 25
	}
	if limit > maxAppInstallationProjectionReconcileLimit {
		limit = maxAppInstallationProjectionReconcileLimit
	}

	requestIDs, err := projector.appInstallationProjectionGapIDs(ctx, workspaceID, limit)
	if err != nil {
		return 0, err
	}
	reconciled := 0
	errs := make([]error, 0)
	for _, requestID := range requestIDs {
		request, loadErr := projector.notifications.GetAppInstallationRequest(ctx, workspaceID, requestID)
		if loadErr != nil {
			errs = append(errs, fmt.Errorf("load App installation request %s for projection reconciliation: %w", requestID, loadErr))
			continue
		}
		switch strings.TrimSpace(request.Status) {
		case app.AppInstallationRequestStatusPending, app.AppInstallationRequestStatusSetupPatching:
			_, err = projector.ProjectPendingAppInstallationRequest(ctx, request)
		case app.AppInstallationRequestStatusApproved, app.AppInstallationRequestStatusRejected:
			_, err = projector.ProjectAppInstallationRequestDecision(ctx, request)
		default:
			err = fmt.Errorf("unsupported App installation request status %q", request.Status)
		}
		if err != nil {
			errs = append(errs, fmt.Errorf("reconcile App installation request %s projection: %w", requestID, err))
			continue
		}
		reconciled++
	}
	return reconciled, errors.Join(errs...)
}

func (projector *Projector) appInstallationProjectionGapIDs(
	ctx context.Context,
	workspaceID string,
	limit int,
) ([]string, error) {
	rows, err := projector.provisioner.db.Query(ctx, `
SELECT request_row.id
FROM service_installation_requests request_row
WHERE request_row.workspace_id = $1
  AND request_row.service_kind = 'app'
  AND request_row.approval_required = TRUE
  AND (
    (
      request_row.status = 'pending'
      AND EXISTS (
        SELECT 1
        FROM members privileged_requester
        WHERE privileged_requester.workspace_id = request_row.workspace_id
          AND privileged_requester.id = request_row.requested_by_member_id
          AND privileged_requester.type = 'human'
          AND privileged_requester.workspace_role IN ('owner', 'admin')
      )
    )
    OR
    (
      request_row.status = 'setup_patching'
      AND EXISTS (
        SELECT 1
        FROM service_notifications notification
        WHERE notification.workspace_id = request_row.workspace_id
          AND notification.service_definition_id = $2
          AND notification.business_object_namespace = $3
          AND notification.business_object_type = $4
          AND notification.business_object_id = request_row.id
          AND notification.recipient_basis_kind = $5
          AND notification.content_kind = 'interactive_card'
          AND notification.status = 'active'
          AND notification.business_object_version = 1
          AND NOT EXISTS (
            SELECT 1
            FROM service_notification_cards card
            WHERE card.service_notification_id = notification.id
              AND card.message_id = notification.message_id
              AND card.state = 'active'
              AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(card.render_payload_json, '$.status')), '') = 'setup_patching'
              AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(card.render_payload_json, '$.textKey')), '') = 'github_setup_patching_retry'
              AND JSON_TYPE(JSON_EXTRACT(card.render_payload_json, '$.actions')) = 'ARRAY'
              AND JSON_LENGTH(JSON_EXTRACT(card.render_payload_json, '$.actions')) = 1
              AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(card.render_payload_json, '$.actions[0].actionKey')), '') = 'approve'
          )
      )
    )
    OR
    (
      request_row.status IN ('pending', 'setup_patching')
      AND EXISTS (
        SELECT 1
        FROM members reviewer
        WHERE reviewer.workspace_id = request_row.workspace_id
          AND reviewer.type = 'human'
          AND reviewer.status = 'active'
          AND reviewer.workspace_role IN ('owner', 'admin')
          AND NOT EXISTS (
            SELECT 1
            FROM service_notifications notification
            WHERE notification.workspace_id = request_row.workspace_id
              AND notification.service_definition_id = $2
              AND notification.business_object_namespace = $3
              AND notification.business_object_type = $4
              AND notification.business_object_id = request_row.id
              AND notification.recipient_basis_kind = $5
              AND notification.recipient_basis_ref = reviewer.id
              AND notification.content_kind = 'interactive_card'
              AND notification.status = 'active'
              AND notification.business_object_version = 1
          )
      )
    )
    OR
    (
      request_row.status = 'pending'
      AND request_row.review_state = $8
      AND EXISTS (
        SELECT 1
        FROM service_notifications notification
        WHERE notification.workspace_id = request_row.workspace_id
          AND notification.service_definition_id = $2
          AND notification.business_object_namespace = $3
          AND notification.business_object_type = $4
          AND notification.business_object_id = request_row.id
          AND notification.recipient_basis_kind = $5
          AND notification.content_kind = 'interactive_card'
          AND notification.status = 'active'
          AND NOT EXISTS (
            SELECT 1
            FROM service_notification_cards card
            WHERE card.service_notification_id = notification.id
              AND card.message_id = notification.message_id
              AND card.state = 'active'
              AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(card.render_payload_json, '$.textKey')), '') = $9
              AND JSON_TYPE(JSON_EXTRACT(card.render_payload_json, '$.actions')) = 'ARRAY'
              AND JSON_LENGTH(JSON_EXTRACT(card.render_payload_json, '$.actions')) = 1
              AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(card.render_payload_json, '$.actions[0].actionKey')), '') = $10
          )
      )
    )
    OR
    (
      request_row.status IN ('pending', 'setup_patching')
      AND EXISTS (
        SELECT 1
        FROM members applicant
        WHERE applicant.workspace_id = request_row.workspace_id
          AND applicant.id = request_row.requested_by_member_id
          AND applicant.type = 'human'
          AND applicant.status = 'active'
          AND NOT EXISTS (
            SELECT 1
            FROM service_notifications notification
            WHERE notification.workspace_id = request_row.workspace_id
              AND notification.service_definition_id = $2
              AND notification.business_object_namespace = $3
              AND notification.business_object_type = $4
              AND notification.business_object_id = request_row.id
              AND notification.recipient_basis_kind = $6
              AND notification.recipient_basis_ref = request_row.id
              AND notification.client_notification_id = CONCAT('app-installation:', request_row.id, ':applicant:pending')
              AND notification.content_kind = 'interactive_card'
              AND notification.status = 'active'
              AND notification.business_object_version = 1
          )
      )
    )
    OR
    (
      request_row.status IN ('approved', 'rejected')
      AND (
        EXISTS (
          SELECT 1
          FROM service_notifications notification
          WHERE notification.workspace_id = request_row.workspace_id
            AND notification.service_definition_id = $2
            AND notification.business_object_namespace = $3
            AND notification.business_object_type = $4
            AND notification.business_object_id = request_row.id
            AND notification.recipient_basis_kind = $5
            AND notification.content_kind = 'interactive_card'
            AND notification.status = 'active'
            AND notification.business_object_version = 1
        )
        OR EXISTS (
          SELECT 1
          FROM service_notifications notification
          WHERE notification.workspace_id = request_row.workspace_id
            AND notification.service_definition_id = $2
            AND notification.business_object_namespace = $3
            AND notification.business_object_type = $4
            AND notification.business_object_id = request_row.id
            AND notification.recipient_basis_kind = $6
            AND notification.recipient_basis_ref = request_row.id
            AND notification.client_notification_id = CONCAT('app-installation:', request_row.id, ':applicant:pending')
            AND notification.content_kind = 'interactive_card'
            AND notification.status = 'active'
        )
        OR (
          NOT (
            request_row.status = 'rejected'
            AND request_row.decided_by_member_id = request_row.requested_by_member_id
            AND request_row.decision_reason = $7
          )
          AND EXISTS (
            SELECT 1
            FROM members applicant
            WHERE applicant.workspace_id = request_row.workspace_id
              AND applicant.id = request_row.requested_by_member_id
              AND applicant.type = 'human'
              AND applicant.status = 'active'
              AND NOT EXISTS (
                SELECT 1
                FROM service_notifications notification
                WHERE notification.workspace_id = request_row.workspace_id
                  AND notification.service_definition_id = $2
                  AND notification.business_object_namespace = $3
                  AND notification.business_object_type = $4
                  AND notification.business_object_id = request_row.id
                  AND notification.recipient_basis_kind = $6
                  AND notification.recipient_basis_ref = request_row.id
                  AND notification.client_notification_id = CONCAT('app-installation:', request_row.id, ':applicant:result')
                  AND notification.content_kind = 'interactive_card'
              )
          )
        )
      )
    )
  )
ORDER BY request_row.updated_at ASC, request_row.id ASC
LIMIT $11
`, strings.TrimSpace(workspaceID), DefinitionID, BusinessNamespace,
		BusinessTypeAppInstallationRequest, RecipientBasisAppInstallationReviewer,
		RecipientBasisAppInstallationApplicant, app.PrivilegedAppRequestDecisionReason,
		app.AppInstallationRequestReviewStateCannotBeApplied,
		appInstallationChangeCannotBeAppliedTextKey, ActionReject, limit)
	if err != nil {
		return nil, fmt.Errorf("list App installation projection gaps: %w", err)
	}
	defer rows.Close()
	requestIDs := make([]string, 0)
	for rows.Next() {
		var requestID string
		if err := rows.Scan(&requestID); err != nil {
			return nil, fmt.Errorf("scan App installation projection gap: %w", err)
		}
		requestIDs = append(requestIDs, strings.TrimSpace(requestID))
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate App installation projection gaps: %w", err)
	}
	return requestIDs, nil
}

func (projector *Projector) listAppInstallationReviewers(ctx context.Context, workspaceID string) ([]string, error) {
	rows, err := projector.provisioner.db.Query(ctx, `
SELECT id
FROM members
WHERE workspace_id = $1
  AND type = 'human'
  AND status = 'active'
  AND workspace_role IN ('owner','admin')
ORDER BY id ASC
LIMIT 201
`, strings.TrimSpace(workspaceID))
	if err != nil {
		return nil, fmt.Errorf("list App installation request reviewers: %w", err)
	}
	defer rows.Close()
	reviewers := make([]string, 0)
	for rows.Next() {
		var memberID string
		if err := rows.Scan(&memberID); err != nil {
			return nil, fmt.Errorf("scan App installation request reviewer: %w", err)
		}
		reviewers = append(reviewers, strings.TrimSpace(memberID))
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate App installation request reviewers: %w", err)
	}
	if len(reviewers) > maxAppInstallationReviewers {
		return nil, fmt.Errorf("App installation request reviewer set exceeds %d members", maxAppInstallationReviewers)
	}
	return reviewers, nil
}

func (projector *Projector) memberDisplayName(
	ctx context.Context,
	workspaceID, memberID, preferred string,
) (string, error) {
	memberID = strings.TrimSpace(memberID)
	if memberID == "" {
		return "", fmt.Errorf("Approval member is required")
	}
	preferred = strings.TrimSpace(preferred)
	namesByMemberID, err := identity.LoadPublicMemberNamesByMemberID(
		ctx,
		projector.provisioner.db,
		workspaceID,
		[]string{memberID},
	)
	if err != nil {
		return "", fmt.Errorf("resolve Approval member display name: %w", err)
	}
	if displayName := strings.TrimSpace(namesByMemberID[memberID]); displayName != "" {
		return displayName, nil
	}
	return preferred, nil
}

func (projector *Projector) memberDisplayNames(
	ctx context.Context,
	workspaceID string,
	memberIDs []string,
) ([]string, error) {
	normalizedMemberIDs := identity.NormalizeNonEmptyStrings(memberIDs)
	namesByMemberID, err := identity.LoadPublicMemberNamesByMemberID(
		ctx,
		projector.provisioner.db,
		workspaceID,
		normalizedMemberIDs,
	)
	if err != nil {
		return nil, fmt.Errorf("resolve Approval member display names: %w", err)
	}
	names := make([]string, 0, len(memberIDs))
	seen := make(map[string]struct{}, len(memberIDs))
	for _, memberID := range normalizedMemberIDs {
		name := strings.TrimSpace(namesByMemberID[memberID])
		if name == "" {
			continue
		}
		if _, found := seen[name]; found {
			continue
		}
		seen[name] = struct{}{}
		names = append(names, name)
	}
	return names, nil
}

func (projector *Projector) assignmentProcessorNames(
	ctx context.Context,
	workspaceID string,
	assignments []approval.Assignment,
) ([]string, error) {
	memberIDs := make([]string, 0, len(assignments))
	for _, assignment := range assignments {
		if assignment.Status == approval.AssignmentPending {
			memberIDs = append(memberIDs, assignment.ApproverMemberID)
		}
	}
	return projector.memberDisplayNames(ctx, workspaceID, memberIDs)
}

func (projector *Projector) withCurrentAppInstallationProcessors(
	ctx context.Context,
	request app.AppInstallationRequestView,
) (app.AppInstallationRequestView, error) {
	reviewers, err := projector.listAppInstallationReviewers(ctx, request.WorkspaceID)
	if err != nil {
		return app.AppInstallationRequestView{}, err
	}
	request.CurrentProcessorNames, err = projector.memberDisplayNames(
		ctx, request.WorkspaceID, reviewers,
	)
	return request, err
}

func (projector *Projector) resolveAppInstallationCards(
	ctx context.Context,
	request app.AppInstallationRequestView,
	currentNotificationID, deliveryID, attemptID, actionID string,
) (app.AppNotificationResult, error) {
	var err error
	request.RequestedByDisplayName, err = projector.memberDisplayName(
		ctx,
		request.WorkspaceID,
		request.RequestedByMemberID,
		request.RequestedByDisplayName,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	processorName, err := projector.memberDisplayName(
		ctx,
		request.WorkspaceID,
		request.DecidedByMemberID,
		"",
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	notifications, err := projector.notifications.ListAppNotificationsByBusinessObject(
		ctx, request.WorkspaceID, IdentityID, BusinessNamespace,
		BusinessTypeAppInstallationRequest, request.ID, maxAppInstallationProjectionNotifications,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	var current app.AppNotificationResult
	for _, notification := range notifications {
		if notification.RecipientBasisKind == nil ||
			strings.TrimSpace(*notification.RecipientBasisKind) != RecipientBasisAppInstallationReviewer {
			continue
		}
		if notification.ContentKind != app.AppNotificationContentInteractiveCard ||
			notification.Status != app.AppNotificationStatusActive {
			if notification.ID == currentNotificationID {
				current, err = projector.notifications.GetAppNotificationForBuiltin(ctx, IdentityID, request.WorkspaceID, notification.ID)
				if err != nil {
					return app.AppNotificationResult{}, err
				}
			}
			continue
		}
		content, err := resolvedAppInstallationCardContent(request, processorName)
		if err != nil {
			return app.AppNotificationResult{}, err
		}
		version := appInstallationRequestBusinessVersion(request)
		mutationID := "app-installation-decision:" + request.ID + ":" + request.Status + ":" + notification.ID
		if strings.TrimSpace(actionID) != "" {
			mutationID = "app-installation-action:" + strings.TrimSpace(actionID) + ":" + notification.ID
		}
		command := app.AppNotificationMutationCommand{
			NotificationID: notification.ID, Kind: app.AppNotificationMutationResolve,
			ExpectedRevision: notification.Revision, ClientMutationID: mutationID,
			Content: &content, BusinessObjectVersion: &version,
			CausedByActor: app.AppNotificationActorRef{Type: "human", ID: request.DecidedByMemberID},
			CorrelationID: request.ID,
		}
		var updated app.AppNotificationResult
		if notification.ID == currentNotificationID {
			updated, err = projector.notifications.MutateAppNotificationForBuiltinDelivery(
				ctx, IdentityID, request.WorkspaceID, deliveryID, attemptID, command,
			)
		} else {
			updated, err = projector.notifications.MutateAppNotificationForBuiltin(ctx, IdentityID, request.WorkspaceID, command)
		}
		if err != nil {
			return app.AppNotificationResult{}, err
		}
		if notification.ID == currentNotificationID {
			current = updated
		}
	}
	if strings.TrimSpace(currentNotificationID) != "" && current.NotificationID == "" {
		return app.AppNotificationResult{}, fmt.Errorf("App installation action notification was not found")
	}
	return current, nil
}

// markAppInstallationChangeCannotBeApplied keeps the business request
// pending while converging every active reviewer card to a safe, reject-only
// state. The card which owns the current action is mutated through its leased
// delivery; peers use the ordinary builtin mutation path. Using the action's
// original notification revision as the generation fence makes retries and
// concurrent reviewers idempotent while still advancing the card revision so
// browser-side processing state clears immediately. Peer reviewer cards use
// their own loaded revisions as CAS fences because harmless earlier projection
// updates may leave reviewers at different revisions; cards already in the
// same reject-only failure state are left untouched on delivery replay.
func (projector *Projector) markAppInstallationChangeCannotBeApplied(
	ctx context.Context,
	request app.AppInstallationRequestView,
	currentNotificationID string,
	deliveryID string,
	attemptID string,
	expectedNotificationRevision int64,
) (app.AppNotificationResult, error) {
	request, err := projector.withCurrentAppInstallationProcessors(ctx, request)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	content, err := pendingAppInstallationCardContentWithChangeCannotBeApplied(request)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	notifications, err := projector.activeAppInstallationReviewerNotifications(ctx, request)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	currentNotificationID = strings.TrimSpace(currentNotificationID)
	if currentNotificationID != "" && expectedNotificationRevision <= 0 {
		return app.AppNotificationResult{}, fmt.Errorf("App installation action notification revision is invalid")
	}

	var current app.AppNotificationResult
	for _, notification := range notifications {
		if notification.RecipientBasisKind == nil ||
			strings.TrimSpace(*notification.RecipientBasisKind) != RecipientBasisAppInstallationReviewer ||
			notification.ContentKind != app.AppNotificationContentInteractiveCard ||
			notification.Status != app.AppNotificationStatusActive {
			continue
		}
		alreadyFailed, err := projector.appInstallationCardHasChangeCannotBeApplied(ctx, notification)
		if err != nil {
			return app.AppNotificationResult{}, err
		}
		if alreadyFailed {
			if notification.ID == currentNotificationID {
				current, err = projector.notifications.GetAppNotificationForBuiltin(
					ctx,
					IdentityID,
					request.WorkspaceID,
					notification.ID,
				)
				if err != nil {
					return app.AppNotificationResult{}, err
				}
			}
			continue
		}
		mutationRevision := notification.Revision
		if notification.ID == currentNotificationID && mutationRevision != expectedNotificationRevision {
			return app.AppNotificationResult{}, fmt.Errorf(
				"App installation action notification revision changed from %d to %d: %w",
				expectedNotificationRevision,
				mutationRevision,
				app.ErrConflict,
			)
		}
		version := appInstallationRequestBusinessVersion(request)
		command := app.AppNotificationMutationCommand{
			NotificationID:   notification.ID,
			Kind:             app.AppNotificationMutationUpdate,
			ExpectedRevision: mutationRevision,
			ClientMutationID: fmt.Sprintf(
				"app-installation-change-cannot-be-applied:%s:%d:%s:%d",
				request.ID,
				expectedNotificationRevision,
				notification.ID,
				mutationRevision,
			),
			Content:               &content,
			BusinessObjectVersion: &version,
			CorrelationID:         request.ID,
		}
		var updated app.AppNotificationResult
		if notification.ID == currentNotificationID {
			updated, err = projector.notifications.MutateAppNotificationForBuiltinDelivery(
				ctx,
				IdentityID,
				request.WorkspaceID,
				deliveryID,
				attemptID,
				command,
			)
		} else {
			updated, err = projector.notifications.MutateAppNotificationForBuiltin(
				ctx,
				IdentityID,
				request.WorkspaceID,
				command,
			)
		}
		if err != nil {
			if !errors.Is(err, app.ErrConflict) {
				return app.AppNotificationResult{}, err
			}
			latest, loadErr := projector.notifications.GetAppNotificationForBuiltin(
				ctx,
				IdentityID,
				request.WorkspaceID,
				notification.ID,
			)
			if loadErr != nil {
				return app.AppNotificationResult{}, loadErr
			}
			if latest.Status != app.AppNotificationStatusActive {
				if notification.ID == currentNotificationID {
					current = latest
				}
				continue
			}
			latestNotification, loadErr := projector.notifications.LoadAppNotification(
				ctx,
				request.WorkspaceID,
				notification.ID,
			)
			if loadErr != nil {
				return app.AppNotificationResult{}, loadErr
			}
			alreadyFailed, loadErr := projector.appInstallationCardHasChangeCannotBeApplied(ctx, latestNotification)
			if loadErr != nil {
				return app.AppNotificationResult{}, loadErr
			}
			if !alreadyFailed {
				return app.AppNotificationResult{}, err
			}
			updated = latest
		}
		if notification.ID == currentNotificationID {
			current = updated
		}
	}
	if currentNotificationID == "" {
		return app.AppNotificationResult{}, nil
	}
	if current.NotificationID == "" {
		current, err = projector.notifications.GetAppNotificationForBuiltin(
			ctx,
			IdentityID,
			request.WorkspaceID,
			currentNotificationID,
		)
		if err != nil {
			return app.AppNotificationResult{}, err
		}
	}
	if current.NotificationID == "" {
		return app.AppNotificationResult{}, fmt.Errorf("App installation action notification was not found")
	}
	return current, nil
}

// refreshAppInstallationSetupPatchingCards converges every still-active
// reviewer card to an approve-only recovery path. setup_patching is a
// durable point of no return: the provider PATCH may already have succeeded,
// so rejecting the request or changing its immutable target set would strand
// provider and local state. The request stays at business version 1 and the
// same Approve action is the only recovery authority.
func (projector *Projector) refreshAppInstallationSetupPatchingCards(
	ctx context.Context,
	request app.AppInstallationRequestView,
) error {
	if strings.TrimSpace(request.Status) != app.AppInstallationRequestStatusSetupPatching {
		return fmt.Errorf("App installation request is not awaiting setup recovery")
	}
	request, err := projector.withCurrentAppInstallationProcessors(ctx, request)
	if err != nil {
		return err
	}
	content, err := pendingAppInstallationSetupPatchingCardContent(request)
	if err != nil {
		return err
	}
	notifications, err := projector.activeAppInstallationReviewerNotifications(ctx, request)
	if err != nil {
		return err
	}
	for _, notification := range notifications {
		if notification.RecipientBasisKind == nil ||
			strings.TrimSpace(*notification.RecipientBasisKind) != RecipientBasisAppInstallationReviewer ||
			notification.ContentKind != app.AppNotificationContentInteractiveCard ||
			notification.Status != app.AppNotificationStatusActive {
			continue
		}
		ready, err := projector.appInstallationCardHasSetupPatchingRetry(ctx, notification)
		if err != nil {
			return err
		}
		if ready {
			continue
		}
		version := appInstallationRequestBusinessVersion(request)
		_, err = projector.notifications.MutateAppNotificationForBuiltin(
			ctx,
			IdentityID,
			request.WorkspaceID,
			app.AppNotificationMutationCommand{
				NotificationID:   notification.ID,
				Kind:             app.AppNotificationMutationUpdate,
				ExpectedRevision: notification.Revision,
				ClientMutationID: fmt.Sprintf(
					"github-setup-patching-retry:%s:%s:%d",
					request.ID,
					notification.ID,
					notification.Revision,
				),
				Content:               &content,
				BusinessObjectVersion: &version,
				CorrelationID:         request.ID,
			},
		)
		if err == nil {
			continue
		}
		if !errors.Is(err, app.ErrConflict) {
			return err
		}
		latest, loadErr := projector.notifications.LoadAppNotification(
			ctx,
			request.WorkspaceID,
			notification.ID,
		)
		if loadErr != nil {
			return loadErr
		}
		if latest.Status != app.AppNotificationStatusActive {
			continue
		}
		ready, loadErr = projector.appInstallationCardHasSetupPatchingRetry(ctx, latest)
		if loadErr != nil {
			return loadErr
		}
		if !ready {
			return err
		}
	}
	return nil
}

func (projector *Projector) appInstallationCardHasSetupPatchingRetry(
	ctx context.Context,
	notification app.AppNotificationRecord,
) (bool, error) {
	if projector == nil || projector.provisioner == nil || projector.provisioner.db == nil {
		return false, fmt.Errorf("Approval projector database is not configured")
	}
	card, err := projector.notifications.LoadAppNotificationCardInTx(
		ctx,
		projector.provisioner.db,
		notification.MessageID,
		false,
	)
	if err != nil {
		return false, err
	}
	var document struct {
		Status  string `json:"status"`
		TextKey string `json:"textKey"`
		Actions []struct {
			ActionKey string `json:"actionKey"`
		} `json:"actions"`
	}
	if err := json.Unmarshal(card.RenderPayloadJSON, &document); err != nil {
		return false, fmt.Errorf("decode GitHub setup recovery card: %w", err)
	}
	return strings.TrimSpace(document.Status) == app.AppInstallationRequestStatusSetupPatching &&
		strings.TrimSpace(document.TextKey) == appInstallationSetupPatchingRetryTextKey &&
		len(document.Actions) == 1 &&
		strings.TrimSpace(document.Actions[0].ActionKey) == ActionApprove, nil
}

func (projector *Projector) appInstallationCardHasChangeCannotBeApplied(
	ctx context.Context,
	notification app.AppNotificationRecord,
) (bool, error) {
	if projector == nil || projector.provisioner == nil || projector.provisioner.db == nil {
		return false, fmt.Errorf("Approval projector database is not configured")
	}
	card, err := projector.notifications.LoadAppNotificationCardInTx(
		ctx,
		projector.provisioner.db,
		notification.MessageID,
		false,
	)
	if err != nil {
		return false, err
	}
	var document struct {
		TextKey string `json:"textKey"`
		Actions []struct {
			ActionKey string `json:"actionKey"`
		} `json:"actions"`
	}
	if err := json.Unmarshal(card.RenderPayloadJSON, &document); err != nil {
		return false, fmt.Errorf("decode App installation review card: %w", err)
	}
	textKey := strings.TrimSpace(document.TextKey)
	return textKey == appInstallationChangeCannotBeAppliedTextKey &&
		len(document.Actions) == 1 &&
		strings.TrimSpace(document.Actions[0].ActionKey) == ActionReject, nil
}

func (projector *Projector) projectAppInstallationApplicantResult(
	ctx context.Context,
	request app.AppInstallationRequestView,
) (app.AppNotificationResult, error) {
	if app.IsPrivilegedAppRequestWithdrawalDecision(request) {
		return app.AppNotificationResult{}, nil
	}
	provisioned, err := projector.provisioner.EnsureWorkspace(ctx, request.WorkspaceID, time.Now().UTC())
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	resultClientNotificationID := appInstallationApplicantResultClientNotificationID(request.ID)
	existingResult, resultFound, err := projector.existingApplicantResult(
		ctx,
		request.WorkspaceID,
		provisioned.Installation.ID,
		resultClientNotificationID,
		request.RequestedByMemberID,
		RecipientBasisAppInstallationApplicant,
		request.ID,
		BusinessTypeAppInstallationRequest,
		request.ID,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	resolvedReceipt, receiptFound, err := projector.resolveAppInstallationApplicantReceiptIfPresent(
		ctx,
		request,
		provisioned.Installation.ID,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	// An authoritative fresh result wins replay even if an older or racing pending
	// receipt also needed convergence. Looking up the exact result before any
	// mutable directory data preserves the existing replay contract.
	if resultFound {
		return existingResult, nil
	}
	processorName, err := projector.memberDisplayName(
		ctx,
		request.WorkspaceID,
		request.DecidedByMemberID,
		"",
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	active, err := projector.activeHumanMember(ctx, request.WorkspaceID, request.RequestedByMemberID)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if !active {
		if receiptFound {
			return resolvedReceipt, nil
		}
		return app.AppNotificationResult{}, nil
	}
	resultContent, err := appInstallationApplicantResultCardContent(request, processorName)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	created, err := projector.notifications.CreateAppNotificationForBuiltin(ctx, IdentityID, request.WorkspaceID, app.AppNotificationCommand{
		ClientNotificationID: resultClientNotificationID,
		Target: app.AppNotificationTarget{
			Kind: app.AppNotificationTargetHumanDM, MemberID: request.RequestedByMemberID,
		},
		Content: resultContent,
		BusinessObject: app.AppNotificationBusinessObject{
			Namespace: BusinessNamespace, Type: BusinessTypeAppInstallationRequest,
			ID: request.ID, Version: appInstallationRequestBusinessVersion(request),
			URL: appInstallationDetailURL(request),
		},
		RecipientBasis: app.AppNotificationRecipientBasis{
			Kind: RecipientBasisAppInstallationApplicant, Ref: request.ID,
		},
		CausedByActor: app.AppNotificationActorRef{
			Type: "human", ID: request.DecidedByMemberID,
		},
		CorrelationID: request.ID,
	})
	if err == nil {
		return created, nil
	}
	if existing, found, lookupErr := projector.existingApplicantResult(
		ctx,
		request.WorkspaceID,
		provisioned.Installation.ID,
		resultClientNotificationID,
		request.RequestedByMemberID,
		RecipientBasisAppInstallationApplicant,
		request.ID,
		BusinessTypeAppInstallationRequest,
		request.ID,
	); lookupErr == nil && found {
		return existing, nil
	}
	// Membership can become inactive after the preflight check but before the
	// Human-DM create. The terminal decision is already durable, so an unavailable
	// recipient must not keep the action delivery retrying forever.
	active, statusErr := projector.activeHumanMember(ctx, request.WorkspaceID, request.RequestedByMemberID)
	if errors.Is(err, app.ErrForbidden) && statusErr == nil && !active {
		if receiptFound {
			return resolvedReceipt, nil
		}
		return app.AppNotificationResult{}, nil
	}
	return app.AppNotificationResult{}, err
}

// resolveAppInstallationApplicantReceiptIfPresent upgrades the applicant's
// stable pending receipt in place. Its absence remains non-fatal because the
// request commit and notification projection are deliberately separate: a
// partial projection failure must not suppress the independent terminal result.
// The exact stable client id and full recipient/business contract keep a
// different applicant notification from being mistaken for the receipt.
func (projector *Projector) resolveAppInstallationApplicantReceiptIfPresent(
	ctx context.Context,
	request app.AppInstallationRequestView,
	installationID string,
) (app.AppNotificationResult, bool, error) {
	receipt, found, err := projector.notifications.FindAppNotificationByClientID(
		ctx,
		strings.TrimSpace(request.WorkspaceID),
		strings.TrimSpace(installationID),
		appInstallationApplicantReceiptClientNotificationID(request.ID),
	)
	if err != nil || !found {
		return app.AppNotificationResult{}, false, err
	}
	if receipt.ContentKind != app.AppNotificationContentInteractiveCard ||
		receipt.RecipientMemberID == nil || strings.TrimSpace(*receipt.RecipientMemberID) != strings.TrimSpace(request.RequestedByMemberID) ||
		receipt.RecipientBasisKind == nil || strings.TrimSpace(*receipt.RecipientBasisKind) != RecipientBasisAppInstallationApplicant ||
		receipt.RecipientBasisRef == nil || strings.TrimSpace(*receipt.RecipientBasisRef) != strings.TrimSpace(request.ID) ||
		receipt.BusinessObjectNamespace == nil || strings.TrimSpace(*receipt.BusinessObjectNamespace) != BusinessNamespace ||
		receipt.BusinessObjectType == nil || strings.TrimSpace(*receipt.BusinessObjectType) != BusinessTypeAppInstallationRequest ||
		receipt.BusinessObjectID == nil || strings.TrimSpace(*receipt.BusinessObjectID) != strings.TrimSpace(request.ID) {
		return app.AppNotificationResult{}, true, fmt.Errorf("App installation applicant receipt contract does not match")
	}
	if receipt.Status != app.AppNotificationStatusActive {
		current, err := projector.notifications.GetAppNotificationForBuiltin(
			ctx,
			IdentityID,
			request.WorkspaceID,
			receipt.ID,
		)
		return current, true, err
	}
	processorName, err := projector.memberDisplayName(
		ctx,
		request.WorkspaceID,
		request.DecidedByMemberID,
		"",
	)
	if err != nil {
		return app.AppNotificationResult{}, true, err
	}
	content, err := resolvedAppInstallationApplicantReceiptCardContent(request, processorName)
	if err != nil {
		return app.AppNotificationResult{}, true, err
	}
	version := appInstallationRequestBusinessVersion(request)
	result, err := projector.resolveApplicantReceipt(
		ctx,
		request.WorkspaceID,
		installationID,
		appInstallationApplicantReceiptClientNotificationID(request.ID),
		RecipientBasisAppInstallationApplicant,
		content,
		version,
		request.DecidedByMemberID,
		request.ID,
		"app-installation-applicant-receipt:"+request.ID+":"+request.Status,
	)
	return result, true, err
}

func appInstallationRequestBusinessVersion(request app.AppInstallationRequestView) int64 {
	if !request.ApprovalRequired {
		return 0
	}
	if appInstallationRequestReviewActive(request.Status, request.ApprovalRequired) {
		return 1
	}
	return 2
}

func appInstallationRequestReviewActive(status string, approvalRequired bool) bool {
	if !approvalRequired {
		return false
	}
	switch strings.TrimSpace(status) {
	case app.AppInstallationRequestStatusPending, app.AppInstallationRequestStatusSetupPatching:
		return true
	default:
		return false
	}
}

func (projector *Projector) resolveApprovalCards(
	ctx context.Context,
	request serviceaccountstore.CreateRequest,
	workspaceID, approvalID, currentNotificationID, deliveryID, attemptID, actionID string,
	instance approval.Instance,
) (app.AppNotificationResult, error) {
	payload, err := decodeWorkspacePayload(request)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	applicantName, err := projector.memberDisplayName(
		ctx,
		request.WorkspaceID,
		request.RequestedByMemberID,
		"",
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	processorName, err := projector.memberDisplayName(
		ctx,
		request.WorkspaceID,
		instance.DecidedByMemberID,
		"",
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	notifications, err := projector.notifications.ListAppNotificationsByBusinessObject(
		ctx, workspaceID, IdentityID, BusinessNamespace, BusinessTypeInstance, approvalID, 200,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	var current app.AppNotificationResult
	for _, notification := range notifications {
		if notification.RecipientBasisKind == nil ||
			strings.TrimSpace(*notification.RecipientBasisKind) != RecipientBasisAssignment {
			continue
		}
		if notification.ContentKind != app.AppNotificationContentInteractiveCard ||
			notification.Status != app.AppNotificationStatusActive {
			if notification.ID == currentNotificationID {
				current, err = projector.notifications.GetAppNotificationForBuiltin(ctx, IdentityID, workspaceID, notification.ID)
				if err != nil {
					return app.AppNotificationResult{}, err
				}
			}
			continue
		}
		content, err := resolvedCardContent(
			request,
			instance,
			payload,
			applicantName,
			processorName,
		)
		if err != nil {
			return app.AppNotificationResult{}, err
		}
		version := instance.Version
		command := app.AppNotificationMutationCommand{
			NotificationID: notification.ID, Kind: app.AppNotificationMutationResolve,
			ExpectedRevision: notification.Revision,
			ClientMutationID: "approval-decision:" + strings.TrimSpace(actionID) + ":" + notification.ID,
			Content:          &content, BusinessObjectVersion: &version,
			CausedByActor: app.AppNotificationActorRef{Type: "human", ID: instance.DecidedByMemberID},
			CorrelationID: instance.CorrelationID,
		}
		var updated app.AppNotificationResult
		if notification.ID == currentNotificationID {
			updated, err = projector.notifications.MutateAppNotificationForBuiltinDelivery(
				ctx, IdentityID, workspaceID, deliveryID, attemptID, command,
			)
		} else {
			updated, err = projector.notifications.MutateAppNotificationForBuiltin(ctx, IdentityID, workspaceID, command)
		}
		if err != nil {
			return app.AppNotificationResult{}, err
		}
		if notification.ID == currentNotificationID {
			current = updated
		}
	}
	if current.NotificationID == "" {
		return app.AppNotificationResult{}, fmt.Errorf("Approval action notification was not found")
	}
	return current, nil
}

func (projector *Projector) projectApplicantResult(
	ctx context.Context,
	request serviceaccountstore.CreateRequest,
	instance approval.Instance,
	_ *serviceaccountstore.Account,
) (app.AppNotificationResult, error) {
	if serviceaccount.IsPrivilegedWorkspaceCreateCancellation(request) {
		return app.AppNotificationResult{}, nil
	}
	provisioned, err := projector.provisioner.EnsureWorkspace(ctx, request.WorkspaceID, time.Now().UTC())
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	resultClientNotificationID := applicantResultClientNotificationID(request.ID)
	if existing, found, err := projector.existingApplicantResult(
		ctx,
		request.WorkspaceID,
		provisioned.Installation.ID,
		resultClientNotificationID,
		request.RequestedByMemberID,
		RecipientBasisApplicant,
		instance.ID,
		BusinessTypeInstance,
		instance.ID,
	); err != nil {
		return app.AppNotificationResult{}, err
	} else if found {
		return existing, nil
	}
	payload, err := decodeWorkspacePayload(request)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	applicantName, err := projector.memberDisplayName(
		ctx,
		request.WorkspaceID,
		request.RequestedByMemberID,
		"",
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	processorName, err := projector.memberDisplayName(
		ctx,
		request.WorkspaceID,
		instance.DecidedByMemberID,
		"",
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	receiptContent, err := resolvedCardContent(
		request,
		instance,
		payload,
		applicantName,
		processorName,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	resolvedReceipt, err := projector.resolveApplicantReceipt(
		ctx,
		request.WorkspaceID,
		provisioned.Installation.ID,
		applicantReceiptClientNotificationID(request.ID),
		RecipientBasisApplicant,
		receiptContent,
		instance.Version,
		instance.DecidedByMemberID,
		instance.CorrelationID,
		"service-account-applicant-receipt:"+request.ID+":"+instance.State,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	active, err := projector.activeHumanMember(ctx, request.WorkspaceID, request.RequestedByMemberID)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if !active {
		return resolvedReceipt, nil
	}
	resultContent, err := applicantResultCardContent(
		request,
		instance,
		payload,
		processorName,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	created, err := projector.notifications.CreateAppNotificationForBuiltin(ctx, IdentityID, request.WorkspaceID, app.AppNotificationCommand{
		ClientNotificationID: resultClientNotificationID,
		Target: app.AppNotificationTarget{
			Kind: app.AppNotificationTargetHumanDM, MemberID: request.RequestedByMemberID,
		},
		Content: resultContent,
		BusinessObject: app.AppNotificationBusinessObject{
			Namespace: BusinessNamespace, Type: BusinessTypeInstance, ID: instance.ID,
			Version: instance.Version, URL: "/service-account/create-requests/" + request.ID,
		},
		RecipientBasis: app.AppNotificationRecipientBasis{Kind: RecipientBasisApplicant, Ref: instance.ID},
		CausedByActor:  app.AppNotificationActorRef{Type: "human", ID: instance.DecidedByMemberID},
		CorrelationID:  instance.CorrelationID,
	})
	if err == nil {
		return created, nil
	}
	if existing, found, lookupErr := projector.existingApplicantResult(
		ctx,
		request.WorkspaceID,
		provisioned.Installation.ID,
		resultClientNotificationID,
		request.RequestedByMemberID,
		RecipientBasisApplicant,
		instance.ID,
		BusinessTypeInstance,
		instance.ID,
	); lookupErr == nil && found {
		return existing, nil
	}
	active, statusErr := projector.activeHumanMember(ctx, request.WorkspaceID, request.RequestedByMemberID)
	if errors.Is(err, app.ErrForbidden) && statusErr == nil && !active {
		return resolvedReceipt, nil
	}
	return app.AppNotificationResult{}, err
}

// existingApplicantResult makes terminal projection replay independent from
// mutable directory data (for example a processor display-name change). Once
// the independently-created result exists, its authoritative payload wins.
func (projector *Projector) existingApplicantResult(
	ctx context.Context,
	workspaceID, installationID, clientNotificationID, recipientMemberID,
	recipientBasisKind, recipientBasisRef, businessObjectType, businessObjectID string,
) (app.AppNotificationResult, bool, error) {
	record, found, err := projector.notifications.FindAppNotificationByClientID(
		ctx,
		strings.TrimSpace(workspaceID),
		strings.TrimSpace(installationID),
		strings.TrimSpace(clientNotificationID),
	)
	if err != nil || !found {
		return app.AppNotificationResult{}, false, err
	}
	if record.ContentKind != app.AppNotificationContentInteractiveCard ||
		record.RecipientMemberID == nil || strings.TrimSpace(*record.RecipientMemberID) != strings.TrimSpace(recipientMemberID) ||
		record.RecipientBasisKind == nil || strings.TrimSpace(*record.RecipientBasisKind) != strings.TrimSpace(recipientBasisKind) ||
		record.RecipientBasisRef == nil || strings.TrimSpace(*record.RecipientBasisRef) != strings.TrimSpace(recipientBasisRef) ||
		record.BusinessObjectNamespace == nil || strings.TrimSpace(*record.BusinessObjectNamespace) != BusinessNamespace ||
		record.BusinessObjectType == nil || strings.TrimSpace(*record.BusinessObjectType) != strings.TrimSpace(businessObjectType) ||
		record.BusinessObjectID == nil || strings.TrimSpace(*record.BusinessObjectID) != strings.TrimSpace(businessObjectID) {
		return app.AppNotificationResult{}, false, fmt.Errorf("Approval applicant result contract does not match")
	}
	result, err := projector.notifications.GetAppNotificationForBuiltin(ctx, IdentityID, workspaceID, record.ID)
	if err != nil {
		return app.AppNotificationResult{}, false, err
	}
	result.AlreadyCreated = true
	return result, true, nil
}

func (projector *Projector) activeHumanMember(
	ctx context.Context,
	workspaceID, memberID string,
) (bool, error) {
	var count int64
	if err := projector.provisioner.db.QueryRow(ctx, `
SELECT COUNT(*)
FROM members
WHERE workspace_id = $1 AND id = $2 AND type = 'human' AND status = 'active'
`, strings.TrimSpace(workspaceID), strings.TrimSpace(memberID)).Scan(&count); err != nil {
		return false, fmt.Errorf("check Approval applicant membership: %w", err)
	}
	return count > 0, nil
}

func (projector *Projector) projectPendingApplicantReceipt(
	ctx context.Context,
	request serviceaccountstore.CreateRequest,
	instance approval.Instance,
	payload serviceaccount.WorkspaceCreatePayload,
	applicantName string,
	currentProcessorNames []string,
) (app.AppNotificationResult, error) {
	content, err := pendingApplicantCardContent(
		request, instance, payload, applicantName, currentProcessorNames,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	return projector.notifications.CreateAppNotificationForBuiltin(ctx, IdentityID, request.WorkspaceID, app.AppNotificationCommand{
		ClientNotificationID: applicantReceiptClientNotificationID(request.ID),
		Target: app.AppNotificationTarget{
			Kind: app.AppNotificationTargetHumanDM, MemberID: request.RequestedByMemberID,
		},
		Content: content,
		BusinessObject: app.AppNotificationBusinessObject{
			Namespace: BusinessNamespace, Type: BusinessTypeInstance, ID: instance.ID,
			Version: instance.Version, URL: "/service-account/create-requests/" + request.ID,
		},
		RecipientBasis: app.AppNotificationRecipientBasis{Kind: RecipientBasisApplicant, Ref: instance.ID},
		CausedByActor:  app.AppNotificationActorRef{Type: "human", ID: request.RequestedByMemberID},
		CorrelationID:  instance.CorrelationID,
	})
}

func (projector *Projector) projectPendingAppInstallationApplicantReceipt(
	ctx context.Context,
	request app.AppInstallationRequestView,
) (app.AppNotificationResult, error) {
	active, err := projector.activeHumanMember(ctx, request.WorkspaceID, request.RequestedByMemberID)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if !active {
		return app.AppNotificationResult{}, nil
	}
	content, err := pendingAppInstallationApplicantCardContent(request)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	provisioned, err := projector.provisioner.EnsureWorkspace(
		ctx, request.WorkspaceID, time.Now().UTC(),
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	existing, found, err := projector.notifications.FindAppNotificationByClientID(
		ctx,
		request.WorkspaceID,
		provisioned.Installation.ID,
		appInstallationApplicantReceiptClientNotificationID(request.ID),
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if found {
		current, err := projector.notifications.GetAppNotificationForBuiltin(
			ctx, IdentityID, request.WorkspaceID, existing.ID,
		)
		if err != nil || current.Status != app.AppNotificationStatusActive || len(request.CurrentProcessorNames) == 0 {
			return current, err
		}
		card, err := projector.notifications.LoadAppNotificationCardInTx(
			ctx, projector.provisioner.db, existing.MessageID, false,
		)
		if err != nil {
			return app.AppNotificationResult{}, err
		}
		var document struct {
			Approval struct {
				CurrentProcessorNames []string `json:"currentProcessorNames"`
			} `json:"approval"`
		}
		if err := json.Unmarshal(card.RenderPayloadJSON, &document); err != nil {
			return app.AppNotificationResult{}, fmt.Errorf("decode App installation applicant receipt: %w", err)
		}
		if slices.Equal(document.Approval.CurrentProcessorNames, request.CurrentProcessorNames) {
			return current, nil
		}
		version := appInstallationRequestBusinessVersion(request)
		return projector.notifications.MutateAppNotificationForBuiltin(
			ctx, IdentityID, request.WorkspaceID, app.AppNotificationMutationCommand{
				NotificationID:   existing.ID,
				Kind:             app.AppNotificationMutationUpdate,
				ExpectedRevision: existing.Revision,
				ClientMutationID: fmt.Sprintf(
					"app-installation-applicant-processors:%s:%d",
					request.ID, existing.Revision,
				),
				Content: &content, BusinessObjectVersion: &version,
				CorrelationID: request.ID,
			},
		)
	}
	created, err := projector.notifications.CreateAppNotificationForBuiltin(ctx, IdentityID, request.WorkspaceID, app.AppNotificationCommand{
		ClientNotificationID: appInstallationApplicantReceiptClientNotificationID(request.ID),
		Target: app.AppNotificationTarget{
			Kind: app.AppNotificationTargetHumanDM, MemberID: request.RequestedByMemberID,
		},
		Content: content,
		BusinessObject: app.AppNotificationBusinessObject{
			Namespace: BusinessNamespace, Type: BusinessTypeAppInstallationRequest,
			ID: request.ID, Version: appInstallationRequestBusinessVersion(request),
			URL: appInstallationDetailURL(request),
		},
		RecipientBasis: app.AppNotificationRecipientBasis{
			Kind: RecipientBasisAppInstallationApplicant, Ref: request.ID,
		},
		CausedByActor: app.AppNotificationActorRef{
			Type: "human", ID: request.RequestedByMemberID,
		},
		CorrelationID: request.ID,
	})
	if err == nil {
		return created, nil
	}
	// Membership or request state can change after the preflight check. Both
	// make a v1 applicant receipt unavailable by design and must not leave the
	// already-durable request or reviewer projection retrying forever.
	if errors.Is(err, app.ErrForbidden) {
		stillActive, statusErr := projector.activeHumanMember(ctx, request.WorkspaceID, request.RequestedByMemberID)
		if statusErr == nil && !stillActive {
			return app.AppNotificationResult{}, nil
		}
		current, loadErr := projector.notifications.GetAppInstallationRequest(ctx, request.WorkspaceID, request.ID)
		if loadErr == nil && !appInstallationRequestReviewActive(current.Status, current.ApprovalRequired) {
			return app.AppNotificationResult{}, nil
		}
	}
	return app.AppNotificationResult{}, err
}

// resolveApplicantReceipt converges the applicant's stable pending card to its
// terminal state. This edit intentionally preserves the original message
// identity and read frontier; the caller appends an independent result message
// afterwards so normal message-create fan-out produces the unread notification.
func (projector *Projector) resolveApplicantReceipt(
	ctx context.Context,
	workspaceID, installationID, clientNotificationID, recipientBasisKind string,
	content app.AppNotificationContent,
	businessVersion int64,
	decidedByMemberID, correlationID, clientMutationID string,
) (app.AppNotificationResult, error) {
	receipt, found, err := projector.notifications.FindAppNotificationByClientID(
		ctx, workspaceID, installationID, clientNotificationID,
	)
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	if !found {
		return app.AppNotificationResult{}, fmt.Errorf("Approval applicant receipt was not found")
	}
	if receipt.ContentKind != app.AppNotificationContentInteractiveCard ||
		receipt.RecipientMemberID == nil ||
		receipt.RecipientBasisKind == nil ||
		strings.TrimSpace(*receipt.RecipientBasisKind) != strings.TrimSpace(recipientBasisKind) {
		return app.AppNotificationResult{}, fmt.Errorf("Approval applicant receipt contract does not match")
	}
	var result app.AppNotificationResult
	switch receipt.Status {
	case app.AppNotificationStatusActive:
		result, err = projector.notifications.MutateAppNotificationForBuiltin(ctx, IdentityID, workspaceID, app.AppNotificationMutationCommand{
			NotificationID:        receipt.ID,
			Kind:                  app.AppNotificationMutationResolve,
			ExpectedRevision:      receipt.Revision,
			ClientMutationID:      strings.TrimSpace(clientMutationID),
			Content:               &content,
			BusinessObjectVersion: &businessVersion,
			CausedByActor: app.AppNotificationActorRef{
				Type: "human", ID: strings.TrimSpace(decidedByMemberID),
			},
			CorrelationID: strings.TrimSpace(correlationID),
		})
	case app.AppNotificationStatusResolved:
		result, err = projector.notifications.GetAppNotificationForBuiltin(ctx, IdentityID, workspaceID, receipt.ID)
	default:
		return app.AppNotificationResult{}, fmt.Errorf("Approval applicant receipt cannot converge from status %q", receipt.Status)
	}
	if err != nil {
		return app.AppNotificationResult{}, err
	}
	return result, nil
}

func decodeWorkspacePayload(request serviceaccountstore.CreateRequest) (serviceaccount.WorkspaceCreatePayload, error) {
	var payload serviceaccount.WorkspaceCreatePayload
	if err := json.Unmarshal(request.PayloadSnapshot, &payload); err != nil {
		return payload, fmt.Errorf("decode service account create request for Approval card: %w", err)
	}
	return payload, nil
}

func pendingCardContent(
	request serviceaccountstore.CreateRequest,
	instance approval.Instance,
	assignment approval.Assignment,
	payload serviceaccount.WorkspaceCreatePayload,
	applicantName string,
	currentProcessorNames []string,
) (app.AppNotificationContent, error) {
	card := map[string]any{
		"schemaVersion": "1",
		"title":         "审批服务号创建申请",
		"titleKey":      "service_account_create_review",
		"status":        approval.StatePending,
		"request": map[string]any{
			"id": request.ID, "name": payload.Name, "description": payload.Description,
			"applicantName": strings.TrimSpace(applicantName),
		},
		"actions": []map[string]any{
			{"actionKey": ActionApprove, "type": "callback", "label": "通过", "semantics": ActionSemanticsCommit},
			{
				"actionKey": ActionReject, "type": "callback", "label": "拒绝", "semantics": ActionSemanticsRevise,
				"inputSchema": map[string]any{
					"type": "object",
					"properties": map[string]any{
						"reason": map[string]any{"type": "string", "maxLength": 1000},
					},
					"additionalProperties": false,
				},
			},
		},
		"approval": map[string]any{
			"id": instance.ID, "version": instance.Version, "assignmentId": assignment.ID,
			"currentProcessorNames": currentProcessorNames,
		},
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal Approval card: %w", err)
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: "待审批：创建服务号 " + payload.Name,
		CardJSON:     encoded,
	}, nil
}

func resolvedCardContent(
	request serviceaccountstore.CreateRequest,
	instance approval.Instance,
	payload serviceaccount.WorkspaceCreatePayload,
	applicantName string,
	processorName string,
) (app.AppNotificationContent, error) {
	title := "服务号创建申请已通过"
	titleKey := "service_account_create_approved"
	if instance.State == approval.StateRejected {
		title = "服务号创建申请已拒绝"
		titleKey = "service_account_create_rejected"
	}
	card := map[string]any{
		"schemaVersion": "1", "title": title, "titleKey": titleKey, "status": instance.State,
		"request": map[string]any{
			"id": request.ID, "name": payload.Name, "description": payload.Description,
			"applicantName": strings.TrimSpace(applicantName),
		},
		"approval": map[string]any{
			"id": instance.ID, "version": instance.Version, "state": instance.State,
			"processorName": strings.TrimSpace(processorName),
			"reason":        instance.DecisionReason,
		},
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal resolved Approval card: %w", err)
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: title + "：" + payload.Name,
		CardJSON:     encoded,
	}, nil
}

func applicantResultCardContent(
	request serviceaccountstore.CreateRequest,
	instance approval.Instance,
	payload serviceaccount.WorkspaceCreatePayload,
	processorName string,
) (app.AppNotificationContent, error) {
	title := "你的服务号创建申请已通过"
	titleKey := "service_account_create_result_approved"
	if instance.State == approval.StateRejected {
		title = "你的服务号创建申请已拒绝"
		titleKey = "service_account_create_result_rejected"
	}
	card := map[string]any{
		"schemaVersion": "1", "template": "approval_result",
		"title": title, "titleKey": titleKey, "status": instance.State,
		"request": map[string]any{
			"id": request.ID, "name": payload.Name, "description": payload.Description,
		},
		"approval": map[string]any{
			"id": instance.ID, "version": instance.Version, "state": instance.State,
			"processorName": strings.TrimSpace(processorName),
			"reason":        instance.DecisionReason,
		},
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal applicant Approval result card: %w", err)
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: title + "：" + payload.Name,
		CardJSON:     encoded,
	}, nil
}

func pendingApplicantCardContent(
	request serviceaccountstore.CreateRequest,
	instance approval.Instance,
	payload serviceaccount.WorkspaceCreatePayload,
	applicantName string,
	currentProcessorNames []string,
) (app.AppNotificationContent, error) {
	card := map[string]any{
		"schemaVersion": "1",
		"title":         "服务号创建申请已提交",
		"titleKey":      "service_account_create_submitted",
		"status":        approval.StatePending,
		"request": map[string]any{
			"id": request.ID, "name": payload.Name, "description": payload.Description,
			"applicantName": strings.TrimSpace(applicantName),
		},
		"approval": map[string]any{
			"id": instance.ID, "version": instance.Version,
			"currentProcessorNames": currentProcessorNames,
		},
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal applicant Approval card: %w", err)
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: "服务号创建申请已提交：" + payload.Name,
		CardJSON:     encoded,
	}, nil
}

func pendingAppInstallationCardContent(request app.AppInstallationRequestView) (app.AppNotificationContent, error) {
	if strings.TrimSpace(request.Status) == app.AppInstallationRequestStatusSetupPatching {
		return pendingAppInstallationSetupPatchingCardContent(request)
	}
	return pendingAppInstallationCardContentWithReviewFailure(request, "", "")
}

func pendingAppInstallationCardContentWithChangeCannotBeApplied(request app.AppInstallationRequestView) (app.AppNotificationContent, error) {
	return pendingAppInstallationCardContentWithReviewFailure(
		request,
		appInstallationChangeCannotBeAppliedText,
		appInstallationChangeCannotBeAppliedTextKey,
	)
}

func pendingAppInstallationSetupPatchingCardContent(request app.AppInstallationRequestView) (app.AppNotificationContent, error) {
	return pendingAppInstallationCardContentWithReviewFailure(
		request,
		appInstallationSetupPatchingRetryText,
		appInstallationSetupPatchingRetryTextKey,
	)
}

func pendingAppInstallationCardContentWithReviewFailure(
	request app.AppInstallationRequestView,
	failureText string,
	failureTextKey string,
) (app.AppNotificationContent, error) {
	presentation, err := appInstallationRequestPresentation(request)
	if err != nil {
		return app.AppNotificationContent{}, err
	}
	requestDocument, fields, err := appInstallationRequestCardDetails(request, presentation.kind, true)
	if err != nil {
		return app.AppNotificationContent{}, err
	}
	reviewTitlePrefix := "审批 "
	if presentation.titleKeyPrefix == "apps_integration_creation" {
		reviewTitlePrefix = "审批"
	}
	rejectAction := map[string]any{
		"actionKey": ActionReject, "type": "callback", "label": "拒绝", "semantics": ActionSemanticsRevise,
		"inputSchema": map[string]any{
			"type": "object",
			"properties": map[string]any{
				"reason": map[string]any{"type": "string", "maxLength": 1000},
			},
			"additionalProperties": false,
		},
	}
	actions := []map[string]any{
		{"actionKey": ActionApprove, "type": "callback", "label": "通过", "semantics": ActionSemanticsCommit},
		rejectAction,
	}
	card := map[string]any{
		"schemaVersion": "1",
		"title":         reviewTitlePrefix + presentation.requestTitle,
		"titleKey":      presentation.titleKeyPrefix + "_review",
		"status":        app.AppInstallationRequestStatusPending,
		"request":       requestDocument,
		"fields":        fields,
		"actions":       actions,
		"approval": map[string]any{
			"currentProcessorNames": request.CurrentProcessorNames,
		},
	}
	if strings.TrimSpace(request.Status) == app.AppInstallationRequestStatusSetupPatching {
		card["status"] = app.AppInstallationRequestStatusSetupPatching
		card["actions"] = []map[string]any{{
			"actionKey": ActionApprove, "type": "callback", "label": "重试通过", "semantics": ActionSemanticsCommit,
		}}
	}
	if failureText = strings.TrimSpace(failureText); failureText != "" {
		card["text"] = failureText
		card["textKey"] = strings.TrimSpace(failureTextKey)
		if strings.TrimSpace(request.Status) != app.AppInstallationRequestStatusSetupPatching {
			card["actions"] = []map[string]any{rejectAction}
		}
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal App installation Approval card: %w", err)
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: "待审批：" + presentation.operationTitle + " " + request.AppName,
		CardJSON:     encoded,
	}, nil
}

// pendingAppInstallationApplicantCardContent is intentionally smaller than
// the reviewer card. It confirms only the durable request identity, App, and
// operation; proposal config, provider targets, reasons, runtime endpoints,
// credential metadata, scopes, and callback actions remain out of the
// applicant Inbox.
func pendingAppInstallationApplicantCardContent(
	request app.AppInstallationRequestView,
) (app.AppNotificationContent, error) {
	presentation, err := appInstallationRequestPresentation(request)
	if err != nil {
		return app.AppNotificationContent{}, err
	}
	requestDocument, fields := appInstallationApplicantReceiptCardDetails(request, presentation.kind)
	card := map[string]any{
		"schemaVersion": "1",
		"title":         presentation.requestTitle + "已提交",
		"titleKey":      presentation.titleKeyPrefix + "_submitted",
		"status":        app.AppInstallationRequestStatusPending,
		"request":       requestDocument,
		"fields":        fields,
		"approval": map[string]any{
			"currentProcessorNames": request.CurrentProcessorNames,
		},
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal App installation applicant receipt card: %w", err)
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: presentation.requestTitle + "已提交：" + request.AppName,
		CardJSON:     encoded,
	}, nil
}

func resolvedAppInstallationApplicantReceiptCardContent(
	request app.AppInstallationRequestView,
	processorName string,
) (app.AppNotificationContent, error) {
	presentation, err := appInstallationRequestPresentation(request)
	if err != nil {
		return app.AppNotificationContent{}, err
	}
	title := presentation.requestTitle + "已通过"
	titleKey := presentation.titleKeyPrefix + "_approved"
	if request.Status == app.AppInstallationRequestStatusRejected {
		title = presentation.requestTitle + "已拒绝"
		titleKey = presentation.titleKeyPrefix + "_rejected"
	}
	requestDocument, fields := appInstallationApplicantReceiptCardDetails(request, presentation.kind)
	card := map[string]any{
		"schemaVersion": "1", "title": title, "titleKey": titleKey, "status": request.Status,
		"request": requestDocument,
		"fields":  fields,
		"approval": map[string]any{
			"state": request.Status, "processorName": strings.TrimSpace(processorName),
		},
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal resolved App installation applicant receipt card: %w", err)
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: title + "：" + request.AppName,
		CardJSON:     encoded,
	}, nil
}

func appInstallationApplicantReceiptCardDetails(
	request app.AppInstallationRequestView,
	requestKind string,
) (map[string]any, []map[string]any) {
	if requestKind == app.AppInstallationRequestKindInstall &&
		len(normalizedAppInstallationReviewTransports(request.AppRuntimeTransports)) > 0 {
		requestDocument := map[string]any{
			"id":      request.ID,
			"appName": request.AppName,
		}
		if request.AppDescription != "" {
			requestDocument["description"] = request.AppDescription
		}
		return requestDocument, []map[string]any{{"label": "integration_type", "value": "app"}}
	}
	requestDocument := map[string]any{
		"id":        request.ID,
		"appName":   request.AppName,
		"operation": requestKind,
	}
	if request.AppDescription != "" {
		requestDocument["description"] = request.AppDescription
	}
	return requestDocument, []map[string]any{{"label": "operation", "value": requestKind}}
}

func resolvedAppInstallationCardContent(
	request app.AppInstallationRequestView,
	processorName string,
) (app.AppNotificationContent, error) {
	presentation, err := appInstallationRequestPresentation(request)
	if err != nil {
		return app.AppNotificationContent{}, err
	}
	title := presentation.requestTitle + "已通过"
	titleKey := presentation.titleKeyPrefix + "_approved"
	if request.Status == app.AppInstallationRequestStatusRejected {
		title = presentation.requestTitle + "已拒绝"
		titleKey = presentation.titleKeyPrefix + "_rejected"
	}
	requestDocument, fields, err := appInstallationRequestCardDetails(request, presentation.kind, true)
	if err != nil {
		return app.AppNotificationContent{}, err
	}
	card := map[string]any{
		"schemaVersion": "1", "title": title, "titleKey": titleKey, "status": request.Status,
		"request": requestDocument,
		"fields":  fields,
		"approval": map[string]any{
			"state": request.Status, "processorName": strings.TrimSpace(processorName),
			"reason": request.DecisionReason,
		},
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal resolved App installation Approval card: %w", err)
	}
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: title + "：" + request.AppName,
		CardJSON:     encoded,
	}, nil
}

func appInstallationApplicantResultCardContent(
	request app.AppInstallationRequestView,
	processorName string,
) (app.AppNotificationContent, error) {
	presentation, err := appInstallationRequestPresentation(request)
	if err != nil {
		return app.AppNotificationContent{}, err
	}
	applicantTitlePrefix := "你的 "
	if presentation.titleKeyPrefix == "apps_integration_creation" {
		applicantTitlePrefix = "你的"
	}
	title := applicantTitlePrefix + presentation.requestTitle + "已通过"
	titleKey := presentation.titleKeyPrefix + "_result_approved"
	if request.Status == app.AppInstallationRequestStatusRejected {
		title = applicantTitlePrefix + presentation.requestTitle + "已拒绝"
		titleKey = presentation.titleKeyPrefix + "_result_rejected"
	}
	requestDocument, fields, err := appInstallationRequestCardDetails(request, presentation.kind, false)
	if err != nil {
		return app.AppNotificationContent{}, err
	}
	card := map[string]any{
		"schemaVersion": "1", "template": "approval_result",
		"title": title, "titleKey": titleKey, "status": request.Status,
		"request": requestDocument,
		"fields":  fields,
		"approval": map[string]any{
			"state": request.Status, "processorName": strings.TrimSpace(processorName),
			"reason": request.DecisionReason,
		},
	}
	resultURL := appInstallationDetailURL(request)
	if request.Status == app.AppInstallationRequestStatusApproved &&
		presentation.kind != app.AppInstallationRequestKindUninstall &&
		resultURL != "" {
		actionKey := "open_app"
		label := "打开 App"
		if presentation.kind == app.AppInstallationRequestKindInstall &&
			len(normalizedAppInstallationReviewTransports(request.AppRuntimeTransports)) > 0 {
			actionKey = "open_app_setup"
			label = "配置应用"
		}
		card["actions"] = []map[string]any{{
			"actionKey": actionKey,
			"type":      "open_url",
			"label":     label,
			"semantics": ActionSemanticsNavigate,
			"url":       resultURL,
		}}
	}
	encoded, err := json.Marshal(card)
	if err != nil {
		return app.AppNotificationContent{}, fmt.Errorf("marshal App installation applicant result card: %w", err)
	}
	fallbackText := title + "：" + request.AppName
	return app.AppNotificationContent{
		Kind:         app.AppNotificationContentInteractiveCard,
		FallbackText: fallbackText,
		CardJSON:     encoded,
	}, nil
}

func appInstallationDetailURL(request app.AppInstallationRequestView) string {
	if strings.TrimSpace(request.AppSlug) != "" {
		return "/apps/" + url.PathEscape(strings.TrimSpace(request.AppSlug))
	}
	return ""
}

type appInstallationCardPresentation struct {
	kind           string
	requestTitle   string
	operationTitle string
	titleKeyPrefix string
}

func appInstallationRequestPresentation(request app.AppInstallationRequestView) (appInstallationCardPresentation, error) {
	switch strings.TrimSpace(request.RequestKind) {
	case app.AppInstallationRequestKindInstall:
		if len(normalizedAppInstallationReviewTransports(request.AppRuntimeTransports)) > 0 {
			return appInstallationCardPresentation{
				kind:           app.AppInstallationRequestKindInstall,
				requestTitle:   "应用集成创建申请",
				operationTitle: "创建应用集成",
				titleKeyPrefix: "apps_integration_creation",
			}, nil
		}
		return appInstallationCardPresentation{
			kind:           app.AppInstallationRequestKindInstall,
			requestTitle:   "App 安装申请",
			operationTitle: "安装 App",
			titleKeyPrefix: "app_installation",
		}, nil
	case app.AppInstallationRequestKindConfigure:
		return appInstallationCardPresentation{
			kind:           app.AppInstallationRequestKindConfigure,
			requestTitle:   "App 配置变更申请",
			operationTitle: "变更 App 配置",
			titleKeyPrefix: "app_configuration",
		}, nil
	case app.AppInstallationRequestKindConnect:
		providerName, providerKey, err := providerConnectionPresentationIdentity(request)
		if err != nil {
			return appInstallationCardPresentation{}, err
		}
		return appInstallationCardPresentation{
			kind:           app.AppInstallationRequestKindConnect,
			requestTitle:   providerName + " 连接新增申请",
			operationTitle: "新增 " + providerName + " 连接",
			titleKeyPrefix: providerKey + "_connection_connect",
		}, nil
	case app.AppInstallationRequestKindDisconnect:
		providerName, providerKey, err := providerConnectionPresentationIdentity(request)
		if err != nil {
			return appInstallationCardPresentation{}, err
		}
		return appInstallationCardPresentation{
			kind:           app.AppInstallationRequestKindDisconnect,
			requestTitle:   providerName + " 连接断开申请",
			operationTitle: "断开 " + providerName + " 连接",
			titleKeyPrefix: providerKey + "_connection_disconnect",
		}, nil
	case app.AppInstallationRequestKindUninstall:
		return appInstallationCardPresentation{
			kind:           app.AppInstallationRequestKindUninstall,
			requestTitle:   "App 卸载申请",
			operationTitle: "卸载 App",
			titleKeyPrefix: "app_uninstallation",
		}, nil
	default:
		return appInstallationCardPresentation{}, fmt.Errorf("unsupported App installation request kind %q", request.RequestKind)
	}
}

func appInstallationRequestCardDetails(
	request app.AppInstallationRequestView,
	requestKind string,
	includeApplicant bool,
) (map[string]any, []map[string]any, error) {
	if requestKind == app.AppInstallationRequestKindConnect || requestKind == app.AppInstallationRequestKindDisconnect {
		if _, _, err := providerConnectionPresentationIdentity(request); err != nil {
			return nil, nil, err
		}
		return providerConnectionRequestCardDetails(request, requestKind, includeApplicant)
	}
	if requestKind == app.AppInstallationRequestKindUninstall {
		requestDocument := map[string]any{
			"appName":   request.AppName,
			"operation": requestKind,
			"reason":    request.RequestReason,
		}
		if request.AppDescription != "" {
			requestDocument["description"] = request.AppDescription
		}
		if includeApplicant {
			requestDocument["applicantName"] = request.RequestedByDisplayName
		}
		return requestDocument, []map[string]any{{"label": "operation", "value": requestKind}}, nil
	}
	if transports := normalizedAppInstallationReviewTransports(request.AppRuntimeTransports); requestKind == app.AppInstallationRequestKindInstall && len(transports) > 0 {
		appRuntime := map[string]any{"transports": transports}
		catalogVisibility := normalizedAppInstallationReviewCatalogVisibility(request.AppCatalogVisibility)
		requestDocument := map[string]any{
			"id":                request.ID,
			"appName":           request.AppName,
			"appRuntime":        appRuntime,
			"catalogVisibility": catalogVisibility,
		}
		if request.AppDescription != "" {
			requestDocument["description"] = request.AppDescription
		}
		if includeApplicant {
			requestDocument["applicantName"] = request.RequestedByDisplayName
		}
		fields := []map[string]any{{
			"label": "integration_type",
			"value": "app",
		}, {
			"label": "app_runtime_transport",
			"value": strings.Join(transports, ", "),
		}, {
			"label": "app_catalog_visibility",
			"value": catalogVisibility,
		}}
		if callbackURL := strings.TrimSpace(request.AppCallbackURL); callbackURL != "" {
			appRuntime["callbackUrl"] = callbackURL
			fields = append(fields, map[string]any{
				"label": "app_callback_url",
				"value": callbackURL,
			})
		}
		return requestDocument, fields, nil
	}
	if strings.EqualFold(strings.TrimSpace(request.AppSlug), "gitea") {
		requestDocument, fields := giteaAppInstallationRequestCardDetails(request, requestKind, includeApplicant)
		return requestDocument, fields, nil
	}
	if strings.TrimSpace(request.AppID) == registry.GitHubAppID {
		return githubAppInstallationRequestCardDetails(request, requestKind, includeApplicant)
	}

	baseConfigDocument := manifestConfigOrEmpty(request.BaseConfig)
	requestedConfigDocument := manifestConfigOrEmpty(request.RequestedConfig)
	baseConfig, err := marshalManifestConfig(baseConfigDocument)
	if err != nil {
		return nil, nil, fmt.Errorf("marshal base App manifest config: %w", err)
	}
	requestedConfig, err := marshalManifestConfig(requestedConfigDocument)
	if err != nil {
		return nil, nil, fmt.Errorf("marshal requested App manifest config: %w", err)
	}
	requestDocument := map[string]any{
		"id":              request.ID,
		"appName":         request.AppName,
		"operation":       requestKind,
		"baseConfig":      baseConfigDocument,
		"requestedConfig": requestedConfigDocument,
		"reason":          request.RequestReason,
	}
	if request.AppDescription != "" {
		requestDocument["description"] = request.AppDescription
	}
	if includeApplicant {
		requestDocument["applicantName"] = request.RequestedByDisplayName
	}
	fields := []map[string]any{
		{"label": "operation", "value": requestKind},
		{"label": "base_config", "value": baseConfig},
	}
	fields = append(fields, map[string]any{"label": "requested_config", "value": requestedConfig})
	if transports := normalizedAppInstallationReviewTransports(request.AppRuntimeTransports); len(transports) > 0 {
		appRuntime := map[string]any{"transports": transports}
		fields = append(fields, map[string]any{
			"label": "app_runtime_transport",
			"value": strings.Join(transports, ", "),
		})
		if callbackURL := strings.TrimSpace(request.AppCallbackURL); callbackURL != "" {
			appRuntime["callbackUrl"] = callbackURL
			fields = append(fields, map[string]any{
				"label": "app_callback_url",
				"value": callbackURL,
			})
		}
		requestDocument["appRuntime"] = appRuntime
	}
	return requestDocument, fields, nil
}

func normalizedAppInstallationReviewCatalogVisibility(value string) string {
	if strings.EqualFold(strings.TrimSpace(value), registry.CatalogVisibilityPublic) {
		return registry.CatalogVisibilityPublic
	}
	return registry.CatalogVisibilityPrivate
}

func normalizedAppInstallationReviewTransports(values []string) []string {
	seen := map[string]struct{}{}
	result := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value != "callback" && value != "runner_stream" {
			continue
		}
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	return result
}

func providerConnectionPresentationIdentity(request app.AppInstallationRequestView) (string, string, error) {
	switch strings.TrimSpace(request.AppID) {
	case registry.GitHubAppID:
		return "GitHub", "github", nil
	case registry.GiteaAppID:
		return "Gitea", "gitea", nil
	default:
		return "", "", fmt.Errorf("provider connection request must target the GitHub or Gitea App")
	}
}

func providerConnectionRequestCardDetails(
	request app.AppInstallationRequestView,
	requestKind string,
	includeApplicant bool,
) (map[string]any, []map[string]any, error) {
	connectionTarget := strings.TrimSpace(request.ConnectionTarget)
	connectionLabel := strings.TrimSpace(request.ConnectionLabel)
	if connectionTarget == "" {
		return nil, nil, fmt.Errorf("provider connection request target is required")
	}
	if connectionLabel == "" {
		return nil, nil, fmt.Errorf("provider connection request label is required")
	}

	requestDocument := map[string]any{
		"id":               request.ID,
		"appName":          request.AppName,
		"operation":        requestKind,
		"connectionTarget": connectionTarget,
		"connectionLabel":  connectionLabel,
		"reason":           request.RequestReason,
	}
	if includeApplicant {
		requestDocument["applicantName"] = request.RequestedByDisplayName
	}
	fields := []map[string]any{
		{"label": "operation", "value": requestKind},
		{"label": "connection_target", "value": connectionTarget},
		{"label": "connection_label", "value": connectionLabel},
	}
	if strings.TrimSpace(request.AppID) == registry.GitHubAppID &&
		len(request.GitHubConnectionTargets) > 0 {
		targets, targetFields, err := githubConnectionTargetReviewDetails(request.GitHubConnectionTargets)
		if err != nil {
			return nil, nil, err
		}
		requestDocument["connectionTargets"] = targets
		fields = append(fields, targetFields...)
	}
	return requestDocument, fields, nil
}

// Gitea installation requests carry encrypted provider credentials in their
// internal config snapshot. Approval cards only expose the non-secret fields
// and a fixed mask for both secret inputs; neither the plaintext, encrypted
// storage envelope, nor whether a value is new/kept belongs in durable card JSON.
func giteaAppInstallationRequestCardDetails(
	request app.AppInstallationRequestView,
	requestKind string,
	includeApplicant bool,
) (map[string]any, []map[string]any) {
	requestedConfig := map[string]any{
		"instance_url":   giteaApprovalConfigString(request.RequestedConfig, "instance_url"),
		"display_name":   giteaApprovalConfigString(request.RequestedConfig, "display_name"),
		"api_token":      "********",
		"webhook_secret": "********",
	}
	baseConfig := map[string]any{
		"instance_url": giteaApprovalConfigString(request.BaseConfig, "instance_url"),
		"display_name": giteaApprovalConfigString(request.BaseConfig, "display_name"),
	}
	requestDocument := map[string]any{
		"id":              request.ID,
		"appName":         request.AppName,
		"operation":       requestKind,
		"baseConfig":      baseConfig,
		"requestedConfig": requestedConfig,
		"reason":          request.RequestReason,
	}
	if includeApplicant {
		requestDocument["applicantName"] = request.RequestedByDisplayName
	}
	return requestDocument, []map[string]any{
		{"label": "operation", "value": requestKind},
		{"label": "instance_url", "value": requestedConfig["instance_url"]},
		{"label": "display_name", "value": requestedConfig["display_name"]},
		{"label": "api_token", "value": requestedConfig["api_token"]},
		{"label": "webhook_secret", "value": requestedConfig["webhook_secret"]},
	}
}

func giteaApprovalConfigString(config map[string]any, key string) string {
	value, _ := config[key].(string)
	return strings.TrimSpace(value)
}

// GitHub setup proposals carry encrypted provider credentials in their
// internal execution snapshot. Keep a second explicit allowlist at the card
// boundary: projector tests and future callers may construct a request view
// without going through the store's public projection first. Unknown fields,
// structured values under metadata keys, and credential envelopes must never
// enter durable approval App JSON.
func githubAppInstallationRequestCardDetails(
	request app.AppInstallationRequestView,
	requestKind string,
	includeApplicant bool,
) (map[string]any, []map[string]any, error) {
	baseConfig := githubApprovalConfig(request.BaseConfig)
	requestedConfig := githubApprovalConfig(request.RequestedConfig)
	requestDocument := map[string]any{
		"id":              request.ID,
		"appName":         request.AppName,
		"operation":       requestKind,
		"baseConfig":      baseConfig,
		"requestedConfig": requestedConfig,
		"reason":          request.RequestReason,
	}
	if includeApplicant {
		requestDocument["applicantName"] = request.RequestedByDisplayName
	}
	fields := []map[string]any{{"label": "operation", "value": requestKind}}
	if len(request.GitHubConnectionTargets) > 0 {
		targets, targetFields, err := githubConnectionTargetReviewDetails(request.GitHubConnectionTargets)
		if err != nil {
			return nil, nil, err
		}
		requestDocument["connectionTargets"] = targets
		fields = append(fields, targetFields...)
	}
	for _, field := range []struct {
		key   string
		label string
	}{
		{key: "app_name", label: "app_name"},
		{key: "owner_login", label: "owner_login"},
		{key: "app_slug", label: "app_slug"},
		{key: "html_url", label: "html_url"},
		{key: "app_id", label: "app_id"},
		{key: "owner_id", label: "owner_id"},
		{key: "client_id", label: "client_id"},
		{key: "client_secret", label: "client_secret"},
		{key: "private_key_pem", label: "private_key_pem"},
		{key: "webhook_secret", label: "webhook_secret"},
	} {
		value, exists := requestedConfig[field.key]
		if !exists {
			continue
		}
		fields = append(fields, map[string]any{"label": field.label, "value": value})
	}
	return requestDocument, fields, nil
}

func githubConnectionTargetReviewDetails(
	targets []app.GitHubConnectionTargetView,
) ([]map[string]any, []map[string]any, error) {
	documents := make([]map[string]any, 0, len(targets))
	fields := make([]map[string]any, 0, len(targets))
	for _, target := range targets {
		installationID := strings.TrimSpace(target.InstallationID)
		accountLogin := strings.TrimSpace(target.AccountLogin)
		if installationID == "" || accountLogin == "" {
			return nil, nil, fmt.Errorf("GitHub connection review target identity is incomplete")
		}
		documents = append(documents, map[string]any{
			"installationId": installationID,
			"accountLogin":   accountLogin,
		})
		fields = append(fields, map[string]any{
			"label": "github_connection_target",
			"value": accountLogin + " (installation " + installationID + ")",
		})
	}
	return documents, fields, nil
}

func githubApprovalConfig(config map[string]any) map[string]any {
	projected := map[string]any{}
	for _, key := range []string{"app_slug", "app_name", "html_url", "client_id", "owner_login"} {
		value, ok := config[key].(string)
		if !ok {
			continue
		}
		projected[key] = strings.TrimSpace(value)
	}
	if value, ok := githubApprovalDecimalID(config["app_id"], false); ok {
		projected["app_id"] = value
	}
	if value, ok := githubApprovalDecimalID(config["owner_id"], true); ok {
		projected["owner_id"] = value
	}
	for _, key := range []string{"client_secret", "private_key_pem", "webhook_secret"} {
		raw, exists := config[key]
		if !exists {
			continue
		}
		value, isString := raw.(string)
		if isString && strings.TrimSpace(value) == "" {
			projected[key] = ""
			continue
		}
		projected[key] = "********"
	}
	return projected
}

func githubApprovalDecimalID(raw any, allowZero bool) (string, bool) {
	value, ok := raw.(string)
	if !ok {
		return "", false
	}
	value = strings.TrimSpace(value)
	parsed, err := strconv.ParseInt(value, 10, 64)
	if err != nil || parsed < 0 || (!allowZero && parsed == 0) || strconv.FormatInt(parsed, 10) != value {
		return "", false
	}
	return value, true
}

// Generic installation request configs are validated manifest snapshots.
// Provider-specific request renderers above must keep their internal execution
// snapshots and credentials out of this payload.
func marshalManifestConfig(config map[string]any) (string, error) {
	encoded, err := json.MarshalIndent(config, "", "  ")
	if err != nil {
		return "", err
	}
	return string(encoded), nil
}

func manifestConfigOrEmpty(config map[string]any) map[string]any {
	if config == nil {
		return map[string]any{}
	}
	return config
}
