package metadata

import (
	"context"
	"encoding/json"
	"fmt"
	"regexp"
	"strconv"
	"strings"

	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/fieldtypes"
	"github.com/nexuscrm/backend/pkg/formula"
	"github.com/nexuscrm/backend/pkg/validator"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/rs/zerolog/log"
)

var (
	isChangedValidationPattern       = regexp.MustCompile(`(?i)\bISCHANGED\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)`)
	isBlankPriorValuePattern         = regexp.MustCompile(`(?i)\bISBLANK\s*\(\s*PRIORVALUE\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*\)`)
	isNullPriorValuePattern          = regexp.MustCompile(`(?i)\bISNULL\s*\(\s*PRIORVALUE\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*\)`)
	priorValueEqualsNullPattern      = regexp.MustCompile(`(?i)\bPRIORVALUE\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*(?:==|=)\s*(?:nil|null)\b`)
	priorValueNotEqualsNullPattern   = regexp.MustCompile(`(?i)\bPRIORVALUE\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)\s*(?:!=|<>)\s*(?:nil|null)\b`)
	nullEqualsPriorValuePattern      = regexp.MustCompile(`(?i)\b(?:nil|null)\s*(?:==|=)\s*PRIORVALUE\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)`)
	nullNotEqualsPriorValuePattern   = regexp.MustCompile(`(?i)\b(?:nil|null)\s*(?:!=|<>)\s*PRIORVALUE\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)`)
	isBlankValidationPattern         = regexp.MustCompile(`(?i)\bISBLANK\s*\(\s*(?:record\.)?([A-Za-z_][A-Za-z0-9_]*)\s*\)`)
	isNullValidationPattern          = regexp.MustCompile(`(?i)\bISNULL\s*\(\s*(?:record\.)?([A-Za-z_][A-Za-z0-9_]*)\s*\)`)
	fieldEqualsNullValidationPattern = regexp.MustCompile(`(?i)\b(?:record\.)?([A-Za-z_][A-Za-z0-9_]*)\s*(?:==|=)\s*(?:nil|null)\b`)
	fieldNotEqualsNullPattern        = regexp.MustCompile(`(?i)\b(?:record\.)?([A-Za-z_][A-Za-z0-9_]*)\s*(?:!=|<>)\s*(?:nil|null)\b`)
	nullEqualsFieldValidationPattern = regexp.MustCompile(`(?i)\b(?:nil|null)\s*(?:==|=)\s*(?:record\.)?([A-Za-z_][A-Za-z0-9_]*)\b`)
	nullNotEqualsFieldPattern        = regexp.MustCompile(`(?i)\b(?:nil|null)\s*(?:!=|<>)\s*(?:record\.)?([A-Za-z_][A-Za-z0-9_]*)\b`)
	validationRelationPattern        = regexp.MustCompile(`\b([A-Za-z_][A-Za-z0-9_]*)\s*\.`)
	priorValueCallPattern            = regexp.MustCompile(`(?i)\bPRIORVALUE\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*\)`)
	formulaStringLiteralPattern      = regexp.MustCompile(`'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"`)
	formulaIdentifierPattern         = regexp.MustCompile(`\b[A-Za-z_][A-Za-z0-9_]*\b`)
)

// AutoNumberFieldResolver reports the field API names on `objectName`
// whose `_System_Field.auto_number_format` is non-empty. The engine
// renders these inside the insert transaction, so the validator must
// not reject them as required-but-missing on the inbound payload.
// Optional; validator treats nil resolver as "no autonumber fields".
type AutoNumberFieldResolver func(objectName string) map[string]struct{}

// FormulaRelationResolver resolves a relationship segment referenced from a
// validation formula (for example RecordType.DeveloperName or Owner.Name).
// Returning nil preserves the old "relationship missing" behaviour so the
// formula engine can decide whether to skip or fail closed.
type FormulaRelationResolver func(ctx context.Context, objectAPIName string, record models.SObject, relationName string) (models.SObject, error)

