package runtimeapi

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/nexuscrm/automation"
	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	approvalsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/approvalv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
	recordsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordsv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
	"github.com/rs/zerolog/log"
)

type ApprovalInvocationStore interface {
	Lookup(context.Context, string, string) (persistence.RuntimeApprovalInvocationRow, bool, error)
	Accept(context.Context, persistence.RuntimeApprovalInvocationAcceptance) (persistence.RuntimeApprovalInvocationRow, bool, error)
	Complete(context.Context, string, []byte) error
}

type approvalResolvedExecution struct {
	authority         *catalogApprovalAuthority
	definitionID      string
	definitionVersion string
	closureDigest     string
	submit            *automation.SubmitEligibility
	workItem          automation.ApprovalWorkItemBinding
}

type ApprovalExecutionEngine struct {
	service *automation.ApprovalService
	anchors approvalIdentityAnchorStore
}

type approvalPresentationRollback struct {
	response approvalsemantic.ExecuteResponse
}

func (e *approvalPresentationRollback) Error() string {
	return "runtime Approval execution rejected before commit"
}

func NewApprovalExecutionEngine(service *automation.ApprovalService, anchors approvalIdentityAnchorStore) (*ApprovalExecutionEngine, error) {
	if service == nil || anchors == nil {
		return nil, errors.New("runtime Approval engine requires the existing Approval service and identity-anchor store")
	}
	return &ApprovalExecutionEngine{service: service, anchors: anchors}, nil
}

func (e *ApprovalExecutionEngine) resolve(
	ctx context.Context,
	view *catalogsnapshot.RuntimeMetadataView,
	request approvalsemantic.ExecuteRequest,
	user *models.UserSession,
) (approvalResolvedExecution, error) {
	authority, err := newCatalogApprovalAuthority(view, e.anchors)
	if err != nil {
		return approvalResolvedExecution{}, err
	}
	authorityCtx := automation.WithRuntimeApprovalAuthority(ctx, authority)
	resolved := approvalResolvedExecution{authority: authority}
	var storageDefinitionID string
	if request.Operation == approvalsemantic.OperationSubmit {
		eligibility, resolveErr := e.service.ResolveRuntimeSubmitEligibilityWithinObjectScope(authorityCtx, automation.SubmitRequest{
			ObjectAPIName: request.ObjectID, RecordID: request.TargetRecordID,
			Comments: request.Comments, SkipEntryCriteria: request.SkipEntryCriteria,
			// Objectless submit may probe only objects for which this immutable
			// Catalog view actually publishes an active Approval definition.  The
			// wider record/mutation closure also contains internal operational
			// objects; probing those reintroduces the monolith's repository-global
			// object scan and can execute malformed or unrelated physical views.
		}, view.ActiveApprovalObjectAPINames(), user)
		if resolveErr != nil {
			return approvalResolvedExecution{}, resolveErr
		}
		resolved.submit = eligibility
		storageDefinitionID = eligibility.ProcessDefinitionID
	} else {
		binding, resolveErr := e.service.ResolveWorkItemBinding(authorityCtx, request.WorkItemID, user)
		if resolveErr != nil {
			return approvalResolvedExecution{}, resolveErr
		}
		action, actionErr := runtimeApprovalAction(request.Operation)
		if actionErr != nil {
			return approvalResolvedExecution{}, actionErr
		}
		if fenceErr := e.service.FenceRuntimeApprovalAction(authorityCtx, request.WorkItemID, binding, action, user); fenceErr != nil {
			return approvalResolvedExecution{}, fenceErr
		}
		resolved.workItem = binding
		storageDefinitionID = binding.ProcessDefinitionID
	}
	binding, err := authority.bindingForStorageID(authorityCtx, storageDefinitionID)
	if err != nil {
		return approvalResolvedExecution{}, err
	}
	if err := requireCatalogApprovalOperation(binding, request.Operation); err != nil {
		return approvalResolvedExecution{}, err
	}
	definitionVersion, _, ok := view.Definition(binding.catalog.DefinitionID)
	if !ok || strings.TrimSpace(string(definitionVersion)) == "" {
		return approvalResolvedExecution{}, errors.New("runtime Approval Catalog definition version is missing")
	}
	resolved.definitionID = binding.catalog.DefinitionID
	resolved.definitionVersion = string(definitionVersion)
	closurePayload := struct {
		DefinitionID        string   `json:"definitionId"`
		DefinitionVersion   string   `json:"definitionVersion"`
		DefinitionDigest    string   `json:"definitionDigest"`
		StorageDefinitionID string   `json:"storageDefinitionId"`
		StorageNodeIDs      []string `json:"storageNodeIds"`
	}{
		DefinitionID:        binding.catalog.DefinitionID,
		DefinitionVersion:   resolved.definitionVersion,
		DefinitionDigest:    string(binding.catalog.ContentDigest),
		StorageDefinitionID: binding.liveID,
	}
	for _, node := range binding.nodes {
		closurePayload.StorageNodeIDs = append(closurePayload.StorageNodeIDs, approvalAuthorityString(node[constants.FieldID]))
	}
	encoded, err := json.Marshal(closurePayload)
	if err != nil {
		return approvalResolvedExecution{}, err
	}
	sum := sha256.Sum256(encoded)
	resolved.closureDigest = "sha256:" + hex.EncodeToString(sum[:])
	return resolved, nil
}

