package metadata

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

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/formulastorage"
	"github.com/nexuscrm/shared/pkg/models"
)

func extractValidationRelationRoots(expression string) []string {
	sanitized, _ := extractValidationStringLiterals(expression)
	matchIndexes := validationRelationPattern.FindAllStringSubmatchIndex(sanitized, -1)
	matches := validationRelationPattern.FindAllStringSubmatch(sanitized, -1)
	if len(matches) == 0 {
		return nil
	}
	// Formula-grammar reserved roots (`record`, `prior`, `env`, `var`) are
	// language tokens and stay literal here. Session-bound formula merge-field
	// globals ($User / $Profile / $UserRole / $Permission) are owned by the
	// formula package - read them through formula.IsSessionFormulaGlobalRoot
	// rather than restating the SObject-shaped names in this engine.
	ignoredGrammarRoots := map[string]struct{}{
		"record": {}, "prior": {}, "env": {}, "var": {},
	}
	seen := make(map[string]struct{}, len(matches))
	roots := make([]string, 0, len(matches))
	for idx, match := range matches {
		root := match[1]
		if idx < len(matchIndexes) && len(matchIndexes[idx]) >= 2 {
			matchStart := matchIndexes[idx][0]
			if matchStart > 0 && sanitized[matchStart-1] == '$' {
				continue
			}
		}
		if _, skip := ignoredGrammarRoots[root]; skip {
			continue
		}
		if formula.IsSessionFormulaGlobalRoot(root) {
			continue
		}
		if strings.Contains(root, "VAL_STR_") {
			continue
		}
		if strings.HasPrefix(root, "__") {
			continue
		}
		if _, dup := seen[root]; dup {
			continue
		}
		seen[root] = struct{}{}
		roots = append(roots, root)
	}
	return roots
}

// prepareValidationFormulaRecord applies the formula-evaluation coercions to a
// validation record image and returns the set of fields whose blankness the
// coercion erased — a numeric field that was nil, or absent, and now reads 0.
//
// The two steps are wrapped together because their erased-blank sets are only
// meaningful merged: a rule cannot tell "the field was present and null" from
// "the field was never in the payload", and both are blank to the vendor.
// Callers pass the result to buildFormulaNullSentinels, which is the only way
// a rule can still observe blankness after the coercion (#4161).
func prepareValidationFormulaRecord(record models.SObject, schema *models.ObjectMetadata) map[string]struct{} {
	erasedBlanks := coerceForFormulaEval(record, schema)
	for name := range ensureFormulaFieldsPresent(record, schema) {
		erasedBlanks[name] = struct{}{}
	}
	return erasedBlanks
}

// zeroBlankNumericForValidation writes the typed zero the formula engine needs
// for comparisons over a blank numeric, and reports whether doing so erased a
// blank that a rule could otherwise have observed.
//
// `handled` is false for every non-numeric type; the caller keeps its own
// treatment for those, which is to leave them nil so they report their own
// blankness.
//
// The erasure is NOT reported for a field the active-computed projection will
// overwrite after the coercion runs. There the zero is a placeholder for a value
// that has not arrived yet, and forcing the sentinel true would discard the
// projected value. The carve-out is keyed to the very predicate the projection
// itself is driven by (validationActiveComputedFields), so the two sets cannot
// drift apart into a stale sentinel.
//
// This is the single definition of the validation path's erasure rule; both
// coercion entry points call it, so a future change to the numeric set or the
// carve-out cannot land in only one of them.
func zeroBlankNumericForValidation(record models.SObject, field *models.FieldMetadata) (handled, erasedBlank bool) {
	switch constants.NormalizeFieldType(string(field.Type)) {
	case constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent:
		record[field.APIName] = float64(0)
		return true, !formulastorage.IsActiveComputedField(field)
	}
	return false, false
}

// ensureFormulaFieldsPresent seeds schema fields the payload omitted, and
// returns the numeric ones it seeded with a zero (see coerceForFormulaEval's
// contract for what the returned set is for).
func ensureFormulaFieldsPresent(record models.SObject, schema *models.ObjectMetadata) map[string]struct{} {
	erasedBlanks := make(map[string]struct{})
	for i := range schema.Fields {
		field := &schema.Fields[i]
		if _, exists := record[field.APIName]; exists {
			continue
		}
		// 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.
		handled, erased := zeroBlankNumericForValidation(record, field)
		if !handled {
			record[field.APIName] = nil
			continue
		}
		if erased {
			erasedBlanks[field.APIName] = struct{}{}
		}
	}
	return erasedBlanks
}

