package layoutfls

import (
	"context"
	"encoding/json"
	"strings"

	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

// SchemaResolver resolves object metadata by API name. Unknown objects should
// return nil, nil so expression text that happens to contain dots is ignored.
type SchemaResolver func(context.Context, string) (*models.ObjectMetadata, error)

// AccessResolver resolves effective object/field access by object API name.
type AccessResolver func(context.Context, string) (*contract.EffectiveObjectAccess, error)

// Filter removes layout metadata that would disclose unreadable field API names.
type Filter struct {
	ctx            context.Context
	baseObject     string
	describe       SchemaResolver
	access         AccessResolver
	accessByObject map[string]*contract.EffectiveObjectAccess
	hiddenByObject map[string][]string
	schemaByObject map[string]*models.ObjectMetadata
}

var actionObjectIdentityConfigKeys = []string{"targetObject", "TargetObject", "objectApiName", "objectAPIName"}

func New(ctx context.Context, baseObject string, baseAccess *contract.EffectiveObjectAccess, baseSchema *models.ObjectMetadata, describe SchemaResolver, access AccessResolver) *Filter {
	filter := &Filter{
		ctx:            ctx,
		baseObject:     strings.TrimSpace(baseObject),
		describe:       describe,
		access:         access,
		accessByObject: map[string]*contract.EffectiveObjectAccess{},
		hiddenByObject: map[string][]string{},
		schemaByObject: map[string]*models.ObjectMetadata{},
	}
	baseKey := objectKey(baseObject)
	filter.accessByObject[baseKey] = baseAccess
	if baseSchema != nil {
		filter.schemaByObject[baseKey] = baseSchema
	}
	filter.hiddenByObject[baseKey] = hiddenUnreadableFieldNames(baseAccess, fieldIndex(baseSchema))
	return filter
}

func (f *Filter) FilterActions(actions []models.ActionConfig) ([]models.ActionConfig, error) {
	if actions == nil {
		return nil, nil
	}
	out := make([]models.ActionConfig, 0, len(actions))
	for _, action := range actions {
		filtered, keep, err := f.FilterAction(action)
		if err != nil {
			return nil, err
		}
		if keep {
			out = append(out, filtered)
		}
	}
	return out, nil
}

func (f *Filter) FilterAction(action models.ActionConfig) (models.ActionConfig, bool, error) {
	targetObject := actionTargetObject(action)
	if strings.TrimSpace(action.Label) != "" {
		hiddenRef, err := f.actionLabelReferencesHiddenField(action.Label, targetObject)
		if err != nil {
			return models.ActionConfig{}, false, err
		}
		if hiddenRef {
			return models.ActionConfig{}, false, nil
		}
	}
	if action.VisibilityCondition != nil {
		hiddenRef, err := f.TextReferencesHiddenField(*action.VisibilityCondition)
		if err != nil {
			return models.ActionConfig{}, false, err
		}
		if hiddenRef {
			return models.ActionConfig{}, false, nil
		}
	}

	config, keep, err := f.filterActionConfig(action.Config, targetObject)
	if err != nil || !keep {
		return models.ActionConfig{}, false, err
	}
	action.Config = config
	return action, true, nil
}

func (f *Filter) FilterRecordStateRules(rules []models.RecordStateRule) ([]models.RecordStateRule, error) {
	if len(rules) == 0 {
		return nil, nil
	}
	out := make([]models.RecordStateRule, 0, len(rules))
	for _, rule := range rules {
		hiddenRef, err := f.RecordStateRuleTextReferencesHiddenField(rule)
		if err != nil {
			return nil, err
		}
		if hiddenRef {
			continue
		}
		links, err := f.FilterRecordStateLinks(rule.Links)
		if err != nil {
			return nil, err
		}
		rule.Links = links
		out = append(out, rule)
	}
	return out, nil
}

func (f *Filter) RecordStateRuleReferencesHiddenField(rule models.RecordStateRule) (bool, error) {
	hiddenRef, err := f.RecordStateRuleTextReferencesHiddenField(rule)
	if err != nil || hiddenRef {
		return hiddenRef, err
	}
	return f.RecordStateLinksReferenceHiddenField(rule.Links)
}

func (f *Filter) RecordStateRuleTextReferencesHiddenField(rule models.RecordStateRule) (bool, error) {
	for _, text := range recordStateRuleTexts(rule) {
		hiddenRef, err := f.TextReferencesHiddenField(text)
		if err != nil || hiddenRef {
			return hiddenRef, err
		}
	}
	return false, nil
}

func (f *Filter) FilterRecordStateLinks(links []models.RecordStateLink) ([]models.RecordStateLink, error) {
	if len(links) == 0 {
		return nil, nil
	}
	out := make([]models.RecordStateLink, 0, len(links))
	for _, link := range links {
		hiddenRef, err := f.RecordStateLinkReferencesHiddenField(link)
		if err != nil {
			return nil, err
		}
		if hiddenRef {
			continue
		}
		out = append(out, link)
	}
	return out, nil
}

func (f *Filter) RecordStateLinksReferenceHiddenField(links []models.RecordStateLink) (bool, error) {
	for _, link := range links {
		hiddenRef, err := f.RecordStateLinkReferencesHiddenField(link)
		if err != nil || hiddenRef {
			return hiddenRef, err
		}
	}
	return false, nil
}

func (f *Filter) RecordStateLinkReferencesHiddenField(link models.RecordStateLink) (bool, error) {
	fieldName := strings.TrimSpace(link.ReferenceFieldAPIName)
	if fieldName != "" {
		access, err := f.accessForObject(f.baseObject)
		if err != nil {
			return false, err
		}
		if !fieldReadable(access, fieldName) {
			return true, nil
		}
	}
	if strings.TrimSpace(link.Label) == "" {
		return false, nil
	}
	targetHints, forceHidden, err := f.recordStateLinkTargetHints(link)
	if err != nil {
		return false, err
	}
	if forceHidden {
		return true, nil
	}
	return f.TextReferencesHiddenField(link.Label, targetHints...)
}

func (f *Filter) recordStateLinkTargetHints(link models.RecordStateLink) ([]string, bool, error) {
	if target := strings.TrimSpace(link.ObjectAPIName); target != "" {
		return []string{target}, false, nil
	}
	fieldName := strings.TrimSpace(link.ReferenceFieldAPIName)
	if fieldName == "" {
		return nil, false, nil
	}
	schema, err := f.schemaForObject(f.baseObject)
	if err != nil {
		return nil, false, err
	}
	if schema == nil {
		return nil, false, nil
	}
	for i := range schema.Fields {
		field := &schema.Fields[i]
		if !strings.EqualFold(field.APIName, fieldName) {
			continue
		}
		targets := referenceTargetObjects(field)
		if len(targets) == 0 {
			return nil, false, nil
		}
		if len(targets) > 1 {
			return nil, true, nil
		}
		return targets, false, nil
	}
	return nil, false, nil
}

func (f *Filter) PayloadReferencesHiddenField(payload any, objectHints ...string) (bool, error) {
	if payload == nil {
		return false, nil
	}
	raw, err := json.Marshal(payload)
	if err != nil {
		return true, nil
	}
	return f.TextReferencesHiddenField(string(raw), objectHints...)
}

func (f *Filter) TextReferencesHiddenField(text string, objectHints ...string) (bool, error) {
	if strings.TrimSpace(text) == "" {
		return false, nil
	}
	// Resolve each dotted path precisely. A path whose terminal is hidden /
	// unreadable discloses. A path whose terminals are all readable is fully
	// accounted for, so its text is stripped from the residual before the
	// bare-token catch-all — otherwise a readable related field such as
	// record.Owner.Rating would be re-flagged simply because "Rating" collides
	// with a hidden field of the base object (a cross-object name collision).
	// An unrecognized dotted path is left in the residual so the catch-all can
	// still fail closed on it.
	residual := text
	for _, path := range dottedIdentifierPaths(text) {
		refs, ok, err := f.resolveDottedFieldReferences(path)
		if err != nil {
			return false, err
		}
		if !ok {
			continue
		}
		for _, ref := range refs {
			if ref.ForceHidden {
				return true, nil
			}
			access, err := f.accessForObject(ref.Object)
			if err != nil {
				return false, err
			}
			if !fieldReadable(access, ref.Field) {
				return true, nil
			}
		}
		residual = strings.ReplaceAll(residual, path, " ")
	}
	objects := append([]string{f.baseObject}, objectHints...)
	objects = append(objects, ObjectNamesFromDottedPaths(text)...)
	for _, objectName := range dedupTrimmedStrings(objects) {
		hiddenFields, err := f.hiddenFieldNames(objectName)
		if err != nil {
			return false, err
		}
		if TextReferencesHiddenField(residual, hiddenFields) {
			return true, nil
		}
	}
	return false, nil
}

func (f *Filter) filterActionConfig(config map[string]interface{}, targetObject string) (map[string]interface{}, bool, error) {
	if len(config) == 0 {
		return nil, true, nil
	}
	out := cloneConfig(config)
	opaqueConfig := cloneConfig(config)
	removeActionObjectIdentityConfig(opaqueConfig)

	if lookupField, ok := stringValue(out["lookupField"]); ok {
		targetAccess, err := f.accessForObject(defaultObject(targetObject, f.baseObject))
		if err != nil {
			return nil, false, err
		}
		if !fieldReadableAndCreateable(targetAccess, lookupField) {
			delete(out, "lookupField")
		}
	}
	delete(opaqueConfig, "lookupField")

	if rawMappings, ok := out["prefillMappings"]; ok {
		mappings, ok := stringMap(rawMappings)
		if !ok {
			hiddenRef, err := f.PayloadReferencesHiddenField(rawMappings, targetObject)
			if err != nil || hiddenRef {
				return nil, false, err
			}
		} else {
			filtered, err := f.FilterPrefillMappings(targetObject, mappings)
			if err != nil {
				return nil, false, err
			}
			if len(filtered) == 0 {
				delete(out, "prefillMappings")
			} else {
				out["prefillMappings"] = filtered
			}
		}
	}
	delete(opaqueConfig, "prefillMappings")

	if len(opaqueConfig) > 0 {
		hiddenRef, err := f.PayloadReferencesHiddenField(opaqueConfig, targetObject)
		if err != nil || hiddenRef {
			return nil, false, err
		}
	}
	if len(out) == 0 {
		return nil, true, nil
	}
	return out, true, nil
}

func (f *Filter) actionLabelReferencesHiddenField(label string, targetObject string) (bool, error) {
	hiddenRef, err := f.TextReferencesHiddenField(label)
	if err != nil || hiddenRef {
		return hiddenRef, err
	}
	targetObject = strings.TrimSpace(targetObject)
	if targetObject == "" {
		return false, nil
	}
	hiddenTargetFields, err := f.hiddenFieldNames(targetObject)
	if err != nil || len(hiddenTargetFields) == 0 {
		return false, err
	}
	baseAccess, err := f.accessForObject(f.baseObject)
	if err != nil {
		return false, err
	}
	for _, fieldName := range hiddenTargetFields {
		if fieldReadable(baseAccess, fieldName) {
			continue
		}
		if TextReferencesHiddenField(label, []string{fieldName}) {
			return true, nil
		}
	}
	return false, nil
}

func (f *Filter) FilterPrefillMappings(targetObject string, mappings map[string]string) (map[string]string, error) {
	if len(mappings) == 0 {
		return nil, nil
	}
	targetAccess, err := f.accessForObject(defaultObject(targetObject, f.baseObject))
	if err != nil {
		return nil, err
	}
	out := make(map[string]string, len(mappings))
	for targetField, source := range mappings {
		sourceReadable, err := f.prefillSourceReadable(defaultObject(f.baseObject, targetObject), source)
		if err != nil {
			return nil, err
		}
		if fieldReadableAndCreateable(targetAccess, targetField) && sourceReadable {
			out[targetField] = source
		}
	}
	if len(out) == 0 {
		return nil, nil
	}
	return out, nil
}

// LookupExpansionReadable reports whether a layout field path such as
// AccountId.Name or Parent__c.Safe__c can be exposed without disclosing an
// unreadable relationship FK or target field.
func (f *Filter) LookupExpansionReadable(fieldPath string) (bool, error) {
	return f.fieldPathReadableFromObject(f.baseObject, fieldPath)
}

// FieldPathObjectHints returns the object API names that own the terminal field
// in a layout field path. It lets callers scan adjacent display copy, such as a
// custom field label, against the same object context as the field path.
func (f *Filter) FieldPathObjectHints(fieldPath string) ([]string, error) {
	return f.fieldPathObjectHintsFromObject(f.baseObject, fieldPath)
}

type FieldReference struct {
	Object      string
	Field       string
	ForceHidden bool
}

func (f *Filter) resolveDottedFieldReferences(path string) ([]FieldReference, bool, error) {
	parts := strings.Split(strings.Trim(path, "."), ".")
	if len(parts) < 2 {
		return nil, false, nil
	}
	if strings.EqualFold(parts[0], "user") {
		return nil, false, nil
	}
	recordPrefixed := strings.EqualFold(parts[0], "record")
	if strings.EqualFold(parts[0], "record") {
		parts = parts[1:]
	}
	if len(parts) < 2 {
		return nil, false, nil
	}

	currentObjects := []string{f.baseObject}
	recognizedPath := recordPrefixed
	for i := 0; i < len(parts)-1; i++ {
		segment := strings.TrimSpace(parts[i])
		if segment == "" {
			return nil, false, nil
		}
		nextObjects := make([]string, 0, len(currentObjects))
		for _, currentObject := range currentObjects {
			schema, err := f.schemaForObject(currentObject)
			if err != nil {
				return nil, false, err
			}
			if targetObjects, relationshipField, ok := relationshipTargetObjects(schema, segment); ok {
				recognizedPath = true
				if len(targetObjects) == 0 {
					return []FieldReference{{ForceHidden: true}}, true, nil
				}
				access, err := f.accessForObject(currentObject)
				if err != nil {
					return nil, false, err
				}
				if !fieldReadable(access, relationshipField) {
					return []FieldReference{{Object: currentObject, Field: relationshipField}}, true, nil
				}
				nextObjects = append(nextObjects, targetObjects...)
				continue
			}
			if i == 0 {
				candidateSchema, err := f.schemaForObject(segment)
				if err != nil {
					return nil, false, err
				}
				if candidateSchema != nil {
					recognizedPath = true
					nextObjects = append(nextObjects, schemaAPIName(candidateSchema, segment))
					continue
				}
			}
			if recognizedPath {
				return []FieldReference{{ForceHidden: true}}, true, nil
			}
			return nil, false, nil
		}
		currentObjects = dedupTrimmedStrings(nextObjects)
		if len(currentObjects) == 0 {
			if recognizedPath {
				return []FieldReference{{ForceHidden: true}}, true, nil
			}
			return nil, false, nil
		}
	}

	fieldName := strings.TrimSpace(parts[len(parts)-1])
	if fieldName == "" {
		return nil, false, nil
	}
	refs := make([]FieldReference, 0, len(currentObjects))
	for _, objectName := range currentObjects {
		refs = append(refs, FieldReference{Object: objectName, Field: fieldName})
	}
	return refs, len(refs) > 0, nil
}

func (f *Filter) prefillSourceReadable(baseObject string, source string) (bool, error) {
	refs, err := f.prefillSourceReferences(baseObject, source)
	if err != nil {
		return false, err
	}
	for _, ref := range refs {
		if ref.ForceHidden {
			return false, nil
		}
		access, err := f.accessForObject(ref.Object)
		if err != nil {
			return false, err
		}
		if !fieldReadable(access, ref.Field) {
			return false, nil
		}
	}
	return true, nil
}

func (f *Filter) prefillSourceReferences(baseObject string, source string) ([]FieldReference, error) {
	source = strings.TrimSpace(source)
	if source == "" {
		return []FieldReference{{Object: baseObject}}, nil
	}
	if strings.Contains(source, ".") {
		refs, ok, err := f.resolveDottedFieldReferences(source)
		if err != nil {
			return nil, err
		}
		if ok {
			return refs, nil
		}
	}
	return []FieldReference{{Object: baseObject, Field: source}}, nil
}

func (f *Filter) fieldPathReadableFromObject(objectName string, fieldPath string) (bool, error) {
	fieldPath = strings.TrimSpace(fieldPath)
	if fieldPath == "" {
		return false, nil
	}
	if !strings.Contains(fieldPath, ".") {
		access, err := f.accessForObject(objectName)
		if err != nil {
			return false, err
		}
		return fieldReadable(access, fieldPath), nil
	}

	parts := strings.SplitN(fieldPath, ".", 2)
	segment := strings.TrimSpace(parts[0])
	tail := strings.TrimSpace(parts[1])
	if segment == "" || tail == "" {
		return false, nil
	}

	schema, err := f.schemaForObject(objectName)
	if err != nil {
		return false, err
	}
	if schema == nil {
		return false, nil
	}

	relationshipField := ""
	var targets []string
	if field := fieldByAPIName(schema, segment); field != nil {
		relationshipField = field.APIName
		targets = referenceTargetObjects(field)
	} else if targetObjects, fkField, ok := relationshipTargetObjects(schema, segment); ok {
		relationshipField = fkField
		targets = targetObjects
	} else {
		return false, nil
	}
	if len(targets) == 0 {
		return false, nil
	}

	access, err := f.accessForObject(objectName)
	if err != nil {
		return false, err
	}
	if !fieldReadable(access, relationshipField) {
		return false, nil
	}

	for _, target := range targets {
		readable, err := f.fieldPathReadableFromObject(target, tail)
		if err != nil || !readable {
			return readable, err
		}
	}
	return true, nil
}

func (f *Filter) fieldPathObjectHintsFromObject(objectName string, fieldPath string) ([]string, error) {
	objectName = defaultObject(objectName, f.baseObject)
	fieldPath = strings.TrimSpace(fieldPath)
	if objectName == "" || fieldPath == "" {
		return nil, nil
	}
	if !strings.Contains(fieldPath, ".") {
		return []string{objectName}, nil
	}

	parts := strings.SplitN(fieldPath, ".", 2)
	segment := strings.TrimSpace(parts[0])
	tail := strings.TrimSpace(parts[1])
	if segment == "" || tail == "" {
		return nil, nil
	}

	schema, err := f.schemaForObject(objectName)
	if err != nil {
		return nil, err
	}
	if schema == nil {
		return nil, nil
	}

	var targets []string
	if field := fieldByAPIName(schema, segment); field != nil {
		targets = referenceTargetObjects(field)
	} else if targetObjects, _, ok := relationshipTargetObjects(schema, segment); ok {
		targets = targetObjects
	} else {
		return nil, nil
	}

	hints := make([]string, 0, len(targets))
	for _, target := range targets {
		childHints, err := f.fieldPathObjectHintsFromObject(target, tail)
		if err != nil {
			return nil, err
		}
		hints = append(hints, childHints...)
	}
	return dedupTrimmedStrings(hints), nil
}

func recordStateRuleTexts(rule models.RecordStateRule) []string {
	texts := []string{rule.When}
	if rule.Banner != nil {
		texts = append(texts, rule.Banner.Title, rule.Banner.Body)
	}
	for _, override := range rule.ActionOverrides {
		texts = append(texts, override.Reason)
	}
	if rule.ReadOnly != nil {
		texts = append(texts, rule.ReadOnly.Reason)
	}
	return texts
}

func (f *Filter) hiddenFieldNames(objectName string) ([]string, error) {
	objectName = defaultObject(objectName, f.baseObject)
	key := objectKey(objectName)
	if hiddenFields, ok := f.hiddenByObject[key]; ok {
		return hiddenFields, nil
	}
	schema, err := f.schemaForObject(objectName)
	if err != nil {
		return nil, err
	}
	if schema == nil {
		f.hiddenByObject[key] = nil
		return nil, nil
	}
	if schema.APIName != "" {
		key = objectKey(schema.APIName)
	}
	access, err := f.accessForObject(objectName)
	if err != nil {
		return nil, err
	}
	hiddenFields := hiddenUnreadableFieldNames(access, fieldIndex(schema))
	f.hiddenByObject[key] = hiddenFields
	return hiddenFields, nil
}

func (f *Filter) schemaForObject(objectName string) (*models.ObjectMetadata, error) {
	objectName = defaultObject(objectName, f.baseObject)
	key := objectKey(objectName)
	if schema, ok := f.schemaByObject[key]; ok {
		return schema, nil
	}
	if f.describe == nil {
		f.schemaByObject[key] = nil
		return nil, nil
	}
	schema, err := f.describe(f.ctx, objectName)
	if err != nil {
		return nil, err
	}
	f.schemaByObject[key] = schema
	if schema != nil && schema.APIName != "" {
		f.schemaByObject[objectKey(schema.APIName)] = schema
	}
	return schema, nil
}

func (f *Filter) accessForObject(objectName string) (*contract.EffectiveObjectAccess, error) {
	objectName = defaultObject(objectName, f.baseObject)
	key := objectKey(objectName)
	if access, ok := f.accessByObject[key]; ok {
		return access, nil
	}
	if f.access == nil {
		return nil, nil
	}
	access, err := f.access(f.ctx, objectName)
	if err != nil {
		return nil, err
	}
	f.accessByObject[key] = access
	return access, nil
}

func hiddenUnreadableFieldNames(access *contract.EffectiveObjectAccess, fields map[string]*models.FieldMetadata) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0)
	add := func(fieldName string) {
		fieldName = strings.TrimSpace(fieldName)
		if fieldName == "" {
			return
		}
		key := strings.ToLower(fieldName)
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		out = append(out, fieldName)
	}
	for fieldName := range fields {
		if !fieldReadable(access, fieldName) {
			add(fieldName)
		}
	}
	if access != nil {
		for fieldName, fieldAccess := range access.Fields {
			if !fieldAccess.Readable {
				add(fieldName)
			}
		}
	}
	return out
}

