package standardaction

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/nexuscrm/automation"
	cruntimeports "github.com/nexuscrm/db/pkg/runtimeports"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	seedrows "github.com/pingcap-inc/nexuscrm-contract/pkg/seedrows"
)

// Service implements the standard-action neutral orchestration stages. This
// file starts with stage 1 (ResolveAction); later stages move here as the REST
// dispatcher thins into a dialect adapter.
type Service struct {
	db                     *sql.DB
	permissions            PermissionChecker
	metadata               MetadataProvider
	previewAccess          PreviewAccessChecker
	formulas               *formula.Engine
	execute                FlowRunner
	createPreview          CreatePreviewRunner
	outputPreview          OutputPreviewRunner
	createPreviewSupported CreatePreviewSupportChecker
	standardActions        cruntimeports.StandardActionReader
	listCache              *standardActionListCache
}

// Cross-request standard-action list caching stays disabled until the runtime has
// a monotonic metadata/permission generation or explicit invalidation hook. A
// lossy DB fingerprint can serve stale action visibility/schema facts.
const standardActionListCacheTTL = 0 * time.Second

type standardActionListCache struct {
	mu      sync.Mutex
	entries map[string]standardActionListCacheEntry
}

type standardActionListCacheEntry struct {
	expiresAt   time.Time
	revision    string
	descriptors []contract.StandardActionDescriptor
}

type standardActionDiscoveryBulkReader interface {
	GetActionSchemas(db *sql.DB, standardActionIDs []string) (map[string][]seedrows.ActionSchemaRow, error)
	GetActionFieldDefaultsForActions(db *sql.DB, standardActionIDs []string) (map[string][]seedrows.ActionFieldDefaultRow, error)
	GetActionInputGuardsForActions(db *sql.DB, standardActionIDs []string) (map[string][]seedrows.ActionInputGuardRow, error)
}

type standardActionDiscoveryMetadata struct {
	schemaRowsByActionID       map[string][]seedrows.ActionSchemaRow
	fieldDefaultRowsByActionID map[string][]seedrows.ActionFieldDefaultRow
	guardRowsByActionID        map[string][]seedrows.ActionInputGuardRow
}

func NewService(db *sql.DB, permissions ...PermissionChecker) *Service {
	var perms PermissionChecker
	if len(permissions) > 0 {
		perms = permissions[0]
	}
	return &Service{
		db:          db,
		permissions: perms,
		listCache:   &standardActionListCache{entries: make(map[string]standardActionListCacheEntry)},
	}
}

func (s *Service) WithRuntime(deps RuntimeDeps) *Service {
	if s == nil {
		s = NewService(nil)
	}
	cp := *s
	cp.metadata = deps.Metadata
	cp.previewAccess = deps.PreviewAccess
	cp.formulas = deps.Formulas
	cp.execute = deps.Execute
	cp.createPreview = deps.CreatePreview
	cp.outputPreview = deps.OutputPreview
	cp.createPreviewSupported = deps.CreatePreviewSupported
	cp.standardActions = deps.StandardActions
	return &cp
}

func (s *Service) formulaEngine() *formula.Engine {
	if s != nil && s.formulas != nil {
		return s.formulas
	}
	return formula.NewEngine()
}

func (s *Service) authorizeActiveAction(ctx context.Context, action *Action, user *models.UserSession) error {
	if outcome := s.AuthorizeAction(ctx, action, user); outcome != nil {
		return &Error{
			Kind:    ErrorUnauthorized,
			Code:    RejectInsufficientPerms,
			Message: outcome.Message,
		}
	}
	return nil
}

func errorFromReject(kind ErrorKind, reject *InputReject) *Error {
	if reject == nil {
		return nil
	}
	return &Error{
		Kind:     kind,
		Code:     reject.Code,
		Message:  reject.Message,
		Internal: reject.Internal,
	}
}

func flowResultFromReject(actionName string, reject *InputReject) InvocationResult {
	return InvocationResult{
		ActionName: actionName,
		IsSuccess:  false,
		Errors: []contract.StandardActionError{{
			Code:    reject.Code,
			Message: reject.Message,
		}},
	}
}

func previewResultFromReject(actionName string, reject *InputReject) *contract.StandardActionPreviewResult {
	return &contract.StandardActionPreviewResult{
		ActionName: actionName,
		IsSuccess:  false,
		Errors: []contract.StandardActionError{{
			Code:    reject.Code,
			Message: reject.Message,
		}},
	}
}

func (s *Service) ListStandardActions(ctx context.Context, user *models.UserSession) ([]contract.StandardActionDescriptor, error) {
	revision := ""
	if standardActionListCacheTTL > 0 {
		var err error
		revision, err = s.standardActionListRevision(ctx)
		if err != nil {
			return nil, err
		}
		if cached, ok := s.cachedStandardActionList(user, revision); ok {
			return cached, nil
		}
	}
	entries, err := s.ListActiveActions(ctx)
	if err != nil {
		return nil, err
	}
	out, err := s.listStandardActionDescriptors(ctx, user, entries)
	if err != nil {
		return nil, err
	}
	if standardActionListCacheTTL > 0 {
		s.storeStandardActionList(user, revision, out)
	}
	return out, nil
}

// ListStandardActionsForTarget is the object-scoped discovery path for record
// pages. It keeps permission/guard/schema evaluation live, but avoids doing
// that work for actions whose TargetObjects metadata cannot apply.
func (s *Service) ListStandardActionsForTarget(ctx context.Context, user *models.UserSession, targetObject string) ([]contract.StandardActionDescriptor, error) {
	targetObject = strings.TrimSpace(targetObject)
	if targetObject == "" {
		return s.ListStandardActions(ctx, user)
	}
	entries, err := s.ListActiveActionsForTarget(ctx, targetObject)
	if err != nil {
		return nil, err
	}
	return s.listStandardActionDescriptors(ctx, user, entries)
}

func (s *Service) listStandardActionDescriptors(ctx context.Context, user *models.UserSession, entries []ListEntry) ([]contract.StandardActionDescriptor, error) {
	engine := s.formulaEngine()
	permitted := make([]ListEntry, 0, len(entries))
	for _, entry := range entries {
		if !s.HasAllPermissions(ctx, user, entry.RequiredPermissions) {
			continue
		}
		permitted = append(permitted, entry)
	}
	discovery, err := s.loadStandardActionDiscoveryMetadata(permitted)
	if err != nil {
		return nil, err
	}

	out := make([]contract.StandardActionDescriptor, 0, len(permitted))
	for _, entry := range permitted {
		guardRows := discovery.guardRowsByActionID[entry.ID]
		visible, guardVerdicts, err := s.discoveryGuardsFromRows(ctx, user, guardRows, engine)
		if err != nil {
			return nil, fmt.Errorf("evaluate action guards for %q: %w", entry.Name, err)
		}
		if !visible {
			continue
		}
		schemaRows := discovery.schemaRowsByActionID[entry.ID]
		fieldDefaultRows := discovery.fieldDefaultRowsByActionID[entry.ID]
		schema, err := s.schemaContractFromRows(ctx, schemaRows, fieldDefaultRows, s.metadata, engine)
		if err != nil {
			return nil, fmt.Errorf("resolve action schema for %q: %w", entry.Name, err)
		}
		desc := standardActionDescriptorFromListEntry(entry)
		desc.InputGuards = inputGuardVerdictsToContract(guardVerdicts)
		desc.Inputs = schemaFieldsToContract(schema.Inputs)
		desc.Outputs = schemaFieldsToContract(schema.Outputs)
		if s.createPreviewSupported != nil && strings.TrimSpace(entry.FlowDeveloperName) != "" {
			supported, err := s.createPreviewSupported(ctx, entry.FlowDeveloperName, user)
			if err != nil {
				return nil, fmt.Errorf("resolve preview capability for %q: %w", entry.Name, err)
			}
			desc.CreatePreviewSupported = supported
		}
		out = append(out, desc)
	}
	return out, nil
}