func requireCatalogApprovalOperation(binding *catalogApprovalBinding, operation approvalsemantic.Operation) error {
	if binding == nil {
		return errors.New("runtime Approval Catalog binding is missing")
	}
	if operation == approvalsemantic.OperationRecall && !binding.catalog.Definition.AllowRecall {
		return automation.ErrRecallNotAllowed
	}
	return nil
}

func runtimeApprovalAction(operation approvalsemantic.Operation) (automation.RuntimeApprovalAction, error) {
	switch operation {
	case approvalsemantic.OperationApprove:
		return automation.RuntimeApprovalActionApprove, nil
	case approvalsemantic.OperationReject:
		return automation.RuntimeApprovalActionReject, nil
	case approvalsemantic.OperationReassign:
		return automation.RuntimeApprovalActionReassign, nil
	case approvalsemantic.OperationRecall:
		return automation.RuntimeApprovalActionRecall, nil
	default:
		return "", errors.New("runtime Approval work-item operation is unsupported")
	}
}

func (e *ApprovalExecutionEngine) execute(
	ctx context.Context,
	resolved approvalResolvedExecution,
	request approvalsemantic.ExecuteRequest,
	user *models.UserSession,
) (approvalsemantic.ExecuteResponse, error) {
	ctx = automation.WithRuntimeApprovalAuthority(ctx, resolved.authority)
	ctx = persistence.WithApprovalLockAuthority(ctx, resolved.authority)
	var result models.SObject
	var err error
	switch request.Operation {
	case approvalsemantic.OperationSubmit:
		result, err = e.service.SubmitResolved(ctx, automation.SubmitRequest{
			ObjectAPIName: request.ObjectID, RecordID: request.TargetRecordID,
			Comments: request.Comments, SkipEntryCriteria: request.SkipEntryCriteria,
		}, resolved.submit, user)
	case approvalsemantic.OperationApprove:
		result, err = e.service.Approve(ctx, request.WorkItemID, request.Comments, user)
	case approvalsemantic.OperationReject:
		result, err = e.service.Reject(ctx, request.WorkItemID, request.Comments, user)
	case approvalsemantic.OperationRecall:
		result, err = e.service.Recall(ctx, request.WorkItemID, request.Comments, user)
	case approvalsemantic.OperationReassign:
		err = e.service.Reassign(ctx, request.WorkItemID, request.NextActorID, user)
		if err == nil {
			result = models.SObject{
				"ProcessInstanceId": resolved.workItem.ProcessInstanceID,
				"TargetObjectId":    resolved.workItem.TargetRecordID,
				"InstanceStatus":    constants.ApprovalStatusPending,
				"ActorIds":          []string{request.NextActorID},
				"NewWorkitemIds":    []string{request.WorkItemID},
			}
		}
	default:
		return approvalsemantic.ExecuteResponse{}, errors.New("runtime Approval operation is unsupported")
	}
	if err != nil {
		return approvalsemantic.ExecuteResponse{}, err
	}
	return approvalExecuteResponseFromEngine(request, result)
}