// ValidationService handles record validation logic
type ValidationService struct {
	formula           *formula.Engine
	validator         *validator.Registry
	autoNumberResolve AutoNumberFieldResolver
	relationResolve   FormulaRelationResolver
}

// NewValidationService creates a new ValidationService
func NewValidationService(formula *formula.Engine) *ValidationService {
	return &ValidationService{
		formula:   formula,
		validator: validator.GetRegistry(),
	}
}

// SetAutoNumberResolver wires the autonumber field resolver. Called by
// the service-manager initializer once the autonumber MetadataSource is
// available. Safe to call multiple times (e.g. after metadata refresh).
func (vs *ValidationService) SetAutoNumberResolver(r AutoNumberFieldResolver) {
	vs.autoNumberResolve = r
}

// SetFormulaRelationResolver wires the relationship resolver used by
// validation formulas when they reference dot-path roots such as
// RecordType.DeveloperName or Owner.Name.
func (vs *ValidationService) SetFormulaRelationResolver(r FormulaRelationResolver) {
	vs.relationResolve = r
}

// ValidateRecord performs comprehensive validation on a record
func (vs *ValidationService) ValidateRecord(
	record models.SObject,
	schema *models.ObjectMetadata,
	rules []*models.ValidationRule,
	oldRecord *models.SObject,
	userContext map[string]interface{},
) error {
	userContext = withFormulaPermissionContext(userContext)

	// Autonumber v2: fields with `_System_Field.auto_number_format`
	// set are rendered by the engine inside the insert transaction.
	// Skip them from required-field validation; the persistence layer
	// fills the value before any constraint that would care.
	var autoNumberFields map[string]struct{}
	if vs.autoNumberResolve != nil && schema != nil {
		autoNumberFields = vs.autoNumberResolve(schema.APIName)
	}

	// 1. Static Schema Constraints
	for _, field := range schema.Fields {
		// Skip system fields for required checks (unless input provided, but typically server-set)
		if field.IsSystem {
			continue
		}

		val, exists := record[field.APIName]

		// On update, skip validation entirely for fields whose value didn't change.
		// This prevents legacy NULL values in unchanged fields from blocking
		// unrelated PATCH operations (e.g., PATCH {IsActive: false} failing
		// because FirstName is NULL in migrated data).
		if oldRecord != nil {
			oldVal, oldExists := (*oldRecord)[field.APIName]
			if oldExists && valuesEqualForValidation(val, oldVal) {
				continue
			}
		}

		// Required fields: enforced on INSERT (oldRecord == nil) and on UPDATE
		// only for fields that were actually changed (unchanged fields skipped above).
		if field.Required {
			// Server-computed types are populated post-validation (autonumber
			// by generateAutoNumbers, formula/rollupSummary by the compute
			// layer). Rejecting them here produces false negatives — the
			// persistence layer would have filled the value itself.
			switch field.Type {
			case constants.FieldTypeAutonumber,
				constants.FieldTypeFormula,
				constants.FieldTypeRollupSummary:
				// skip required-check; value is server-assigned
			default:
				// Autonumber v2: field.Type may be `string` (e.g.
				// Quote.Name) while `auto_number_format` is set —
				// engine renders it inside the insert tx.
				if _, ok := autoNumberFields[field.APIName]; ok {
					// skip; value will be server-rendered
				} else if !exists || val == nil || val == "" {
					return apperrors.NewValidationError(field.APIName, "is required")
				}
			}
		}

		if val == nil {
			continue
		}

		// Type Validation & Constraints - Metadata-driven approach
		fieldType := string(field.Type)

		// Check if this field type has a validation pattern defined in metadata
		pattern, message := fieldtypes.GetValidationPattern(fieldType)
		if pattern != "" {
			if strVal, ok := val.(string); ok {
				if strVal != "" {
					matched, err := regexp.MatchString(pattern, strVal)
					if err == nil && !matched {
						if message == "" {
							message = "invalid format"
						}
						return apperrors.NewValidationError(field.APIName, message)
					}
				}
			} else {
				return apperrors.NewValidationError(field.APIName, "expected string for "+fieldType)
			}
		}

		// Type-specific validations that can't be expressed as regex
		switch fieldType {
		case string(constants.FieldTypeBoolean):
			if _, ok := val.(bool); !ok {
				// Also allow string "true"/"false" or "0"/"1"
				if strVal, ok := val.(string); ok {
					if _, err := strconv.ParseBool(strVal); err != nil {
						return apperrors.NewValidationError(field.APIName, "expected boolean")
					}
				} else if floatVal, ok := val.(float64); ok {
					// JSON unmarshals numeric 0/1 as float64
					if floatVal != 0 && floatVal != 1 {
						return apperrors.NewValidationError(field.APIName, "expected boolean (0/1)")
					}
				} else if intVal, ok := val.(int); ok {
					// Allow 0 or 1
					if intVal != 0 && intVal != 1 {
						return apperrors.NewValidationError(field.APIName, "expected boolean (0/1)")
					}
				} else if int64Val, ok := val.(int64); ok {
					// Allow 0 or 1
					if int64Val != 0 && int64Val != 1 {
						return apperrors.NewValidationError(field.APIName, "expected boolean (0/1)")
					}
				} else {
					return apperrors.NewValidationError(field.APIName, "expected boolean")
				}
			}
		case string(constants.FieldTypeDouble), string(constants.FieldTypeCurrency), string(constants.FieldTypePercent):
			// Numeric checks below handles values, but here we ensure type compatibility
			switch v := val.(type) {
			case float64, int, int64:
				// OK
			case string:
				if _, err := strconv.ParseFloat(v, 64); err != nil {
					return apperrors.NewValidationError(field.APIName, "expected numeric value")
				}
			default:
				return apperrors.NewValidationError(field.APIName, "expected numeric value")
			}
		}

		// Length Checks for String types
		if strVal, ok := val.(string); ok {
			if field.MinLength != nil && len(strVal) < *field.MinLength {
				return apperrors.NewValidationError(field.APIName, "is too short")
			}
			if field.MaxLength != nil && len(strVal) > *field.MaxLength {
				return apperrors.NewValidationError(field.APIName, "is too long")
			}

			// Regex Check
			if field.Regex != nil && *field.Regex != "" {
				matched, err := regexp.MatchString(*field.Regex, strVal)
				if err == nil && !matched {
					msg := "invalid format"
					if field.RegexMessage != nil {
						msg = *field.RegexMessage
					}
					return apperrors.NewValidationError(field.APIName, msg)
				}
			}
		}

		// Numeric Checks
		if field.MinValue != nil || field.MaxValue != nil {
			// Try to interpret as number
			var numVal float64
			isNum := false

			switch v := val.(type) {
			case float64:
				numVal = v
				isNum = true
			case int:
				numVal = float64(v)
				isNum = true
			case int64:
				numVal = float64(v)
				isNum = true
			case string:
				if f, err := strconv.ParseFloat(v, 64); err == nil {
					numVal = f
					isNum = true
				}
			}

			if isNum {
				if field.MinValue != nil && numVal < *field.MinValue {
					return apperrors.NewValidationError(field.APIName, "value is too small")
				}
				if field.MaxValue != nil && numVal > *field.MaxValue {
					return apperrors.NewValidationError(field.APIName, "value is too large")
				}
			}
		}

		// Pluggable Validator Check
		// Uses the validator registry for extensible validation
		if field.Validator != nil && *field.Validator != "" {
			validatorName := *field.Validator
			var config map[string]interface{}

			// Parse validator config if provided
			if field.ValidatorConfig != nil && *field.ValidatorConfig != "" {
				if err := json.Unmarshal([]byte(*field.ValidatorConfig), &config); err != nil {
					// Invalid config, skip validator
					config = nil
				}
			}

			if err := vs.validator.Validate(validatorName, val, config); err != nil {
				return apperrors.NewValidationError(field.APIName, err.Error())
			}
		}
	}

	// 2. Custom Validation Rules
	if len(rules) > 0 {
		// Build formula-specific record as a COPY of the original record.
		// All coercion and field population happens on the copy, NOT on the
		// original record, to prevent mutation from leaking to the persistence
		// layer (which writes the same map after validation).
		formulaFields := buildFormulaNullSentinels(record, schema, "__null_")
		for k, v := range buildFormulaNullSentinels(nil, schema, "__prior_null_") {
			formulaFields[k] = v
		}
		formulaRecord := make(map[string]interface{}, len(schema.Fields))
		for k, v := range record {
			formulaRecord[k] = v
		}
		coerceForFormulaEval(formulaRecord, schema)
		ensureFormulaFieldsPresent(formulaRecord, schema)

		// SUPPORT 'null' keyword:
		if _, exists := formulaRecord["null"]; !exists {
			formulaRecord["null"] = nil
		}

		// Build prior record with the same coercion for correct ISCHANGED comparisons
		var priorRecord map[string]interface{}
		if oldRecord != nil {
			priorRecord = make(map[string]interface{}, len(schema.Fields))
			for k, v := range *oldRecord {
				priorRecord[k] = v
			}
			for k, v := range buildFormulaNullSentinels(*oldRecord, schema, "__prior_null_") {
				formulaFields[k] = v
			}
			coerceForFormulaEval(priorRecord, schema)
			ensureFormulaFieldsPresent(priorRecord, schema)
		}
		ctx := &formula.Context{
			Record: formulaRecord,
			Prior:  priorRecord,
			User:   userContext,
			Fields: formulaFields,
		}
		if schema != nil && schema.APIName != "" && vs.relationResolve != nil {
			ctx.Fetcher = func(record map[string]interface{}, relationName string) (map[string]interface{}, error) {
				related, err := vs.relationResolve(context.Background(), schema.APIName, models.SObject(record), relationName)
				if related == nil || err != nil {
					return nil, err
				}
				return map[string]interface{}(related), nil
			}
		}

		for _, rule := range rules {
			if !rule.Active {
				continue
			}
			if schema != nil && schema.APIName != "" && vs.relationResolve != nil {
				for _, relationName := range extractValidationRelationRoots(rule.Condition) {
					if _, exists := formulaRecord[relationName]; exists {
						continue
					}
					related, _ := vs.relationResolve(context.Background(), schema.APIName, formulaRecord, relationName)
					formulaRecord[relationName] = related
				}
			}

			result, err := vs.formula.Evaluate(rewriteValidationFormulaExpression(rule.Condition, schema), ctx)
			if err != nil {
				errMsg := err.Error()
				// Skip rules referencing known-unsupported Salesforce functions/relationships.
				// expr-lang produces "unknown name <IDENT>" for missing identifiers.
				// We only skip if the identifier is a known Salesforce import artifact;
				// genuinely malformed rules (typos, random garbage) fail closed.
				if ident := extractUnknownIdentifier(errMsg); ident != "" &&
					!schemaHasField(schema, ident) &&
					isSalesforceImportArtifact(ident) {
					log.Warn().Str("rule", rule.Name).Str("object", schema.APIName).
						Str("identifier", ident).
						Msg("Skipping validation rule: known-unsupported Salesforce function/relationship")
					continue
				}
				// Fail Closed for all other evaluation errors: stop to prevent invalid data.
				return apperrors.NewInternalError(fmt.Sprintf("Validation rule '%s' failed to evaluate", rule.Name), err)
			}

			if isTrue, ok := result.(bool); ok && isTrue {
				return apperrors.NewValidationError("", rule.ErrorMessage)
			}
		}
	}

	return nil
}