func (s *Service) loadStandardActionDiscoveryMetadata(entries []ListEntry) (standardActionDiscoveryMetadata, error) {
	out := standardActionDiscoveryMetadata{
		schemaRowsByActionID:       map[string][]seedrows.ActionSchemaRow{},
		fieldDefaultRowsByActionID: map[string][]seedrows.ActionFieldDefaultRow{},
		guardRowsByActionID:        map[string][]seedrows.ActionInputGuardRow{},
	}
	if s == nil || s.db == nil || len(entries) == 0 {
		return out, nil
	}
	actionIDs := actionIDsFromListEntries(entries)
	if len(actionIDs) == 0 {
		return out, nil
	}
	if bulk, ok := s.standardActions.(standardActionDiscoveryBulkReader); ok {
		var err error
		out.schemaRowsByActionID, err = bulk.GetActionSchemas(s.db, actionIDs)
		if err != nil {
			return out, fmt.Errorf("bulk load standard action schema: %w", err)
		}
		out.fieldDefaultRowsByActionID, err = bulk.GetActionFieldDefaultsForActions(s.db, actionIDs)
		if err != nil {
			return out, fmt.Errorf("bulk load standard action field defaults: %w", err)
		}
		out.guardRowsByActionID, err = bulk.GetActionInputGuardsForActions(s.db, actionIDs)
		if err != nil {
			return out, fmt.Errorf("bulk load standard action input guards: %w", err)
		}
		return out, nil
	}
	for _, actionID := range actionIDs {
		schemaRows, err := s.standardActions.GetActionSchema(s.db, actionID)
		if err != nil {
			return out, err
		}
		out.schemaRowsByActionID[actionID] = schemaRows
		defaultRows, err := s.standardActions.GetActionFieldDefaults(s.db, actionID)
		if err != nil {
			return out, err
		}
		out.fieldDefaultRowsByActionID[actionID] = defaultRows
		guardRows, err := s.standardActions.GetActionInputGuards(s.db, actionID)
		if err != nil {
			return out, err
		}
		out.guardRowsByActionID[actionID] = guardRows
	}
	return out, nil
}