func (e *ApprovalExecutionEngine) read(
	ctx context.Context,
	view *catalogsnapshot.RuntimeMetadataView,
	request approvalsemantic.ReadRequest,
	user *models.UserSession,
) (approvalsemantic.ReadResponse, error) {
	authority, err := newCatalogApprovalAuthority(view, e.anchors)
	if err != nil {
		return approvalsemantic.ReadResponse{}, err
	}
	ctx = automation.WithRuntimeApprovalAuthority(ctx, authority)
	response := approvalsemantic.ReadResponse{}
	switch request.Kind {
	case approvalsemantic.ReadPendingForCaller:
		items, readErr := e.service.ListPendingApprovals(ctx, user)
		if readErr != nil {
			return approvalsemantic.ReadResponse{}, readErr
		}
		for _, item := range items {
			definitionID, mapErr := authority.CatalogDefinitionID(ctx, item.ProcessDefinitionID)
			if mapErr != nil {
				return approvalsemantic.ReadResponse{}, mapErr
			}
			response.Pending = append(response.Pending, approvalsemantic.PendingItem{
				WorkItemID: item.WorkItemID, InstanceID: item.ProcessInstanceID,
				DefinitionID: definitionID, TargetRecordID: item.TargetObjectID,
				ObjectID: item.ObjectAPIName, SubmittedByID: item.SubmittedByID,
				SubmittedByName: item.SubmittedByName, SubmittedAt: item.SubmittedDate,
				ActorID: item.ActorID, OriginalActorID: item.OriginalActorID,
				Comments: item.Comments, RecordName: item.RecordName,
			})
		}
	case approvalsemantic.ReadLatestForRecord:
		latest, readErr := e.service.GetLatestProcessInstance(ctx, request.TargetRecordID, user)
		if readErr != nil {
			return approvalsemantic.ReadResponse{}, readErr
		}
		if latest != nil {
			definitionID, mapErr := authority.CatalogDefinitionID(ctx, latest.ProcessDefinitionID)
			if mapErr != nil {
				return approvalsemantic.ReadResponse{}, mapErr
			}
			state, stateErr := approvalInstanceState(latest.Status)
			if stateErr != nil {
				return approvalsemantic.ReadResponse{}, stateErr
			}
			value := approvalsemantic.InstanceSummary{
				InstanceID: latest.ID, DefinitionID: definitionID,
				TargetRecordID: latest.TargetObjectID, State: state,
				SubmittedByID: latest.SubmittedByID, SubmittedByName: latest.SubmittedByName,
				CreatedAt: latest.CreatedDate, CompletedAt: latest.CompletedDate,
			}
			for _, item := range latest.PendingWorkItems {
				value.PendingWorkItems = append(value.PendingWorkItems, approvalsemantic.PendingWorkItem{
					WorkItemID: item.ID, ActorID: item.ActorID, ActorName: item.ActorName,
					OriginalActorID: item.OriginalActorID, OriginalActorName: item.OriginalActorName,
				})
			}
			response.Latest = &value
		}
	case approvalsemantic.ReadStepDisplayForRecord:
		steps, readErr := e.service.GetRecordApprovalStepDisplayMetadata(ctx, request.TargetRecordID, user)
		if readErr != nil {
			return approvalsemantic.ReadResponse{}, readErr
		}
		for _, step := range steps {
			response.Steps = append(response.Steps, approvalsemantic.StepDisplay{
				StepID: step.ProcessNodeID, StepOrder: uint32(step.StepOrder),
				StepName: step.StepName, StepLabel: step.StepLabel,
				HasCriteria:     step.HasEntryCriteria,
				NoMatchBehavior: approvalStepNoMatchBehavior(step.IfCriteriaNotMet),
			})
		}
	default:
		return approvalsemantic.ReadResponse{}, errors.New("runtime Approval read kind is unsupported")
	}
	return response, nil
}

type ApprovalServer struct {
	runtimev1.UnimplementedApprovalServiceServer
	engine       approvalRuntimeEngine
	store        ApprovalInvocationStore
	transactions ApprovalTransactionRunner
	permissions  approvalPermissionFence
	execute      bool
	read         bool
}

type approvalRuntimeEngine interface {
	resolve(context.Context, *catalogsnapshot.RuntimeMetadataView, approvalsemantic.ExecuteRequest, *models.UserSession) (approvalResolvedExecution, error)
	execute(context.Context, approvalResolvedExecution, approvalsemantic.ExecuteRequest, *models.UserSession) (approvalsemantic.ExecuteResponse, error)
	read(context.Context, *catalogsnapshot.RuntimeMetadataView, approvalsemantic.ReadRequest, *models.UserSession) (approvalsemantic.ReadResponse, error)
}

