package sfcrmappservice

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	approvalsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/approvalv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

const (
	approvalUnexpectedErrorMessage     = "An unexpected error occurred"
	approvalAlreadyPendingMessage      = "This record is already pending approval"
	approvalNoApplicableProcessMessage = "No applicable approval process was found."
)

type RuntimeExecuteApprovalFunc func(
	context.Context,
	string,
	approvalsemantic.ExecuteRequest,
) (approvalsemantic.ExecuteResponse, error)

type RuntimeReadApprovalFunc func(
	context.Context,
	string,
	approvalsemantic.ReadRequest,
) (approvalsemantic.ReadResponse, error)

// RuntimeApprovalsClient owns semantic validation around the released neutral
// ports. It deliberately has no metadata repository, engine, or process-local
// fallback: Runtime owns selection, authority, execution and durable replay.
type RuntimeApprovalsClient struct {
	execute RuntimeExecuteApprovalFunc
	read    RuntimeReadApprovalFunc
}

func NewRuntimeApprovalsClient(
	execute RuntimeExecuteApprovalFunc,
	read RuntimeReadApprovalFunc,
) (*RuntimeApprovalsClient, error) {
	if execute == nil || read == nil {
		return nil, errors.New("sfcrm AppService: Approval execute and read ports are required")
	}
	return &RuntimeApprovalsClient{execute: execute, read: read}, nil
}

func (client *RuntimeApprovalsClient) Execute(
	ctx context.Context,
	bearer string,
	request approvalsemantic.ExecuteRequest,
) (approvalsemantic.ExecuteResponse, error) {
	if client == nil || client.execute == nil {
		return approvalsemantic.ExecuteResponse{}, &commonsemantic.Failure{
			Category: commonsemantic.FailureUnavailable,
		}
	}
	if err := approvalsemantic.ValidateExecuteRequest(ctx, request); err != nil {
		return approvalsemantic.ExecuteResponse{}, err
	}
	response, err := client.execute(ctx, bearer, request)
	if err != nil {
		return approvalsemantic.ExecuteResponse{}, normalizeRuntimeError(err)
	}
	if err := approvalsemantic.ValidateExecuteResponse(request, response); err != nil {
		return approvalsemantic.ExecuteResponse{}, err
	}
	if request.Operation == approvalsemantic.OperationRecall && response.ActingPrincipalID == "" {
		return approvalsemantic.ExecuteResponse{}, &commonsemantic.Failure{
			Category: commonsemantic.FailureInternal,
		}
	}
	return response, nil
}

func (client *RuntimeApprovalsClient) Read(
	ctx context.Context,
	bearer string,
	request approvalsemantic.ReadRequest,
) (approvalsemantic.ReadResponse, error) {
	if client == nil || client.read == nil {
		return approvalsemantic.ReadResponse{}, &commonsemantic.Failure{
			Category: commonsemantic.FailureUnavailable,
		}
	}
	if err := approvalsemantic.ValidateReadRequest(ctx, request); err != nil {
		return approvalsemantic.ReadResponse{}, err
	}
	response, err := client.read(ctx, bearer, request)
	if err != nil {
		return approvalsemantic.ReadResponse{}, normalizeRuntimeError(err)
	}
	if err := approvalsemantic.ValidateReadResponse(request, response); err != nil {
		return approvalsemantic.ReadResponse{}, err
	}
	return response, nil
}

type approvalRouteKind uint8

const (
	approvalRouteExecute approvalRouteKind = iota + 1
	approvalRoutePending
	approvalRouteLatest
	approvalRouteStepDisplay
)

type approvalRoute struct {
	kind     approvalRouteKind
	recordID string
	err      error
}

