package schemautil

import (
	"fmt"
	"strings"

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

// CompoundMemberNames returns the canonical member list for a compound parent.
// The normal source of truth is fieldprovenance, which is rebuilt from L0
// system metadata plus active/installing source-backed pack payloads. Virtual
// compound decorators may also declare IsCompound/ComposedFrom directly on the
// effective schema because those fields do not have _System_Field rows.
func CompoundMemberNames(schema *models.ObjectMetadata, parentAPIName string) []string {
	if schema == nil || strings.TrimSpace(parentAPIName) == "" {
		return nil
	}
	if field := models.FindField(schema, parentAPIName); field != nil && field.IsCompound && len(field.ComposedFrom) > 0 {
		return canonicalSchemaFieldNames(schema, field.ComposedFrom)
	}
	isCompound, members := fieldprovenance.ClassifyCompound(schema.APIName, parentAPIName)
	if !isCompound || len(members) == 0 {
		return nil
	}
	return canonicalSchemaFieldNames(schema, members)
}

// HasCompoundMemberChange reports whether a mutation payload changes any
// canonical member of a compound parent.
func HasCompoundMemberChange(schema *models.ObjectMetadata, parentAPIName string, changes map[string]interface{}) bool {
	if len(changes) == 0 {
		return false
	}
	for _, member := range CompoundMemberNames(schema, parentAPIName) {
		if _, ok := lookupCaseInsensitive(changes, member); ok {
			return true
		}
	}
	return false
}

// CompoundFieldValue synthesizes the parent display value from canonical
// compound members present in data. Blank members are skipped; a fully blank
// compound returns ok=false.
func CompoundFieldValue(schema *models.ObjectMetadata, parentAPIName string, data map[string]interface{}) (value string, ok bool) {
	if len(data) == 0 {
		return "", false
	}
	members := CompoundMemberNames(schema, parentAPIName)
	if len(members) == 0 {
		return "", false
	}
	parts := make([]string, 0, len(members))
	for _, member := range members {
		raw, present := lookupCaseInsensitive(data, member)
		if !present || isBlankCompoundValue(raw) {
			continue
		}
		parts = append(parts, stringifyCompoundValue(raw))
	}
	if len(parts) == 0 {
		return "", false
	}
	return strings.Join(parts, " "), true
}

// SynthesizeVirtualCompoundFields fills in read-only virtual compound parent
// fields (e.g. User.Name composed from FirstName+LastName) on a single
// retrieved record. These parents have no physical column, so the persistence
// layer returns them absent or nil. The SOQL/analytics read path already
// projects them (analytics.projectVirtualCompoundFields); the single-record
// REST read paths — GET /sobjects/{object}/{id} and the ui-api record-ui
// aggregator — must do the same, otherwise reference lookups that fetch a
// referenced record to resolve its label fall back to the raw Id.
//
// Issue #976: user reference fields (Owner, AE, …) rendered the raw User Id in
// the Lead edit form because the lookup component fetched the User via
// record-ui / GetRecord, both of which returned Name=null.
//
// Only fields flagged IsVirtual + VirtualKind==VirtualFieldCompound are
// synthesized, matching the analytics predicate; compound parents that carry a
// physical/autonumber-rendered value keep it (the non-blank guard).
//
// FLS: the single-record read paths call this AFTER the persistence layer has
// applied field-level security (Retrieve / RetrieveWithAccess →
// CheckFieldVisibilityWithUser). Synthesizing unconditionally would reintroduce
// a virtual parent (e.g. User.Name) that the caller's profile explicitly denies
// via FieldPermissions, which the SOQL/analytics path never does — it gates the
// virtual parent on the parent's effective read permission (buildQueryFields /
// projectVirtualCompoundFields only project FLS-visible requested fields).
//
// The fieldVisible predicate restores that parity and is checked for BOTH the
// virtual parent AND every component that defines it. Because component values
// arrive already FLS-filtered, a denied component is simply absent from the
// record; CompoundFieldValue skips absent members and would otherwise build a
// PARTIAL parent from the remaining visible components — a field-visibility
// bypass at the exact read paths this guards. A parent is therefore synthesized
// only when the caller can read the parent and every component, and every
// component key is present in the filtered record (present nil/blank is fine; a
// missing key means the component was dropped by FLS → fail closed). A nil
// predicate means "no FLS gate" and must only be used in trusted/test contexts
// that have already enforced visibility.
func SynthesizeVirtualCompoundFields(schema *models.ObjectMetadata, record models.SObject, fieldVisible func(fieldAPIName string) bool) {
	if schema == nil || record == nil {
		return
	}
	for i := range schema.Fields {
		f := &schema.Fields[i]
		if !f.IsVirtual || f.VirtualKind != constants.VirtualFieldCompound {
			continue
		}
		if fieldVisible != nil && !fieldVisible(f.APIName) {
			// Caller's profile cannot read this virtual parent — do not
			// reintroduce it after FLS filtering.
			continue
		}
		if !compoundMembersFullyReadable(schema, record, f.APIName, fieldVisible) {
			// At least one defining component is denied (or dropped post-FLS) —
			// fail closed rather than synthesize a partial parent value.
			continue
		}
		if existing, ok := record[f.APIName]; ok && !isBlankCompoundValue(existing) {
			continue
		}
		if value, ok := CompoundFieldValue(schema, f.APIName, record); ok {
			record[f.APIName] = value
		}
	}
}

// compoundMembersFullyReadable reports whether every canonical member of the
// virtual compound parent is readable by the caller AND present in the
// (already FLS-filtered) record. It is the fail-closed gate that prevents a
// synthesized parent from being built out of a denied component: a denied
// member is dropped from the filtered record, so a missing key is treated as
// "not readable". Present nil/blank values are allowed, so the legitimate
// "only one component populated" case still synthesizes.
func compoundMembersFullyReadable(schema *models.ObjectMetadata, record models.SObject, parentAPIName string, fieldVisible func(fieldAPIName string) bool) bool {
	members := CompoundMemberNames(schema, parentAPIName)
	if len(members) == 0 {
		return false
	}
	for _, member := range members {
		if fieldVisible != nil && !fieldVisible(member) {
			return false
		}
		if _, present := lookupCaseInsensitive(record, member); !present {
			return false
		}
	}
	return true
}

func canonicalSchemaFieldNames(schema *models.ObjectMetadata, fieldNames []string) []string {
	out := make([]string, 0, len(fieldNames))
	for _, name := range fieldNames {
		canonical, ok := schemaFieldAPIName(schema, name)
		if !ok {
			continue
		}
		out = append(out, canonical)
	}
	return out
}

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

func lookupCaseInsensitive(data map[string]interface{}, key string) (interface{}, bool) {
	if data == nil {
		return nil, false
	}
	if value, ok := data[key]; ok {
		return value, true
	}
	for existingKey, value := range data {
		if strings.EqualFold(existingKey, key) {
			return value, true
		}
	}
	return nil, false
}

func isBlankCompoundValue(value interface{}) bool {
	if value == nil {
		return true
	}
	if s, ok := value.(string); ok {
		return strings.TrimSpace(s) == ""
	}
	return false
}

func stringifyCompoundValue(value interface{}) string {
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed)
	case []byte:
		return strings.TrimSpace(string(typed))
	default:
		return strings.TrimSpace(fmt.Sprint(typed))
	}
}