type approvalPermissionFence interface {
	FencePermissionFloorWithinTransaction(context.Context, permissionstatev1.Version) error
}

type ApprovalTransactionRunner interface {
	RunInPessimisticTransactionWithOutcome(
		context.Context,
		func(context.Context) error,
	) (ports.TransactionCompletionOutcome, error)
}

func NewApprovalServer(engine approvalRuntimeEngine, store ApprovalInvocationStore, transactions ApprovalTransactionRunner, permissions approvalPermissionFence, enabled ...bool) (*ApprovalServer, error) {
	executeEnabled, readEnabled := true, true
	if len(enabled) != 0 {
		if len(enabled) != 2 {
			return nil, errors.New("runtime Approval server enablement is incomplete")
		}
		executeEnabled, readEnabled = enabled[0], enabled[1]
	}
	if engine == nil || (!executeEnabled && !readEnabled) {
		return nil, errors.New("runtime Approval server requires an engine and at least one enabled capability")
	}
	if executeEnabled && (store == nil || transactions == nil || permissions == nil) {
		return nil, errors.New("runtime Approval execution requires a durable invocation store, transaction owner, and permission fence")
	}
	return &ApprovalServer{
		engine: engine, store: store, transactions: transactions, permissions: permissions,
		execute: executeEnabled, read: readEnabled,
	}, nil
}