// BlocksRecordUpdateWithPriorOnlyRules reports whether admin-authored
// ValidationRule metadata makes the current record immutable for update.
//
// This intentionally recognizes only conservative record-lock shapes: active
// rules whose condition is driven by PRIORVALUE(...) and formula syntax, with no
// current-record field identifiers outside PRIORVALUE. Rules that users could
// satisfy by editing the current value stay out of recordAccess and are enforced
// by the write path at save time.
func (vs *ValidationService) BlocksRecordUpdateWithPriorOnlyRules(
	record models.SObject,
	schema *models.ObjectMetadata,
	rules []*models.ValidationRule,
	userContext map[string]interface{},
) bool {
	if vs == nil || record == nil || schema == nil || len(rules) == 0 {
		return false
	}

	for _, rule := range rules {
		if rule == nil || !rule.Active || !isPriorOnlyValidationCondition(rule.Condition) {
			continue
		}

		current := cloneValidationRecord(record)
		prior := cloneValidationRecord(record)
		if err := vs.ValidateRecord(current, schema, []*models.ValidationRule{rule}, &prior, userContext); err != nil {
			return true
		}
	}

	return false
}

func cloneValidationRecord(record models.SObject) models.SObject {
	cloned := make(models.SObject, len(record))
	for key, value := range record {
		cloned[key] = value
	}
	return cloned
}

