package sfcrmappservice

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

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
	flowsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/flowv1"
)

const (
	flowComponentKind        = "metadata.pack.standard-flow.v1"
	invocableActionKind      = "metadata.pack.invocable-action.v1"
	flowRunPermission        = "RunFlow"
	flowExecutionErrorCode   = "FLOW_EXECUTION_ERROR"
	flowUnknownErrorCode     = "UNKNOWN_ERROR"
	flowLimitErrorCode       = "LIMIT_EXCEEDED"
	flowInsufficientAccess   = "INSUFFICIENT_ACCESS"
	flowInvalidRequestReason = "Invalid flow request"
	flowActionMaxInputs      = 200
)

type RuntimeExecuteFlowFunc func(
	context.Context,
	string,
	flowsemantic.Request,
) (flowsemantic.ResponseV2, error)

type RuntimeFlowActionsClient struct {
	execute              RuntimeExecuteFlowFunc
	appMetadata          RuntimeGetAppMetadataViewV3Func
	readEffective        RuntimeReadEffectivePermissionsFunc
	reconcileAppMetadata RuntimeReconcileAppMetadataV3ReceiptsFunc
}

type flowActionsRoute uint8

const (
	flowActionsRouteList flowActionsRoute = iota + 1
	flowActionsRouteDefinition
)

type matchedFlowActionsRoute struct {
	kind flowActionsRoute
	name string
	err  error
}

type flowActionDefinition struct {
	component     catalogsemantic.AppMetadataComponent
	label         string
	description   string
	variables     []flowActionVariable
	runAtomically bool
	status        string
	startTrigger  string
	subflowNames  []string
}

type flowActionVariable struct {
	Name         string `json:"name"`
	DataType     string `json:"dataType"`
	IsInput      bool   `json:"isInput"`
	IsOutput     bool   `json:"isOutput"`
	IsCollection bool   `json:"isCollection"`
}

type flowActionWireDefinition struct {
	Label       string                `json:"label"`
	Name        string                `json:"name"`
	Type        string                `json:"type"`
	URL         string                `json:"url"`
	Description string                `json:"description,omitempty"`
	Inputs      []flowActionWireField `json:"inputs,omitempty"`
	Outputs     []flowActionWireField `json:"outputs,omitempty"`
}

type flowActionWireField struct {
	Name         string `json:"name"`
	Label        string `json:"label"`
	Type         string `json:"type"`
	Required     bool   `json:"required,omitempty"`
	IsCollection bool   `json:"isCollection,omitempty"`
}

type flowActionExecutionRequest struct {
	Inputs []map[string]any `json:"inputs"`
}

type flowActionExecutionResult struct {
	ActionName   string                     `json:"actionName"`
	IsSuccess    bool                       `json:"isSuccess"`
	Errors       []flowActionExecutionError `json:"errors"`
	OutputValues map[string]any             `json:"outputValues,omitempty"`
}

type flowActionExecutionError struct {
	StatusCode string   `json:"statusCode"`
	Message    string   `json:"message"`
	Fields     []string `json:"fields,omitempty"`
}

func NewRuntimeFlowActionsClient(
	execute RuntimeExecuteFlowFunc,
	appMetadata RuntimeGetAppMetadataViewV3Func,
	readEffective RuntimeReadEffectivePermissionsFunc,
) (*RuntimeFlowActionsClient, error) {
	if execute == nil || appMetadata == nil || readEffective == nil {
		return nil, errors.New(
			"sfcrm AppService: ExecuteFlow, AppMetadata and effective permissions are required",
		)
	}
	return &RuntimeFlowActionsClient{
		execute: execute, appMetadata: appMetadata, readEffective: readEffective,
	}, nil
}