func matchApprovalRoute(escapedPath string) approvalRoute {
	parts := strings.Split(strings.Trim(escapedPath, "/"), "/")
	if len(parts) < 5 || parts[0] != "services" || parts[1] != "data" {
		return approvalRoute{}
	}
	if _, supported := recordViewVersions[parts[2]]; !supported ||
		parts[3] != "process" || parts[4] != "approvals" {
		return approvalRoute{}
	}
	if len(parts) == 5 {
		return approvalRoute{kind: approvalRouteExecute}
	}
	if len(parts) == 6 && parts[5] == "pending" {
		return approvalRoute{kind: approvalRoutePending}
	}
	if len(parts) != 8 || parts[5] != "records" ||
		(parts[7] != "instance" && parts[7] != "step-display") {
		return approvalRoute{}
	}
	recordID, err := url.PathUnescape(parts[6])
	if err != nil || recordID == "" || strings.TrimSpace(recordID) != recordID ||
		strings.Contains(recordID, "/") {
		return approvalRoute{kind: approvalRouteLatest, err: errors.New("recordId is required")}
	}
	kind := approvalRouteLatest
	if parts[7] == "step-display" {
		kind = approvalRouteStepDisplay
	}
	return approvalRoute{kind: kind, recordID: recordID}
}

type approvalProcessRequest struct {
	ActionType                string   `json:"actionType"`
	ContextActorID            string   `json:"contextActorId,omitempty"`
	ContextID                 string   `json:"contextId"`
	Comments                  string   `json:"comments,omitempty"`
	NextApproverIDs           []string `json:"nextApproverIds,omitempty"`
	ProcessDefinitionNameOrID string   `json:"processDefinitionNameOrId,omitempty"`
	RecordID                  string   `json:"RecordId"`
	SkipEntryCriteria         bool     `json:"skipEntryCriteria,omitempty"`
	legacyRecordIDAllowed     bool
}

type approvalProcessRequestWrapper struct {
	Requests []approvalProcessRequest `json:"requests"`
}

type legacyApprovalProcessRequest struct {
	ActionType                string   `json:"actionType"`
	ContextActorID            string   `json:"contextActorId,omitempty"`
	ContextID                 string   `json:"contextId"`
	Comments                  string   `json:"comments"`
	NextApproverIDs           []string `json:"nextApproverIds,omitempty"`
	ObjectAPIName             string   `json:"ObjectApiName"`
	ProcessDefinitionNameOrID string   `json:"processDefinitionNameOrId,omitempty"`
	RecordID                  string   `json:"RecordId"`
	SkipEntryCriteria         bool     `json:"skipEntryCriteria,omitempty"`
}

type approvalProcessResult struct {
	ActorIDs       []string                   `json:"actorIds"`
	EntityID       string                     `json:"entityId"`
	Errors         []string                   `json:"errors"`
	InstanceID     string                     `json:"instanceId"`
	InstanceStatus string                     `json:"instanceStatus"`
	NewWorkitemIDs []string                   `json:"newWorkitemIds"`
	Success        bool                       `json:"success"`
	ActionFailures []approvalActionFailureDTO `json:"actionFailures,omitempty"`
}

type approvalActionFailureDTO struct {
	ActionName string `json:"actionName"`
	ActionType string `json:"actionType"`
	Reason     string `json:"reason"`
}

type approvalPendingViewDTO struct {
	WorkItemID          string `json:"workItemId"`
	ProcessInstanceID   string `json:"processInstanceId"`
	ProcessDefinitionID string `json:"processDefinitionId"`
	TargetObjectID      string `json:"targetObjectId"`
	ObjectAPIName       string `json:"objectApiName"`
	SubmittedByID       string `json:"submittedById"`
	SubmittedByName     string `json:"submittedByName"`
	SubmittedDate       string `json:"submittedDate"`
	ActorID             string `json:"actorId"`
	OriginalActorID     string `json:"originalActorId"`
	Comments            string `json:"comments,omitempty"`
	RecordName          string `json:"recordName,omitempty"`
}