func relationshipTargetObjects(schema *models.ObjectMetadata, segment string) ([]string, string, bool) {
	segment = strings.TrimSpace(segment)
	if schema == nil || segment == "" {
		return nil, "", false
	}
	for i := range schema.Fields {
		field := &schema.Fields[i]
		if !relationshipSegmentMatches(field, segment) {
			continue
		}
		// Recognize the relationship even when its targets are not yet declared
		// (a transitional reference field with empty ReferenceTo — issue #153).
		// Returning ok=true with empty targets lets resolveDottedFieldReferences
		// fail closed on the traversal instead of treating the path as an
		// unrecognized non-relationship, which would let a bare path such as
		// "Owner.Secret__c" leak the hidden terminal field name.
		return referenceTargetObjects(field), field.APIName, true
	}
	return nil, "", false
}

func relationshipSegmentMatches(field *models.FieldMetadata, segment string) bool {
	// Match only actual relationship fields (by TYPE), not a non-reference field
	// whose API name happens to infer to the segment (e.g. a plain "StatusId"
	// text field), which would otherwise be failed closed as a relationship.
	if field == nil || !models.IsRelationshipField(field) {
		return false
	}
	if field.RelationshipName != nil && strings.EqualFold(strings.TrimSpace(*field.RelationshipName), segment) {
		return true
	}
	inferred := inferRelationshipName(field.APIName)
	return inferred != "" && strings.EqualFold(inferred, segment)
}