func isPriorOnlyValidationCondition(condition string) bool {
	if !strings.Contains(strings.ToUpper(condition), "PRIORVALUE") {
		return false
	}

	scrubbed := formulaStringLiteralPattern.ReplaceAllString(condition, "")
	scrubbed = priorValueCallPattern.ReplaceAllString(scrubbed, "")

	allowed := map[string]struct{}{
		"AND":        {},
		"OR":         {},
		"NOT":        {},
		"ISBLANK":    {},
		"ISNULL":     {},
		"BLANKVALUE": {},
		"NULLVALUE":  {},
		"TRUE":       {},
		"FALSE":      {},
		"NULL":       {},
		"NIL":        {},
	}
	for _, ident := range formulaIdentifierPattern.FindAllString(scrubbed, -1) {
		if _, ok := allowed[strings.ToUpper(ident)]; !ok {
			return false
		}
	}
	return true
}

func withFormulaPermissionContext(userContext map[string]interface{}) map[string]interface{} {
	if userContext == nil {
		userContext = map[string]interface{}{}
	}
	if _, ok := userContext["__SystemPermissions"]; !ok {
		if perms, ok := userContext[constants.ContextKeySystemPermissions]; ok {
			userContext["__SystemPermissions"] = perms
		} else {
			userContext["__SystemPermissions"] = map[string]bool{}
		}
	}
	if _, ok := userContext["__CustomPermissions"]; !ok {
		if perms, ok := userContext[constants.ContextKeyCustomPermissions]; ok {
			userContext["__CustomPermissions"] = perms
		} else {
			userContext["__CustomPermissions"] = map[string]bool{}
		}
	}
	return userContext
}