type approvalProcessInstanceSummaryDTO struct {
	ID                  string                              `json:"id"`
	ProcessDefinitionID string                              `json:"processDefinitionId"`
	TargetObjectID      string                              `json:"targetObjectId"`
	Status              string                              `json:"status"`
	SubmittedByID       string                              `json:"submittedById"`
	SubmittedByName     string                              `json:"submittedByName"`
	CreatedDate         string                              `json:"createdDate"`
	CompletedDate       string                              `json:"completedDate,omitempty"`
	PendingWorkItems    []approvalPendingWorkItemSummaryDTO `json:"pendingWorkItems"`
}

type approvalPendingWorkItemSummaryDTO struct {
	ID                string `json:"id"`
	ActorID           string `json:"actorId"`
	ActorName         string `json:"actorName,omitempty"`
	OriginalActorID   string `json:"originalActorId,omitempty"`
	OriginalActorName string `json:"originalActorName,omitempty"`
}

type approvalStepDisplayMetadataDTO struct {
	ProcessNodeID    string `json:"processNodeId"`
	StepOrder        uint32 `json:"stepOrder,omitempty"`
	StepName         string `json:"stepName,omitempty"`
	StepLabel        string `json:"stepLabel,omitempty"`
	HasEntryCriteria bool   `json:"hasEntryCriteria"`
	IfCriteriaNotMet string `json:"ifCriteriaNotMet,omitempty"`
}

func (service *Service) serveApprovals(
	writer http.ResponseWriter,
	request *http.Request,
) bool {
	route := matchApprovalRoute(request.URL.EscapedPath())
	if route.kind == 0 {
		return false
	}
	wantMethod := http.MethodGet
	if route.kind == approvalRouteExecute {
		wantMethod = http.MethodPost
	}
	if request.Method != wantMethod {
		writer.Header().Set("Allow", wantMethod)
		writeCRMErrorWith(
			writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest,
			"Method not allowed", writeLegacyJSON,
		)
		return true
	}
	service.serveMatchedApproval(writer, request, route)
	return true
}

func (service *Service) serveMatchedApproval(
	writer http.ResponseWriter,
	request *http.Request,
	route approvalRoute,
) {
	service.requests.Add(1)
	if service.draining.Load() {
		service.drainRejected.Add(1)
		service.failures.Add(1)
		writeCRMErrorWith(
			writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException,
			"Service is draining", writeLegacyJSON,
		)
		return
	}
	if route.err != nil {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMErrorWith(
			writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest,
			route.err.Error(), writeLegacyJSON,
		)
		return
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMErrorWith(
			writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID,
			"Session expired or invalid", writeLegacyJSON,
		)
		return
	}
	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()
	if route.kind == approvalRouteExecute {
		service.executeApprovals(writer, request, ctx, bearer)
		return
	}
	service.readApprovals(writer, ctx, bearer, route)
}

