package sfcrmappservice

import "strings"

type layoutFieldReference struct {
	ObjectAPIName string
	FieldAPIName  string
	Terminal      bool
	ForceHidden   bool
}

func inferredLayoutRelationshipName(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 layoutRelationshipField(
	spec searchObjectSpec,
	segment string,
) (searchFieldSpec, bool) {
	segment = strings.TrimSpace(segment)
	for _, field := range spec.Fields {
		if len(field.ReferenceTo) == 0 {
			continue
		}
		if strings.EqualFold(field.APIName, segment) ||
			strings.EqualFold(inferredLayoutRelationshipName(field.APIName), segment) {
			return field, true
		}
	}
	return searchFieldSpec{}, false
}

// resolveLayoutFieldReferences is the one schema-aware interpretation of a
// field path used by layout fields, expressions, labels, and opaque renderer
// config. It accepts field API names (OwnerId.Name), relationship names
// (Owner.Name / Parent__r.Secret__c), and explicit object qualifiers
// (Quote.HiddenTarget__c). Once a path is recognized, every malformed or
// missing continuation fails closed instead of falling back to token scanning.
func resolveLayoutFieldReferences(
	baseObject string,
	path string,
	specs map[string]searchObjectSpec,
) ([]layoutFieldReference, bool) {
	parts := strings.Split(strings.Trim(strings.TrimSpace(path), "."), ".")
	if len(parts) == 0 {
		return nil, false
	}
	recordPrefixed := strings.EqualFold(parts[0], "record")
	if recordPrefixed {
		parts = parts[1:]
	}
	if len(parts) == 0 {
		return []layoutFieldReference{{ForceHidden: true}}, true
	}

	base, found := specs[strings.ToLower(strings.TrimSpace(baseObject))]
	if !found || !describeAuthorityIsCurrent(base) {
		return []layoutFieldReference{{ForceHidden: true}}, true
	}
	currentObjects := []string{base.APIName}
	recognized := recordPrefixed
	references := make([]layoutFieldReference, 0, len(parts))

	for position := 0; position < len(parts)-1; position++ {
		segment := strings.TrimSpace(parts[position])
		if segment == "" {
			return []layoutFieldReference{{ForceHidden: true}}, true
		}
		nextObjects := make([]string, 0, len(currentObjects))
		for _, currentObject := range currentObjects {
			current, exists := specs[strings.ToLower(currentObject)]
			if !exists || !describeAuthorityIsCurrent(current) {
				return []layoutFieldReference{{ForceHidden: true}}, true
			}
			if relationship, exists := layoutRelationshipField(current, segment); exists {
				recognized = true
				references = append(references, layoutFieldReference{
					ObjectAPIName: current.APIName,
					FieldAPIName:  relationship.APIName,
				})
				for _, targetName := range relationship.ReferenceTo {
					target, targetExists := specs[strings.ToLower(strings.TrimSpace(targetName))]
					if !targetExists || !describeAuthorityIsCurrent(target) {
						return []layoutFieldReference{{ForceHidden: true}}, true
					}
					nextObjects = append(nextObjects, target.APIName)
				}
				continue
			}
			if position == 0 {
				candidate, candidateExists := specs[strings.ToLower(segment)]
				if candidateExists && describeAuthorityIsCurrent(candidate) {
					recognized = true
					nextObjects = append(nextObjects, candidate.APIName)
					continue
				}
			}
			if recognized {
				return []layoutFieldReference{{ForceHidden: true}}, true
			}
			return nil, false
		}
		currentObjects = dedupLayoutObjectNames(nextObjects)
		if len(currentObjects) == 0 {
			return []layoutFieldReference{{ForceHidden: true}}, true
		}
	}

	terminalName := strings.TrimSpace(parts[len(parts)-1])
	if terminalName == "" {
		return []layoutFieldReference{{ForceHidden: true}}, true
	}
	for _, currentObject := range currentObjects {
		current := specs[strings.ToLower(currentObject)]
		field, exists := layoutFieldSpec(current, terminalName)
		if !exists {
			if recognized || len(parts) > 1 {
				return []layoutFieldReference{{ForceHidden: true}}, true
			}
			return nil, false
		}
		references = append(references, layoutFieldReference{
			ObjectAPIName: current.APIName,
			FieldAPIName:  field.APIName,
			Terminal:      true,
		})
	}
	return references, true
}

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

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

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

func layoutTextResourceTypes(
	text string,
	baseObject string,
	specs map[string]searchObjectSpec,
) []string {
	var resources []string
	for _, path := range dottedLayoutIdentifierPaths(text) {
		references, recognized := resolveLayoutFieldReferences(baseObject, path, specs)
		if !recognized {
			continue
		}
		for _, reference := range references {
			if !reference.ForceHidden {
				resources = append(resources, reference.ObjectAPIName)
			}
		}
	}
	return dedupLayoutObjectNames(resources)
}

func layoutTextReferencesUnreadableFields(
	text string,
	baseObject string,
	index pageLayoutAuthorityIndex,
	objectHints ...string,
) bool {
	if strings.TrimSpace(text) == "" {
		return false
	}
	residual := text
	objects := append([]string{baseObject}, objectHints...)
	for _, path := range dottedLayoutIdentifierPaths(text) {
		references, recognized := resolveLayoutFieldReferences(baseObject, path, index.specs)
		if !recognized {
			continue
		}
		for _, reference := range references {
			if reference.ForceHidden {
				return true
			}
			objectKey := strings.ToLower(reference.ObjectAPIName)
			if !strings.EqualFold(reference.ObjectAPIName, baseObject) &&
				!index.readableObjects[objectKey] {
				return true
			}
			_, permission, exists := index.field(
				reference.ObjectAPIName, reference.FieldAPIName,
			)
			if !exists || !permission.CanRead {
				return true
			}
			objects = append(objects, reference.ObjectAPIName)
		}
		residual = strings.ReplaceAll(residual, path, " ")
	}
	for _, objectName := range dedupLayoutObjectNames(objects) {
		spec, found := index.specs[strings.ToLower(objectName)]
		if !found || !describeAuthorityIsCurrent(spec) {
			return true
		}
		if !strings.EqualFold(spec.APIName, baseObject) &&
			!index.readableObjects[strings.ToLower(spec.APIName)] {
			return true
		}
		if textReferencesUnreadableField(
			residual, spec, index.fieldPermissions[strings.ToLower(spec.APIName)],
		) {
			return true
		}
	}
	return false
}

func layoutFieldPathObjectHints(
	baseObject string,
	fieldPath string,
	specs map[string]searchObjectSpec,
) []string {
	references, recognized := resolveLayoutFieldReferences(baseObject, fieldPath, specs)
	if !recognized {
		return nil
	}
	var result []string
	for _, reference := range references {
		if reference.Terminal && !reference.ForceHidden {
			result = append(result, reference.ObjectAPIName)
		}
	}
	return dedupLayoutObjectNames(result)
}

func appRecordStateRuleTexts(rule appRecordStateRule) []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
}