// ValidateObjectMetadata enforces naming conventions
func (vs *ValidationService) ValidateObjectMetadata(obj *models.ObjectMetadata) error {
	if !isValidIdentifier(obj.APIName) {
		return apperrors.NewValidationError(constants.FieldAPIName, fmt.Sprintf("Invalid API name '%s': must be alphanumeric (PascalCase/snake_case)", obj.APIName))
	}
	return nil
}

// ValidateFieldMetadata enforces naming conventions and, for multi-picklist
// fields, rejects option values containing ';'. Salesforce API v63.0 uses ';'
// as the multi-picklist wire separator; storing an option value that contains
// that separator cannot round-trip through the FromStorageRecord / wire-format
// join path and, under a record-triggered flow that writes the field, has been
// observed to re-open the self-update loop fixed in issue #1683 / PR #1684.
// The guard belongs at the authoring boundary — once the option is persisted,
// records can be created and the loop can start. Closes #1686.
func (vs *ValidationService) ValidateFieldMetadata(field *models.FieldMetadata) error {
	if !isValidIdentifier(field.APIName) {
		return apperrors.NewValidationError(constants.FieldAPIName, fmt.Sprintf("Invalid API name '%s': must be alphanumeric (PascalCase/snake_case)", field.APIName))
	}
	if field.Type == constants.FieldTypeMultiPicklist {
		for _, opt := range field.Options {
			if strings.Contains(opt.Value, ";") {
				return apperrors.NewValidationError(
					"options",
					fmt.Sprintf("multi-picklist option value %q cannot contain ';' — it is the Salesforce wire-format separator; use a comma or dash instead", opt.Value),
				)
			}
		}
	}
	return nil
}