func (service *Service) executeApprovals(
	writer http.ResponseWriter,
	request *http.Request,
	ctx context.Context,
	bearer string,
) {
	requests, wrapper, objectID, err := service.decodeApprovalRequests(writer, request)
	if err != nil {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMErrorWith(
			writer, http.StatusBadRequest, crmwire.SFCodeJSONParserError,
			err.Error(), writeLegacyJSON,
		)
		return
	}
	if len(requests) == 0 {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMErrorWith(
			writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest,
			"At least one approval request is required", writeLegacyJSON,
		)
		return
	}
	if len(requests) > approvalsemantic.MaxWorkItemIDs {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMErrorWith(
			writer, http.StatusBadRequest, crmwire.SFCodeLimitExceeded,
			"Maximum 200 approval requests allowed", writeLegacyJSON,
		)
		return
	}
	baseKey, err := mutationCommandID(request.Header.Values(crmMutationIdempotencyHeader))
	if err == nil && baseKey == "" {
		baseKey, err = newMutationCommandID()
	}
	if err != nil {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMErrorWith(
			writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest,
			err.Error(), writeLegacyJSON,
		)
		return
	}
	results := make([]approvalProcessResult, 0, len(requests))
	for index, productRequest := range requests {
		neutralRequest, localFailure := approvalExecuteRequest(
			productRequest, objectID, baseKey+"-"+strconv.Itoa(index),
		)
		if localFailure != "" {
			// The established Salesforce wire reports malformed local action
			// requests without an entity identity. A valid contextId is not proof
			// that the malformed action reached or owns that entity.
			if !wrapper && len(requests) == 1 &&
				neutralRequest.Operation == approvalsemantic.OperationSubmit &&
				(productRequest.ContextID != "" || productRequest.RecordID != "") {
				service.failures.Add(1)
				writeCRMErrorWith(
					writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest,
					localFailure, writeLegacyJSON,
				)
				return
			}
			results = append(results, failedApprovalProcessResult(localFailure, ""))
			continue
		}
		response, executeErr := service.approvals.Execute(ctx, bearer, neutralRequest)
		if executeErr != nil {
			if approvalRequestTerminated(executeErr) {
				service.failures.Add(1)
				writeRuntimeFailureWith(writer, executeErr, writeLegacyJSON)
				return
			}
			if !wrapper && len(requests) == 1 && neutralRequest.Operation == approvalsemantic.OperationSubmit {
				service.failures.Add(1)
				if approvalSubmitOperationConflict(executeErr) {
					writeCRMErrorWith(
						writer, http.StatusConflict, crmwire.SFCodeConflict,
						approvalAlreadyPendingMessage, writeLegacyJSON,
					)
					return
				}
				writeRuntimeFailureWith(writer, executeErr, writeLegacyJSON)
				return
			}
			results = append(results, failedApprovalProcessResult(
				approvalBatchFailureMessage(executeErr), productRequest.ContextID,
			))
			continue
		}
		results = append(results, approvalProcessResultFromResponse(
			productRequest, neutralRequest, response,
		))
	}
	service.successes.Add(1)
	writeLegacyJSON(writer, http.StatusOK, results)
}

func (service *Service) decodeApprovalRequests(
	writer http.ResponseWriter,
	request *http.Request,
) ([]approvalProcessRequest, bool, string, error) {
	request.Body = http.MaxBytesReader(writer, request.Body, service.maxRequestBytes)
	decoder := json.NewDecoder(request.Body)
	var raw map[string]json.RawMessage
	if err := decoder.Decode(&raw); err != nil || requireJSONEOF(decoder) != nil {
		return nil, false, "", errors.New("Invalid JSON")
	}
	var envelope struct {
		ObjectAPIName string `json:"ObjectApiName"`
	}
	encoded, err := json.Marshal(raw)
	if err != nil || json.Unmarshal(encoded, &envelope) != nil {
		return nil, false, "", errors.New("Invalid approval request format")
	}
	if _, wrapped := raw["requests"]; wrapped {
		var wrapper approvalProcessRequestWrapper
		if err := json.Unmarshal(encoded, &wrapper); err != nil {
			return nil, true, "", errors.New("Invalid approval requests format")
		}
		return wrapper.Requests, true, envelope.ObjectAPIName, nil
	}
	var legacy legacyApprovalProcessRequest
	if err := json.Unmarshal(encoded, &legacy); err != nil {
		return nil, false, "", errors.New("Invalid approval request format")
	}
	return []approvalProcessRequest{{
		ActionType: legacy.ActionType, ContextActorID: legacy.ContextActorID,
		ContextID: legacy.ContextID, Comments: legacy.Comments,
		NextApproverIDs:           legacy.NextApproverIDs,
		ProcessDefinitionNameOrID: legacy.ProcessDefinitionNameOrID,
		RecordID:                  legacy.RecordID,
		SkipEntryCriteria:         legacy.SkipEntryCriteria,
		legacyRecordIDAllowed:     true,
	}}, false, legacy.ObjectAPIName, nil
}