func referenceTargetObjects(field *models.FieldMetadata) []string {
	if field == nil || len(field.ReferenceTo) == 0 {
		return nil
	}
	targets := make([]string, 0, len(field.ReferenceTo))
	for _, target := range field.ReferenceTo {
		target = strings.TrimSpace(target)
		if target == "" {
			continue
		}
		targets = append(targets, target)
	}
	return dedupTrimmedStrings(targets)
}

func inferRelationshipName(apiName string) string {
	apiName = strings.TrimSpace(apiName)
	switch {
	case strings.HasSuffix(apiName, "__c") && len(apiName) > len("__c"):
		return strings.TrimSuffix(apiName, "__c") + "__r"
	case strings.HasSuffix(apiName, "Id") && len(apiName) > len("Id"):
		return strings.TrimSuffix(apiName, "Id")
	default:
		return ""
	}
}

func schemaAPIName(schema *models.ObjectMetadata, fallback string) string {
	if schema != nil && strings.TrimSpace(schema.APIName) != "" {
		return strings.TrimSpace(schema.APIName)
	}
	return strings.TrimSpace(fallback)
}

func fieldIndex(schema *models.ObjectMetadata) map[string]*models.FieldMetadata {
	out := map[string]*models.FieldMetadata{}
	if schema == nil {
		return out
	}
	for i := range schema.Fields {
		out[schema.Fields[i].APIName] = &schema.Fields[i]
	}
	return out
}