func isValidIdentifier(s string) bool {
	validName := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
	return validName.MatchString(s)
}

func extractValidationRelationRoots(expression string) []string {
	matches := validationRelationPattern.FindAllStringSubmatch(expression, -1)
	if len(matches) == 0 {
		return nil
	}
	ignored := map[string]struct{}{
		"record": {}, "prior": {}, "env": {}, "var": {},
	}
	seen := make(map[string]struct{}, len(matches))
	roots := make([]string, 0, len(matches))
	for _, match := range matches {
		root := match[1]
		if _, skip := ignored[root]; skip {
			continue
		}
		if strings.HasPrefix(root, "__") {
			continue
		}
		if _, dup := seen[root]; dup {
			continue
		}
		seen[root] = struct{}{}
		roots = append(roots, root)
	}
	return roots
}

func ensureFormulaFieldsPresent(record models.SObject, schema *models.ObjectMetadata) {
	for _, field := range schema.Fields {
		if _, exists := record[field.APIName]; !exists {
			// Numeric fields get zero values so formula comparisons
			// (e.g. Probability >= 0.4) don't crash with "mismatched types
			// unknown and float64". Other types stay nil for ISBLANK/nil checks.
			ft := constants.NormalizeFieldType(string(field.Type))
			switch ft {
			case constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent:
				record[field.APIName] = float64(0)
			default:
				record[field.APIName] = nil
			}
		}
	}
}

func schemaHasField(schema *models.ObjectMetadata, apiName string) bool {
	for _, field := range schema.Fields {
		if field.APIName == apiName {
			return true
		}
	}
	return false
}

func valuesEqualForValidation(left, right interface{}) bool {
	return fmt.Sprintf("%v", left) == fmt.Sprintf("%v", right)
}

func buildFormulaNullSentinels(record models.SObject, schema *models.ObjectMetadata, prefix string) map[string]interface{} {
	fields := make(map[string]interface{}, len(schema.Fields))
	for _, field := range schema.Fields {
		key := prefix + field.APIName
		fields[key] = false
		if record == nil {
			continue
		}
		val, exists := record[field.APIName]
		if !exists {
			continue
		}
		if isFormulaBlankValue(val, field) {
			fields[key] = true
		}
	}
	return fields
}

func isFormulaBlankValue(val interface{}, field models.FieldMetadata) bool {
	if val == nil {
		return true
	}
	strVal, ok := val.(string)
	if !ok || strVal != "" {
		return false
	}
	switch constants.NormalizeFieldType(string(field.Type)) {
	case constants.FieldTypeDate, constants.FieldTypeDatetime,
		constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent,
		constants.FieldTypeBoolean, constants.FieldTypeReference:
		return true
	default:
		return false
	}
}

