package approvalapp

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/runeffect"
	"github.com/tidbcloud/AgentTeam/internal/service/appruntime"
	"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/workflow/orchestrator"
)

type Handler struct {
	approvals    *approval.Store
	appRequests  AppInstallationRequestAuthority
	subjects     *SubjectRegistry
	projector    *Projector
	realtime     WorkspaceAppRealtime
	workflowSink orchestrator.DecisionSink
	now          func() time.Time
}

// WorkspaceAppRealtime publishes only a workspace-scoped invalidation fact.
// App identifiers and role-sensitive projections must never be included in the
// broadcast; each client refetches its own authorized App catalog.
type WorkspaceAppRealtime interface {
	EmitWorkspaceAppsUpdated(workspaceID string)
}

// Provider connection approvals must not leave an owner action processing
// forever when a nominally transient provider or transport failure persists.
// The first two attempts retain the shared delivery retry behavior; the third
// attempt converts the still-pending request to the same safe reject-only card
// used for deterministic provider and stale-business failures.
const maxProviderConnectionReviewAttempts = 3

type AppInstallationRequestAuthority interface {
	ApproveAppInstallationRequestForUser(context.Context, string, string, string, app.DecideAppInstallationRequestParams) (app.AppInstallationRequestView, error)
	RejectAppInstallationRequestForUser(context.Context, string, string, string, app.DecideAppInstallationRequestParams) (app.AppInstallationRequestView, error)
	GetAppInstallationRequest(context.Context, string, string) (app.AppInstallationRequestView, error)
	MarkAppInstallationRequestCannotBeApplied(context.Context, string, string) (app.AppInstallationRequestView, error)
}

func requestedDecision(payload actionEnvelope, subjectType string) (string, string, string, error) {
	if payload.Action.ActionKey == ActionApprove {
		return approval.DecisionApproved, "", "", nil
	}
	if subjectType == SubjectTypeTeamRoleProposal {
		reason, locale, err := teamRoleRejectionInput(payload.Action.Input)
		return approval.DecisionRejected, reason, locale, err
	}
	reason, err := rejectReason(payload.Action.Input)
	if err != nil {
		return "", "", "", err
	}
	return approval.DecisionRejected, reason, "", nil
}

func NewHandler(
	approvals *approval.Store,
	appRequests AppInstallationRequestAuthority,
	projector *Projector,
	subjects *SubjectRegistry,
	realtime ...WorkspaceAppRealtime,
) *Handler {
	handler := &Handler{
		approvals: approvals, appRequests: appRequests,
		subjects: subjects, projector: projector, now: time.Now,
	}
	if len(realtime) > 0 {
		handler.realtime = realtime[0]
	}
	return handler
}

// SetWorkflowRunDecisionSink binds the durable Run authority used by the
// Workflow Run business object. It is deliberately separate from the generic
// Approval store and may be nil in profiles that do not own Run effects.
func (handler *Handler) SetWorkflowRunDecisionSink(sink orchestrator.DecisionSink) {
	if handler != nil {
		handler.workflowSink = sink
	}
}

// WithWorkflowRunDecisionSink is the fluent composition-root form of
// SetWorkflowRunDecisionSink.
func (handler *Handler) WithWorkflowRunDecisionSink(sink orchestrator.DecisionSink) *Handler {
	handler.SetWorkflowRunDecisionSink(sink)
	return handler
}

func (handler *Handler) emitWorkspaceAppsUpdated(workspaceID string) {
	if handler != nil && handler.realtime != nil {
		handler.realtime.EmitWorkspaceAppsUpdated(workspaceID)
	}
}