func fieldByAPIName(schema *models.ObjectMetadata, fieldName string) *models.FieldMetadata {
	fieldName = strings.TrimSpace(fieldName)
	if schema == nil || fieldName == "" {
		return nil
	}
	for i := range schema.Fields {
		if strings.EqualFold(strings.TrimSpace(schema.Fields[i].APIName), fieldName) {
			return &schema.Fields[i]
		}
	}
	return nil
}

func fieldAccess(access *contract.EffectiveObjectAccess, fieldName string) (contract.FieldAccess, bool) {
	if access == nil || access.Fields == nil || fieldName == "" {
		return contract.FieldAccess{}, false
	}
	fieldName = strings.TrimSpace(fieldName)
	fieldAccess, ok := access.Fields[fieldName]
	if ok {
		return fieldAccess, true
	}
	for candidate, candidateAccess := range access.Fields {
		if strings.EqualFold(strings.TrimSpace(candidate), fieldName) {
			return candidateAccess, true
		}
	}
	return contract.FieldAccess{}, false
}

func fieldReadable(access *contract.EffectiveObjectAccess, fieldName string) bool {
	fieldAccess, ok := fieldAccess(access, fieldName)
	return ok && fieldAccess.Readable
}

func fieldReadableAndCreateable(access *contract.EffectiveObjectAccess, fieldName string) bool {
	fieldAccess, ok := fieldAccess(access, fieldName)
	return ok && fieldAccess.Readable && fieldAccess.Createable
}