func matchFlowActionsRoute(escapedPath string) matchedFlowActionsRoute {
	parts := strings.Split(strings.Trim(escapedPath, "/"), "/")
	if len(parts) < 6 || parts[0] != "services" || parts[1] != "data" {
		return matchedFlowActionsRoute{}
	}
	if _, supported := recordViewVersions[parts[2]]; !supported ||
		parts[3] != "actions" || parts[4] != "custom" || parts[5] != "flow" {
		return matchedFlowActionsRoute{}
	}
	if len(parts) == 6 {
		return matchedFlowActionsRoute{kind: flowActionsRouteList}
	}
	if len(parts) != 7 {
		return matchedFlowActionsRoute{}
	}
	name, err := url.PathUnescape(parts[6])
	if err != nil || strings.TrimSpace(name) == "" || name != strings.TrimSpace(name) ||
		strings.Contains(name, "/") {
		return matchedFlowActionsRoute{kind: flowActionsRouteDefinition, err: errors.New("invalid flow name")}
	}
	return matchedFlowActionsRoute{kind: flowActionsRouteDefinition, name: name}
}

func (service *Service) serveFlowActions(
	writer http.ResponseWriter,
	request *http.Request,
) bool {
	route := matchFlowActionsRoute(request.URL.EscapedPath())
	if route.kind == 0 {
		return false
	}
	allowedMethod := http.MethodGet
	if route.kind == flowActionsRouteDefinition {
		allowedMethod = "GET, POST"
		if request.Method != http.MethodGet && request.Method != http.MethodPost {
			writer.Header().Set("Allow", allowedMethod)
			writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
			return true
		}
	} else if request.Method != http.MethodGet {
		writer.Header().Set("Allow", allowedMethod)
		writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
		return true
	}
	service.serveMatchedFlowActions(writer, request, route)
	return true
}

func (service *Service) serveMatchedFlowActions(
	writer http.ResponseWriter,
	request *http.Request,
	route matchedFlowActionsRoute,
) {
	service.requests.Add(1)
	if service.draining.Load() {
		service.drainRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return
	}
	if route.err != nil {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest, route.err.Error())
		return
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return
	}
	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()
	view, definitions, err := service.flowActions.authorizedDefinitions(ctx, bearer)
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return
	}
	if route.kind == flowActionsRouteList {
		service.successes.Add(1)
		writeJSON(writer, http.StatusOK, map[string]any{
			"actions": flowActionList(definitions, partsVersion(request.URL.Path)),
		})
		return
	}
	definition, found := definitions[route.name]
	if !found {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusNotFound, crmwire.SFCodeNotFound, "Flow")
		return
	}
	if request.Method == http.MethodGet {
		service.successes.Add(1)
		writeJSON(writer, http.StatusOK, flowActionToWire(definition, partsVersion(request.URL.Path), true))
		return
	}
	service.executeFlowAction(writer, request, ctx, bearer, view, definition)
}

func (client *RuntimeFlowActionsClient) authorizedDefinitions(
	ctx context.Context,
	bearer string,
) (catalogsemantic.AppMetadataViewV3, map[string]flowActionDefinition, error) {
	if client == nil || client.appMetadata == nil || client.readEffective == nil {
		return catalogsemantic.AppMetadataViewV3{}, nil, unavailableFlowActions()
	}
	view, err := client.appMetadata(ctx, catalogsemantic.GetRequest{})
	if err != nil {
		return catalogsemantic.AppMetadataViewV3{}, nil, err
	}
	if err := catalogsemantic.ValidateAppMetadataViewV3(view); err != nil || view.AppID != AppID {
		return catalogsemantic.AppMetadataViewV3{}, nil, unavailableFlowActions()
	}
	revision := view.Receipts.CatalogRevision
	permissionRequest := systemPermissionAuthorityRequest()
	permissionRequest.MinCatalogRevision = &revision
	permissions, err := client.readEffective(ctx, bearer, permissionRequest)
	if err != nil {
		return catalogsemantic.AppMetadataViewV3{}, nil, err
	}
	if err := permissionsemantic.ValidateResponse(permissions); err != nil ||
		validateFlowActionAuthority(
			ctx, client.reconcileAppMetadata, view.Receipts, permissions.Receipts,
		) != nil {
		return catalogsemantic.AppMetadataViewV3{}, nil, unavailableFlowActions()
	}
	if !hasSystemPermission(permissions.SystemPermissions, flowRunPermission) {
		return catalogsemantic.AppMetadataViewV3{}, nil, &commonsemantic.Failure{
			Category: commonsemantic.FailureForbidden,
		}
	}
	definitions, err := flowDefinitionsFromView(view)
	if err != nil {
		return catalogsemantic.AppMetadataViewV3{}, nil, err
	}
	return view, definitions, nil
}

