package dialectsfcrm

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
)

type StandardActionListEntry struct {
	ID                       string                `json:"-"`
	Name                     string                `json:"name"`
	Label                    string                `json:"label"`
	Description              string                `json:"description,omitempty"`
	Category                 string                `json:"category,omitempty"`
	FlowDeveloperName        string                `json:"flowDeveloperName"`
	IsAtomic                 bool                  `json:"isAtomic"`
	RequiredPerms            []string              `json:"requiredPermissions"`
	TargetObjects            []string              `json:"targetObjects"`
	HeaderFallbackEligible   bool                  `json:"headerFallbackEligible"`
	RendererID               string                `json:"rendererId,omitempty"`
	InputGuards              []InputGuardSummary   `json:"inputGuards"`
	Inputs                   []ActionSchemaSummary `json:"inputs"`
	Outputs                  []ActionSchemaSummary `json:"outputs"`
	Endpoint                 string                `json:"endpoint"`
	PreviewEndpoint          string                `json:"previewEndpoint,omitempty"`
	VisibilityCondition      string                `json:"visibilityCondition,omitempty"`
	PreviewFlowDeveloperName string                `json:"previewFlowDeveloperName,omitempty"`
}

type InputGuardSummary struct {
	GatedInput          string `json:"gatedInput"`
	RequiredPermission  string `json:"requiredPermission,omitempty"`
	RequiredObject      string `json:"requiredObject,omitempty"`
	RequiredOperation   string `json:"requiredOperation,omitempty"`
	Condition           string `json:"condition,omitempty"`
	CallerHasPermission bool   `json:"callerHasPermission"`
}

type ActionSchemaSummary struct {
	Name                      string `json:"name"`
	Type                      string `json:"type"`
	IsCollection              bool   `json:"isCollection,omitempty"`
	Required                  bool   `json:"required,omitempty"`
	ReferenceTo               string `json:"referenceTo,omitempty"`
	Label                     string `json:"label,omitempty"`
	UISection                 string `json:"uiSection,omitempty"`
	UIMode                    string `json:"uiMode,omitempty"`
	TargetObject              string `json:"targetObject,omitempty"`
	TargetFieldAPIName        string `json:"targetFieldAPIName,omitempty"`
	RequiredPicklistTag       string `json:"requiredPicklistTag,omitempty"`
	MappingSourceObject       string `json:"mappingSourceObject,omitempty"`
	MappingActionType         string `json:"mappingActionType,omitempty"`
	PrefillSourceFieldAPIName string `json:"prefillSourceFieldAPIName,omitempty"`
	TargetRequired            bool   `json:"targetRequired,omitempty"`
	TargetDefaulted           bool   `json:"targetDefaulted,omitempty"`
	Defaulted                 bool   `json:"defaulted,omitempty"`
	DefaultValue              any    `json:"defaultValue,omitempty"`
}

type StandardActionCreatePreviewImage struct {
	Object                    string         `json:"object"`
	Fields                    models.SObject `json:"fields"`
	SiblingMaxDefaultedFields []string       `json:"siblingMaxDefaultedFields,omitempty"`
}

type StandardActionChildPreview struct {
	Object string           `json:"object"`
	Count  int              `json:"count"`
	Rows   []models.SObject `json:"rows,omitempty"`
}

type StandardActionPreviewResult struct {
	ActionName    string                            `json:"actionName"`
	IsSuccess     bool                              `json:"isSuccess"`
	Errors        []models.FlowExecutionError       `json:"errors,omitempty"`
	ParentPreview *StandardActionCreatePreviewImage `json:"parentPreview,omitempty"`
	ChildPreview  []StandardActionChildPreview      `json:"childPreview,omitempty"`
}

type standardActionPreviewRequest struct {
	Input map[string]any `json:"input"`
}

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

type targetScopedStandardActionService interface {
	ListStandardActionsForTarget(context.Context, *models.UserSession, string) ([]contract.StandardActionDescriptor, error)
}

func ListStandardActionsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	standardActions, resp := standardActionContract(deps, "list standard actions")
	if resp != nil {
		return resp, nil
	}
	targetObject := strings.TrimSpace(firstQueryValue(req, "targetObject"))
	descriptors, err := listStandardActionDescriptors(ctx, standardActions, req.User, targetObject)
	if err != nil {
		return standardActionInternalErrorResponse("list standard actions", err), nil
	}

	allowed := make([]StandardActionListEntry, 0, len(descriptors))
	for _, descriptor := range descriptors {
		entry := standardActionListEntryFromContract(descriptor, req.Version)
		if entry.Endpoint == "" {
			// No addressable endpoint means no invocable action: the dispatch route matches a
			// single path segment, so a name this shape could never be called. Listing it would
			// advertise a surface that does not exist, and it is an authoring fault worth naming.
			log.Printf("standard action %q has no addressable endpoint and was omitted from the action list", descriptor.Name)
			continue
		}
		allowed = append(allowed, entry)
	}
	return jsonResponse(http.StatusOK, map[string]any{"actions": allowed}), nil
}

func listStandardActionDescriptors(ctx context.Context, standardActions contract.StandardActionService, user *models.UserSession, targetObject string) ([]contract.StandardActionDescriptor, error) {
	targetObject = strings.TrimSpace(targetObject)
	if targetObject == "" {
		return standardActions.ListStandardActions(ctx, user)
	}
	if scoped, ok := standardActions.(targetScopedStandardActionService); ok {
		return scoped.ListStandardActionsForTarget(ctx, user, targetObject)
	}
	descriptors, err := standardActions.ListStandardActions(ctx, user)
	if err != nil {
		return nil, err
	}
	return filterStandardActionDescriptorsForTarget(descriptors, targetObject), nil
}

func filterStandardActionDescriptorsForTarget(descriptors []contract.StandardActionDescriptor, targetObject string) []contract.StandardActionDescriptor {
	targetObject = strings.TrimSpace(targetObject)
	if targetObject == "" {
		return descriptors
	}
	out := make([]contract.StandardActionDescriptor, 0, len(descriptors))
	for _, descriptor := range descriptors {
		if standardActionDescriptorTargetsObject(descriptor, targetObject) {
			out = append(out, descriptor)
		}
	}
	return out
}

func standardActionDescriptorTargetsObject(descriptor contract.StandardActionDescriptor, targetObject string) bool {
	for _, target := range descriptor.TargetObjects {
		if strings.EqualFold(strings.TrimSpace(target), targetObject) {
			return true
		}
	}
	return false
}

func DispatchStandardActionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	name, resp := standardActionNameParam(req)
	if resp != nil {
		return resp, nil
	}
	standardActions, resp := standardActionContract(deps, fmt.Sprintf("dispatch standard action %q", name))
	if resp != nil {
		return resp, nil
	}

	resolution, err := standardActions.ResolveStandardAction(ctx, name)
	if err != nil {
		return standardActionInternalErrorResponse(fmt.Sprintf("lookup standard action %q", name), err), nil
	}
	if resolution != nil && resolution.State != contract.StandardActionAbsent {
		if resolution.State != contract.StandardActionActive {
			return standardActionUnsupportedResponse(name), nil
		}
		action, resp := standardActionResolvedAction(resolution, fmt.Sprintf("lookup standard action %q", name))
		if resp != nil {
			return resp, nil
		}
		if outcome := standardActions.AuthorizeResolvedStandardAction(ctx, action, req.User); outcome != nil {
			return standardActionOutcomeResponse(fmt.Sprintf("authorize standard action %q", action.Name), name, outcome), nil
		}
		body, resp := decodeStandardActionInvokeRequest(req)
		if resp != nil {
			return resp, nil
		}
		timeout := standardActionDispatchTimeout()
		if timeout > 0 && len(body.Inputs) > 1 {
			return dispatchResolvedStandardActionBatchWithAggregateTimeout(ctx, action, standardActions, body.Inputs, req.User, timeout), nil
		}
		invokeCtx := ctx
		var cancel context.CancelFunc
		if timeout > 0 {
			invokeCtx, cancel = context.WithTimeout(ctx, timeout)
			defer cancel()
		}
		results, outcome := standardActions.InvokeResolvedStandardAction(invokeCtx, action, body.Inputs, req.User)
		if outcome != nil {
			return standardActionOutcomeResponse(fmt.Sprintf("execute standard action %q", action.Name), name, outcome), nil
		}
		return jsonResponse(http.StatusOK, standardActionInvocationResultsToWire(results)), nil
	}

	if !standardActionLegacyFallbackName(name) {
		return standardActionUnsupportedResponse(name), nil
	}
	body, resp := decodeLegacyStandardActionInvokeRequest(req)
	if resp != nil {
		return resp, nil
	}
	return dispatchLegacyStandardAction(ctx, req, deps, name, body.Inputs), nil
}

func PreviewStandardActionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	name, resp := standardActionNameParam(req)
	if resp != nil {
		return resp, nil
	}
	standardActions, resp := standardActionContract(deps, fmt.Sprintf("preview standard action %q", name))
	if resp != nil {
		return resp, nil
	}

	resolution, err := standardActions.ResolveStandardAction(ctx, name)
	if err != nil {
		return standardActionInternalErrorResponse(fmt.Sprintf("lookup standard action preview %q", name), err), nil
	}
	if resolution == nil || resolution.State != contract.StandardActionActive {
		return standardActionUnsupportedResponse(name), nil
	}
	action, resp := standardActionResolvedAction(resolution, fmt.Sprintf("lookup standard action preview %q", name))
	if resp != nil {
		return resp, nil
	}
	if outcome := standardActions.AuthorizeResolvedStandardAction(ctx, action, req.User); outcome != nil {
		return standardActionOutcomeResponse(fmt.Sprintf("authorize standard action %q", action.Name), name, outcome), nil
	}

	var body standardActionPreviewRequest
	if err := json.NewDecoder(bytes.NewReader(req.Body)).Decode(&body); err != nil {
		return standardActionInvalidRequestResponse(err.Error()), nil
	}
	previewResult, outcome := standardActions.PreviewResolvedRecordCreate(ctx, action, body.Input, req.User)
	if outcome != nil {
		return standardActionOutcomeResponse(fmt.Sprintf("preview standard action %q", name), name, outcome), nil
	}
	if previewResult == nil {
		return standardActionInternalErrorResponse(fmt.Sprintf("preview standard action %q", name), errors.New("standard action preview returned no result")), nil
	}
	return jsonResponse(http.StatusOK, standardActionPreviewResultFromService(*previewResult)), nil
}

func OutputPreviewStandardActionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	name, resp := standardActionNameParam(req)
	if resp != nil {
		return resp, nil
	}
	standardActions, resp := standardActionContract(deps, fmt.Sprintf("output preview standard action %q", name))
	if resp != nil {
		return resp, nil
	}

	resolution, err := standardActions.ResolveStandardAction(ctx, name)
	if err != nil {
		return standardActionInternalErrorResponse(fmt.Sprintf("lookup standard action output preview %q", name), err), nil
	}
	if resolution == nil || resolution.State != contract.StandardActionActive {
		return standardActionUnsupportedResponse(name), nil
	}
	action, resp := standardActionResolvedAction(resolution, fmt.Sprintf("lookup standard action output preview %q", name))
	if resp != nil {
		return resp, nil
	}
	if !resolution.HasOutputPreviewProducer {
		return &dialect.Response{
			Status: http.StatusNotFound,
			Body:   wire.SFErrorResponse(wire.SFCodeNotFound, fmt.Sprintf("standard action %q has no preview producer", name)),
		}, nil
	}
	if outcome := standardActions.AuthorizeResolvedStandardAction(ctx, action, req.User); outcome != nil {
		return standardActionOutcomeResponse(fmt.Sprintf("authorize standard action output preview %q", name), name, outcome), nil
	}

	var body struct {
		RecordID string `json:"recordId"`
	}
	decoder := json.NewDecoder(bytes.NewReader(req.Body))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&body); err != nil {
		return standardActionInvalidRequestResponse("Invalid JSON: " + err.Error()), nil
	}

	result, outcome := standardActions.PreviewResolvedOutputs(ctx, action, body.RecordID, req.User)
	if outcome != nil {
		return standardActionOutcomeResponse(fmt.Sprintf("output preview standard action %q", name), name, outcome), nil
	}
	if result == nil {
		return standardActionInternalErrorResponse(fmt.Sprintf("output preview standard action %q", name), errors.New("flow preview returned no result")), nil
	}
	return jsonResponse(http.StatusOK, map[string]any{
		"outputValues": result.OutputValues,
		"isSuccess":    result.IsSuccess,
		"errors":       standardActionErrorsToFlow(result.Errors),
	}), nil
}