func (s *ApprovalServer) ExecuteApproval(ctx context.Context, wireRequest *runtimev1.ExecuteApprovalRequest) (*runtimev1.ExecuteApprovalResponse, error) {
	if s == nil || !s.execute {
		return nil, approvalExecuteStatus(&recordsemantic.Failure{Category: recordsemantic.FailureUnavailable})
	}
	request, err := mapping.FromWireExecuteApprovalRequest(ctx, wireRequest)
	if err != nil {
		return nil, approvalExecuteStatus(err)
	}
	scope, view, trusted, err := approvalRequestAuthority(ctx, approvalsemantic.ExecuteCapabilityName, request.MinCatalogRevision)
	if err != nil {
		return nil, approvalExecuteStatus(err)
	}
	if !view.SupportsMutationClosure() {
		// A valid empty Catalog projection means this immutable App revision owns
		// no Approval execution authority. Reject before opening the invocation
		// transaction; Runtime must never infer definitions from repository state.
		return nil, approvalExecuteStatus(&recordsemantic.Failure{Category: recordsemantic.FailureUnavailable})
	}
	receipts := receiptsForCapability(scope.receipts, approvalsemantic.ExecuteCapabilityName)
	receiptPayload, err := json.Marshal(receipts)
	if err != nil {
		return nil, approvalExecuteStatus(&recordsemantic.Failure{Category: recordsemantic.FailureInternal})
	}
	requestPayload, err := json.Marshal(struct {
		Operation         approvalsemantic.Operation `json:"operation"`
		ObjectID          string                     `json:"objectId,omitempty"`
		TargetRecordID    string                     `json:"targetRecordId,omitempty"`
		WorkItemID        string                     `json:"workItemId,omitempty"`
		Comments          string                     `json:"comments,omitempty"`
		NextActorID       string                     `json:"nextActorId,omitempty"`
		SkipEntryCriteria bool                       `json:"skipEntryCriteria,omitempty"`
	}{
		Operation: request.Operation, ObjectID: request.ObjectID,
		TargetRecordID: request.TargetRecordID, WorkItemID: request.WorkItemID,
		Comments: request.Comments, NextActorID: request.NextActorID,
		SkipEntryCriteria: request.SkipEntryCriteria,
	})
	if err != nil {
		return nil, approvalExecuteStatus(&recordsemantic.Failure{Category: recordsemantic.FailureInternal})
	}
	scopeSum := sha256.Sum256([]byte(trusted.orgID + "\x00" + trusted.appInstallationID + "\x00" + trusted.callerWorkloadID + "\x00" + trusted.principalID + "\x00" + approvalsemantic.ExecuteCapabilityName))
	scopeDigest := hex.EncodeToString(scopeSum[:])
	requestSum := sha256.Sum256(requestPayload)
	requestDigest := "sha256:" + hex.EncodeToString(requestSum[:])
	rowID := deterministicFileID(scopeDigest, string(request.Operation), request.IdempotencyKey)
	var response approvalsemantic.ExecuteResponse
	completion, err := s.transactions.RunInPessimisticTransactionWithOutcome(ctx, func(txCtx context.Context) error {
		// Keep Approval definitions and notification lookup on the admitted
		// Approval view. Ordinary DML reached below the engine uses the separately
		// bound Mutation sibling captured from the same immutable serving set.
		txCtx = metadata.WithRuntimeMetadataView(txCtx, view)
		response = approvalsemantic.ExecuteResponse{}
		row, found, lookupErr := s.store.Lookup(txCtx, scopeDigest, request.IdempotencyKey)
		if lookupErr != nil {
			return lookupErr
		}
		if found {
			if replayErr := replayApprovalInvocation(row, request, requestDigest, receipts, &response); replayErr != nil {
				return replayErr
			}
			return fenceApprovalPermission(txCtx, s.permissions, trusted.PermissionFloor())
		}
		user := trusted.UserSession()
		resolved, resolveErr := s.engine.resolve(txCtx, view, request, &user)
		if resolveErr != nil {
			if rejected, ok := approvalSubmitRejectionResponse(txCtx, request, resolveErr); ok {
				rejected.Receipts = receipts
				if validationErr := approvalsemantic.ValidateExecuteResponse(request, rejected); validationErr != nil {
					return &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
				}
				if fenceErr := fenceApprovalPermission(
					txCtx, s.permissions, trusted.PermissionFloor(),
				); fenceErr != nil {
					return fenceErr
				}
				response = rejected
				return nil
			}
			return resolveErr
		}
		row, duplicate, acceptErr := s.store.Accept(txCtx, persistence.RuntimeApprovalInvocationAcceptance{
			RowID: rowID, ScopeDigest: scopeDigest, IdempotencyKey: request.IdempotencyKey,
			Operation: string(request.Operation), DefinitionID: resolved.definitionID,
			DefinitionVersion: resolved.definitionVersion, ClosureDigest: resolved.closureDigest,
			RequestDigest: requestDigest, Receipts: receiptPayload,
		})
		if acceptErr != nil {
			return classifyApprovalInvocationError(acceptErr)
		}
		if duplicate {
			if replayErr := replayApprovalInvocation(row, request, requestDigest, receipts, &response); replayErr != nil {
				return replayErr
			}
			return fenceApprovalPermission(txCtx, s.permissions, trusted.PermissionFloor())
		}
		response, err = s.engine.execute(txCtx, resolved, request, &user)
		if err != nil {
			if rejected, ok := approvalSubmitValidationRejectionResponse(request, err); ok {
				rejected.Receipts = receipts
				if validationErr := approvalsemantic.ValidateExecuteResponse(request, rejected); validationErr != nil {
					return &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
				}
				if fenceErr := fenceApprovalPermission(
					txCtx, s.permissions, trusted.PermissionFloor(),
				); fenceErr != nil {
					return fenceErr
				}
				return &approvalPresentationRollback{response: rejected}
			}
			return err
		}
		response.ExecutionID = rowID
		response.ActingPrincipalID = trusted.principalID
		response.Receipts = receipts
		if err := approvalsemantic.ValidateExecuteResponse(request, response); err != nil {
			return &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
		}
		if err := txCtx.Err(); err != nil {
			return err
		}
		if fenceErr := fenceApprovalPermission(
			txCtx, s.permissions, trusted.PermissionFloor(),
		); fenceErr != nil {
			return fenceErr
		}
		resultPayload, err := json.Marshal(response)
		if err != nil {
			return &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
		}
		return s.store.Complete(txCtx, rowID, resultPayload)
	})
	if err != nil {
		if ctxErr := ctx.Err(); ctxErr != nil {
			return nil, approvalExecuteStatus(ctxErr)
		}
		if rejected, ok := approvalResponseAfterRollback(completion, err); ok {
			response = rejected
		} else {
			// The public boundary intentionally returns only the stable neutral
			// classification. Keep the owning cause in Runtime logs so a failed
			// cross-process approval transaction can be diagnosed without weakening
			// disclosure at the L1 wire boundary.
			log.Error().Err(err).Str("operation", string(request.Operation)).Msg("runtime Approval execution failed")
			return nil, approvalExecuteStatus(classifyApprovalEngineError(ctx, err))
		}
	}
	wireResponse, err := mapping.ToWireExecuteApprovalResponse(request, response)
	if err != nil {
		return nil, approvalExecuteStatus(err)
	}
	return wireResponse, nil
}