func hasSystemPermission(
	grants []permissionsemantic.PermissionGrant,
	permissionID string,
) bool {
	for _, grant := range grants {
		if grant.Granted && grant.PermissionID == permissionID {
			return true
		}
	}
	return false
}

func flowDefinitionsFromView(
	view catalogsemantic.AppMetadataViewV3,
) (map[string]flowActionDefinition, error) {
	all := make(map[string]flowActionDefinition)
	protected := make(map[string]struct{})
	flowOwners := make(map[string]catalogsemantic.AppMetadataComponent)
	for _, component := range view.AppComponents {
		switch component.ComponentKind {
		case flowComponentKind:
			if _, duplicate := flowOwners[component.BusinessKey]; duplicate {
				return nil, unavailableFlowActions()
			}
			flowOwners[component.BusinessKey] = component
		case invocableActionKind:
			flowNames, err := invocableActionFlowNames(component)
			if err != nil {
				return nil, unavailableFlowActions()
			}
			for _, name := range flowNames {
				protected[name] = struct{}{}
			}
		}
	}
	for _, effective := range view.EffectiveComponents {
		if effective.ComponentKind != catalogsemantic.EffectiveStandardFlowComponentKind {
			continue
		}
		owner, found := flowOwners[effective.BusinessKey]
		if !found || effective.ComponentFormatVersion != 1 {
			return nil, unavailableFlowActions()
		}
		document, err := catalogsemantic.DecodeEffectiveStandardFlowDocument(
			effective.CanonicalBehavior,
		)
		if err != nil {
			return nil, unavailableFlowActions()
		}
		owner.DefinitionVersion = document.DefinitionVersion
		owner.CanonicalBehavior = document.CanonicalBehavior
		definition, err := decodeFlowActionDefinition(owner)
		if err != nil {
			return nil, unavailableFlowActions()
		}
		if _, duplicate := all[owner.BusinessKey]; duplicate {
			return nil, unavailableFlowActions()
		}
		all[owner.BusinessKey] = definition
		for _, subflow := range definition.subflowNames {
			protected[subflow] = struct{}{}
		}
	}
	result := make(map[string]flowActionDefinition, len(all))
	for name, definition := range all {
		_, reserved := protected[name]
		if reserved || definition.status != "Active" || !definition.runAtomically ||
			definition.startTrigger != "" {
			continue
		}
		result[name] = definition
	}
	return result, nil
}