func schemaHasField(schema *models.ObjectMetadata, apiName string) bool {
	if schema == nil {
		return false
	}
	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)
}

// buildFormulaNullSentinels produces the `__null_<field>` / `__prior_null_<field>`
// booleans that back the ISBLANK / ISNULL / ISCHANGED lowerings.
//
// erasedBlanks names the fields whose blankness prepareValidationFormulaRecord
// erased on the way in; their record value now reads 0 and cannot report it.
// Without this the vendor's required-field idiom — a rule that blocks the save
// when a number is blank — is inert for every numeric field (#4161).
//
// The set is disjoint from the fields the active-computed projection writes,
// because zeroBlankNumericForValidation excludes exactly those. That is what
// makes it safe to read the set here, after the projection has already run: no
// entry can override a value the projection produced.
func buildFormulaNullSentinels(record models.SObject, schema *models.ObjectMetadata, prefix string, erasedBlanks map[string]struct{}) 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
		}
		if _, erased := erasedBlanks[field.APIName]; erased {
			fields[key] = true
			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)
	})
	rewritten = rewriteValidationRelationComparisons(rewritten)
	return restoreValidationStringLiterals(rewritten, literals)
}

func rewriteValidationRelationComparisons(expression string) string {
	var out strings.Builder
	last := 0
	for i := 0; i < len(expression); i++ {
		op, opLen := validationRelationOperatorAt(expression, i)
		if op == "" {
			continue
		}
		lhsStart, lhs, ok := validationRelationLeftOperand(expression, i)
		if !ok {
			continue
		}
		rhsEnd, rhs, ok := validationRelationRightOperand(expression, i+opLen)
		if !ok {
			continue
		}
		if !isValidationRelationOperand(lhs) && !isValidationRelationOperand(rhs) {
			continue
		}
		if !validationComparisonBoundaryBefore(expression, lhsStart) || !validationComparisonBoundaryAfter(expression, rhsEnd) {
			continue
		}
		out.WriteString(expression[last:lhsStart])
		out.WriteString(validationRelationOperatorFunc(op))
		out.WriteByte('(')
		out.WriteString(lhs)
		out.WriteString(", ")
		out.WriteString(rhs)
		out.WriteByte(')')
		last = rhsEnd
		i = rhsEnd - 1
	}
	if last == 0 {
		return expression
	}
	out.WriteString(expression[last:])
	return out.String()
}

func validationRelationOperatorAt(expression string, i int) (string, int) {
	if i < 0 || i >= len(expression) {
		return "", 0
	}
	switch expression[i] {
	case '<':
		if i+1 < len(expression) && expression[i+1] == '=' {
			return "<=", 2
		}
		return "<", 1
	case '>':
		if i+1 < len(expression) && expression[i+1] == '=' {
			return ">=", 2
		}
		return ">", 1
	default:
		return "", 0
	}
}

func validationRelationLeftOperand(expression string, opIndex int) (int, string, bool) {
	end := opIndex
	for end > 0 && expression[end-1] == ' ' {
		end--
	}
	start := end
	for start > 0 && isValidationOperandChar(expression[start-1]) {
		start--
	}
	if start > 0 && expression[start-1] == '$' {
		start--
	}
	if start == end {
		return 0, "", false
	}
	operand := expression[start:end]
	return start, operand, true
}

func validationRelationRightOperand(expression string, start int) (int, string, bool) {
	i := start
	for i < len(expression) && expression[i] == ' ' {
		i++
	}
	if i >= len(expression) {
		return 0, "", false
	}
	end := i
	if isValidationIdentifierStart(expression[i]) {
		end++
		for end < len(expression) && isValidationIdentifierBody(expression[end]) {
			end++
		}
		for end < len(expression) && expression[end] == '.' {
			next := end + 1
			if next >= len(expression) || !isValidationIdentifierStart(expression[next]) {
				break
			}
			end = next + 1
			for end < len(expression) && isValidationIdentifierBody(expression[end]) {
				end++
			}
		}
		if end < len(expression) && expression[end] == '(' {
			callEnd, ok := validationBalancedCallEnd(expression, end)
			if !ok {
				return 0, "", false
			}
			end = callEnd
		}
		return end, expression[i:end], true
	}
	if isValidationLiteralStart(expression[i]) {
		end++
		for end < len(expression) && isValidationLiteralChar(expression[end]) {
			end++
		}
		return end, expression[i:end], true
	}
	return 0, "", false
}

func validationBalancedCallEnd(expression string, openIndex int) (int, bool) {
	depth := 0
	for i := openIndex; i < len(expression); i++ {
		switch expression[i] {
		case '(':
			depth++
		case ')':
			depth--
			if depth == 0 {
				return i + 1, true
			}
		}
	}
	return 0, false
}