func approvalResponseAfterRollback(
	completion ports.TransactionCompletionOutcome,
	err error,
) (approvalsemantic.ExecuteResponse, bool) {
	if completion != ports.TransactionCompletionRolledBack || err == nil || apperrors.IsInternal(err) {
		return approvalsemantic.ExecuteResponse{}, false
	}
	for current := err; current != nil; current = errors.Unwrap(current) {
		if _, joined := current.(interface{ Unwrap() []error }); joined {
			return approvalsemantic.ExecuteResponse{}, false
		}
	}
	var presentation *approvalPresentationRollback
	if !errors.As(err, &presentation) || presentation == nil {
		return approvalsemantic.ExecuteResponse{}, false
	}
	return presentation.response, true
}

func (s *ApprovalServer) ReadApproval(ctx context.Context, wireRequest *runtimev1.ReadApprovalRequest) (*runtimev1.ReadApprovalResponse, error) {
	if s == nil || !s.read {
		return nil, approvalReadStatus(&recordsemantic.Failure{Category: recordsemantic.FailureUnavailable})
	}
	request, err := mapping.FromWireReadApprovalRequest(ctx, wireRequest)
	if err != nil {
		return nil, approvalReadStatus(err)
	}
	scope, view, trusted, err := approvalRequestAuthority(ctx, approvalsemantic.ReadCapabilityName, request.MinCatalogRevision)
	if err != nil {
		return nil, approvalReadStatus(err)
	}
	user := trusted.UserSession()
	response, err := s.engine.read(ctx, view, request, &user)
	if err != nil {
		return nil, approvalReadStatus(classifyApprovalEngineError(ctx, err))
	}
	response.Receipts = receiptsForCapability(scope.receipts, approvalsemantic.ReadCapabilityName)
	if err := approvalsemantic.ValidateReadResponse(request, response); err != nil {
		return nil, approvalReadStatus(&recordsemantic.Failure{Category: recordsemantic.FailureInternal})
	}
	wireResponse, err := mapping.ToWireReadApprovalResponse(request, response)
	if err != nil {
		return nil, approvalReadStatus(err)
	}
	return wireResponse, nil
}

func approvalExecuteStatus(err error) error {
	return mapping.ToPlatformStatusError(err, approvalsemantic.ExecuteCapabilityName)
}

func approvalReadStatus(err error) error {
	return mapping.ToPlatformStatusError(err, approvalsemantic.ReadCapabilityName)
}

func approvalRequestAuthority(ctx context.Context, capability string, minimum *uint64) (RuntimeExecutionScope, *catalogsnapshot.RuntimeMetadataView, TrustedCall, error) {
	scope, scopeOK := runtimeExecutionScopeFromContext(ctx)
	view, viewOK := RuntimeMetadataViewFromContext(ctx)
	trusted, trustedOK := trustedCallFromContext(ctx)
	if !scopeOK || !viewOK || !trustedOK {
		return RuntimeExecutionScope{}, nil, TrustedCall{}, &recordsemantic.Failure{Category: recordsemantic.FailureUnavailable}
	}
	if scope.capability != capability || scope.orgID != trusted.orgID ||
		scope.appInstallationID != trusted.appInstallationID || scope.workloadID != trusted.callerWorkloadID ||
		scope.receipts.SecurityEpoch != trusted.securityEpoch {
		return RuntimeExecutionScope{}, nil, TrustedCall{}, &recordsemantic.Failure{Category: recordsemantic.FailureForbidden}
	}
	if err := requireCatalogRevision(minimum, scope.receipts.CatalogRevision); err != nil {
		return RuntimeExecutionScope{}, nil, TrustedCall{}, err
	}
	return scope, view, trusted, nil
}

func replayApprovalInvocation(row persistence.RuntimeApprovalInvocationRow, request approvalsemantic.ExecuteRequest, requestDigest string, receipts approvalsemantic.Receipts, response *approvalsemantic.ExecuteResponse) error {
	if row.Operation != string(request.Operation) || row.RequestDigest != requestDigest {
		return approvalInvocationConflictFailure()
	}
	storedReceipts, err := json.Marshal(receipts)
	if err != nil || !approvalEquivalentJSON(row.Receipts, storedReceipts) {
		return approvalInvocationConflictFailure()
	}
	if row.State != persistence.RuntimeApprovalInvocationCompleted || len(row.ResultPayload) == 0 {
		return &recordsemantic.Failure{Category: recordsemantic.FailureUnavailable}
	}
	if err := json.Unmarshal(row.ResultPayload, response); err != nil || approvalsemantic.ValidateExecuteResponse(request, *response) != nil {
		return &recordsemantic.Failure{Category: recordsemantic.FailureInternal}
	}
	return nil
}