func decodeFlowActionDefinition(
	component catalogsemantic.AppMetadataComponent,
) (flowActionDefinition, error) {
	if component.ComponentFormatVersion != 1 || component.BusinessKey == "" ||
		component.DefinitionVersion == "" || len(component.CanonicalBehavior) == 0 {
		return flowActionDefinition{}, errors.New("invalid flow component")
	}
	var document struct {
		Label         string               `json:"label"`
		Description   string               `json:"description"`
		Status        string               `json:"status"`
		RunAtomically bool                 `json:"runAtomically"`
		Variables     []flowActionVariable `json:"variables"`
		Start         struct {
			TriggerType string `json:"triggerType"`
		} `json:"start"`
		Subflows []struct {
			FlowName string `json:"flowName"`
		} `json:"subflows"`
	}
	decoder := json.NewDecoder(bytes.NewReader(component.CanonicalBehavior))
	if err := decoder.Decode(&document); err != nil {
		return flowActionDefinition{}, err
	}
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF {
		return flowActionDefinition{}, errors.New("flow component contains trailing JSON")
	}
	if strings.TrimSpace(document.Label) == "" || document.Label != strings.TrimSpace(document.Label) ||
		document.Description != strings.TrimSpace(document.Description) ||
		document.Status != strings.TrimSpace(document.Status) ||
		document.Start.TriggerType != strings.TrimSpace(document.Start.TriggerType) {
		return flowActionDefinition{}, errors.New("flow component contains non-canonical text")
	}
	seenVariables := make(map[string]struct{}, len(document.Variables))
	for _, variable := range document.Variables {
		if strings.TrimSpace(variable.Name) == "" || variable.Name != strings.TrimSpace(variable.Name) ||
			strings.TrimSpace(variable.DataType) == "" || variable.DataType != strings.TrimSpace(variable.DataType) {
			return flowActionDefinition{}, errors.New("flow component contains an invalid variable")
		}
		if _, duplicate := seenVariables[variable.Name]; duplicate {
			return flowActionDefinition{}, errors.New("flow component contains duplicate variables")
		}
		seenVariables[variable.Name] = struct{}{}
	}
	subflows := make([]string, 0, len(document.Subflows))
	for _, subflow := range document.Subflows {
		if strings.TrimSpace(subflow.FlowName) == "" || subflow.FlowName != strings.TrimSpace(subflow.FlowName) {
			return flowActionDefinition{}, errors.New("flow component contains an invalid subflow")
		}
		subflows = append(subflows, subflow.FlowName)
	}
	return flowActionDefinition{
		component: component, label: document.Label, description: document.Description,
		variables:     append([]flowActionVariable(nil), document.Variables...),
		runAtomically: document.RunAtomically, status: document.Status,
		startTrigger: document.Start.TriggerType, subflowNames: subflows,
	}, nil
}

func invocableActionFlowNames(
	component catalogsemantic.AppMetadataComponent,
) ([]string, error) {
	if component.ComponentFormatVersion != 1 || len(component.CanonicalBehavior) == 0 {
		return nil, errors.New("invalid invocable action component")
	}
	var actions []struct {
		FlowDeveloperName        string `json:"flowDeveloperName"`
		PreviewFlowDeveloperName string `json:"previewFlowDeveloperName"`
	}
	decoder := json.NewDecoder(bytes.NewReader(component.CanonicalBehavior))
	if err := decoder.Decode(&actions); err != nil {
		return nil, err
	}
	if actions == nil {
		return nil, errors.New("invocable action component must be a JSON array")
	}
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF {
		return nil, errors.New("invocable action component contains trailing JSON")
	}
	result := make([]string, 0, len(actions)*2)
	for _, action := range actions {
		for _, name := range []string{action.FlowDeveloperName, action.PreviewFlowDeveloperName} {
			if name == "" {
				continue
			}
			if name != strings.TrimSpace(name) {
				return nil, errors.New("invocable action references an invalid flow")
			}
			result = append(result, name)
		}
	}
	return result, nil
}

func flowActionList(
	definitions map[string]flowActionDefinition,
	version string,
) []flowActionWireDefinition {
	names := make([]string, 0, len(definitions))
	for name := range definitions {
		names = append(names, name)
	}
	sort.Strings(names)
	result := make([]flowActionWireDefinition, 0, len(names))
	for _, name := range names {
		result = append(result, flowActionToWire(definitions[name], version, false))
	}
	return result
}

func flowActionToWire(
	definition flowActionDefinition,
	version string,
	includeSchema bool,
) flowActionWireDefinition {
	result := flowActionWireDefinition{
		Label: definition.label, Name: definition.component.BusinessKey,
		Type: "FLOW", Description: definition.description,
		URL: "/services/data/" + version + "/actions/custom/flow/" +
			url.PathEscape(definition.component.BusinessKey),
	}
	if !includeSchema {
		return result
	}
	for _, variable := range definition.variables {
		field := flowActionWireField{
			Name: variable.Name, Label: variable.Name, Type: variable.DataType,
			IsCollection: variable.IsCollection,
		}
		if variable.IsInput {
			result.Inputs = append(result.Inputs, field)
		}
		if variable.IsOutput {
			field.Required = false
			result.Outputs = append(result.Outputs, field)
		}
	}
	return result
}