func standardActionContract(deps dialect.Deps, operation string) (contract.StandardActionService, *dialect.Response) {
	if deps.Contracts.StandardActions != nil {
		return deps.Contracts.StandardActions, nil
	}
	return nil, standardActionInternalErrorResponse(operation, errors.New("standard action contract is not configured"))
}

// standardActionNameParam reads the action name a request addresses, refusing one the action list
// would never advertise.
//
// The refusal is what keeps the advertised set and the executable set the same. The list omits an
// action whose name cannot be spelled as one path segment, so without this check a name like
// `Case..Escalate` would be hidden from discovery and still executable — an action reachable only by
// knowing it exists, which is the opposite of what a discovery surface is for. The predicate is the
// sfcrm/wire owner's, not a local rule, so the two answers cannot drift.
//
// The answer for a refused name is the same not-found the handlers give an unknown action. It is the
// truthful one — no such action is addressable — and it tells a caller nothing it did not supply.
func standardActionNameParam(req *dialect.Request) (string, *dialect.Response) {
	if req == nil {
		return "", standardActionUnsupportedResponse("")
	}
	name := req.Params["name"]
	if !wire.IsAddressableStandardActionID(name) {
		return name, standardActionUnsupportedResponse(name)
	}
	return name, nil
}

func standardActionLegacyFallbackName(name string) bool {
	return name == "changeOwner" || name == "createRecord"
}

func decodeStandardActionInvokeRequest(req *dialect.Request) (*standardActionInvokeRequest, *dialect.Response) {
	var body standardActionInvokeRequest
	if req == nil {
		return nil, standardActionInvalidRequestResponse("EOF")
	}
	if err := json.NewDecoder(bytes.NewReader(req.Body)).Decode(&body); err != nil {
		return nil, standardActionInvalidRequestResponse(err.Error())
	}
	if len(body.Inputs) == 0 {
		return nil, standardActionInvalidRequestResponse("inputs array is required")
	}
	return &body, nil
}

func decodeLegacyStandardActionInvokeRequest(req *dialect.Request) (*standardActionInvokeRequest, *dialect.Response) {
	var body standardActionInvokeRequest
	if req == nil {
		return nil, standardActionInvalidRequestResponse("inputs array is required")
	}
	if err := json.NewDecoder(bytes.NewReader(req.Body)).Decode(&body); err != nil || len(body.Inputs) == 0 {
		return nil, standardActionInvalidRequestResponse("inputs array is required")
	}
	return &body, nil
}

func standardActionDispatchTimeout() time.Duration {
	timeout := 90 * time.Second
	if raw := os.Getenv("STANDARD_ACTION_FLOW_TIMEOUT"); raw != "" {
		if parsed, err := time.ParseDuration(raw); err == nil {
			timeout = parsed
		} else {
			log.Printf("STANDARD_ACTION_FLOW_TIMEOUT %q is not a valid duration, using default 90s: %v", raw, err)
		}
	}
	return timeout
}