func (handler *Handler) HandleInternalDelivery(
	ctx context.Context,
	envelope appruntime.InternalHandlerEnvelope,
) (appruntime.InternalHandlerOutcome, error) {
	if handler == nil || handler.projector == nil {
		return appruntime.InternalHandlerOutcome{}, fmt.Errorf("Approval handler is not configured")
	}
	payload, err := decodeActionEnvelope(envelope.EventPayload)
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	if payload.EventType != "agentteam.app.notification.action" ||
		payload.Action.InvocationID != envelope.Delivery.EventID ||
		payload.Notification.ID != envelope.Delivery.ExternalResourceKey {
		return appruntime.InternalHandlerOutcome{}, fmt.Errorf("Approval action envelope does not match its durable delivery")
	}
	businessType := payload.Notification.BusinessObject.Type
	if payload.Notification.BusinessObject.Namespace != BusinessNamespace ||
		(businessType != BusinessTypeInstance && businessType != BusinessTypeAppInstallationRequest && businessType != BusinessTypeWorkflowRunApproval) ||
		payload.Notification.BusinessObject.ID == "" ||
		payload.Notification.BusinessObject.Version <= 0 {
		return appruntime.InternalHandlerOutcome{}, fmt.Errorf("Approval action has an invalid business object")
	}
	if businessType == BusinessTypeWorkflowRunApproval {
		return handler.handleWorkflowRunAction(ctx, envelope, payload)
	}
	if handler.approvals == nil || handler.subjects == nil {
		return appruntime.InternalHandlerOutcome{}, fmt.Errorf("Approval handler generic authority is not configured")
	}
	if payload.Action.ActionKey != ActionApprove && payload.Action.ActionKey != ActionReject {
		return appruntime.InternalHandlerOutcome{}, fmt.Errorf("Approval action is not supported")
	}
	if payload.Notification.BusinessObject.Type == BusinessTypeAppInstallationRequest {
		return handler.handleAppInstallationAction(ctx, envelope, payload)
	}
	if payload.Notification.BusinessObject.Type != BusinessTypeInstance {
		return appruntime.InternalHandlerOutcome{}, fmt.Errorf("Approval action business object is not supported")
	}
	instance, err := handler.approvals.Load(
		ctx,
		envelope.Delivery.WorkspaceID,
		payload.Notification.BusinessObject.ID,
	)
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	assignmentID, err := handler.assignmentIDForNotification(
		ctx, instance, payload.Notification.ID, payload.Actor.MemberID,
	)
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	decision, reason, feedbackLocale, err := requestedDecision(payload, instance.SubjectType)
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	result, err := handler.subjects.Decide(ctx, DecisionCommand{
		WorkspaceID: envelope.Delivery.WorkspaceID, ApprovalID: instance.ID,
		AssignmentID: assignmentID, ActorMemberID: payload.Actor.MemberID,
		ExpectedVersion:  payload.Notification.BusinessObject.Version,
		ClientDecisionID: payload.Action.InvocationID,
		Decision:         decision, Reason: reason,
		CorrelationID: payload.CorrelationID, Now: time.Now().UTC(),
	})
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	if consumed, ok := result.Value.(serviceaccount.WorkspaceCreateRequestResult); ok &&
		serviceaccount.IsPrivilegedWorkspaceCreateCancellation(consumed.Request) {
		return handler.withdrawnActionOutcome(
			ctx,
			envelope.Delivery.WorkspaceID,
			payload.Notification.ID,
			consumed.Request.ID,
			consumed.Request.BusinessState,
			serviceaccount.PrivilegedWorkspaceCreateCancellationReason,
		)
	}
	resolved, err := handler.subjects.PresentResolved(
		ctx, result, PresentationContext{
			NotificationID: payload.Notification.ID,
			DeliveryID:     envelope.Delivery.ID, AttemptID: envelope.Attempt.ID,
			MutationID: payload.Action.InvocationID,
		},
	)
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	resultJSON := map[string]any{
		"approvalId": result.Instance.ID, "approvalVersion": result.Instance.Version,
		"decision":             result.Instance.Decision,
		"notificationRevision": resolved.NotificationRevision,
	}
	for key, value := range result.Result {
		resultJSON[key] = value
	}
	encoded, err := json.Marshal(resultJSON)
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	if err := handler.postTeamRoleRejectionFeedback(ctx, payload, result, feedbackLocale); err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	return appruntime.InternalHandlerOutcome{
		Effect:     approvalProjectionEffect(resolved),
		ResultJSON: encoded,
	}, nil
}

func approvalProjectionEffect(projected app.AppNotificationResult) runeffect.Effect {
	if projected.Status == app.AppNotificationStatusActive {
		return runeffect.MessageUpdateEffect(
			projected.NotificationID, projected.MessageID,
		)
	}
	return runeffect.NotificationResolvedEffect(
		projected.NotificationID, projected.MessageID,
	)
}