func approvalExecuteRequest(
	request approvalProcessRequest,
	objectID string,
	idempotencyKey string,
) (approvalsemantic.ExecuteRequest, string) {
	actionType := request.ActionType
	if actionType == "" {
		actionType = "Submit"
	}
	result := approvalsemantic.ExecuteRequest{
		IdempotencyKey: idempotencyKey, Comments: request.Comments,
	}
	switch actionType {
	case "Submit":
		result.Operation = approvalsemantic.OperationSubmit
		contextID, failure := approvalRequestContextID(request, actionType)
		if failure != "" {
			return result, failure
		}
		if contextID == "" {
			return approvalsemantic.ExecuteRequest{}, "Missing contextId for Submit action"
		}
		if failure := unsupportedApprovalRequestFields(request, actionType); failure != "" {
			return result, failure
		}
		result.ObjectID = objectID
		result.TargetRecordID = contextID
		result.SkipEntryCriteria = request.SkipEntryCriteria
	case "Approve":
		result.Operation = approvalsemantic.OperationApprove
		contextID, failure := approvalRequestContextID(request, actionType)
		if failure != "" {
			return result, failure
		}
		if contextID == "" {
			return approvalsemantic.ExecuteRequest{}, "Missing contextId (WorkItemId) for Approve action"
		}
		if failure := unsupportedApprovalRequestFields(request, actionType); failure != "" {
			return result, failure
		}
		result.WorkItemID = contextID
	case "Reject":
		result.Operation = approvalsemantic.OperationReject
		contextID, failure := approvalRequestContextID(request, actionType)
		if failure != "" {
			return result, failure
		}
		if contextID == "" {
			return approvalsemantic.ExecuteRequest{}, "Missing contextId (WorkItemId) for Reject action"
		}
		if failure := unsupportedApprovalRequestFields(request, actionType); failure != "" {
			return result, failure
		}
		result.WorkItemID = contextID
	case "Reassign":
		result.Operation = approvalsemantic.OperationReassign
		contextID, failure := approvalRequestContextID(request, actionType)
		if failure != "" {
			return result, failure
		}
		if contextID == "" || len(request.NextApproverIDs) == 0 {
			return approvalsemantic.ExecuteRequest{}, "Missing contextId or nextApproverIds for Reassign action"
		}
		if len(request.NextApproverIDs) != 1 || strings.TrimSpace(request.NextApproverIDs[0]) == "" {
			return result, "Exactly one non-empty nextApproverId is required for Reassign action"
		}
		if failure := unsupportedApprovalRequestFields(request, actionType); failure != "" {
			return result, failure
		}
		result.WorkItemID = contextID
		result.NextActorID = request.NextApproverIDs[0]
		result.Comments = ""
	case "Removed":
		result.Operation = approvalsemantic.OperationRecall
		contextID, failure := approvalRequestContextID(request, actionType)
		if failure != "" {
			return result, failure
		}
		if contextID == "" {
			return approvalsemantic.ExecuteRequest{}, "Missing contextId (WorkItemId) for Removed action"
		}
		if failure := unsupportedApprovalRequestFields(request, actionType); failure != "" {
			return result, failure
		}
		result.WorkItemID = contextID
	default:
		return approvalsemantic.ExecuteRequest{}, "Unsupported actionType: " + actionType
	}
	return result, ""
}

// approvalRequestContextID keeps the two public approval envelopes explicit.
// RecordId is the legacy single-submit alias for a target record. Work-item
// actions require contextId; treating RecordId as a work-item ID could act on a
// different approval entity than the caller named. Conflicting aliases are
// rejected rather than resolved by precedence.
func approvalRequestContextID(request approvalProcessRequest, actionType string) (string, string) {
	if request.RecordID != "" && !request.legacyRecordIDAllowed {
		return "", "RecordId is only supported by legacy single Submit requests"
	}
	if actionType != "Submit" {
		if request.RecordID != "" {
			return "", "RecordId is only supported for Submit action"
		}
		return request.ContextID, ""
	}
	if request.ContextID != "" && request.RecordID != "" && request.ContextID != request.RecordID {
		return "", "contextId and RecordId must identify the same record"
	}
	if request.ContextID != "" {
		return request.ContextID, ""
	}
	return request.RecordID, ""
}