func standardActionListEntryFromContract(descriptor contract.StandardActionDescriptor, apiVersion string) StandardActionListEntry {
	entry := StandardActionListEntry{
		Name:                     descriptor.Name,
		Label:                    descriptor.Label,
		Description:              descriptor.Description,
		Category:                 descriptor.Category,
		FlowDeveloperName:        descriptor.FlowName,
		IsAtomic:                 descriptor.IsAtomic,
		RequiredPerms:            descriptor.RequiredPermissions,
		TargetObjects:            descriptor.TargetObjects,
		HeaderFallbackEligible:   descriptor.HeaderFallbackEligible,
		RendererID:               descriptor.RendererID,
		InputGuards:              inputGuardSummariesFromContract(descriptor.InputGuards),
		Inputs:                   actionSchemaSummariesFromContract(descriptor.Inputs),
		Outputs:                  actionSchemaSummariesFromContract(descriptor.Outputs),
		VisibilityCondition:      descriptor.VisibilityCondition,
		PreviewFlowDeveloperName: descriptor.PreviewFlowName,
	}
	// Spelled by the sfcrm/wire owner rather than here. This used to interpolate descriptor.Name
	// into its own format string, which both duplicated the one place that owns how a standard
	// action endpoint is spelled and left the metadata-authored name unbounded: nothing stopped a
	// name from moving the published path into a different surface class. An empty answer means the
	// name is not addressable, and there is no endpoint to publish.
	entry.Endpoint = wire.StandardActionEndpoint(apiVersionOrDefault(apiVersion), descriptor.Name)
	if entry.Endpoint == "" {
		return entry
	}
	if descriptor.CreatePreviewSupported {
		entry.PreviewEndpoint = entry.Endpoint + "/preview"
	}
	return entry
}

func inputGuardSummariesFromContract(verdicts []contract.StandardActionInputGuard) []InputGuardSummary {
	summaries := make([]InputGuardSummary, 0, len(verdicts))
	for _, verdict := range verdicts {
		summaries = append(summaries, InputGuardSummary{
			GatedInput:          verdict.GatedInput,
			RequiredPermission:  verdict.RequiredPermission,
			RequiredObject:      verdict.RequiredObject,
			RequiredOperation:   verdict.RequiredOperation,
			Condition:           verdict.Condition,
			CallerHasPermission: verdict.CallerHasPermission,
		})
	}
	return summaries
}

func actionSchemaSummariesFromContract(fields []contract.StandardActionSchemaField) []ActionSchemaSummary {
	out := make([]ActionSchemaSummary, 0, len(fields))
	for _, field := range fields {
		out = append(out, ActionSchemaSummary{
			Name:                      field.Name,
			Type:                      field.Type,
			IsCollection:              field.IsCollection,
			Required:                  field.Required,
			ReferenceTo:               field.ReferenceTo,
			Label:                     field.Label,
			UISection:                 field.UISection,
			UIMode:                    field.UIMode,
			TargetObject:              field.TargetObject,
			TargetFieldAPIName:        field.TargetFieldAPIName,
			RequiredPicklistTag:       field.RequiredPicklistTag,
			MappingSourceObject:       field.MappingSourceObject,
			MappingActionType:         field.MappingActionType,
			PrefillSourceFieldAPIName: field.PrefillSourceFieldAPIName,
			TargetRequired:            field.TargetRequired,
			TargetDefaulted:           field.TargetDefaulted,
			Defaulted:                 field.Defaulted,
			DefaultValue:              field.DefaultValue,
		})
	}
	return out
}

func standardActionResolvedAction(resolution *contract.StandardActionResolution, operation string) (*contract.StandardActionRef, *dialect.Response) {
	if resolution == nil || resolution.Action == nil {
		return nil, standardActionInternalErrorResponse(operation, errors.New("active standard action resolution did not include an action reference"))
	}
	return resolution.Action, nil
}