func (handler *Handler) handleAppInstallationAction(
	ctx context.Context,
	envelope appruntime.InternalHandlerEnvelope,
	payload actionEnvelope,
) (appruntime.InternalHandlerOutcome, error) {
	if handler.appRequests == nil {
		return appruntime.InternalHandlerOutcome{}, fmt.Errorf("App installation request authority is not configured")
	}
	if payload.Notification.BusinessObject.Version != 1 || strings.TrimSpace(payload.Actor.UserID) == "" {
		return appruntime.InternalHandlerOutcome{}, fmt.Errorf("App installation action actor or version is invalid")
	}
	if err := handler.validateAppInstallationReviewerAction(
		ctx, envelope.Delivery.WorkspaceID, payload.Notification.ID,
		payload.Notification.BusinessObject.ID, payload.Actor.MemberID,
	); err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	reason := ""
	var err error
	if payload.Action.ActionKey == ActionReject {
		reason, err = rejectReason(payload.Action.Input)
		if err != nil {
			return appruntime.InternalHandlerOutcome{}, err
		}
	}
	params := app.DecideAppInstallationRequestParams{Reason: reason}
	var request app.AppInstallationRequestView
	if payload.Action.ActionKey == ActionApprove {
		request, err = handler.appRequests.ApproveAppInstallationRequestForUser(
			ctx, payload.Actor.UserID, envelope.Delivery.WorkspaceID,
			payload.Notification.BusinessObject.ID, params,
		)
	} else {
		request, err = handler.appRequests.RejectAppInstallationRequestForUser(
			ctx, payload.Actor.UserID, envelope.Delivery.WorkspaceID,
			payload.Notification.BusinessObject.ID, params,
		)
	}
	if err != nil {
		decisionErr := err
		inspectAppChangeFailure := isPotentialAppChangeReviewFailure(err) ||
			envelope.Attempt.AttemptNumber >= maxProviderConnectionReviewAttempts
		if payload.Action.ActionKey == ActionApprove && inspectAppChangeFailure {
			request, err = handler.appRequests.GetAppInstallationRequest(
				ctx, envelope.Delivery.WorkspaceID, payload.Notification.BusinessObject.ID,
			)
			if err != nil {
				return appruntime.InternalHandlerOutcome{}, err
			}
			if request.Status == app.AppInstallationRequestStatusPending &&
				(isDeterministicAppChangeReviewFailure(decisionErr, request) ||
					isProviderConnectionReviewRetryExhausted(envelope.Attempt.AttemptNumber, decisionErr, request)) {
				request, err = handler.appRequests.MarkAppInstallationRequestCannotBeApplied(
					ctx,
					envelope.Delivery.WorkspaceID,
					request.ID,
				)
				if err != nil {
					return appruntime.InternalHandlerOutcome{}, err
				}
				if request.Status == app.AppInstallationRequestStatusPending {
					return handler.appChangeCannotBeAppliedOutcome(ctx, envelope, payload, request)
				}
			}
		} else if !errors.Is(decisionErr, app.ErrConflict) {
			return appruntime.InternalHandlerOutcome{}, err
		}
		if request.ID == "" {
			request, err = handler.appRequests.GetAppInstallationRequest(
				ctx, envelope.Delivery.WorkspaceID, payload.Notification.BusinessObject.ID,
			)
			if err != nil {
				return appruntime.InternalHandlerOutcome{}, err
			}
		}
		if request.Status == app.AppInstallationRequestStatusPending ||
			request.Status == app.AppInstallationRequestStatusSetupPatching {
			return appruntime.InternalHandlerOutcome{}, fmt.Errorf("App installation request decision did not settle: %w", decisionErr)
		}
	}
	// Approval is the normal decision path for member-created Apps. Publish
	// after the durable request has settled so a newly public App appears (or a
	// rejected private definition disappears) without leaking its identifier in
	// the event. Projection retries may repeat this idempotent invalidation.
	handler.emitWorkspaceAppsUpdated(envelope.Delivery.WorkspaceID)
	if app.IsPrivilegedAppRequestWithdrawalDecision(request) {
		return handler.withdrawnActionOutcome(
			ctx,
			envelope.Delivery.WorkspaceID,
			payload.Notification.ID,
			request.ID,
			request.Status,
			app.PrivilegedAppRequestDecisionReason,
		)
	}
	resolved, err := handler.projector.resolveAppInstallationCards(
		ctx, request, payload.Notification.ID, envelope.Delivery.ID,
		envelope.Attempt.ID, payload.Action.InvocationID,
	)
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	if _, err := handler.projector.projectAppInstallationApplicantResult(ctx, request); err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	resultJSON, err := json.Marshal(map[string]any{
		"requestId": request.ID, "requestStatus": request.Status,
		"installationId":       request.InstallationID,
		"notificationRevision": resolved.NotificationRevision,
	})
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	return appruntime.InternalHandlerOutcome{
		Effect:     runeffect.NotificationResolvedEffect(resolved.NotificationID, resolved.MessageID),
		ResultJSON: resultJSON,
	}, nil
}