func (service *Service) executeFlowAction(
	writer http.ResponseWriter,
	request *http.Request,
	ctx context.Context,
	bearer string,
	view catalogsemantic.AppMetadataViewV3,
	definition flowActionDefinition,
) {
	request.Body = http.MaxBytesReader(writer, request.Body, service.maxRequestBytes)
	decoder := json.NewDecoder(request.Body)
	decoder.UseNumber()
	var body flowActionExecutionRequest
	if err := decoder.Decode(&body); err != nil || requireJSONEOF(decoder) != nil ||
		len(body.Inputs) == 0 {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest, "inputs array is required")
		return
	}
	if len(body.Inputs) > flowActionMaxInputs {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(
			writer,
			http.StatusBadRequest,
			crmwire.SFCodeLimitExceeded,
			"Maximum 200 Flow inputs allowed",
		)
		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)
		writeCRMError(writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest, err.Error())
		return
	}
	results := make([]flowActionExecutionResult, 0, len(body.Inputs))
	for index, input := range body.Inputs {
		canonical, marshalErr := canonicalActionInputs(input)
		if marshalErr != nil {
			results = append(results, failedFlowActionResult(
				definition.component.BusinessKey, flowInvalidRequestReason,
				flowExecutionErrorCode,
			))
			continue
		}
		revision := view.Receipts.CatalogRevision
		flowRequest := flowsemantic.Request{
			DefinitionRef:     definition.component.BusinessKey,
			DefinitionVersion: definition.component.DefinitionVersion,
			IdempotencyKey:    baseKey + "-" + strconv.Itoa(index),
			CanonicalInputs:   canonical, MinCatalogRevision: &revision,
		}
		if ctxErr := ctx.Err(); ctxErr != nil {
			results = append(results, flowActionRuntimeFailure(
				definition.component.BusinessKey, ctxErr,
			))
			continue
		}
		if validateErr := flowsemantic.ValidateRequest(ctx, flowRequest); validateErr != nil {
			if errors.Is(validateErr, context.Canceled) || errors.Is(validateErr, context.DeadlineExceeded) {
				results = append(results, flowActionRuntimeFailure(
					definition.component.BusinessKey, validateErr,
				))
				continue
			}
			results = append(results, failedFlowActionResult(
				definition.component.BusinessKey, flowInvalidRequestReason,
				flowExecutionErrorCode,
			))
			continue
		}
		response, executeErr := service.flowActions.execute(ctx, bearer, flowRequest)
		if executeErr == nil {
			executeErr = flowsemantic.ValidateResponseV2(response)
		}
		if executeErr == nil {
			executeErr = validateFlowActionAuthority(
				ctx, service.flowActions.reconcileAppMetadata,
				view.Receipts, response.Receipts,
			)
		}
		if executeErr != nil {
			results = append(results, flowActionRuntimeFailure(
				definition.component.BusinessKey, executeErr,
			))
			continue
		}
		if failed, present := presentedFlowActionFailure(
			definition.component.BusinessKey, response,
		); present {
			results = append(results, failed)
			continue
		}
		outputs, decodeErr := decodeFlowActionOutputs(response.CanonicalOutputs)
		if decodeErr != nil {
			results = append(results, failedFlowActionResult(
				definition.component.BusinessKey,
				crmwire.SFGenericInternalErrorMessage,
				flowUnknownErrorCode,
			))
			continue
		}
		results = append(results, flowActionExecutionResult{
			ActionName: definition.component.BusinessKey,
			IsSuccess:  true, Errors: []flowActionExecutionError{},
			OutputValues: outputs,
		})
	}
	service.successes.Add(1)
	writeJSON(writer, http.StatusOK, results)
}

func presentedFlowActionFailure(
	name string,
	response flowsemantic.ResponseV2,
) (flowActionExecutionResult, bool) {
	return presentedFlowActionFailureWithStatusCode(name, response, flowActionStatusCode)
}