func standardActionPreviewResultFromService(in contract.StandardActionPreviewResult) StandardActionPreviewResult {
	out := StandardActionPreviewResult{
		ActionName: in.ActionName,
		IsSuccess:  in.IsSuccess,
		Errors:     standardActionErrorsToFlow(in.Errors),
	}
	if in.ParentPreview != nil {
		out.ParentPreview = &StandardActionCreatePreviewImage{
			Object:                    in.ParentPreview.Object,
			Fields:                    standardActionRecordToSObject(in.ParentPreview.Fields),
			SiblingMaxDefaultedFields: append([]string(nil), in.ParentPreview.SiblingMaxDefaultedFields...),
		}
	}
	if len(in.ChildPreview) > 0 {
		out.ChildPreview = make([]StandardActionChildPreview, 0, len(in.ChildPreview))
		for _, child := range in.ChildPreview {
			out.ChildPreview = append(out.ChildPreview, StandardActionChildPreview{
				Object: child.Object,
				Count:  child.Count,
				Rows:   standardActionRecordsToSObjects(child.Rows),
			})
		}
	}
	return out
}

func standardActionErrorsToFlow(errs []contract.StandardActionError) []models.FlowExecutionError {
	if len(errs) == 0 {
		return nil
	}
	out := make([]models.FlowExecutionError, 0, len(errs))
	for _, err := range errs {
		out = append(out, models.FlowExecutionError{
			StatusCode: standardActionReasonToFlowStatusCode(err.Code),
			Message:    err.Message,
			Fields:     append([]string(nil), err.Fields...),
		})
	}
	return out
}

func standardActionRecordToSObject(record contract.Record) models.SObject {
	return projectRecordToV63(record, v63RecordProjectionFieldImage)
}

func standardActionRecordsToSObjects(records []contract.Record) []models.SObject {
	out := make([]models.SObject, 0, len(records))
	for _, record := range records {
		out = append(out, standardActionRecordToSObject(record))
	}
	return out
}

func dispatchResolvedStandardActionBatchWithAggregateTimeout(ctx context.Context, action *contract.StandardActionRef, standardActions contract.StandardActionService, inputs []map[string]any, user *models.UserSession, timeout time.Duration) *dialect.Response {
	batchCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	results := make([]contract.StandardActionInvocationResult, 0, len(inputs))
	for _, input := range inputs {
		if batchCtx.Err() != nil {
			results = append(results, standardActionTimeoutInvocationResult(action, nil))
			continue
		}
		inputResults, outcome := standardActions.InvokeResolvedStandardAction(batchCtx, action, []map[string]any{input}, user)
		if outcome != nil {
			if outcome.Kind == contract.StandardActionOutcomeTimeout {
				results = append(results, standardActionTimeoutInvocationResult(action, outcome))
				continue
			}
			return standardActionOutcomeResponse(fmt.Sprintf("execute standard action %q", action.Name), action.Name, outcome)
		}
		results = append(results, inputResults...)
	}
	return jsonResponse(http.StatusOK, standardActionInvocationResultsToWire(results))
}

func standardActionTimeoutInvocationResult(action *contract.StandardActionRef, outcome *contract.StandardActionOutcome) contract.StandardActionInvocationResult {
	name := ""
	if action != nil {
		name = action.Name
	}
	if outcome != nil && outcome.Name != "" {
		name = outcome.Name
	}
	message := ""
	if outcome != nil {
		message = outcome.Message
	}
	if message == "" {
		message = fmt.Sprintf("standard action %q exceeded its execution time budget", name)
	}
	return contract.StandardActionInvocationResult{
		ActionName: name,
		IsSuccess:  false,
		Errors: []contract.StandardActionError{{
			Code:    wire.SFCodeLimitExceeded,
			Message: message,
		}},
	}
}