func actionIDsFromListEntries(entries []ListEntry) []string {
	out := make([]string, 0, len(entries))
	seen := make(map[string]struct{}, len(entries))
	for _, entry := range entries {
		id := strings.TrimSpace(entry.ID)
		if id == "" {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	return out
}

func (s *Service) cachedStandardActionList(user *models.UserSession, revision string) ([]contract.StandardActionDescriptor, bool) {
	key := standardActionListCacheKey(user)
	if standardActionListCacheTTL <= 0 || s == nil || s.listCache == nil || key == "" {
		return nil, false
	}
	now := time.Now()
	s.listCache.mu.Lock()
	defer s.listCache.mu.Unlock()
	entry, ok := s.listCache.entries[key]
	if !ok || entry.revision != revision || !now.Before(entry.expiresAt) {
		if ok {
			delete(s.listCache.entries, key)
		}
		return nil, false
	}
	return cloneStandardActionDescriptors(entry.descriptors), true
}

func (s *Service) storeStandardActionList(user *models.UserSession, revision string, descriptors []contract.StandardActionDescriptor) {
	key := standardActionListCacheKey(user)
	if standardActionListCacheTTL <= 0 || s == nil || s.listCache == nil || key == "" {
		return
	}
	s.listCache.mu.Lock()
	defer s.listCache.mu.Unlock()
	s.listCache.entries[key] = standardActionListCacheEntry{
		expiresAt:   time.Now().Add(standardActionListCacheTTL),
		revision:    revision,
		descriptors: cloneStandardActionDescriptors(descriptors),
	}
}

func (s *Service) standardActionListRevision(ctx context.Context) (string, error) {
	if s == nil || s.db == nil {
		return "", nil
	}
	row := s.db.QueryRowContext(ctx, `
SELECT CONCAT(
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_StandardAction),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_ActionInputGuard),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_ActionSchema),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_ActionFieldDefault),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_StandardActionInternalAccessScope),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM FlowDefinition),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM Flow),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_PermissionSetPermission),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM PermissionSetAssignment),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM ObjectPermissions),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM FieldPermissions),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_CustomPermission),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_Permission),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM PermissionSet),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM ProfileCustomPermission),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM PermissionSetCustomPermission),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM PermissionSetGroup),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM PermissionSetGroupAssignment),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM PermissionSetGroupComponent),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM MutingPermissionSet),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_MutingPermissionSetPermission),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_Object),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_Field),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_ObjectFieldMapping),
  '|',
  (SELECT CONCAT(COUNT(*), ':', COALESCE(CAST(MAX(LastModifiedDate) AS CHAR), '')) FROM _System_TriggerSubscription)
) AS Revision`)
	var revision sql.NullString
	if err := row.Scan(&revision); err != nil {
		return "", fmt.Errorf("standard action list cache revision: %w", err)
	}
	return revision.String, nil
}

func standardActionListCacheKey(user *models.UserSession) string {
	if user == nil || strings.TrimSpace(user.ID) == "" {
		return ""
	}
	roleID := ""
	if user.RoleID != nil {
		roleID = *user.RoleID
	}
	return strings.Join([]string{
		strings.TrimSpace(user.ID),
		strings.TrimSpace(user.ProfileID),
		strings.TrimSpace(roleID),
		fmt.Sprintf("%t", user.IsSystemAdmin),
		strings.TrimSpace(user.Scope),
		strings.TrimSpace(user.GrantType),
		strings.TrimSpace(user.SessionClass),
	}, "\x00")
}

func cloneStandardActionDescriptors(in []contract.StandardActionDescriptor) []contract.StandardActionDescriptor {
	if in == nil {
		return nil
	}
	out := make([]contract.StandardActionDescriptor, len(in))
	for i := range in {
		out[i] = in[i]
		out[i].RequiredPermissions = append([]string(nil), in[i].RequiredPermissions...)
		out[i].TargetObjects = append([]string(nil), in[i].TargetObjects...)
		out[i].InputGuards = append([]contract.StandardActionInputGuard(nil), in[i].InputGuards...)
		out[i].Inputs = append([]contract.StandardActionSchemaField(nil), in[i].Inputs...)
		out[i].Outputs = append([]contract.StandardActionSchemaField(nil), in[i].Outputs...)
	}
	return out
}

func (s *Service) ResolveStandardAction(ctx context.Context, actionName string) (*contract.StandardActionResolution, error) {
	resolution, err := s.ResolveAction(ctx, actionName)
	if err != nil {
		return nil, err
	}
	out := &contract.StandardActionResolution{}
	switch resolution.State {
	case ActionActive:
		out.State = contract.StandardActionActive
	case ActionDisabled:
		out.State = contract.StandardActionDisabled
	default:
		out.State = contract.StandardActionAbsent
	}
	if resolution.State == ActionActive && resolution.Action != nil {
		out.Action = contractRefFromAction(resolution.Action)
		out.HasOutputPreviewProducer = strings.TrimSpace(resolution.Action.PreviewFlowDeveloperName) != ""
	}
	return out, nil
}

func contractRefFromAction(action *Action) *contract.StandardActionRef {
	if action == nil {
		return nil
	}
	return &contract.StandardActionRef{
		State: contract.StandardActionActive,
		Name:  action.Name,
		Binding: &standardActionBinding{action: Action{
			ID:                       action.ID,
			Name:                     action.Name,
			FlowDeveloperName:        action.FlowDeveloperName,
			PreviewFlowDeveloperName: action.PreviewFlowDeveloperName,
			RequiredPermissions:      append([]string(nil), action.RequiredPermissions...),
			IsAtomic:                 action.IsAtomic,
			IsActive:                 action.IsActive,
			IsDeleted:                action.IsDeleted,
		}},
	}
}

func actionFromContractRef(ref *contract.StandardActionRef) (*Action, error) {
	if ref == nil || strings.TrimSpace(ref.Name) == "" {
		return nil, &Error{
			Kind:    ErrorInvalidRequest,
			Code:    RejectInvalidRequest,
			Message: "resolved standard action is required",
		}
	}
	if ref.State != contract.StandardActionActive {
		return nil, &Error{
			Kind:     ErrorDisabled,
			Message:  fmt.Sprintf("resolved standard action %q is not active", ref.Name),
			Internal: true,
		}
	}
	binding, ok := ref.Binding.(*standardActionBinding)
	if !ok || binding == nil || !binding.action.IsInvokable() {
		return nil, &Error{
			Kind:     ErrorDisabled,
			Message:  fmt.Sprintf("resolved standard action %q does not carry an active engine binding", ref.Name),
			Internal: true,
		}
	}
	if binding.action.Name != ref.Name {
		return nil, &Error{
			Kind:     ErrorDisabled,
			Message:  fmt.Sprintf("resolved standard action %q carries binding for %q", ref.Name, binding.action.Name),
			Internal: true,
		}
	}
	action := binding.action
	action.RequiredPermissions = append([]string(nil), action.RequiredPermissions...)
	return &action, nil
}

func (s *Service) AuthorizeResolvedStandardAction(ctx context.Context, ref *contract.StandardActionRef, user *models.UserSession) *contract.StandardActionOutcome {
	action, err := actionFromContractRef(ref)
	if err != nil {
		return outcomeFromError(actionRefName(ref), err)
	}
	return outcomeFromError(action.Name, s.authorizeActiveAction(ctx, action, user))
}

func (s *Service) InvokeResolvedStandardAction(ctx context.Context, ref *contract.StandardActionRef, inputs []map[string]any, user *models.UserSession) ([]InvocationResult, *contract.StandardActionOutcome) {
	action, err := actionFromContractRef(ref)
	if err != nil {
		return nil, outcomeFromError(actionRefName(ref), err)
	}
	results, err := s.invokeAuthorizedAction(ctx, action, inputs, user)
	return results, outcomeFromActionError(ctx, action, err)
}

func (s *Service) invokeAuthorizedAction(ctx context.Context, action *Action, inputs []map[string]any, user *models.UserSession) ([]InvocationResult, error) {
	if len(inputs) == 0 {
		return nil, &Error{
			Kind:    ErrorInvalidRequest,
			Code:    RejectInvalidRequest,
			Message: "inputs array is required",
		}
	}
	prepared, err := s.PrepareInvocation(ctx, action)
	if err != nil {
		return nil, fmt.Errorf("prepare standard action %q: %w", action.Name, err)
	}
	results := make([]InvocationResult, 0, len(inputs))
	for _, input := range inputs {
		if reject := s.ValidateFullInput(prepared.Context, prepared, input, user, s.metadata, s.formulaEngine()); reject != nil {
			results = append(results, flowResultFromReject(action.Name, reject))
			continue
		}
		result, err := s.ExecuteAction(prepared.Context, action, input, user, s.execute)
		if err != nil {
			return nil, fmt.Errorf("execute standard action %q: %w", action.Name, err)
		}
		results = append(results, *result)
	}
	return results, nil
}

func (s *Service) PreviewResolvedRecordCreate(ctx context.Context, ref *contract.StandardActionRef, input map[string]any, user *models.UserSession) (*contract.StandardActionPreviewResult, *contract.StandardActionOutcome) {
	action, err := actionFromContractRef(ref)
	if err != nil {
		return nil, outcomeFromError(actionRefName(ref), err)
	}
	result, err := s.previewRecordCreateAuthorized(ctx, action, input, user)
	return result, outcomeFromError(action.Name, err)
}

func (s *Service) previewRecordCreateAuthorized(ctx context.Context, action *Action, input map[string]any, user *models.UserSession) (*contract.StandardActionPreviewResult, error) {
	if input == nil {
		return nil, &Error{
			Kind:    ErrorInvalidRequest,
			Code:    RejectInvalidRequest,
			Message: "input object is required",
		}
	}
	prepared, err := s.PrepareInvocation(ctx, action)
	if err != nil {
		return nil, fmt.Errorf("prepare standard action %q: %w", action.Name, err)
	}
	if s.createPreviewSupported == nil {
		return nil, fmt.Errorf("resolve preview capability for %q: flow executor unavailable", action.Name)
	}
	supported, err := s.createPreviewSupported(prepared.Context, action.FlowDeveloperName, user)
	if err != nil {
		return nil, fmt.Errorf("resolve preview capability for %q: %w", action.Name, err)
	}
	if !supported {
		return nil, &Error{
			Kind:    ErrorPreviewUnsupported,
			Code:    RejectInvalidRequest,
			Message: "standard action preview is not supported for this action",
		}
	}
	if reject := s.ValidateFullInput(prepared.Context, prepared, input, user, s.metadata, s.formulaEngine()); reject != nil {
		return previewResultFromReject(action.Name, reject), nil
	}
	preview, err := s.PreviewCreate(prepared.Context, action, input, user, s.createPreview)
	if err != nil {
		return nil, fmt.Errorf("preview standard action %q: %w", action.Name, err)
	}
	projected, err := s.BuildCreatePreviewResult(prepared.Context, user, action, preview, s.metadata, s.previewAccess)
	if err != nil {
		return nil, err
	}
	return &projected, nil
}

func (s *Service) PreviewResolvedOutputs(ctx context.Context, ref *contract.StandardActionRef, recordID string, user *models.UserSession) (*contract.StandardActionOutputPreview, *contract.StandardActionOutcome) {
	action, err := actionFromContractRef(ref)
	if err != nil {
		return nil, outcomeFromError(actionRefName(ref), err)
	}
	if action.PreviewFlowDeveloperName == "" {
		return nil, outcomeFromError(action.Name, &Error{
			Kind:    ErrorNoPreviewProducer,
			Message: fmt.Sprintf("standard action %q has no preview producer", action.Name),
		})
	}
	result, err := s.previewOutputsAuthorized(ctx, action, recordID, user)
	return result, outcomeFromError(action.Name, err)
}

func (s *Service) previewOutputsAuthorized(ctx context.Context, action *Action, recordID string, user *models.UserSession) (*contract.StandardActionOutputPreview, error) {
	outputInput, reject, err := s.ValidateOutputPreviewInput(ctx, action, recordID, user, s.formulaEngine())
	if err != nil {
		return nil, fmt.Errorf("prepare output preview input for %q: %w", action.Name, err)
	}
	if reject != nil {
		return nil, errorFromReject(ErrorInvalidRequest, reject)
	}
	result, err := s.PreviewOutputsScalar(ctx, action, outputInput, user, s.outputPreview)
	if err != nil {
		return nil, fmt.Errorf("output preview standard action %q: %w", action.Name, err)
	}
	return result, nil
}

func standardActionDescriptorFromListEntry(entry ListEntry) contract.StandardActionDescriptor {
	return contract.StandardActionDescriptor{
		Name:                   entry.Name,
		Label:                  entry.Label,
		Description:            entry.Description,
		Category:               entry.Category,
		FlowName:               entry.FlowDeveloperName,
		IsAtomic:               entry.IsAtomic,
		RequiredPermissions:    entry.RequiredPermissions,
		TargetObjects:          entry.TargetObjects,
		RendererID:             entry.RendererID,
		VisibilityCondition:    entry.VisibilityCondition,
		PreviewFlowName:        entry.PreviewFlowDeveloperName,
		HeaderFallbackEligible: entry.HeaderFallbackEligible,
		CreatePreviewSupported: false,
		InputGuards:            []contract.StandardActionInputGuard{},
		Inputs:                 []contract.StandardActionSchemaField{},
		Outputs:                []contract.StandardActionSchemaField{},
	}
}

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

func schemaFieldsToContract(fields []SchemaField) []contract.StandardActionSchemaField {
	out := make([]contract.StandardActionSchemaField, 0, len(fields))
	for _, field := range fields {
		out = append(out, contract.StandardActionSchemaField{
			Name:                      field.Name,
			Type:                      field.Type,
			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,
			IsCollection:              field.IsCollection,
			PrefillSourceFieldAPIName: field.PrefillSourceFieldAPIName,
			TargetRequired:            field.TargetRequired,
			TargetDefaulted:           field.TargetDefaulted,
			Defaulted:                 field.Defaulted,
			HasDefaultValue:           field.HasDefaultValue,
			DefaultValue:              field.DefaultValue,
		})
	}
	return out
}

// ResolveAction returns the three-state registry resolution required before
// any dialect decides whether a legacy fallback applies.
func (s *Service) ResolveAction(ctx context.Context, name string) (ActionResolution, error) {
	if s == nil || s.db == nil {
		return ActionResolution{State: ActionAbsent}, nil
	}
	row := s.db.QueryRowContext(ctx,
		`SELECT Id, Name, FlowDeveloperName, PreviewFlowDeveloperName, RequiredPermissions, COALESCE(IsAtomic, 0),
		        COALESCE(IsActive, 0), COALESCE(IsDeleted, 0)
		   FROM _System_StandardAction
		  WHERE Name = ?
		  LIMIT 1`,
		name,
	)
	var action Action
	var reqPermsJSON sql.NullString
	var previewFlowName sql.NullString
	var isAtomicRaw, isActiveRaw, isDeletedRaw interface{}
	if err := row.Scan(&action.ID, &action.Name, &action.FlowDeveloperName, &previewFlowName, &reqPermsJSON, &isAtomicRaw, &isActiveRaw, &isDeletedRaw); err != nil {
		if err == sql.ErrNoRows {
			return ActionResolution{State: ActionAbsent}, nil
		}
		return ActionResolution{}, err
	}
	action.IsAtomic = sqlBool(isAtomicRaw)
	action.IsActive = sqlBool(isActiveRaw)
	action.IsDeleted = sqlBool(isDeletedRaw)
	if previewFlowName.Valid {
		action.PreviewFlowDeveloperName = strings.TrimSpace(previewFlowName.String)
	}
	if !action.IsInvokable() {
		return ActionResolution{State: ActionDisabled, Action: &action}, nil
	}
	if reqPermsJSON.Valid && reqPermsJSON.String != "" {
		if err := json.Unmarshal([]byte(reqPermsJSON.String), &action.RequiredPermissions); err != nil {
			return ActionResolution{}, fmt.Errorf("standard action %q is misconfigured: RequiredPermissions is not valid JSON: %w",
				name, err)
		}
	}
	return ActionResolution{State: ActionActive, Action: &action}, nil
}

func (s *Service) ListActiveActions(ctx context.Context) ([]ListEntry, error) {
	return s.listActiveActions(ctx, "")
}

func (s *Service) ListActiveActionsForTarget(ctx context.Context, targetObject string) ([]ListEntry, error) {
	targetObject = strings.TrimSpace(targetObject)
	if targetObject == "" {
		return s.ListActiveActions(ctx)
	}
	return s.listActiveActions(ctx, targetObject)
}

func (s *Service) listActiveActions(ctx context.Context, targetObject string) ([]ListEntry, error) {
	if s == nil || s.db == nil {
		return nil, nil
	}
	query := `SELECT Id, Name, Label, Description, Category, FlowDeveloperName,
			        IsAtomic, RequiredPermissions, TargetObjects, RendererId, VisibilityCondition, PreviewFlowDeveloperName,
			        COALESCE(HeaderFallbackEligible, 1)
			   FROM _System_StandardAction
		  WHERE IsActive = 1 AND IsDeleted = 0`
	args := []any{}
	if targetObject != "" {
		query += ` AND TargetObjects LIKE ?`
		args = append(args, fmt.Sprintf("%%\"%s\"%%", targetObject))
	}
	query += ` ORDER BY SortOrder ASC, Name ASC`

	rows, err := s.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("list standard actions: %w", err)
	}
	defer rows.Close()

	var out []ListEntry
	for rows.Next() {
		var (
			id, name, label, flowName string
			description               sql.NullString
			category                  sql.NullString
			isAtomic                  bool
			reqPermsJSON              sql.NullString
			targetObjectsJSON         sql.NullString
			rendererID                sql.NullString
			visibilityCondition       sql.NullString
			previewFlowDeveloperName  sql.NullString
			headerFallbackEligibleRaw interface{}
		)
		if err := rows.Scan(&id, &name, &label, &description, &category, &flowName, &isAtomic, &reqPermsJSON, &targetObjectsJSON, &rendererID, &visibilityCondition, &previewFlowDeveloperName, &headerFallbackEligibleRaw); err != nil {
			return nil, fmt.Errorf("scan standard action row: %w", err)
		}
		headerFallbackEligible := true
		if headerFallbackEligibleRaw != nil {
			headerFallbackEligible = sqlBool(headerFallbackEligibleRaw)
		}
		entry := ListEntry{
			ID:                       id,
			Name:                     name,
			Label:                    label,
			Description:              description.String,
			Category:                 category.String,
			FlowDeveloperName:        flowName,
			IsAtomic:                 isAtomic,
			TargetObjects:            []string{},
			RendererID:               strings.TrimSpace(rendererID.String),
			VisibilityCondition:      visibilityCondition.String,
			PreviewFlowDeveloperName: previewFlowDeveloperName.String,
			HeaderFallbackEligible:   headerFallbackEligible,
		}
		if reqPermsJSON.Valid && reqPermsJSON.String != "" {
			if err := json.Unmarshal([]byte(reqPermsJSON.String), &entry.RequiredPermissions); err != nil {
				return nil, fmt.Errorf("parse RequiredPermissions for %q: %w", name, err)
			}
		}
		if targetObjectsJSON.Valid && targetObjectsJSON.String != "" {
			if err := json.Unmarshal([]byte(targetObjectsJSON.String), &entry.TargetObjects); err != nil {
				return nil, fmt.Errorf("parse TargetObjects for %q: %w", name, err)
			}
		}
		if targetObject != "" && !standardActionTargetsObject(entry.TargetObjects, targetObject) {
			continue
		}
		out = append(out, entry)
	}
	return out, rows.Err()
}

func standardActionTargetsObject(targets []string, targetObject string) bool {
	targetObject = strings.TrimSpace(targetObject)
	if targetObject == "" {
		return false
	}
	for _, target := range targets {
		if strings.EqualFold(strings.TrimSpace(target), targetObject) {
			return true
		}
	}
	return false
}

// AuthorizeAction enforces action-level RequiredPermissions. It returns a
// neutral request outcome; dialects map that to their wire status/envelope.
func (s *Service) AuthorizeAction(ctx context.Context, action *Action, user *models.UserSession) *RequestOutcome {
	if action == nil {
		return &RequestOutcome{
			Kind:    RequestOutcomeUnauthorized,
			Message: "permission denied: standard action is required",
		}
	}
	if len(action.RequiredPermissions) == 0 {
		return &RequestOutcome{
			Kind:    RequestOutcomeUnauthorized,
			Message: fmt.Sprintf("standard action %q is misconfigured: _System_StandardAction row must declare at least one RequiredPermission", action.Name),
		}
	}
	for _, perm := range action.RequiredPermissions {
		if s == nil || s.permissions == nil || !s.permissions.HasPermission(ctx, user, perm) {
			return &RequestOutcome{
				Kind:    RequestOutcomeUnauthorized,
				Message: fmt.Sprintf("permission denied: %q is required", perm),
			}
		}
	}
	return nil
}

// HasAllPermissions is the discovery-side equivalent of AuthorizeAction's
// fail-closed RequiredPermissions check.
func (s *Service) HasAllPermissions(ctx context.Context, user *models.UserSession, permissions []string) bool {
	if len(permissions) == 0 {
		return false
	}
	if s == nil || s.permissions == nil {
		return false
	}
	for _, permission := range permissions {
		if !s.permissions.HasPermission(ctx, user, permission) {
			return false
		}
	}
	return true
}

func (s *Service) ActionVisibleByGuards(ctx context.Context, user *models.UserSession, guards []seedrows.ActionInputGuardRow, engine *formula.Engine) (bool, error) {
	if len(guards) == 0 {
		return true, nil
	}
	if engine == nil {
		engine = formula.NewEngine()
	}
	fctx := &formula.Context{Variables: map[string]interface{}{"Input": map[string]interface{}{}}}
	for _, guard := range guards {
		if strings.TrimSpace(guard.GatedInput) != "" {
			continue
		}
		if guardConditionDependsOnInput(guard.Condition) {
			continue
		}
		raw, err := engine.Evaluate(guard.Condition, fctx)
		if err != nil {
			return false, fmt.Errorf("input guard condition %q is invalid: %w", guard.Condition, err)
		}
		if !automation.IsFormulaTruthy(raw) {
			continue
		}
		allowed, err := s.callerPassesGuard(ctx, user, guard)
		if err != nil {
			return false, err
		}
		if !allowed {
			return false, nil
		}
	}
	return true, nil
}

func (s *Service) InputGuardVerdicts(ctx context.Context, user *models.UserSession, guards []seedrows.ActionInputGuardRow) ([]InputGuardVerdict, error) {
	out := make([]InputGuardVerdict, 0, len(guards))
	for _, guard := range guards {
		allowed, err := s.callerPassesGuard(ctx, user, guard)
		if err != nil {
			return nil, err
		}
		out = append(out, InputGuardVerdict{
			GatedInput:          guard.GatedInput,
			RequiredPermission:  guard.RequiredPermission,
			RequiredObject:      guard.RequiredObject,
			RequiredOperation:   guard.RequiredOperation,
			Condition:           guard.Condition,
			CallerHasPermission: allowed,
		})
	}
	return out, nil
}

func (s *Service) DiscoveryGuards(ctx context.Context, actionID string, user *models.UserSession, engine *formula.Engine) (bool, []InputGuardVerdict, error) {
	guards, err := s.loadGuards(actionID)
	if err != nil {
		return false, nil, err
	}
	return s.discoveryGuardsFromRows(ctx, user, guards, engine)
}

func (s *Service) discoveryGuardsFromRows(ctx context.Context, user *models.UserSession, guards []seedrows.ActionInputGuardRow, engine *formula.Engine) (bool, []InputGuardVerdict, error) {
	visible, err := s.ActionVisibleByGuards(ctx, user, guards, engine)
	if err != nil {
		return false, nil, err
	}
	if !visible {
		return false, nil, nil
	}
	verdicts, err := s.InputGuardVerdicts(ctx, user, guards)
	if err != nil {
		return false, nil, err
	}
	return true, verdicts, nil
}

func (s *Service) SchemaContract(ctx context.Context, actionID string, metadata MetadataProvider, engine *formula.Engine) (SchemaContract, error) {
	if s == nil || s.db == nil || actionID == "" {
		return SchemaContract{Inputs: []SchemaField{}, Outputs: []SchemaField{}}, nil
	}
	rows, err := s.standardActions.GetActionSchema(s.db, actionID)
	if err != nil {
		return SchemaContract{Inputs: []SchemaField{}, Outputs: []SchemaField{}}, err
	}
	fieldDefaults, err := s.loadFieldDefaults(actionID)
	if err != nil {
		return SchemaContract{Inputs: []SchemaField{}, Outputs: []SchemaField{}}, err
	}
	return s.schemaContractFromRows(ctx, rows, fieldDefaults, metadata, engine)
}

func (s *Service) schemaContractFromRows(ctx context.Context, rows []seedrows.ActionSchemaRow, fieldDefaults []seedrows.ActionFieldDefaultRow, metadata MetadataProvider, engine *formula.Engine) (SchemaContract, error) {
	contract := SchemaContract{
		Inputs:  []SchemaField{},
		Outputs: []SchemaField{},
	}
	inputDefaults, err := actionInputDefaultSummariesFromRows(fieldDefaults, engine)
	if err != nil {
		return contract, err
	}
	for _, row := range rows {
		field := SchemaField{
			Name:                      row.APIName,
			Type:                      row.DataType,
			Required:                  row.IsRequired,
			ReferenceTo:               row.ReferenceTo,
			Label:                     row.Label,
			UISection:                 row.UISection,
			UIMode:                    row.UIMode,
			TargetObject:              row.TargetObject,
			TargetFieldAPIName:        row.TargetFieldAPIName,
			PrefillSourceFieldAPIName: row.PrefillSourceFieldAPIName,
			RequiredPicklistTag:       row.RequiredPicklistTag,
			MappingSourceObject:       row.MappingSourceObject,
			MappingActionType:         row.MappingActionType,
			IsCollection:              row.IsCollection,
		}
		annotateTargetFieldMetadata(ctx, metadata, &field)
		if field.PrefillSourceFieldAPIName == "" {
			sourceField, err := s.prefillSourceField(row)
			if err != nil {
				return contract, err
			}
			if sourceField != "" {
				field.PrefillSourceFieldAPIName = sourceField
			}
		}
		if row.Direction == "output" {
			contract.Outputs = append(contract.Outputs, field)
			continue
		}
		if def, ok := inputDefaults[row.APIName]; ok {
			field.Defaulted = def.defaulted
			field.HasDefaultValue = def.hasDefaultValue
			if def.hasDefaultValue {
				field.DefaultValue = def.defaultValue
			}
		}
		contract.Inputs = append(contract.Inputs, field)
	}
	return contract, nil
}

// PrepareInvocation loads the FULL invocation metadata bundle and stamps
// object-scoped defaults onto the execution context. The returned FieldDefaults
// still include $Input rows for the later per-input validation stage.
func (s *Service) PrepareInvocation(ctx context.Context, action *Action) (PreparedInvocation, error) {
	prepared := PreparedInvocation{
		Action:  action,
		Context: ctx,
	}
	if action == nil {
		return prepared, nil
	}

	schema, err := s.loadInputSchema(action.ID)
	if err != nil {
		return prepared, fmt.Errorf("load input schema for %q: %w", action.Name, err)
	}
	prepared.Schema = schema

	fieldDefaults, err := s.loadFieldDefaults(action.ID)
	if err != nil {
		return prepared, fmt.Errorf("load field defaults for %q: %w", action.Name, err)
	}
	prepared.FieldDefaults = fieldDefaults
	recordDefaults := recordScopedActionDefaults(fieldDefaults)
	if len(recordDefaults) > 0 {
		prepared.Context = automation.WithActionFieldDefaults(prepared.Context, recordDefaults)
	}

	guards, err := s.loadGuards(action.ID)
	if err != nil {
		return prepared, fmt.Errorf("load input guards for %q: %w", action.Name, err)
	}
	prepared.Guards = guards
	return prepared, nil
}

// ValidateFullInput applies the FULL per-input pipeline:
// defaults -> required/unknown shape -> picklist tags -> guards.
func (s *Service) ValidateFullInput(ctx context.Context, prepared PreparedInvocation, inputs map[string]interface{}, user *models.UserSession, metadata MetadataProvider, engine *formula.Engine) *InputReject {
	if reject := s.applyInputDefaults(ctx, prepared.FieldDefaults, inputs, user, engine); reject != nil {
		return reject
	}
	if reject := validateInputShape(prepared.Schema, inputs); reject != nil {
		return reject
	}
	if reject := s.validateInputPicklistTags(ctx, prepared.Schema, inputs, metadata); reject != nil {
		return reject
	}
	if reject := s.evaluateGuards(ctx, user, prepared.Guards, inputs, engine); reject != nil {
		return reject
	}
	return nil
}

// ExecutionContext stamps the standard-action root-flow context used by Flow
// authorization. Invoke and create-preview include internal scopes; output
// preview deliberately does not.
func (s *Service) ExecutionContext(ctx context.Context, action *Action, rootFlowName string, includeInternalScopes bool) (context.Context, error) {
	if action == nil {
		return ctx, nil
	}
	execCtx := constants.WithStandardActionExecutionRootFlow(ctx, action.ID, rootFlowName)
	if !includeInternalScopes {
		return execCtx, nil
	}
	internalScopes, err := s.loadInternalAccessScopes(action.ID)
	if err != nil {
		return nil, err
	}
	if len(internalScopes) > 0 {
		execCtx = constants.WithStandardActionInternalAccessScopes(execCtx, action.ID, internalScopes)
	}
	return execCtx, nil
}

// ValidateOutputPreviewInput applies the NARROW output-preview input pipeline:
// load schema -> derive one record-context input -> load relevant guards.
func (s *Service) ValidateOutputPreviewInput(ctx context.Context, action *Action, recordID string, user *models.UserSession, engine *formula.Engine) (OutputPreviewInput, *InputReject, error) {
	var out OutputPreviewInput
	if recordID == "" {
		return out, &InputReject{
			Code:    RejectInvalidRequest,
			Message: "recordId is required",
		}, nil
	}
	if action == nil {
		return out, &InputReject{
			Code:    RejectInvalidRequest,
			Message: "standard action is required",
		}, nil
	}
	schema, err := s.loadInputSchema(action.ID)
	if err != nil {
		return out, nil, fmt.Errorf("load input schema for %q: %w", action.Name, err)
	}
	recordCtxInput, err := deriveOutputPreviewRecordContextInput(schema, action.Name)
	if err != nil {
		return out, &InputReject{
			Code:    RejectInvalidRequest,
			Message: err.Error(),
		}, nil
	}
	out.RecordContextInput = recordCtxInput
	out.FlowInputs = map[string]interface{}{recordCtxInput: recordID}

	guards, err := s.loadGuards(action.ID)
	if err != nil {
		return out, nil, fmt.Errorf("load input guards for %q: %w", action.Name, err)
	}
	relevantGuards := filterOutputPreviewGuards(guards, recordCtxInput)
	if reject := s.evaluateGuards(ctx, user, relevantGuards, out.FlowInputs, engine); reject != nil {
		if reject.Code == RejectInvalidRequest {
			reject = &InputReject{
				Code:     reject.Code,
				Message:  reject.Message,
				Internal: true,
			}
		}
		return out, reject, nil
	}
	return out, nil, nil
}

func (s *Service) ExecuteAction(ctx context.Context, action *Action, inputs map[string]interface{}, user *models.UserSession, run FlowRunner) (*InvocationResult, error) {
	if action == nil {
		return nil, fmt.Errorf("standard action is required")
	}
	if run == nil {
		return nil, fmt.Errorf("flow executor unavailable")
	}
	execCtx, err := s.ExecutionContext(ctx, action, action.FlowDeveloperName, true)
	if err != nil {
		return nil, err
	}
	execCtx = automation.WithTransientFlowInterviews(execCtx)
	result, err := run(execCtx, action.FlowDeveloperName, inputs, user)
	if err != nil {
		return nil, err
	}
	if result == nil {
		return &InvocationResult{
			ActionName: action.Name,
			IsSuccess:  false,
			Errors: []contract.StandardActionError{{
				Code:    RejectUnknownException,
				Message: "Flow execution returned no result",
			}},
		}, nil
	}
	return standardActionInvocationResultFromFlow(action.Name, result), nil
}

func standardActionInvocationResultFromFlow(actionName string, result *contract.FlowExecutionResult) *InvocationResult {
	if result == nil {
		return nil
	}
	return &InvocationResult{
		ActionName:   actionName,
		IsSuccess:    result.IsSuccess,
		Errors:       standardActionErrorsFromContractFlow(result.Errors),
		OutputValues: copyStandardActionOutputValues(result.OutputValues),
	}
}

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

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

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

func (s *Service) PreviewCreate(ctx context.Context, action *Action, inputs map[string]interface{}, user *models.UserSession, run CreatePreviewRunner) (*automation.FlowRecordCreatePreviewResult, error) {
	if action == nil {
		return nil, fmt.Errorf("standard action is required")
	}
	if run == nil {
		return nil, fmt.Errorf("flow executor unavailable")
	}
	execCtx, err := s.ExecutionContext(ctx, action, action.FlowDeveloperName, true)
	if err != nil {
		return nil, err
	}
	result, err := run(execCtx, action.FlowDeveloperName, inputs, user)
	if err != nil {
		return nil, err
	}
	if result == nil {
		return &automation.FlowRecordCreatePreviewResult{
			IsSuccess: false,
			Errors: []models.FlowExecutionError{{
				StatusCode: RejectUnknownException,
				Message:    "Flow preview returned no result",
			}},
		}, nil
	}
	return result, nil
}

func (s *Service) BuildCreatePreviewResult(ctx context.Context, user *models.UserSession, action *Action, result *automation.FlowRecordCreatePreviewResult, metadata MetadataProvider, access PreviewAccessChecker) (CreatePreviewResult, error) {
	actionName := ""
	if action != nil {
		actionName = action.Name
	}
	if result == nil {
		return CreatePreviewResult{
			ActionName: actionName,
			IsSuccess:  false,
			Errors: []contract.StandardActionError{{
				Code:    RejectUnknownException,
				Message: "Flow preview returned no result",
			}},
		}, nil
	}
	out := CreatePreviewResult{
		ActionName: actionName,
		IsSuccess:  result.IsSuccess,
		Errors:     standardActionErrorsFromFlow(result.Errors),
	}
	if !result.IsSuccess {
		return out, nil
	}

	parentIndex := SelectParentPreviewIndex(result.Creates)
	if parentIndex < 0 {
		return out, nil
	}

	parent := result.Creates[parentIndex]
	if canReadPreviewObject(ctx, user, access, parent.Object) {
		fields, err := projectPreviewFieldsForRead(ctx, user, metadata, access, parent.Object, parent.Fields)
		if err != nil {
			return CreatePreviewResult{}, err
		}
		out.ParentPreview = &CreatePreviewImage{
			Object:                    parent.Object,
			Fields:                    fields,
			SiblingMaxDefaultedFields: siblingMaxDefaultedPreviewFields(parent.SiblingMaxDefaultedFields, fields),
		}
	}
	childPreview, err := groupChildPreviews(ctx, user, metadata, access, result.Creates, parentIndex)
	if err != nil {
		return CreatePreviewResult{}, err
	}
	out.ChildPreview = childPreview
	return out, nil
}

func (s *Service) PreviewOutputsScalar(ctx context.Context, action *Action, input OutputPreviewInput, user *models.UserSession, run OutputPreviewRunner) (*OutputPreviewResult, error) {
	if action == nil {
		return nil, fmt.Errorf("standard action is required")
	}
	if run == nil {
		return nil, fmt.Errorf("flow executor unavailable")
	}
	execCtx, err := s.ExecutionContext(ctx, action, action.PreviewFlowDeveloperName, false)
	if err != nil {
		return nil, err
	}
	result, err := run(execCtx, action.PreviewFlowDeveloperName, input.FlowInputs, user)
	if err != nil {
		return nil, err
	}
	if result == nil {
		return nil, fmt.Errorf("flow preview returned no result")
	}
	return &OutputPreviewResult{
		OutputValues: FilterScalarOutputValues(result.OutputValues),
		IsSuccess:    result.IsSuccess,
		Errors:       standardActionErrorsFromFlow(result.Errors),
	}, nil
}

func (s *Service) applyInputDefaults(ctx context.Context, rows []seedrows.ActionFieldDefaultRow, inputs map[string]interface{}, user *models.UserSession, engine *formula.Engine) *InputReject {
	if len(rows) == 0 || inputs == nil {
		return nil
	}
	if engine == nil {
		engine = formula.NewEngine()
	}
	fctx := actionInputFormulaContext(inputs, user)
	isSuperUser := s.hasSuperUserCapability(ctx, user)
	claimed := make(map[string]struct{})
	for _, row := range rows {
		if row.TargetObject != InputDefaultTarget {
			continue
		}
		if _, already := claimed[row.FieldAPIName]; already {
			continue
		}
		if val, present := inputs[row.FieldAPIName]; present && !isEmptyInputValue(val) {
			claimed[row.FieldAPIName] = struct{}{}
			continue
		}
		if row.Condition != "" {
			formula.InjectSuperUserPermissions(fctx.Variables, row.Condition, isSuperUser)
			raw, err := engine.Evaluate(row.Condition, fctx)
			if err != nil {
				return &InputReject{
					Code:           RejectInvalidRequest,
					Message:        fmt.Sprintf("action input default condition %q for %s is invalid: %s", row.Condition, row.FieldAPIName, err.Error()),
					diagnosticKind: diagnosticInputDefaultCondition,
				}
			}
			if !automation.IsFormulaTruthy(raw) {
				continue
			}
		}
		formula.InjectSuperUserPermissions(fctx.Variables, row.ValueFormula, isSuperUser)
		value, err := engine.Evaluate(row.ValueFormula, fctx)
		if err != nil {
			return &InputReject{
				Code:           RejectInvalidRequest,
				Message:        fmt.Sprintf("action input default formula %q for %s is invalid: %s", row.ValueFormula, row.FieldAPIName, err.Error()),
				diagnosticKind: diagnosticInputDefaultFormula,
			}
		}
		inputs[row.FieldAPIName] = value
		claimed[row.FieldAPIName] = struct{}{}
	}
	return nil
}

func (s *Service) hasSuperUserCapability(ctx context.Context, user *models.UserSession) bool {
	if s == nil || s.permissions == nil || user == nil {
		return false
	}
	type checker interface {
		HasSuperUserCapability(context.Context, *models.UserSession) bool
	}
	if c, ok := s.permissions.(checker); ok {
		return c.HasSuperUserCapability(ctx, user)
	}
	return false
}

func actionInputDefaultSummariesFromRows(rows []seedrows.ActionFieldDefaultRow, engine *formula.Engine) (map[string]inputDefaultSummary, error) {
	if len(rows) == 0 {
		return nil, nil
	}
	if engine == nil {
		engine = formula.NewEngine()
	}
	fctx := &formula.Context{Variables: map[string]interface{}{"Input": map[string]interface{}{}}}
	out := make(map[string]inputDefaultSummary)
	for _, row := range rows {
		if row.TargetObject != InputDefaultTarget {
			continue
		}
		if row.Condition != "" {
			continue
		}
		summary := out[row.FieldAPIName]
		summary.defaulted = true
		if !summary.hasDefaultValue {
			if value, evalErr := engine.Evaluate(row.ValueFormula, fctx); evalErr == nil {
				summary.defaultValue = value
				summary.hasDefaultValue = true
			}
		}
		out[row.FieldAPIName] = summary
	}
	return out, nil
}

func annotateTargetFieldMetadata(ctx context.Context, metadata MetadataProvider, field *SchemaField) {
	if metadata == nil || field.TargetObject == "" || field.TargetFieldAPIName == "" {
		if field.Label == "" {
			field.Label = field.Name
		}
		return
	}
	schema := metadata.GetSchema(ctx, field.TargetObject)
	targetField := models.FindField(schema, field.TargetFieldAPIName)
	if targetField == nil {
		if field.Label == "" {
			field.Label = field.Name
		}
		return
	}
	if targetField.Label != "" {
		field.Label = targetField.Label
	}
	field.TargetRequired = targetField.Required
	if targetField.DefaultValue != nil && strings.TrimSpace(*targetField.DefaultValue) != "" {
		field.TargetDefaulted = true
	}
	if field.Label == "" {
		field.Label = field.Name
	}
}

func (s *Service) prefillSourceField(row seedrows.ActionSchemaRow) (string, error) {
	if s == nil || s.db == nil ||
		row.MappingSourceObject == "" ||
		row.TargetObject == "" ||
		row.TargetFieldAPIName == "" ||
		row.MappingActionType == "" {
		return "", nil
	}
	return s.standardActions.GetFieldMappingSourceForTarget(
		s.db,
		row.MappingSourceObject,
		row.TargetObject,
		row.MappingActionType,
		row.TargetFieldAPIName,
	)
}

func actionInputFormulaContext(inputs map[string]interface{}, user *models.UserSession) *formula.Context {
	variables := map[string]interface{}{
		"Input": inputs,
	}
	for key, value := range formula.BuildFormulaUserContext(user) {
		variables[key] = value
	}
	return &formula.Context{
		Variables:   variables,
		UserSession: user,
	}
}

func validateInputShape(schema []seedrows.ActionSchemaRow, inputs map[string]interface{}) *InputReject {
	if len(schema) == 0 {
		return nil
	}
	if reject := validateRequiredFields(schema, inputs); reject != nil {
		return reject
	}
	return validateInputUnknownFields(schema, inputs)
}

func validateRequiredFields(schema []seedrows.ActionSchemaRow, inputs map[string]interface{}) *InputReject {
	for _, row := range schema {
		if !row.IsRequired {
			continue
		}
		val, present := inputs[row.APIName]
		if !present || isEmptyInputValue(val) {
			return &InputReject{
				Code:           RejectRequiredFieldMissing,
				Message:        fmt.Sprintf("required field %q is missing", row.APIName),
				diagnosticKind: diagnosticRequiredInput,
			}
		}
	}
	return nil
}

func validateInputUnknownFields(schema []seedrows.ActionSchemaRow, inputs map[string]interface{}) *InputReject {
	if len(schema) == 0 {
		return nil
	}
	declared := make(map[string]seedrows.ActionSchemaRow, len(schema))
	for _, row := range schema {
		declared[row.APIName] = row
	}
	for name := range inputs {
		if _, ok := declared[name]; !ok {
			return &InputReject{
				Code:           RejectInvalidField,
				Message:        fmt.Sprintf("unknown input field %q", name),
				diagnosticKind: diagnosticUnknownInput,
			}
		}
	}
	return nil
}

func (s *Service) validateInputPicklistTags(ctx context.Context, schema []seedrows.ActionSchemaRow, inputs map[string]interface{}, metadata MetadataProvider) *InputReject {
	for _, row := range schema {
		tag := strings.TrimSpace(row.RequiredPicklistTag)
		if tag == "" {
			continue
		}
		val, present := inputs[row.APIName]
		if !present || isEmptyInputValue(val) {
			continue
		}
		if metadata == nil {
			return &InputReject{
				Code:           RejectInvalidField,
				Message:        fmt.Sprintf("input %q requires target picklist metadata", row.APIName),
				diagnosticKind: diagnosticPicklistMetadata,
			}
		}
		targetSchema := metadata.GetSchema(ctx, row.TargetObject)
		field := models.FindField(targetSchema, row.TargetFieldAPIName)
		if field == nil {
			return &InputReject{
				Code:           RejectInvalidField,
				Message:        fmt.Sprintf("input %q target field %s.%s is not available", row.APIName, row.TargetObject, row.TargetFieldAPIName),
				diagnosticKind: diagnosticPicklistTargetField,
			}
		}
		if !automation.PicklistOptionHasTag(field, val, tag) {
			return &InputReject{
				Code:           RejectInvalidField,
				Message:        fmt.Sprintf("input %q value %q is not an active option tagged %q on %s.%s", row.APIName, fmt.Sprintf("%v", val), tag, row.TargetObject, row.TargetFieldAPIName),
				diagnosticKind: diagnosticPicklistTag,
			}
		}
	}
	return nil
}

func (s *Service) evaluateGuards(ctx context.Context, user *models.UserSession, guards []seedrows.ActionInputGuardRow, inputs map[string]interface{}, engine *formula.Engine) *InputReject {
	if len(guards) == 0 {
		return nil
	}
	if engine == nil {
		engine = formula.NewEngine()
	}
	fctx := &formula.Context{Variables: map[string]interface{}{"Input": inputs}}
	for _, guard := range guards {
		raw, err := engine.Evaluate(guard.Condition, fctx)
		if err != nil {
			return &InputReject{
				Code:           RejectInvalidRequest,
				Message:        fmt.Sprintf("input guard condition %q is invalid: %s", guard.Condition, err.Error()),
				diagnosticKind: diagnosticGuardCondition,
			}
		}
		if !automation.IsFormulaTruthy(raw) {
			continue
		}
		allowed, err := s.callerPassesGuard(ctx, user, guard)
		if err != nil {
			return &InputReject{
				Code:           RejectInvalidRequest,
				Message:        err.Error(),
				diagnosticKind: diagnosticGuardAuthorization,
			}
		}
		if !allowed {
			msg := guard.ErrorMessage
			if msg == "" {
				msg = guardDefaultErrorMessage(guard)
			}
			target := "permission:" + guard.RequiredPermission
			if guard.RequiredPermission == "" {
				target = "object:" + guard.RequiredObject + ":" + guard.RequiredOperation
			}
			return &InputReject{
				Code:             RejectInsufficientPerms,
				Message:          msg,
				diagnosticKind:   diagnosticGuardDenied,
				diagnosticTarget: target,
			}
		}
	}
	return nil
}

func (s *Service) callerPassesGuard(ctx context.Context, user *models.UserSession, guard seedrows.ActionInputGuardRow) (bool, error) {
	hasPermission := guard.RequiredPermission != ""
	hasObject := guard.RequiredObject != "" || guard.RequiredOperation != ""
	if hasPermission == hasObject {
		return false, fmt.Errorf("input guard %q must declare exactly one target: requiredPermission or requiredObject+requiredOperation", guard.Condition)
	}
	if s == nil || s.permissions == nil {
		return false, nil
	}
	if hasPermission {
		return s.permissions.HasPermission(ctx, user, guard.RequiredPermission), nil
	}
	if guard.RequiredObject == "" || guard.RequiredOperation == "" {
		return false, fmt.Errorf("input guard %q must declare both requiredObject and requiredOperation", guard.Condition)
	}
	return s.permissions.CheckObjectPermissionWithUser(ctx, guard.RequiredObject, guard.RequiredOperation, user), nil
}

func guardDefaultErrorMessage(guard seedrows.ActionInputGuardRow) string {
	if guard.RequiredPermission != "" {
		return fmt.Sprintf("permission denied: %q is required", guard.RequiredPermission)
	}
	return fmt.Sprintf("permission denied: %s.%s is required", guard.RequiredObject, guard.RequiredOperation)
}

func filterOutputPreviewGuards(guards []seedrows.ActionInputGuardRow, recordCtxInput string) []seedrows.ActionInputGuardRow {
	var out []seedrows.ActionInputGuardRow
	for _, guard := range guards {
		gated := strings.TrimSpace(guard.GatedInput)
		if gated == "" || gated == recordCtxInput {
			out = append(out, guard)
		}
	}
	return out
}

func guardConditionDependsOnInput(condition string) bool {
	return strings.Contains(condition, "Input.") || strings.Contains(condition, "$Input.")
}

func deriveOutputPreviewRecordContextInput(schema []seedrows.ActionSchemaRow, actionName string) (string, error) {
	var candidates []string
	for _, row := range schema {
		if strings.EqualFold(row.UIMode, "hidden") && strings.EqualFold(row.TargetFieldAPIName, "Id") {
			candidates = append(candidates, row.APIName)
		}
	}
	if len(candidates) == 0 {
		return "", fmt.Errorf("standard action %q has no schema row matching the record-context predicate (UIMode=hidden, TargetFieldAPIName=Id)", actionName)
	}
	if len(candidates) > 1 {
		return "", fmt.Errorf("standard action %q has multiple schema rows matching the record-context predicate: %v", actionName, candidates)
	}
	return candidates[0], nil
}

// FilterScalarOutputValues returns a copy of m that contains only scalar-typed
// values. Maps, slices, and structs are excluded so output-preview responses
// cannot serialize record-shaped data without an explicit projection contract.
func FilterScalarOutputValues(m map[string]interface{}) map[string]interface{} {
	out := make(map[string]interface{}, len(m))
	for k, v := range m {
		switch v.(type) {
		case nil, bool,
			int, int8, int16, int32, int64,
			uint, uint8, uint16, uint32, uint64,
			float32, float64, string:
			out[k] = v
		}
	}
	return out
}

func canReadPreviewObject(ctx context.Context, user *models.UserSession, access PreviewAccessChecker, objectAPIName string) bool {
	if access == nil || strings.TrimSpace(objectAPIName) == "" {
		return false
	}
	return access.CheckObjectPermissionWithUser(ctx, objectAPIName, constants.PermRead, user)
}

func projectPreviewFieldsForRead(ctx context.Context, user *models.UserSession, metadata MetadataProvider, access PreviewAccessChecker, objectAPIName string, fields models.SObject) (contract.Record, error) {
	out := make(contract.Record)
	if len(fields) == 0 || !canReadPreviewObject(ctx, user, access, objectAPIName) {
		return out, nil
	}
	var schema *models.ObjectMetadata
	if metadata != nil {
		schema = metadata.GetSchema(ctx, objectAPIName)
	}
	for field, value := range fields {
		if schema != nil && models.FindField(schema, field) == nil {
			continue
		}
		visible, err := access.CheckFieldVisibilityWithUser(ctx, objectAPIName, field, user)
		if err != nil {
			return nil, err
		}
		if visible {
			out[field] = value
		}
	}
	return out, nil
}

func siblingMaxDefaultedPreviewFields(defaultedFields []string, fields contract.Record) []string {
	if len(defaultedFields) == 0 || len(fields) == 0 {
		return nil
	}
	out := []string{}
	seen := map[string]struct{}{}
	for _, fieldName := range defaultedFields {
		fieldName = strings.TrimSpace(fieldName)
		if fieldName == "" {
			continue
		}
		if _, dup := seen[fieldName]; dup {
			continue
		}
		if _, ok := fields[fieldName]; ok {
			seen[fieldName] = struct{}{}
			out = append(out, fieldName)
		}
	}
	return out
}

func groupChildPreviews(ctx context.Context, user *models.UserSession, metadata MetadataProvider, access PreviewAccessChecker, creates []automation.FlowRecordCreatePreview, parentIndex int) ([]ChildPreview, error) {
	if len(creates) <= 1 {
		return nil, nil
	}
	order := []string{}
	byObject := map[string]*ChildPreview{}
	for i, create := range creates {
		if i == parentIndex {
			continue
		}
		if !canReadPreviewObject(ctx, user, access, create.Object) {
			continue
		}
		group, ok := byObject[create.Object]
		if !ok {
			order = append(order, create.Object)
			group = &ChildPreview{Object: create.Object}
			byObject[create.Object] = group
		}
		group.Count++
		fields, err := projectPreviewFieldsForRead(ctx, user, metadata, access, create.Object, create.Fields)
		if err != nil {
			return nil, err
		}
		group.Rows = append(group.Rows, fields)
	}
	out := make([]ChildPreview, 0, len(order))
	for _, object := range order {
		out = append(out, *byObject[object])
	}
	return out, nil
}

func SelectParentPreviewIndex(creates []automation.FlowRecordCreatePreview) int {
	if len(creates) == 0 {
		return -1
	}
	if idx := selectParentPreviewIndexFromReferenceGraph(creates); idx >= 0 {
		return idx
	}
	return 0
}

func selectParentPreviewIndexFromReferenceGraph(creates []automation.FlowRecordCreatePreview) int {
	indexByID := make(map[string]int, len(creates))
	for i, create := range creates {
		id := strings.TrimSpace(create.PreviewID)
		if id != "" {
			indexByID[id] = i
		}
	}
	if len(indexByID) == 0 {
		return -1
	}

	inbound := make([]int, len(creates))
	outbound := make([]int, len(creates))
	for i, create := range creates {
		seenTargets := map[int]struct{}{}
		for _, value := range create.Fields {
			target, ok := indexByID[previewReferenceString(value)]
			if !ok || target == i {
				continue
			}
			if _, seen := seenTargets[target]; seen {
				continue
			}
			seenTargets[target] = struct{}{}
			inbound[target]++
			outbound[i]++
		}
	}

	for i := range creates {
		if inbound[i] > 0 && outbound[i] == 0 {
			return i
		}
	}
	return -1
}

func previewReferenceString(value interface{}) string {
	switch v := value.(type) {
	case string:
		return strings.TrimSpace(v)
	case fmt.Stringer:
		return strings.TrimSpace(v.String())
	default:
		return ""
	}
}

func (s *Service) loadInputSchema(actionID string) ([]seedrows.ActionSchemaRow, error) {
	if s == nil || s.db == nil {
		return nil, nil
	}
	all, err := s.standardActions.GetActionSchema(s.db, actionID)
	if err != nil {
		return nil, err
	}
	inputs := make([]seedrows.ActionSchemaRow, 0, len(all))
	for _, row := range all {
		if row.Direction == "input" {
			inputs = append(inputs, row)
		}
	}
	return inputs, nil
}

func (s *Service) loadFieldDefaults(actionID string) ([]seedrows.ActionFieldDefaultRow, error) {
	if s == nil || s.db == nil {
		return nil, nil
	}
	return s.standardActions.GetActionFieldDefaults(s.db, actionID)
}

func (s *Service) loadGuards(actionID string) ([]seedrows.ActionInputGuardRow, error) {
	if s == nil || s.db == nil {
		return nil, nil
	}
	return s.standardActions.GetActionInputGuards(s.db, actionID)
}

func (s *Service) loadInternalAccessScopes(actionID string) ([]constants.StandardActionInternalAccessScope, error) {
	if s == nil || s.db == nil {
		return nil, nil
	}
	rows, err := s.standardActions.GetStandardActionInternalAccessScopes(s.db, actionID)
	if err != nil {
		return nil, err
	}
	out := make([]constants.StandardActionInternalAccessScope, 0, len(rows))
	for _, row := range rows {
		out = append(out, constants.StandardActionInternalAccessScope{
			StandardActionID: row.StandardActionID,
			ObjectAPIName:    row.ObjectAPIName,
			Operation:        row.Operation,
		})
	}
	return out, nil
}

func recordScopedActionDefaults(rows []seedrows.ActionFieldDefaultRow) []seedrows.ActionFieldDefaultRow {
	if len(rows) == 0 {
		return nil
	}
	out := make([]seedrows.ActionFieldDefaultRow, 0, len(rows))
	for _, row := range rows {
		if row.TargetObject == InputDefaultTarget {
			continue
		}
		out = append(out, row)
	}
	return out
}

func isEmptyInputValue(v interface{}) bool {
	switch typed := v.(type) {
	case nil:
		return true
	case string:
		return typed == ""
	default:
		return false
	}
}

func sqlBool(v interface{}) bool {
	switch x := v.(type) {
	case bool:
		return x
	case int64:
		return x != 0
	case int:
		return x != 0
	case []byte:
		return string(x) == "1" || strings.EqualFold(string(x), "true")
	case string:
		return x == "1" || strings.EqualFold(x, "true")
	default:
		return false
	}
}