func classifyApprovalInvocationError(err error) error {
	if errors.Is(err, persistence.ErrRuntimeApprovalInvocationConflict) || errors.Is(err, persistence.ErrRuntimeApprovalInvocationServingBindingChanged) {
		return approvalInvocationConflictFailure()
	}
	return err
}

func classifyApprovalEngineError(ctx context.Context, err error) error {
	classified := classifyPlatformEngineError(ctx, err, commonsemantic.InvalidComponentCommandData)
	var classifiedFailure *commonsemantic.Failure
	if !errors.As(classified, &classifiedFailure) ||
		classifiedFailure.Category != commonsemantic.FailureConflict {
		return classified
	}
	// Common classification has already resolved infrastructure, cancellation,
	// permission and ambiguous ownership. Approval's published grammar maps a
	// domain conflict to INVALID_REQUEST rather than exposing CONFLICT.
	component := commonsemantic.InvalidComponentCommandData
	if errors.Is(err, automation.ErrRecordAlreadyPending) ||
		errors.Is(err, automation.ErrInstanceNotPending) {
		component = commonsemantic.InvalidComponentOperation
	}
	return &recordsemantic.Failure{
		Category:         recordsemantic.FailureInvalidRequest,
		InvalidComponent: component,
	}
}

func approvalSubmitRejectionResponse(
	ctx context.Context,
	request approvalsemantic.ExecuteRequest,
	err error,
) (approvalsemantic.ExecuteResponse, bool) {
	if request.Operation != approvalsemantic.OperationSubmit || err == nil {
		return approvalsemantic.ExecuteResponse{}, false
	}
	for current := err; current != nil; current = errors.Unwrap(current) {
		if _, joined := current.(interface{ Unwrap() []error }); joined {
			return approvalsemantic.ExecuteResponse{}, false
		}
	}
	classified := classifyApprovalEngineError(ctx, err)
	var failure *commonsemantic.Failure
	if !errors.As(classified, &failure) {
		return approvalsemantic.ExecuteResponse{}, false
	}
	var reason approvalsemantic.SubmitRejectionReason
	switch {
	case errors.Is(err, automation.ErrNoActiveApprovalProcess) &&
		failure.Category == commonsemantic.FailureMissing:
		reason = approvalsemantic.SubmitRejectionNoActiveProcess
	case errors.Is(err, automation.ErrEntryCriteriaNotMet) &&
		failure.Category == commonsemantic.FailureInvalidRequest &&
		!request.SkipEntryCriteria:
		reason = approvalsemantic.SubmitRejectionEntryCriteriaMiss
	default:
		return approvalsemantic.ExecuteResponse{}, false
	}
	return approvalsemantic.ExecuteResponse{
		TargetRecordID: request.TargetRecordID,
		SubmitRejection: &approvalsemantic.SubmitRejection{
			Reason: reason,
		},
	}, true
}

func approvalSubmitValidationRejectionResponse(
	request approvalsemantic.ExecuteRequest,
	err error,
) (approvalsemantic.ExecuteResponse, bool) {
	if request.Operation != approvalsemantic.OperationSubmit || err == nil {
		return approvalsemantic.ExecuteResponse{}, false
	}
	clientValidation, ok := apperrors.AsClientValidationError(err)
	if !ok {
		return approvalsemantic.ExecuteResponse{}, false
	}
	failures, ok := apperrors.AsValidationRuleFailures(clientValidation)
	if !ok || len(failures) == 0 || len(failures) > approvalsemantic.MaxValidationIssues {
		return approvalsemantic.ExecuteResponse{}, false
	}
	issues := make([]approvalsemantic.ValidationIssue, 0, len(failures))
	for _, failure := range failures {
		issues = append(issues, approvalsemantic.ValidationIssue{
			RuleName: failure.RuleName,
			Field:    failure.Field,
			Message:  failure.Message,
		})
	}
	return approvalsemantic.ExecuteResponse{
		TargetRecordID: request.TargetRecordID,
		SubmitRejection: &approvalsemantic.SubmitRejection{
			Reason:           approvalsemantic.SubmitRejectionRecordValidationFailed,
			ValidationIssues: issues,
		},
	}, true
}