func standardActionInvocationResultsToWire(results []contract.StandardActionInvocationResult) []map[string]any {
	out := make([]map[string]any, 0, len(results))
	for _, result := range results {
		row := map[string]any{"isSuccess": result.IsSuccess}
		if result.ActionName != "" {
			row["actionName"] = result.ActionName
		}
		if len(result.Errors) > 0 {
			row["errors"] = standardActionErrorsToFlow(result.Errors)
		} else if result.ActionName == "" {
			row["errors"] = []models.FlowExecutionError{}
		}
		if len(result.OutputValues) > 0 {
			row["outputValues"] = standardActionOutputValuesToWire(result.OutputValues)
		}
		out = append(out, row)
	}
	return out
}

func standardActionOutputValuesToWire(values map[string]any) map[string]any {
	if len(values) == 0 {
		return nil
	}
	out := make(map[string]any, len(values))
	for key, value := range values {
		out[key] = projectRecordValueToV63(value, v63RecordProjectionFieldImage)
	}
	return out
}

// Neutral reason codes emitted by the L0 engine's standard-action service. The
// engine is app-blind: it names the rejection reason in neutral snake_case and
// this dialect renders the Salesforce v63 wire spelling. Keep in lockstep with
// nexuscrm-automation/standardaction/types.go Reject* constant VALUES.
const (
	standardActionReasonInvalidRequest       = "invalid_request"
	standardActionReasonRequiredFieldMissing = "required_field_missing"
	standardActionReasonInvalidField         = "invalid_field"
	standardActionReasonInsufficientPerms    = "insufficient_permissions"
	standardActionReasonUnknownException     = "unknown_exception"
)

// standardActionReasonToSFCode maps a neutral standard-action rejection reason to
// the Salesforce v63 errorCode spelling AND the HTTP status the outcome-response
// path returns.
//
// STATUS-CODE FIDELITY (do not "improve"): the legacy dialect returned HTTP 403
// for every InvalidRequest reason EXCEPT invalid_request (which returned 400).
// This is preserved verbatim: invalid_request → 400, everything else → 403.
//
// Fail-closed: an unknown reason renders as INVALID_REQUEST with the default 400
// status (the legacy "outcome.Code == SFCodeInvalidRequest → 400" branch), and
// the drift is logged so a new engine reason that outruns this mapper is visible.
func standardActionReasonToSFCode(reason string) (sfCode string, httpStatus int) {
	switch reason {
	case standardActionReasonInvalidRequest:
		return wire.SFCodeInvalidRequest, http.StatusBadRequest
	case standardActionReasonRequiredFieldMissing:
		return wire.SFCodeRequiredFieldMissing, http.StatusForbidden
	case standardActionReasonInvalidField:
		return wire.SFCodeInvalidField, http.StatusForbidden
	case standardActionReasonInsufficientPerms:
		return wire.SFCodeInsufficientPerms, http.StatusForbidden
	case standardActionReasonUnknownException:
		return wire.SFCodeUnknownException, http.StatusForbidden
	default:
		log.Printf("standard action dispatcher: unknown neutral reason %q, failing closed to %s", reason, wire.SFCodeInvalidRequest)
		return wire.SFCodeInvalidRequest, http.StatusBadRequest
	}
}

// standardActionReasonToFlowStatusCode maps a neutral rejection reason to the SF
// errorCode string used as FlowExecutionError.StatusCode (the Flow path emits no
// HTTP status).
//
// Unlike the outcome path, the Flow path also carries ARBITRARY codes emitted by
// user-defined flows (e.g. FIELD_CUSTOM_VALIDATION_EXCEPTION or a custom code),
// passed through verbatim by the engine's standardActionErrorsFromFlow. Those
// must stay byte-identical, so only the five neutral standard-action reasons and
// the shared neutral Flow execution conditions owned by this dialect are
// translated. Collapsing every unknown to INVALID_REQUEST here would corrupt
// flow-authored error codes.
func standardActionReasonToFlowStatusCode(code string) string {
	switch code {
	case standardActionReasonInvalidRequest,
		standardActionReasonRequiredFieldMissing,
		standardActionReasonInvalidField,
		standardActionReasonInsufficientPerms,
		standardActionReasonUnknownException:
		sfCode, _ := standardActionReasonToSFCode(code)
		return sfCode
	default:
		return flowExecutionErrorCodeToV63(code)
	}
}