func TextReferencesHiddenField(text string, hiddenFieldNames []string) bool {
	if strings.TrimSpace(text) == "" || len(hiddenFieldNames) == 0 {
		return false
	}
	for _, fieldName := range hiddenFieldNames {
		if ContainsFieldToken(text, fieldName) {
			return true
		}
	}
	return false
}

func ContainsFieldToken(text string, fieldName string) bool {
	fieldName = strings.TrimSpace(fieldName)
	if fieldName == "" {
		return false
	}
	fieldLen := len(fieldName)
	if len(text) < fieldLen {
		return false
	}
	for abs := 0; abs <= len(text)-fieldLen; abs++ {
		if !strings.EqualFold(text[abs:abs+fieldLen], fieldName) {
			continue
		}
		beforeOK := abs == 0 || !identifierByte(text[abs-1])
		after := abs + len(fieldName)
		afterOK := after >= len(text) || !identifierByte(text[after])
		if beforeOK && afterOK {
			return true
		}
	}
	return false
}

func identifierByte(b byte) bool {
	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_'
}

func actionTargetObject(action models.ActionConfig) string {
	if action.TargetObject != nil {
		if target := strings.TrimSpace(*action.TargetObject); target != "" {
			return target
		}
	}
	for _, key := range actionObjectIdentityConfigKeys {
		if target, ok := action.Config[key].(string); ok {
			if target = strings.TrimSpace(target); target != "" {
				return target
			}
		}
	}
	return ""
}