func rewriteValidationFormulaExpression(expression string, schema *models.ObjectMetadata) string {
	if strings.TrimSpace(expression) == "" {
		return expression
	}

	rewritten, literals := extractValidationStringLiterals(expression)
	rewritten = replaceValidationFieldMatch(rewritten, isChangedValidationPattern, schema, func(field string) string {
		return fmt.Sprintf("ISCHANGEDX(_has_prior_, %s, _prior_%s, __null_%s, __prior_null_%s)", field, field, field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, isBlankPriorValuePattern, schema, func(field string) string {
		return fmt.Sprintf("ISBLANKX(_prior_%s, __prior_null_%s)", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, isNullPriorValuePattern, schema, func(field string) string {
		return fmt.Sprintf("ISNULLX(_prior_%s, __prior_null_%s)", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, priorValueEqualsNullPattern, schema, func(field string) string {
		return fmt.Sprintf("ISNULLX(_prior_%s, __prior_null_%s)", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, priorValueNotEqualsNullPattern, schema, func(field string) string {
		return fmt.Sprintf("NOT(ISNULLX(_prior_%s, __prior_null_%s))", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, nullEqualsPriorValuePattern, schema, func(field string) string {
		return fmt.Sprintf("ISNULLX(_prior_%s, __prior_null_%s)", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, nullNotEqualsPriorValuePattern, schema, func(field string) string {
		return fmt.Sprintf("NOT(ISNULLX(_prior_%s, __prior_null_%s))", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, isBlankValidationPattern, schema, func(field string) string {
		return fmt.Sprintf("ISBLANKX(%s, __null_%s)", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, isNullValidationPattern, schema, func(field string) string {
		return fmt.Sprintf("ISNULLX(%s, __null_%s)", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, fieldEqualsNullValidationPattern, schema, func(field string) string {
		return fmt.Sprintf("ISNULLX(%s, __null_%s)", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, fieldNotEqualsNullPattern, schema, func(field string) string {
		return fmt.Sprintf("NOT(ISNULLX(%s, __null_%s))", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, nullEqualsFieldValidationPattern, schema, func(field string) string {
		return fmt.Sprintf("ISNULLX(%s, __null_%s)", field, field)
	})
	rewritten = replaceValidationFieldMatch(rewritten, nullNotEqualsFieldPattern, schema, func(field string) string {
		return fmt.Sprintf("NOT(ISNULLX(%s, __null_%s))", field, field)
	})
	return restoreValidationStringLiterals(rewritten, literals)
}

func replaceValidationFieldMatch(expression string, pattern *regexp.Regexp, schema *models.ObjectMetadata, replacer func(field string) string) string {
	return pattern.ReplaceAllStringFunc(expression, func(match string) string {
		submatches := pattern.FindStringSubmatch(match)
		if len(submatches) < 2 {
			return match
		}
		field := submatches[1]
		if !schemaHasField(schema, field) {
			return match
		}
		return replacer(field)
	})
}

func extractValidationStringLiterals(expr string) (string, []string) {
	var b strings.Builder
	var literals []string
	i := 0
	for i < len(expr) {
		ch := expr[i]
		if ch == '"' || ch == '\'' {
			quote := ch
			var lit strings.Builder
			lit.WriteByte(ch)
			i++
			for i < len(expr) {
				c := expr[i]
				if c == '\\' && i+1 < len(expr) && expr[i+1] == quote {
					lit.WriteByte(c)
					lit.WriteByte(expr[i+1])
					i += 2
					continue
				}
				if c == quote && i+1 < len(expr) && expr[i+1] == quote {
					lit.WriteByte(c)
					lit.WriteByte(expr[i+1])
					i += 2
					continue
				}
				lit.WriteByte(c)
				i++
				if c == quote {
					break
				}
			}
			b.WriteString(fmt.Sprintf("__VAL_STR_%d__", len(literals)))
			literals = append(literals, lit.String())
			continue
		}
		b.WriteByte(ch)
		i++
	}
	return b.String(), literals
}

func restoreValidationStringLiterals(expr string, literals []string) string {
	for i, lit := range literals {
		expr = strings.Replace(expr, fmt.Sprintf("__VAL_STR_%d__", i), lit, 1)
	}
	return expr
}

// coerceForFormulaEval normalizes record field values to types that the formula
// engine can compare correctly. Applied to both current and prior records so that
// ISCHANGED comparisons use consistent types.
func coerceForFormulaEval(record models.SObject, schema *models.ObjectMetadata) {
	for _, field := range schema.Fields {
		val, ok := record[field.APIName]
		if !ok {
			continue
		}

		// Coerce nil numeric fields to 0.0 so comparisons don't crash with
		// "mismatched types unknown and float64". Matches Salesforce behavior
		// where null numbers default to 0 in formula expressions.
		if val == nil {
			ft := constants.NormalizeFieldType(string(field.Type))
			switch ft {
			case constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent:
				record[field.APIName] = float64(0)
			case constants.FieldTypeBoolean:
				record[field.APIName] = false
			}
			continue
		}

		ft := constants.NormalizeFieldType(string(field.Type))

		// Convert empty strings to nil for non-text types
		if strVal, isStr := val.(string); isStr && strVal == "" {
			switch ft {
			case constants.FieldTypeDate, constants.FieldTypeDatetime,
				constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent,
				constants.FieldTypeBoolean, constants.FieldTypeReference:
				record[field.APIName] = nil
				continue
			}
		}

		// Coerce boolean fields from int64 (TiDB TINYINT) to Go bool
		if ft == constants.FieldTypeBoolean {
			switch v := val.(type) {
			case int64:
				record[field.APIName] = v != 0
			case int:
				record[field.APIName] = v != 0
			case float64:
				record[field.APIName] = v != 0
			}
			continue
		}

		// Coerce string-typed numeric fields to float64
		switch ft {
		case constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent:
			if strVal, isStr := val.(string); isStr && strVal != "" {
				if f, err := strconv.ParseFloat(strVal, 64); err == nil {
					record[field.APIName] = f
				}
			}
		}
	}
}

// extractUnknownIdentifier parses the identifier from an expr-lang
// "unknown name FOO (line:col)" error. Returns empty string if not matched.
func extractUnknownIdentifier(errMsg string) string {
	const prefix = "unknown name "
	idx := strings.Index(errMsg, prefix)
	if idx < 0 {
		return ""
	}
	rest := errMsg[idx+len(prefix):]
	end := strings.IndexAny(rest, " (")
	if end < 0 {
		return rest
	}
	return rest[:end]
}

// knownUnsupportedSalesforceFunctions are Salesforce formula functions not yet
// implemented in NexusCRM. Rules referencing these are skipped (not fail-closed).
var knownUnsupportedSalesforceFunctions = map[string]bool{
	"EXCLUDES": true, "VLOOKUP": true, "REGEX": true,
	"IMAGE": true, "BLANKVALUE": true, "BR": true, "GEOLOCATION": true,
	"DISTANCE": true, "CURRENCYRATE": true, "JSENCODE": true, "URLENCODE": true,
	"HTMLENCODE": true, "LPAD": true, "RPAD": true, "ISNUMBER": true, "ISTEXT": true,
}

// knownSalesforceRelationships are standard Salesforce relationship names that
// appear in formulas as dot-path roots (e.g. RecordType.DeveloperName).
var knownSalesforceRelationships = map[string]bool{
	"Account": true, "Contact": true, "Parent": true, "Profile": true,
}

// isSalesforceImportArtifact returns true if the identifier is a known
// Salesforce-specific construct not supported by NexusCRM.
func isSalesforceImportArtifact(ident string) bool {
	if knownUnsupportedSalesforceFunctions[strings.ToUpper(ident)] {
		return true
	}
	if knownSalesforceRelationships[ident] {
		return true
	}
	// Standard Salesforce custom fields (e.g. Partner_Margin__c) that don't exist
	// in NexusCRM yet. These were imported with validation rules but the corresponding
	// fields haven't been created.
	if strings.HasSuffix(ident, "__c") {
		return true
	}
	// Salesforce namespace-prefixed fields (e.g. awsapn__APN_CRM_Id__c, Elvance__Elvance_Owner__c)
	if strings.Contains(ident, "__") && strings.Count(ident, "__") >= 2 {
		return true
	}
	return false
}