func validationRelationOperatorFunc(op string) string {
	switch op {
	case "<":
		return "STRICTSAFELT"
	case "<=":
		return "STRICTSAFELTE"
	case ">":
		return "STRICTSAFEGT"
	case ">=":
		return "STRICTSAFEGTE"
	default:
		return ""
	}
}

func isValidationRelationOperand(operand string) bool {
	return validationRelationOperandPattern.MatchString(operand)
}

func isValidationOperandChar(ch byte) bool {
	return isValidationIdentifierBody(ch) || ch == '.'
}

func isValidationIdentifierStart(ch byte) bool {
	return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '_'
}

func isValidationIdentifierBody(ch byte) bool {
	return isValidationIdentifierStart(ch) || (ch >= '0' && ch <= '9')
}

func isValidationLiteralStart(ch byte) bool {
	return ch == '-' || (ch >= '0' && ch <= '9')
}

func isValidationLiteralChar(ch byte) bool {
	return (ch >= '0' && ch <= '9') || ch == '.' || ch == '-'
}

func validationComparisonBoundaryBefore(expression string, operandStart int) bool {
	i := operandStart - 1
	for i >= 0 && expression[i] == ' ' {
		i--
	}
	if i < 0 {
		return true
	}
	switch expression[i] {
	case '(', ',', '&', '|', '!', '=', '<', '>':
		return true
	default:
		return false
	}
}

func validationComparisonBoundaryAfter(expression string, operandEnd int) bool {
	i := operandEnd
	for i < len(expression) && expression[i] == ' ' {
		i++
	}
	if i >= len(expression) {
		return true
	}
	switch expression[i] {
	case ')', ',', '&', '|', '!', '=', '<', '>':
		return true
	default:
		return false
	}
}

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.
//
// It returns the set of fields whose blank value it replaced with a typed zero.
// The coercion is deliberately kept — comparison type-safety is a real
// requirement — but it destroys the only evidence a rule has that the field was
// blank, so the erased set has to travel to buildFormulaNullSentinels for the
// ISBLANK family to stay observable (#4161). See
// zeroBlankNumericForValidation for which fields are reported and why.
func coerceForFormulaEval(record models.SObject, schema *models.ObjectMetadata) map[string]struct{} {
	erasedBlanks := make(map[string]struct{})
	for i := range schema.Fields {
		field := &schema.Fields[i]
		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.
		//
		// NOTE: this is a validation-path sibling of the formula-FIELD
		// blank-as-zero rule (#4152, schemautil.IsBlankAsZeroNumericType +
		// formula.Context.TreatBlankNumericAsZero). The two buckets are
		// deliberately not unified today: this one covers double/currency/
		// percent by raw declared type and also coerces nil booleans to
		// false, while the formula-field bucket includes int-like types,
		// resolves rollups through EffectiveFieldType, and excludes booleans.
		// If either set changes, decide explicitly whether the other follows.
		// The erased-blank set reported for the ISBLANK sentinels is derived
		// from THIS function's own switch, not from either bucket, so unifying
		// them later is a decision about coercion only.
		if val == nil {
			handled, erased := zeroBlankNumericForValidation(record, field)
			switch {
			case handled:
				if erased {
					erasedBlanks[field.APIName] = struct{}{}
				}
			case constants.NormalizeFieldType(string(field.Type)) == constants.FieldTypeBoolean:
				// Not reported as an erased blank: the vendor's checkboxes are
				// never blank either, so false is the value a rule should see.
				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
		}

		if ft == constants.FieldTypeMultiPicklist {
			switch typed := val.(type) {
			case schemautil.NormalizedMultiPicklistJSON:
				var arr []string
				if err := json.Unmarshal([]byte(string(typed)), &arr); err == nil {
					record[field.APIName] = strings.Join(arr, ";")
				} else {
					record[field.APIName] = string(typed)
				}
			case string:
				strVal := typed
				trimmed := strings.TrimSpace(strVal)
				if len(field.Options) > 0 && strings.HasPrefix(trimmed, "[") && !multiPicklistHasDeclaredOptionValue(field.Options, trimmed) {
					var arr []string
					if err := json.Unmarshal([]byte(trimmed), &arr); err == nil {
						record[field.APIName] = strings.Join(arr, ";")
					}
				}
			}
			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
				}
			}
		}
	}
	return erasedBlanks
}

// 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]
}

func multiPicklistHasDeclaredOptionValue(options []models.PicklistOption, value string) bool {
	for _, option := range options {
		if option.Value == value {
			return true
		}
	}
	return false
}