func removeActionObjectIdentityConfig(config map[string]interface{}) {
	for _, key := range actionObjectIdentityConfigKeys {
		delete(config, key)
	}
}

func ObjectNamesFromDottedPaths(text string) []string {
	paths := dottedIdentifierPaths(text)
	objects := make([]string, 0, len(paths))
	for _, path := range paths {
		parts := strings.Split(path, ".")
		if len(parts) < 2 {
			continue
		}
		if strings.EqualFold(parts[0], "record") {
			if len(parts) >= 3 {
				objects = append(objects, parts[1])
			}
			continue
		}
		if strings.EqualFold(parts[0], "user") {
			continue
		}
		objects = append(objects, parts[0])
	}
	return dedupTrimmedStrings(objects)
}

func dottedIdentifierPaths(text string) []string {
	var paths []string
	for i := 0; i < len(text); {
		for i < len(text) && !identifierByte(text[i]) {
			i++
		}
		start := i
		hasDot := false
		for i < len(text) && (identifierByte(text[i]) || text[i] == '.') {
			if text[i] == '.' {
				hasDot = true
			}
			i++
		}
		if hasDot && start < i {
			path := strings.Trim(text[start:i], ".")
			if strings.Contains(path, ".") {
				paths = append(paths, path)
			}
		}
	}
	return paths
}

func stringMap(raw any) (map[string]string, bool) {
	switch typed := raw.(type) {
	case map[string]string:
		out := make(map[string]string, len(typed))
		for key, value := range typed {
			out[key] = value
		}
		return out, true
	case map[string]interface{}:
		out := make(map[string]string, len(typed))
		for key, value := range typed {
			str, ok := value.(string)
			if !ok {
				return nil, false
			}
			out[key] = str
		}
		return out, true
	default:
		return nil, false
	}
}

func stringValue(raw any) (string, bool) {
	value, ok := raw.(string)
	if !ok {
		return "", false
	}
	value = strings.TrimSpace(value)
	return value, value != ""
}

func cloneConfig(config map[string]interface{}) map[string]interface{} {
	out := make(map[string]interface{}, len(config))
	for key, value := range config {
		out[key] = value
	}
	return out
}

func defaultObject(objectName string, fallback string) string {
	objectName = strings.TrimSpace(objectName)
	if objectName != "" {
		return objectName
	}
	return strings.TrimSpace(fallback)
}

func objectKey(objectName string) string {
	return strings.ToLower(strings.TrimSpace(objectName))
}

func dedupTrimmedStrings(values []string) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		key := strings.ToLower(value)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, value)
	}
	return out
}