// unsupportedApprovalRequestFields is the explicit L1-to-L0 request matrix.
// The public Salesforce envelope contains selectors that neutral Approval v1
// cannot represent. Silently discarding them could execute a different
// definition or actor assignment, so unsupported authored values fail before
// the Runtime boundary instead of being guessed or ignored.
func unsupportedApprovalRequestFields(request approvalProcessRequest, actionType string) string {
	if request.ContextActorID != "" {
		return "contextActorId is not supported for " + actionType + " action"
	}
	if request.ProcessDefinitionNameOrID != "" {
		return "processDefinitionNameOrId is not supported for " + actionType + " action"
	}
	if actionType != "Submit" && request.SkipEntryCriteria {
		return "skipEntryCriteria is only supported for Submit action"
	}
	if actionType != "Reassign" && len(request.NextApproverIDs) != 0 {
		return "nextApproverIds is only supported for Reassign action"
	}
	if actionType == "Reassign" && request.Comments != "" {
		return "comments are not supported for Reassign action"
	}
	return ""
}

func approvalProcessResultFromResponse(
	productRequest approvalProcessRequest,
	request approvalsemantic.ExecuteRequest,
	response approvalsemantic.ExecuteResponse,
) approvalProcessResult {
	if response.SubmitRejection != nil {
		return failedApprovalProcessResultWithErrors(
			approvalSubmitRejectionErrors(response.SubmitRejection),
			response.TargetRecordID,
		)
	}
	result := approvalProcessResult{
		ActorIDs:       []string{},
		EntityID:       response.TargetRecordID,
		Errors:         []string{},
		InstanceID:     response.InstanceID,
		InstanceStatus: approvalStateToCRM(response.State),
		NewWorkitemIDs: []string{},
		Success:        true,
	}
	for _, assignment := range response.PendingWorkItems {
		result.ActorIDs = append(result.ActorIDs, assignment.ActorID)
		result.NewWorkitemIDs = append(result.NewWorkitemIDs, assignment.WorkItemID)
	}
	if request.Operation == approvalsemantic.OperationRecall {
		// The established Recall projection falls back to the authenticated
		// actor because the engine returns no pending ActorIds after withdrawal.
		result.ActorIDs = []string{response.ActingPrincipalID}
	}
	for _, failure := range response.ActionFailures {
		result.ActionFailures = append(result.ActionFailures, approvalActionFailureDTO{
			ActionName: failure.ActionRef,
			ActionType: approvalActionFailureType(failure.Kind),
			Reason:     approvalActionFailureReason(failure.Reason),
		})
	}
	if request.Operation == approvalsemantic.OperationReassign {
		// Preserve the established Salesforce-compatible result shape. The
		// neutral response was still fully validated before this projection.
		result.ActorIDs = []string{productRequest.NextApproverIDs[0]}
		result.EntityID = ""
		result.InstanceID = productRequest.ContextID
		result.NewWorkitemIDs = []string{productRequest.ContextID}
	}
	return result
}

func approvalSubmitRejectionErrors(rejection *approvalsemantic.SubmitRejection) []string {
	// RuntimeApprovalsClient validates the complete semantic response before it
	// reaches this projection. Keep a defensive fallback here so future callers
	// cannot turn an unreviewed rejection shape into a client-visible message.
	if rejection == nil {
		return []string{approvalUnexpectedErrorMessage}
	}
	switch rejection.Reason {
	case approvalsemantic.SubmitRejectionNoActiveProcess,
		approvalsemantic.SubmitRejectionEntryCriteriaMiss:
		return []string{approvalNoApplicableProcessMessage}
	case approvalsemantic.SubmitRejectionRecordValidationFailed:
		messages := make([]string, 0, len(rejection.ValidationIssues))
		for _, issue := range rejection.ValidationIssues {
			if issue.Message == "" {
				return []string{approvalUnexpectedErrorMessage}
			}
			messages = append(messages, issue.Message)
		}
		if len(messages) != 0 {
			return messages
		}
	default:
		return []string{approvalUnexpectedErrorMessage}
	}
	return []string{approvalUnexpectedErrorMessage}
}