func isPotentialAppChangeReviewFailure(err error) bool {
	return errors.Is(err, app.ErrAppInstallationApprovalStale) ||
		isPotentialProviderConnectionReviewFailure(err)
}

func isPotentialProviderConnectionReviewFailure(err error) bool {
	return errors.Is(err, app.ErrGiteaConnectionConfigurationFailed) ||
		errors.Is(err, app.ErrConflict) ||
		errors.Is(err, app.ErrForbidden) ||
		errors.Is(err, app.ErrNotFound)
}

func isDeterministicProviderConnectionReviewFailure(err error, request app.AppInstallationRequestView) bool {
	if !isProviderConnectionRequest(request) {
		return false
	}
	// Ordinary HTTP/store classes are intentionally not enough here. In
	// particular, Forbidden or Conflict can describe the acting reviewer's own
	// authority changing between card invocation and async dispatch; that must
	// not remove Approve from every other administrator. Provider rejections use
	// the dedicated Gitea sentinel, while local proposal drift uses the App
	// installation stale sentinel handled below.
	return errors.Is(err, app.ErrGiteaConnectionConfigurationFailed)
}

func isDeterministicAppChangeReviewFailure(err error, request app.AppInstallationRequestView) bool {
	if errors.Is(err, app.ErrAppInstallationApprovalStale) {
		return isAppChangeRequest(request)
	}
	return isDeterministicProviderConnectionReviewFailure(err, request)
}

func isProviderConnectionReviewRetryExhausted(attemptNumber int, err error, request app.AppInstallationRequestView) bool {
	if attemptNumber < maxProviderConnectionReviewAttempts || !isProviderConnectionRequest(request) {
		return false
	}
	// These ordinary store classes can describe the acting reviewer's own
	// authority changing while an async action waits. Proposal drift carries
	// ErrAppInstallationApprovalStale, so never infer a global reject-only state
	// from raw authority/not-found/conflict errors merely because retries ran out.
	return !errors.Is(err, app.ErrForbidden) &&
		!errors.Is(err, app.ErrConflict) &&
		!errors.Is(err, app.ErrNotFound)
}

func isProviderConnectionRequest(request app.AppInstallationRequestView) bool {
	requestKind := strings.TrimSpace(request.RequestKind)
	if requestKind != app.AppInstallationRequestKindConnect && requestKind != app.AppInstallationRequestKindDisconnect {
		return false
	}
	appID := strings.TrimSpace(request.AppID)
	appSlug := strings.TrimSpace(request.AppSlug)
	if appID != registry.GiteaAppID && appID != registry.GitHubAppID &&
		appSlug != registry.GiteaAppSlug && appSlug != registry.GitHubAppSlug {
		return false
	}
	return true
}

func isAppChangeRequest(request app.AppInstallationRequestView) bool {
	switch strings.TrimSpace(request.RequestKind) {
	case app.AppInstallationRequestKindInstall,
		app.AppInstallationRequestKindConfigure,
		app.AppInstallationRequestKindConnect,
		app.AppInstallationRequestKindDisconnect,
		app.AppInstallationRequestKindUninstall:
		return true
	default:
		return false
	}
}

func (handler *Handler) appChangeCannotBeAppliedOutcome(
	ctx context.Context,
	envelope appruntime.InternalHandlerEnvelope,
	payload actionEnvelope,
	request app.AppInstallationRequestView,
) (appruntime.InternalHandlerOutcome, error) {
	updated, err := handler.projector.markAppInstallationChangeCannotBeApplied(
		ctx,
		request,
		payload.Notification.ID,
		envelope.Delivery.ID,
		envelope.Attempt.ID,
		payload.Notification.Revision,
	)
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	resultJSON, err := json.Marshal(map[string]any{
		"requestId":            request.ID,
		"requestStatus":        request.Status,
		"appChangeErrorCode":   "app_change_cannot_be_applied",
		"notificationRevision": updated.NotificationRevision,
	})
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	return appruntime.InternalHandlerOutcome{
		Effect:     runeffect.MessageUpdateEffect(updated.NotificationID, updated.MessageID),
		ResultJSON: resultJSON,
	}, nil
}