func presentedFlowActionFailureWithStatusCode(
	name string,
	response flowsemantic.ResponseV2,
	projectStatusCode func(string) string,
) (flowActionExecutionResult, bool) {
	if len(response.Errors) == 0 {
		return flowActionExecutionResult{}, false
	}
	presented := make([]flowActionExecutionError, len(response.Errors))
	for index, executionError := range response.Errors {
		presented[index] = flowActionExecutionError{
			StatusCode: projectStatusCode(executionError.Code),
			Message:    executionError.Message,
			Fields:     append([]string(nil), executionError.Fields...),
		}
	}
	return flowActionExecutionResult{
		ActionName: name,
		IsSuccess:  false,
		Errors:     presented,
	}, true
}

func flowActionStatusCode(code string) string {
	switch code {
	case "FIELD_WRITE_DENIED":
		return crmwire.SFCodeInvalidFieldForInsertUpdate
	default:
		// Metadata-authored Flow errors already carry their L1-owned product
		// code. Only the exact shared neutral Runtime condition above is
		// translated; every other code remains byte-identical.
		return code
	}
}

func decodeFlowActionOutputs(payload []byte) (map[string]any, error) {
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var outputs map[string]any
	if err := decoder.Decode(&outputs); err != nil || outputs == nil {
		return nil, errors.New("invalid Flow outputs")
	}
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF {
		return nil, errors.New("Flow outputs contain trailing JSON")
	}
	return outputs, nil
}

func validateFlowActionAuthority(
	ctx context.Context,
	reconcile RuntimeReconcileAppMetadataV3ReceiptsFunc,
	expected commonsemantic.Receipts,
	actual commonsemantic.Receipts,
) error {
	if expected.CatalogRevision == actual.CatalogRevision &&
		expected.SchemaGeneration == actual.SchemaGeneration &&
		expected.EngineGeneration == actual.EngineGeneration &&
		expected.SecurityEpoch != actual.SecurityEpoch {
		reconciled, err := reconcileAppMetadataSecurityEpoch(
			ctx, reconcile, expected, actual,
		)
		if err != nil {
			return err
		}
		if reconciled {
			return nil
		}
	}
	if expected.CatalogRevision != actual.CatalogRevision ||
		expected.SchemaGeneration != actual.SchemaGeneration ||
		expected.EngineGeneration != actual.EngineGeneration ||
		expected.SecurityEpoch != actual.SecurityEpoch {
		return unavailableFlowActions()
	}
	return nil
}

func flowActionRuntimeFailure(
	name string,
	err error,
) flowActionExecutionResult {
	code := flowUnknownErrorCode
	message := crmwire.SFGenericInternalErrorMessage
	var failure *commonsemantic.Failure
	if errors.As(err, &failure) {
		switch failure.Category {
		case commonsemantic.FailureInvalidRequest:
			code, message = flowExecutionErrorCode, flowInvalidRequestReason
		case commonsemantic.FailureResourceExhausted:
			code, message = flowLimitErrorCode, "Flow execution exceeds a supported limit"
		case commonsemantic.FailureForbidden:
			code, message = flowInsufficientAccess, "Insufficient access"
		case commonsemantic.FailureCanceled:
			message = "Flow execution canceled"
		case commonsemantic.FailureDeadlineExceeded:
			message = "Flow execution deadline exceeded"
		case commonsemantic.FailureIncompatible,
			commonsemantic.FailureStaleState,
			commonsemantic.FailureUnavailable:
			message = "Flow execution temporarily unavailable"
		}
	} else if errors.Is(err, context.Canceled) {
		message = "Flow execution canceled"
	} else if errors.Is(err, context.DeadlineExceeded) {
		message = "Flow execution deadline exceeded"
	}
	return failedFlowActionResult(name, message, code)
}

func failedFlowActionResult(
	name string,
	message string,
	code string,
) flowActionExecutionResult {
	return flowActionExecutionResult{
		ActionName: name,
		IsSuccess:  false,
		Errors: []flowActionExecutionError{{
			StatusCode: code,
			Message:    message,
		}},
	}
}

func unavailableFlowActions() error {
	return &commonsemantic.Failure{Category: commonsemantic.FailureUnavailable}
}

func partsVersion(path string) string {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) > 2 {
		return parts[2]
	}
	return "v63.0"
}