func approvalStateToCRM(state approvalsemantic.InstanceState) string {
	switch state {
	case approvalsemantic.InstanceStatePending:
		return "Pending"
	case approvalsemantic.InstanceStateApproved:
		return "Approved"
	case approvalsemantic.InstanceStateRejected:
		return "Rejected"
	case approvalsemantic.InstanceStateWithdrawn:
		return "Removed"
	default:
		return ""
	}
}

func approvalActionFailureType(kind approvalsemantic.ActionFailureKind) string {
	switch kind {
	case approvalsemantic.ActionFailureAlert:
		return "Alert"
	case approvalsemantic.ActionFailureFlow:
		return "Flow"
	case approvalsemantic.ActionFailureUnsupported:
		return "UnsupportedAction"
	default:
		return ""
	}
}

func approvalActionFailureReason(reason approvalsemantic.ActionFailureReason) string {
	switch reason {
	case approvalsemantic.ActionFailureUnavailable:
		return "Action temporarily unavailable"
	case approvalsemantic.ActionFailureInvalidBinding:
		return "Invalid action binding"
	case approvalsemantic.ActionFailureUnsafeDefinition:
		return "Unsafe action definition"
	case approvalsemantic.ActionFailureExecutionFailed:
		return "Action execution failed"
	case approvalsemantic.ActionFailureUnsupportedAction:
		return "Unsupported action"
	default:
		return approvalUnexpectedErrorMessage
	}
}

func failedApprovalProcessResult(message, entityID string) approvalProcessResult {
	return failedApprovalProcessResultWithErrors([]string{message}, entityID)
}

func failedApprovalProcessResultWithErrors(messages []string, entityID string) approvalProcessResult {
	if len(messages) == 0 {
		messages = []string{approvalUnexpectedErrorMessage}
	}
	return approvalProcessResult{
		ActorIDs:       []string{},
		EntityID:       entityID,
		Errors:         append([]string(nil), messages...),
		InstanceStatus: "Error",
		NewWorkitemIDs: []string{},
		Success:        false,
	}
}

func approvalBatchFailureMessage(err error) string {
	var failure *commonsemantic.Failure
	if !errors.As(err, &failure) {
		return approvalUnexpectedErrorMessage
	}
	switch failure.Category {
	case commonsemantic.FailureInvalidRequest:
		return "Invalid approval request"
	case commonsemantic.FailureResourceExhausted:
		return "Approval request exceeds a supported limit"
	case commonsemantic.FailureUnauthenticated:
		return "Session expired or invalid"
	case commonsemantic.FailureForbidden:
		return "Insufficient access"
	case commonsemantic.FailureMissing:
		return "Record not found"
	case commonsemantic.FailureUnavailable, commonsemantic.FailureIncompatible,
		commonsemantic.FailureStaleState:
		return "Service temporarily unavailable"
	default:
		return approvalUnexpectedErrorMessage
	}
}

func approvalRequestTerminated(err error) bool {
	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
		return true
	}
	var failure *commonsemantic.Failure
	return errors.As(err, &failure) &&
		(failure.Category == commonsemantic.FailureCanceled ||
			failure.Category == commonsemantic.FailureDeadlineExceeded)
}

func approvalSubmitOperationConflict(err error) bool {
	var failure *commonsemantic.Failure
	return errors.As(err, &failure) &&
		failure.Category == commonsemantic.FailureInvalidRequest &&
		failure.InvalidComponent == commonsemantic.InvalidComponentOperation
}