func standardActionOutcomeResponse(operation string, actionName string, outcome *contract.StandardActionOutcome) *dialect.Response {
	if outcome == nil {
		return nil
	}
	name := actionName
	if outcome.Name != "" {
		name = outcome.Name
	}
	switch outcome.Kind {
	case contract.StandardActionOutcomeNotRegistered, contract.StandardActionOutcomeDisabled:
		return standardActionUnsupportedResponse(actionName)
	case contract.StandardActionOutcomeNoPreviewProducer:
		message := outcome.Message
		if message == "" {
			message = fmt.Sprintf("standard action %q has no preview producer", name)
		}
		return &dialect.Response{Status: http.StatusNotFound, Body: wire.SFErrorResponse(wire.SFCodeNotFound, message)}
	case contract.StandardActionOutcomeUnauthorized:
		return &dialect.Response{Status: http.StatusForbidden, Body: wire.SFErrorResponse(wire.SFCodeInsufficientPerms, outcome.Message)}
	case contract.StandardActionOutcomePreviewUnsupported:
		// Preview-unsupported always returns HTTP 400 regardless of reason; only
		// the errorCode SPELLING is dialect-mapped from the neutral reason.
		sfCode, _ := standardActionReasonToSFCode(outcome.Code)
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(sfCode, outcome.Message)}
	case contract.StandardActionOutcomeInvalidRequest:
		// Map the neutral reason to the SF errorCode + the reason-specific HTTP
		// status (invalid_request → 400, all others → 403). See fidelity note on
		// standardActionReasonToSFCode.
		sfCode, statusCode := standardActionReasonToSFCode(outcome.Code)
		return &dialect.Response{Status: statusCode, Body: wire.SFErrorResponse(sfCode, outcome.Message)}
	case contract.StandardActionOutcomeTimeout:
		message := outcome.Message
		if message == "" {
			message = fmt.Sprintf("standard action %q exceeded its execution time budget", name)
		}
		if outcome.Cause != nil {
			standardActionLogInternalError(operation, fmt.Errorf("%s: %w", message, outcome.Cause))
		}
		return &dialect.Response{Status: http.StatusServiceUnavailable, Body: wire.SFErrorResponse(wire.SFCodeLimitExceeded, message)}
	case contract.StandardActionOutcomeInternal:
		if outcome.Cause != nil {
			return standardActionInternalErrorResponse(operation, outcome.Cause)
		}
		return standardActionInternalErrorResponse(operation, fmt.Errorf("%s", outcome.Message))
	default:
		return standardActionInternalErrorResponse(operation, fmt.Errorf("unsupported standard action outcome kind %s", outcome.Kind))
	}
}

func standardActionUnsupportedResponse(name string) *dialect.Response {
	return &dialect.Response{
		Status: http.StatusNotFound,
		Body:   wire.SFErrorResponse(wire.SFCodeNotFound, fmt.Sprintf("standard action %q not found", name)),
	}
}

func standardActionInvalidRequestResponse(message string) *dialect.Response {
	return &dialect.Response{
		Status: http.StatusBadRequest,
		Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, message),
	}
}

func standardActionInternalErrorResponse(operation string, err error) *dialect.Response {
	standardActionLogInternalError(operation, err)
	return &dialect.Response{
		Status: http.StatusInternalServerError,
		Body:   wire.SFGenericInternalErrorResponse(),
	}
}

func standardActionLogInternalError(operation string, err error) {
	if err != nil {
		log.Printf("standard action dispatcher internal error during %s: %v", operation, err)
	} else {
		log.Printf("standard action dispatcher internal error during %s", operation)
	}
}