func (handler *Handler) withdrawnActionOutcome(
	ctx context.Context,
	workspaceID string,
	notificationID string,
	requestID string,
	requestState string,
	reason string,
) (appruntime.InternalHandlerOutcome, error) {
	notification, err := handler.projector.notifications.LoadAppNotification(
		ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(notificationID),
	)
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, fmt.Errorf("load withdrawn Approval action notification: %w", err)
	}
	if notification.Status != app.AppNotificationStatusWithdrawn {
		return appruntime.InternalHandlerOutcome{}, fmt.Errorf("Approval action notification did not converge to withdrawn")
	}
	resultJSON, err := json.Marshal(map[string]any{
		"requestId":    strings.TrimSpace(requestID),
		"requestState": strings.TrimSpace(requestState),
		"withdrawn":    true,
		"reason":       strings.TrimSpace(reason),
	})
	if err != nil {
		return appruntime.InternalHandlerOutcome{}, err
	}
	return appruntime.InternalHandlerOutcome{
		Effect:     runeffect.MessageWithdrawalEffect(notification.ID, notification.MessageID),
		ResultJSON: resultJSON,
	}, nil
}

func (handler *Handler) validateAppInstallationReviewerAction(
	ctx context.Context,
	workspaceID, notificationID, requestID, actorMemberID string,
) error {
	notification, err := handler.projector.notifications.LoadAppNotification(
		ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(notificationID),
	)
	if err != nil {
		return fmt.Errorf("resolve App installation reviewer notification: %w", err)
	}
	if notification.RecipientBasisKind == nil || notification.RecipientBasisRef == nil ||
		notification.RecipientMemberID == nil || notification.BusinessObjectID == nil ||
		strings.TrimSpace(*notification.RecipientBasisKind) != RecipientBasisAppInstallationReviewer ||
		strings.TrimSpace(*notification.RecipientBasisRef) != strings.TrimSpace(actorMemberID) ||
		strings.TrimSpace(*notification.RecipientMemberID) != strings.TrimSpace(actorMemberID) ||
		strings.TrimSpace(*notification.BusinessObjectID) != strings.TrimSpace(requestID) {
		return fmt.Errorf("App installation action notification does not identify the reviewer request")
	}
	return nil
}

func (handler *Handler) assignmentIDForNotification(
	ctx context.Context,
	instance approval.Instance,
	notificationID, actorMemberID string,
) (string, error) {
	notification, err := handler.projector.notifications.LoadAppNotification(
		ctx, instance.WorkspaceID, strings.TrimSpace(notificationID),
	)
	if err != nil {
		return "", fmt.Errorf("resolve Approval action assignment: %w", err)
	}
	if notification.BusinessObjectNamespace == nil ||
		notification.BusinessObjectType == nil ||
		notification.BusinessObjectID == nil ||
		strings.TrimSpace(*notification.BusinessObjectNamespace) != BusinessNamespace ||
		strings.TrimSpace(*notification.BusinessObjectType) != BusinessTypeInstance ||
		strings.TrimSpace(*notification.BusinessObjectID) != instance.ID {
		return "", fmt.Errorf("Approval action notification does not identify the Approval")
	}
	if notification.RecipientBasisKind != nil &&
		notification.RecipientBasisRef != nil &&
		notification.RecipientMemberID != nil &&
		strings.TrimSpace(*notification.RecipientBasisKind) == RecipientBasisAssignment &&
		strings.TrimSpace(*notification.RecipientBasisRef) != "" &&
		strings.TrimSpace(*notification.RecipientMemberID) == strings.TrimSpace(actorMemberID) {
		assignmentID := strings.TrimSpace(*notification.RecipientBasisRef)
		assignment, authorityInstance, err := handler.approvals.LoadAssignment(
			ctx, instance.WorkspaceID, assignmentID,
		)
		if err != nil {
			return "", fmt.Errorf("load Approval action assignment authority: %w", err)
		}
		if assignment.ApprovalInstanceID != instance.ID ||
			assignment.ApproverMemberID != strings.TrimSpace(actorMemberID) ||
			authorityInstance.ID != instance.ID ||
			authorityInstance.SubjectType != instance.SubjectType {
			return "", fmt.Errorf("Approval action assignment does not belong to the request")
		}
		return assignmentID, nil
	}
	return "", fmt.Errorf("Approval action actor does not own a pending assignment")
}