func (service *Service) readApprovals(
	writer http.ResponseWriter,
	ctx context.Context,
	bearer string,
	route approvalRoute,
) {
	request := approvalsemantic.ReadRequest{TargetRecordID: route.recordID}
	switch route.kind {
	case approvalRoutePending:
		request.Kind = approvalsemantic.ReadPendingForCaller
	case approvalRouteLatest:
		request.Kind = approvalsemantic.ReadLatestForRecord
	case approvalRouteStepDisplay:
		request.Kind = approvalsemantic.ReadStepDisplayForRecord
	}
	response, err := service.approvals.Read(ctx, bearer, request)
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailureWith(writer, err, writeLegacyJSON)
		return
	}
	service.successes.Add(1)
	switch route.kind {
	case approvalRoutePending:
		writeLegacyJSON(writer, http.StatusOK, approvalPendingViewsToDTO(response.Pending))
	case approvalRouteLatest:
		writeLegacyJSON(writer, http.StatusOK, approvalProcessInstanceSummaryToDTO(response.Latest))
	case approvalRouteStepDisplay:
		writeLegacyJSON(writer, http.StatusOK, approvalStepDisplayMetadataToDTO(response.Steps))
	}
}

func approvalPendingViewsToDTO(
	items []approvalsemantic.PendingItem,
) []approvalPendingViewDTO {
	result := make([]approvalPendingViewDTO, 0, len(items))
	for _, item := range items {
		result = append(result, approvalPendingViewDTO{
			WorkItemID: item.WorkItemID, ProcessInstanceID: item.InstanceID,
			ProcessDefinitionID: item.DefinitionID, TargetObjectID: item.TargetRecordID,
			ObjectAPIName: item.ObjectID, SubmittedByID: item.SubmittedByID,
			SubmittedByName: item.SubmittedByName, SubmittedDate: item.SubmittedAt,
			ActorID: item.ActorID, OriginalActorID: item.OriginalActorID,
			Comments: item.Comments, RecordName: item.RecordName,
		})
	}
	return result
}

func approvalProcessInstanceSummaryToDTO(
	summary *approvalsemantic.InstanceSummary,
) *approvalProcessInstanceSummaryDTO {
	if summary == nil {
		return nil
	}
	result := &approvalProcessInstanceSummaryDTO{
		ID: summary.InstanceID, ProcessDefinitionID: summary.DefinitionID,
		TargetObjectID: summary.TargetRecordID, Status: approvalStateToCRM(summary.State),
		SubmittedByID: summary.SubmittedByID, SubmittedByName: summary.SubmittedByName,
		CreatedDate: summary.CreatedAt, CompletedDate: summary.CompletedAt,
		PendingWorkItems: make([]approvalPendingWorkItemSummaryDTO, 0, len(summary.PendingWorkItems)),
	}
	for _, item := range summary.PendingWorkItems {
		result.PendingWorkItems = append(result.PendingWorkItems, approvalPendingWorkItemSummaryDTO{
			ID: item.WorkItemID, ActorID: item.ActorID, ActorName: item.ActorName,
			OriginalActorID: item.OriginalActorID, OriginalActorName: item.OriginalActorName,
		})
	}
	return result
}

func approvalStepDisplayMetadataToDTO(
	steps []approvalsemantic.StepDisplay,
) []approvalStepDisplayMetadataDTO {
	result := make([]approvalStepDisplayMetadataDTO, 0, len(steps))
	for _, step := range steps {
		result = append(result, approvalStepDisplayMetadataDTO{
			ProcessNodeID: step.StepID, StepOrder: step.StepOrder,
			StepName: step.StepName, StepLabel: step.StepLabel,
			HasEntryCriteria: step.HasCriteria,
			IfCriteriaNotMet: approvalStepNoMatchToCRM(step.NoMatchBehavior),
		})
	}
	return result
}

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