func approvalEquivalentJSON(left, right []byte) bool {
	leftCanonical, err := canonicalApprovalJSON(left)
	if err != nil {
		return false
	}
	rightCanonical, err := canonicalApprovalJSON(right)
	return err == nil && bytes.Equal(leftCanonical, rightCanonical)
}

func canonicalApprovalJSON(payload []byte) ([]byte, error) {
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return nil, err
	}
	var trailing any
	if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
		if err == nil {
			return nil, errors.New("multiple JSON values")
		}
		return nil, err
	}
	return json.Marshal(value)
}

func approvalInvocationConflictFailure() error {
	return &recordsemantic.Failure{
		Category:         recordsemantic.FailureInvalidRequest,
		InvalidComponent: commonsemantic.InvalidComponentCommandData,
	}
}

func fenceApprovalPermission(ctx context.Context, permissions approvalPermissionFence, floor permissionstatev1.Version) error {
	if err := permissions.FencePermissionFloorWithinTransaction(ctx, floor); err != nil {
		return classifyEffectivePermissionError(ctx, err)
	}
	return nil
}

func approvalExecuteResponseFromEngine(request approvalsemantic.ExecuteRequest, result models.SObject) (approvalsemantic.ExecuteResponse, error) {
	instanceID := strings.TrimSpace(approvalAuthorityString(result["ProcessInstanceId"]))
	targetRecordID := strings.TrimSpace(approvalAuthorityString(result["TargetObjectId"]))
	if instanceID == "" || targetRecordID == "" {
		return approvalsemantic.ExecuteResponse{}, errors.New("Approval engine returned an incomplete result")
	}
	stateValue := approvalAuthorityString(result["InstanceStatus"])
	if request.Operation == approvalsemantic.OperationRecall {
		stateValue = constants.ApprovalStatusRemoved
	}
	state, err := approvalInstanceState(stateValue)
	if err != nil {
		return approvalsemantic.ExecuteResponse{}, err
	}
	actors := approvalStringSlice(result["ActorIds"])
	workItems := approvalStringSlice(result["NewWorkitemIds"])
	if len(actors) != len(workItems) {
		return approvalsemantic.ExecuteResponse{}, errors.New("Approval engine returned mismatched actor and work-item sets")
	}
	response := approvalsemantic.ExecuteResponse{InstanceID: instanceID, TargetRecordID: targetRecordID, State: state}
	if request.Operation != approvalsemantic.OperationSubmit {
		response.SourceWorkItemID = request.WorkItemID
	}
	for index := range actors {
		response.PendingWorkItems = append(response.PendingWorkItems, approvalsemantic.WorkItemAssignment{WorkItemID: workItems[index], ActorID: actors[index]})
	}
	return response, nil
}

func approvalStringSlice(value interface{}) []string {
	switch typed := value.(type) {
	case []string:
		return append([]string{}, typed...)
	case []interface{}:
		result := make([]string, 0, len(typed))
		for _, item := range typed {
			result = append(result, approvalAuthorityString(item))
		}
		return result
	default:
		return nil
	}
}

func approvalInstanceState(value string) (approvalsemantic.InstanceState, error) {
	switch value {
	case constants.ApprovalStatusPending:
		return approvalsemantic.InstanceStatePending, nil
	case constants.ApprovalStatusApproved:
		return approvalsemantic.InstanceStateApproved, nil
	case constants.ApprovalStatusRejected:
		return approvalsemantic.InstanceStateRejected, nil
	case constants.ApprovalStatusRemoved:
		return approvalsemantic.InstanceStateWithdrawn, nil
	default:
		return "", fmt.Errorf("unsupported Approval instance state %q", value)
	}
}

func approvalStepNoMatchBehavior(value string) approvalsemantic.StepNoMatchBehavior {
	switch value {
	case "GotoNextStep":
		return approvalsemantic.StepNoMatchNext
	case "RejectRecord":
		return approvalsemantic.StepNoMatchReject
	case "ApproveRecord":
		return approvalsemantic.StepNoMatchApprove
	default:
		return ""
	}
}