type actionEnvelope struct {
	EventType    string `json:"eventType"`
	Notification struct {
		ID             string                          `json:"id"`
		Revision       int64                           `json:"revision"`
		MessageID      string                          `json:"messageId"`
		BusinessObject appBusinessObjectActionEnvelope `json:"businessObject"`
	} `json:"notification"`
	Action struct {
		InvocationID string         `json:"invocationId"`
		ActionKey    string         `json:"actionKey"`
		Input        map[string]any `json:"input"`
	} `json:"action"`
	Actor struct {
		MemberID string `json:"memberId"`
		UserID   string `json:"userId"`
	} `json:"actor"`
	CorrelationID string `json:"correlationId"`
}

type appBusinessObjectActionEnvelope struct {
	Namespace string `json:"namespace"`
	Type      string `json:"type"`
	ID        string `json:"id"`
	Version   int64  `json:"version"`
}

func decodeActionEnvelope(raw []byte) (actionEnvelope, error) {
	var payload actionEnvelope
	if err := json.Unmarshal(raw, &payload); err != nil {
		return payload, fmt.Errorf("decode Approval action envelope: %w", err)
	}
	payload.EventType = strings.TrimSpace(payload.EventType)
	payload.Notification.ID = strings.TrimSpace(payload.Notification.ID)
	payload.Notification.MessageID = strings.TrimSpace(payload.Notification.MessageID)
	payload.Notification.BusinessObject.Namespace = strings.TrimSpace(payload.Notification.BusinessObject.Namespace)
	payload.Notification.BusinessObject.Type = strings.TrimSpace(payload.Notification.BusinessObject.Type)
	payload.Notification.BusinessObject.ID = strings.TrimSpace(payload.Notification.BusinessObject.ID)
	payload.Action.InvocationID = strings.TrimSpace(payload.Action.InvocationID)
	payload.Action.ActionKey = strings.TrimSpace(payload.Action.ActionKey)
	payload.Actor.MemberID = strings.TrimSpace(payload.Actor.MemberID)
	payload.Actor.UserID = strings.TrimSpace(payload.Actor.UserID)
	payload.CorrelationID = strings.TrimSpace(payload.CorrelationID)
	if payload.Action.Input == nil {
		payload.Action.Input = map[string]any{}
	}
	return payload, nil
}

func rejectReason(input map[string]any) (string, error) {
	if len(input) == 0 {
		return "", nil
	}
	if len(input) != 1 {
		return "", fmt.Errorf("Approval rejection input is invalid")
	}
	value, ok := input["reason"]
	if !ok {
		return "", fmt.Errorf("Approval rejection input is invalid")
	}
	reason, ok := value.(string)
	if !ok {
		return "", fmt.Errorf("Approval rejection reason must be text")
	}
	reason = strings.TrimSpace(reason)
	if len([]rune(reason)) > 1000 {
		return "", fmt.Errorf("Approval rejection reason is too long")
	}
	return reason, nil
}

func teamRoleRejectionInput(input map[string]any) (string, string, error) {
	for key := range input {
		if key != "reason" && key != "locale" {
			return "", "", fmt.Errorf("Team role rejection input is invalid")
		}
	}
	reason := ""
	if value, ok := input["reason"]; ok {
		var valid bool
		reason, valid = value.(string)
		if !valid {
			return "", "", fmt.Errorf("Approval rejection reason must be text")
		}
		reason = strings.TrimSpace(reason)
		if len([]rune(reason)) > 1000 {
			return "", "", fmt.Errorf("Approval rejection reason is too long")
		}
	}
	locale := "en"
	if value, ok := input["locale"]; ok {
		var valid bool
		locale, valid = value.(string)
		if !valid {
			return "", "", fmt.Errorf("Team role rejection locale must be text")
		}
		locale = strings.TrimSpace(locale)
		if locale == "" {
			locale = "en"
		}
	}
	switch locale {
	case "en", "zh-CN", "ja":
		return reason, locale, nil
	default:
		return "", "", fmt.Errorf("Team role rejection locale is not supported")
	}
}
