package records

import (
	"fmt"
	"strings"

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

// resolveCompoundFields derives compound-parent field values from their
// declared member fields. Salesforce-parity contract (RFC §3.2 "strict,
// SF-correct"): compound parents are read-only; explicit payload entries
// for the parent are **silently dropped**, then the value is re-derived
// from ComposedFrom members.
//
// This matches SF's handling of createable=false compound fields: a POST
// with {"Name": "x", "FirstName": "Ada", "LastName": "Lovelace"} ends up
// with Name="Ada Lovelace", not "x". The prune step
// (pruneNonCreateableFields) already strips compound parents for
// non-system callers; this resolver re-applies the strip for defense-in-
// depth and covers system-context callers that bypass prune.
//
// Empty-member handling: blank members are skipped (not emitted as empty
// parts), so a 3-member compound with a blank middle produces
// "First Last" rather than "First  Last". If ALL members are blank the
// parent stays absent — validateRequiredForCreate already skips compound
// parents (they are never required-for-create from the caller's side).
//
// Replaces the hardcoded Quote.Name ← QuoteNumber fallback that lived in
// persistence_insert.go. Issue #1582 RFC v1.5 PR C.
//
// Post-review fix: the earlier "preserve existing non-blank parent"
// behavior deviated from the RFC strict-strip contract (code-reviewer
// MAJOR #3). Reverted to RFC-canonical strip-then-derive.
//
// Autonumber coexistence (post-#1582): resolveCompoundFields runs AFTER
// assignAutoNumbers inside the transaction. By that point user-supplied
// values have already been stripped by pruneNonCreateableFields (compound
// parents are non-createable). Any non-blank parent value therefore comes
// from the autonumber engine — a system-rendered value that must not be
// overwritten. This matters for tenant orgs where Quote.Name carries a
// rich per-prefix format (e.g. "JP-QT-260423-01") alongside the standard
// compound declaration (composedFrom: ["QuoteNumber"]). Without this
// guard the compound resolver would replace the formatted value with the
// plain QuoteNumber ("00000001").
//
// Non-tenant orgs have no autonumber format on the compound parent, so
// the parent arrives blank → the resolver derives it from members as
// before. Both paths converge correctly.
func resolveCompoundFields(schema *models.ObjectMetadata, data models.SObject) {
	resolveCompoundFieldsWithDeclarations(schema, data, func(field models.FieldMetadata) (bool, []string) {
		return fieldprovenance.ClassifyCompound(schema.APIName, field.APIName)
	})
}

// resolveRequestBoundCompoundFields uses only the immutable ObjectMetadata
// selected from the request's RuntimeMetadataView. The independently deployed
// Runtime must not consult the process-global legacy field-provenance
// classifier: that classifier is installation/process composition state and is
// not bound to the request's Catalog revision.
func resolveRequestBoundCompoundFields(schema *models.ObjectMetadata, data models.SObject) {
	resolveCompoundFieldsWithDeclarations(schema, data, func(field models.FieldMetadata) (bool, []string) {
		return field.IsCompound, field.ComposedFrom
	})
}

func resolveCompoundFieldsWithDeclarations(
	schema *models.ObjectMetadata,
	data models.SObject,
	declaration func(models.FieldMetadata) (bool, []string),
) {
	if schema == nil || len(data) == 0 {
		return
	}
	for _, f := range schema.Fields {
		isCompound, composedFrom := declaration(f)
		if !isCompound || len(composedFrom) == 0 {
			continue
		}
		// If the parent already holds a non-blank value (rendered by the
		// autonumber engine earlier in this transaction), keep it.
		// User-supplied values were already stripped by
		// pruneNonCreateableFields before the transaction began.
		if existing, ok := data[f.APIName]; ok && !isBlankValue(existing) {
			continue
		}
		delete(data, f.APIName)
		var parts []string
		for _, member := range composedFrom {
			v, ok := data[member]
			if !ok || isBlankValue(v) {
				continue
			}
			if s, ok := v.(string); ok {
				parts = append(parts, strings.TrimSpace(s))
				continue
			}
			parts = append(parts, stringifyCompoundMember(v))
		}
		if len(parts) == 0 {
			continue
		}
		data[f.APIName] = strings.Join(parts, " ")
	}
}

// stringifyCompoundMember coerces a non-string member value to its
// display string. Autonumber renders arrive as strings today, but
// formulas / numeric fields can surface as numeric types; fall back to
// fmt.Sprint so the compound join never silently drops a numeric member.
func stringifyCompoundMember(v interface{}) string {
	switch val := v.(type) {
	case string:
		return val
	case []byte:
		return string(val)
	}
	return strings.TrimSpace(fmt.Sprint(v))
}
