package packs

import (
	"fmt"
	"strings"

	"github.com/nexuscrm/analytics"
	domainschema "github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/fieldcapabilities"
	"github.com/nexuscrm/shared/pkg/formulastorage"
	"github.com/nexuscrm/shared/pkg/models"
)

type packFieldAuthoringCompat struct {
	allowLegacyAggregateFormulaAuthoring bool
}

func buildPackFieldSpecWrite(packName, objectAPIName, objectID string, fSpec FieldSpec, sm *metadata.SchemaManager, discriminatorParent string) (metadata.FieldWithContext, error) {
	return buildPackFieldSpecWriteWithResolver(packName, objectAPIName, objectID, fSpec, sm, discriminatorParent, objectSpecFieldResolver{}, packFieldAuthoringCompat{})
}

func buildPackFieldSpecWriteWithResolver(packName, objectAPIName, objectID string, fSpec FieldSpec, sm *metadata.SchemaManager, discriminatorParent string, resolver objectSpecFieldResolver, compat packFieldAuthoringCompat) (metadata.FieldWithContext, error) {
	if err := rejectPackAggregateFormulaAuthoring(fSpec.APIName, fSpec.Formula, compat); err != nil {
		return metadata.FieldWithContext{}, err
	}
	if err := validateSiblingMaxFieldSpec(objectAPIName, fSpec, resolver); err != nil {
		return metadata.FieldWithContext{}, err
	}
	field := buildPackFieldSpecMetadata(fSpec, discriminatorParent, resolver)
	if constants.NormalizeFieldType(fSpec.Type) == constants.FieldTypeRollupSummary {
		resolved, err := resolvePackRollupSummary(sm, objectAPIName, fSpec, resolver, nil)
		if err != nil {
			return metadata.FieldWithContext{}, err
		}
		applyResolvedPackRollupResult(field, resolved)
	}
	colDef, err := buildPackFieldSpecColumnDefinitionWithResolver(sm, objectAPIName, fSpec, discriminatorParent, resolver)
	if err != nil {
		return metadata.FieldWithContext{}, err
	}
	return metadata.FieldWithContext{
		Field:                field,
		TableName:            objectAPIName,
		ObjectID:             objectID,
		FieldID:              domainschema.GenerateFieldID(objectAPIName, fSpec.APIName),
		ColumnDefinition:     colDef,
		CapabilitiesDeclared: true,
		OwnerKind:            constants.FieldOwnerKindPack,
		OwnerPack:            packName,
	}, nil
}

func applyResolvedPackRollupResult(field *models.FieldMetadata, resolved domainschema.ResolvedRollup) {
	if field == nil {
		return
	}
	field.RollupConfig = domainschema.CloneRollupConfig(&resolved.CanonicalConfig)
	resultType := resolved.Result.Type
	field.ReturnType = &resultType
	field.Precision = clonePackRollupInt(resolved.Result.Precision)
	field.Scale = clonePackRollupInt(resolved.Result.Scale)
}

func clonePackRollupInt(value *int) *int {
	if value == nil {
		return nil
	}
	cloned := *value
	return &cloned
}

func buildPackFieldSpecMetadata(fSpec FieldSpec, discriminatorParent string, resolver objectSpecFieldResolver) *models.FieldMetadata {
	createable := true
	if fSpec.Createable != nil {
		createable = *fSpec.Createable
	}
	updateable := true
	if fSpec.Updateable != nil {
		updateable = *fSpec.Updateable
	}
	if discriminatorParent != "" {
		createable = false
		updateable = false
	}
	field := &models.FieldMetadata{
		APIName:               fSpec.APIName,
		Label:                 fSpec.Label,
		Type:                  constants.SchemaFieldType(fSpec.Type),
		Required:              packFieldRequired(fSpec),
		IsMasterDetail:        packFieldIsMasterDetail(fSpec),
		IsUnique:              fSpec.Unique,
		TrackHistory:          fSpec.TrackHistory,
		IsNameField:           fSpec.IsNameField,
		ReferenceTo:           append([]string(nil), fSpec.ReferenceTo...),
		WritableReferenceTo:   append([]string(nil), fSpec.WritableReferenceTo...),
		RelationshipName:      strPtr(fSpec.RelationshipName),
		ChildRelationshipName: strPtr(fSpec.ChildRelationshipName),
		IsPolymorphic:         fSpec.isPolymorphicReference(),
		Createable:            createable,
		Updateable:            updateable,
		Options:               fSpec.Options,
		RollupConfig:          cloneNormalizedRollupConfigWithResolver(fSpec.RollupConfig, resolver),
		WriteProtectionPolicy: fSpec.WriteProtectionPolicy,
		ReadProtectionPolicy:  fSpec.ReadProtectionPolicy,
	}
	if discriminatorParent != "" {
		field.GeneratedForPolymorphicReference = discriminatorParent
	}
	if rule := fSpec.normalizedDeleteConstraint(); rule != "" {
		field.DeleteRule = &rule
	}
	if fSpec.Default != "" {
		field.DefaultValue = &fSpec.Default
	}
	if fSpec.HelpText != "" {
		field.HelpText = &fSpec.HelpText
	}
	if strings.TrimSpace(fSpec.SiblingMaxParentField) != "" {
		parentField := strings.TrimSpace(fSpec.SiblingMaxParentField)
		field.SiblingMaxParentField = &parentField
	}
	if fSpec.Validator != "" {
		field.Validator = &fSpec.Validator
	}
	if fSpec.ValidatorConfig != "" {
		field.ValidatorConfig = &fSpec.ValidatorConfig
	}
	if fSpec.ControllingField != "" {
		field.ControllingField = &fSpec.ControllingField
	}
	if fSpec.Formula != "" {
		field.Formula = &fSpec.Formula
	}
	if fSpec.ReturnType != "" {
		returnType := constants.SchemaFieldType(fSpec.ReturnType)
		field.ReturnType = &returnType
	}
	if fSpec.Length > 0 {
		length := fSpec.Length
		field.MaxLength = &length
	}
	if fSpec.FormulaMode != "" {
		field.FormulaMode = &fSpec.FormulaMode
	}
	if fSpec.FormulaStorageMode != "" {
		field.FormulaStorageMode = &fSpec.FormulaStorageMode
	}
	if fSpec.ProjectionConfig != nil {
		field.ProjectionConfig = cloneProjectionConfig(fSpec.ProjectionConfig)
	}
	if fSpec.IsCompound {
		field.IsCompound = true
		field.ComposedFrom = append([]string(nil), fSpec.ComposedFrom...)
	}
	return field
}

func rejectPackAggregateFormulaAuthoring(fieldAPIName, expr string, compat packFieldAuthoringCompat) error {
	if compat.allowLegacyAggregateFormulaAuthoring {
		return nil
	}
	if strings.TrimSpace(expr) == "" {
		return nil
	}
	aggregates, err := analytics.FindAggregateAuthoringExprs(expr)
	if err != nil || len(aggregates) == 0 {
		return nil
	}
	return fmt.Errorf("field %s: %s", fieldAPIName, analytics.AggregateAuthoringRequiresRollupMessage)
}

func buildPackFieldSpecColumnDefinitionWithResolver(sm *metadata.SchemaManager, objectAPIName string, fSpec FieldSpec, discriminatorParent string, resolver objectSpecFieldResolver) (domainschema.ColumnDefinition, error) {
	if err := validateSiblingMaxFieldSpec(objectAPIName, fSpec, resolver); err != nil {
		return domainschema.ColumnDefinition{}, err
	}
	var sqlType string
	var resolvedRollup *domainschema.ResolvedRollup
	if constants.NormalizeFieldType(fSpec.Type) == constants.FieldTypeRollupSummary {
		resolved, err := resolvePackRollupSummary(sm, objectAPIName, fSpec, resolver, nil)
		if err != nil {
			return domainschema.ColumnDefinition{}, err
		}
		sqlType = resolved.SQLType
		resolvedRollup = &resolved
	} else {
		var err error
		sqlType, err = resolvePackFieldSpecStorageType(sm, objectAPIName, fSpec, resolver, nil)
		if err != nil {
			return domainschema.ColumnDefinition{}, err
		}
	}
	formulaStorage, err := formulastorage.Resolve(formulastorage.Spec{
		FieldType:           fSpec.Type,
		Formula:             fSpec.Formula,
		FormulaMode:         fSpec.FormulaMode,
		FormulaStorageMode:  fSpec.FormulaStorageMode,
		HasFormula:          strings.TrimSpace(fSpec.Formula) != "" || constants.NormalizeFieldType(fSpec.Type) == constants.FieldTypeFormula,
		HasReturnType:       strings.TrimSpace(fSpec.ReturnType) != "",
		HasProjectionConfig: fSpec.ProjectionConfig != nil,
	})
	if err != nil {
		return domainschema.ColumnDefinition{}, fmt.Errorf("field %s: %w", fSpec.APIName, err)
	}
	if capacity, ok := varcharCapacity(sqlType); ok && formulaStorage.RequiresPhysicalColumn() && fSpec.Length > capacity {
		return domainschema.ColumnDefinition{}, fmt.Errorf(
			"field %s length %d exceeds physical %s capacity",
			fSpec.APIName,
			fSpec.Length,
			sqlType,
		)
	}

	colDef := domainschema.ColumnDefinition{
		Name:                  fSpec.APIName,
		Label:                 fSpec.Label,
		Type:                  sqlType,
		LogicalType:           fSpec.Type,
		Nullable:              packFieldPhysicalNullable(fSpec),
		Unique:                fSpec.Unique,
		SiblingMaxParentField: strings.TrimSpace(fSpec.SiblingMaxParentField),
		ReferenceTo:           append([]string(nil), fSpec.ReferenceTo...),
		WritableReferenceTo:   append([]string(nil), fSpec.WritableReferenceTo...),
		IsPolymorphic:         fSpec.isPolymorphicReference(),
		IsMasterDetail:        packFieldIsMasterDetail(fSpec),
		WriteProtectionPolicy: fSpec.WriteProtectionPolicy,
		ReadProtectionPolicy:  fSpec.ReadProtectionPolicy,
	}
	if discriminatorParent != "" {
		colDef.GeneratedForPolymorphicReference = discriminatorParent
	}
	if fSpec.RelationshipName != "" {
		colDef.RelationshipName = fSpec.RelationshipName
	}
	if fSpec.ChildRelationshipName != "" {
		colDef.ChildRelationshipName = fSpec.ChildRelationshipName
	}
	switch fSpec.normalizedDeleteConstraint() {
	case constants.DeleteRuleCascade:
		colDef.OnDelete = "CASCADE"
	case constants.DeleteRuleSetNull:
		colDef.OnDelete = "SET NULL"
	case constants.DeleteRuleRestrict:
		colDef.OnDelete = "RESTRICT"
	}
	if fSpec.Default != "" {
		colDef.Default = "'" + fSpec.Default + "'"
	}
	if fSpec.Formula != "" {
		colDef.Formula = fSpec.Formula
	}
	if fSpec.ReturnType != "" {
		colDef.ReturnType = fSpec.ReturnType
	}
	if resolvedRollup != nil {
		colDef.RollupConfig = domainschema.CloneRollupConfig(&resolvedRollup.CanonicalConfig)
		colDef.ReturnType = string(resolvedRollup.Result.Type)
	}
	if fSpec.FormulaMode != "" {
		colDef.FormulaMode = fSpec.FormulaMode
	}
	if fSpec.FormulaStorageMode != "" {
		colDef.FormulaStorageMode = fSpec.FormulaStorageMode
	}
	if fSpec.ProjectionConfig != nil {
		colDef.ProjectionConfig = cloneProjectionConfig(fSpec.ProjectionConfig)
	}
	if fSpec.IsNameField {
		colDef.IsNameField = true
	}
	if len(fSpec.Options) > 0 {
		colDef.Options = fSpec.Options
	}
	if fSpec.IsCompound {
		colDef.IsCompound = true
		colDef.ComposedFrom = append([]string(nil), fSpec.ComposedFrom...)
	}
	return colDef, nil
}

func packFieldIsMasterDetail(fSpec FieldSpec) bool {
	return constants.NormalizeFieldType(fSpec.Type) == constants.FieldTypeMasterDetail
}

func packFieldRequired(fSpec FieldSpec) bool {
	return fSpec.Required || packFieldIsMasterDetail(fSpec)
}

func packFieldPhysicalNullable(fSpec FieldSpec) bool {
	return !packFieldIsMasterDetail(fSpec)
}

func validateSiblingMaxFieldSpec(objectAPIName string, fSpec FieldSpec, resolver objectSpecFieldResolver) error {
	parentFieldName := strings.TrimSpace(fSpec.SiblingMaxParentField)
	if parentFieldName == "" {
		return nil
	}
	targetFieldName := strings.TrimSpace(fSpec.APIName)
	if strings.EqualFold(parentFieldName, targetFieldName) {
		return fmt.Errorf("field %s siblingMaxParentField cannot point to itself", fSpec.APIName)
	}
	if strings.TrimSpace(fSpec.Default) != "" {
		return fmt.Errorf("field %s siblingMaxParentField cannot be combined with default", fSpec.APIName)
	}
	if !siblingMaxPackTargetWriteable(objectAPIName, fSpec, resolver) {
		return fmt.Errorf("field %s siblingMaxParentField requires a createable and updateable numeric target field", fSpec.APIName)
	}
	if strings.TrimSpace(fSpec.Formula) != "" ||
		fSpec.RollupConfig != nil ||
		constants.NormalizeFieldType(fSpec.Type) == constants.FieldTypeFormula ||
		constants.NormalizeFieldType(fSpec.Type) == constants.FieldTypeRollupSummary ||
		constants.NormalizeFieldType(fSpec.Type) == constants.FieldTypeAutonumber {
		return fmt.Errorf("field %s siblingMaxParentField requires a writable numeric target field", fSpec.APIName)
	}
	if !siblingMaxPackTargetTypeAllowed(fSpec.Type) {
		return fmt.Errorf("field %s siblingMaxParentField requires an integer-compatible numeric target field, got %q", fSpec.APIName, fSpec.Type)
	}
	if strings.TrimSpace(objectAPIName) == "" {
		return nil
	}
	if parentSpec, ok := resolver.find(objectAPIName, parentFieldName); ok {
		if !siblingMaxPackParentSpecAllowed(parentSpec) {
			return fmt.Errorf("field %s siblingMaxParentField %s must be a persisted non-polymorphic reference or master-detail field", fSpec.APIName, parentFieldName)
		}
		return nil
	}
	if parentMeta, ok := resolver.findSchemaField(objectAPIName, parentFieldName); ok {
		if !siblingMaxPackParentMetadataAllowed(parentMeta) {
			return fmt.Errorf("field %s siblingMaxParentField %s must be a persisted non-polymorphic reference or master-detail field", fSpec.APIName, parentFieldName)
		}
		return nil
	}
	return fmt.Errorf("field %s siblingMaxParentField %s not found on object %s", fSpec.APIName, parentFieldName, objectAPIName)
}

func siblingMaxPackTargetWriteable(objectAPIName string, fSpec FieldSpec, resolver objectSpecFieldResolver) bool {
	field := buildPackFieldSpecMetadata(fSpec, "", resolver)
	createable, updateable := fieldcapabilities.ApplyIntrinsicBounds(objectAPIName, field, field.Createable, field.Updateable)
	return createable && updateable
}

func siblingMaxPackTargetTypeAllowed(typeName string) bool {
	switch constants.NormalizeFieldType(typeName) {
	case "integer", "int":
		return true
	default:
		return false
	}
}

func siblingMaxPackParentSpecAllowed(field FieldSpec) bool {
	t := constants.NormalizeFieldType(field.Type)
	if t != constants.FieldTypeReference && t != constants.FieldTypeMasterDetail {
		return false
	}
	if field.IsPolymorphic || field.isPolymorphicReference() {
		return false
	}
	if len(field.ReferenceTo) != 1 || strings.TrimSpace(field.ReferenceTo[0]) == "" {
		return false
	}
	if strings.TrimSpace(field.Formula) != "" ||
		field.ProjectionConfig != nil ||
		field.RollupConfig != nil ||
		field.IsCompound {
		return false
	}
	return true
}

func siblingMaxPackParentMetadataAllowed(field *models.FieldMetadata) bool {
	if field == nil {
		return false
	}
	t := constants.NormalizeFieldType(string(field.Type))
	if t != constants.FieldTypeReference && t != constants.FieldTypeMasterDetail {
		return false
	}
	if field.IsPolymorphic {
		return false
	}
	if len(field.ReferenceTo) != 1 || strings.TrimSpace(field.ReferenceTo[0]) == "" {
		return false
	}
	if field.IsVirtual ||
		field.ProjectionConfig != nil ||
		field.RollupConfig != nil ||
		strings.TrimSpace(field.GeneratedForPolymorphicReference) != "" {
		return false
	}
	if field.Formula != nil && strings.TrimSpace(*field.Formula) != "" {
		return false
	}
	return true
}

type objectSpecFieldResolver struct {
	fieldsByObject  map[string]map[string]FieldSpec
	objectAPINames  map[string]string
	schemasByObject map[string]*models.ObjectMetadata
	fallbackSchema  func(string) *models.ObjectMetadata
}

func newObjectSpecFieldResolver(specs []ObjectSpec) objectSpecFieldResolver {
	return newObjectSpecFieldResolverWithSchemas(specs, nil)
}

func newObjectSpecFieldResolverWithSchemas(specs []ObjectSpec, schemas map[string]*models.ObjectMetadata) objectSpecFieldResolver {
	return newObjectSpecFieldResolverWithFallback(specs, schemas, nil)
}

func newObjectSpecFieldResolverWithFallback(specs []ObjectSpec, schemas map[string]*models.ObjectMetadata, fallbackSchema func(string) *models.ObjectMetadata) objectSpecFieldResolver {
	fieldsByObject := make(map[string]map[string]FieldSpec, len(specs))
	objectAPINames := make(map[string]string, len(specs)+len(schemas))
	for _, spec := range specs {
		objectKey := strings.ToLower(strings.TrimSpace(spec.APIName))
		if objectKey == "" {
			continue
		}
		objectAPINames[objectKey] = spec.APIName
		fields := make(map[string]FieldSpec, len(spec.Fields))
		for _, field := range spec.Fields {
			fieldKey := strings.ToLower(strings.TrimSpace(field.APIName))
			if fieldKey == "" {
				continue
			}
			fields[fieldKey] = field
		}
		fieldsByObject[objectKey] = fields
	}
	schemasByObject := make(map[string]*models.ObjectMetadata, len(schemas))
	for objectAPIName, schema := range schemas {
		if schema == nil {
			continue
		}
		canonicalObjectAPIName := strings.TrimSpace(schema.APIName)
		if canonicalObjectAPIName == "" {
			canonicalObjectAPIName = strings.TrimSpace(objectAPIName)
		}
		key := strings.ToLower(canonicalObjectAPIName)
		if key == "" {
			continue
		}
		objectAPINames[key] = canonicalObjectAPIName
		schemasByObject[key] = schema
	}
	return objectSpecFieldResolver{fieldsByObject: fieldsByObject, objectAPINames: objectAPINames, schemasByObject: schemasByObject, fallbackSchema: fallbackSchema}
}

func (r objectSpecFieldResolver) find(objectAPIName, fieldAPIName string) (FieldSpec, bool) {
	fields := r.fieldsByObject[strings.ToLower(strings.TrimSpace(objectAPIName))]
	if fields == nil {
		return FieldSpec{}, false
	}
	field, ok := fields[strings.ToLower(strings.TrimSpace(fieldAPIName))]
	return field, ok
}

func (r objectSpecFieldResolver) findSchemaField(objectAPIName, fieldAPIName string) (*models.FieldMetadata, bool) {
	schema := r.schemaForObject(objectAPIName)
	if schema == nil {
		return nil, false
	}
	field := models.FindField(schema, fieldAPIName)
	if field == nil {
		return nil, false
	}
	return field, true
}

func (r objectSpecFieldResolver) canonicalObjectAPIName(objectAPIName string) (string, bool) {
	key := strings.ToLower(strings.TrimSpace(objectAPIName))
	if objectName, ok := r.objectAPINames[key]; ok {
		return objectName, true
	}
	if schema := r.fallbackObjectSchema(objectAPIName); schema != nil {
		canonical := strings.TrimSpace(schema.APIName)
		if canonical == "" {
			canonical = strings.TrimSpace(objectAPIName)
		}
		if canonical != "" {
			return canonical, true
		}
	}
	return "", false
}

func (r objectSpecFieldResolver) schemaForObject(objectAPIName string) *models.ObjectMetadata {
	if schema := r.schemasByObject[strings.ToLower(strings.TrimSpace(objectAPIName))]; schema != nil {
		return schema
	}
	return r.fallbackObjectSchema(objectAPIName)
}

func (r objectSpecFieldResolver) fallbackObjectSchema(objectAPIName string) *models.ObjectMetadata {
	if r.fallbackSchema == nil {
		return nil
	}
	return r.fallbackSchema(strings.TrimSpace(objectAPIName))
}

func (r objectSpecFieldResolver) displayNameChildFieldType(objectAPIName, fieldAPIName string) (constants.SchemaFieldType, bool) {
	if field, ok := r.find(objectAPIName, fieldAPIName); ok {
		if field.APIName != fieldAPIName {
			return "", false
		}
		return constants.NormalizeFieldType(field.Type), true
	}
	if field, ok := r.findSchemaField(objectAPIName, fieldAPIName); ok {
		if field.APIName != fieldAPIName {
			return "", false
		}
		return constants.NormalizeFieldType(string(field.Type)), true
	}
	return "", false
}

func resolvePackFieldSpecStorageType(sm *metadata.SchemaManager, objectAPIName string, fSpec FieldSpec, resolver objectSpecFieldResolver, seen map[string]bool) (string, error) {
	if strings.TrimSpace(fSpec.Formula) != "" || constants.NormalizeFieldType(fSpec.Type) == constants.FieldTypeFormula {
		return resolveFormulaBackedStorageType(sm, fSpec)
	}
	if constants.NormalizeFieldType(fSpec.Type) != constants.FieldTypeRollupSummary {
		sqlType := sm.MapFieldTypeToSQL(fSpec.Type)
		if sqlType == "" || sqlType == "INVALID_TYPE_ERROR" {
			return "", fmt.Errorf("unknown logical type %q", fSpec.Type)
		}
		return sqlType, nil
	}
	return resolveRollupSummaryStorageType(sm, objectAPIName, fSpec, resolver, seen)
}

func resolveRollupSummaryStorageType(sm *metadata.SchemaManager, objectAPIName string, fSpec FieldSpec, resolver objectSpecFieldResolver, seen map[string]bool) (string, error) {
	resolved, err := resolvePackRollupSummary(sm, objectAPIName, fSpec, resolver, seen)
	if err != nil {
		return "", err
	}
	return resolved.SQLType, nil
}

func resolvePackRollupSummary(sm *metadata.SchemaManager, objectAPIName string, fSpec FieldSpec, resolver objectSpecFieldResolver, seen map[string]bool) (domainschema.ResolvedRollup, error) {
	if fSpec.RollupConfig == nil {
		return domainschema.ResolvedRollup{}, fmt.Errorf("rollupsummary field %s missing rollupConfig", fSpec.APIName)
	}

	canonical, err := domainschema.ValidateAndCanonicalizeRollupConfig(objectAPIName, *fSpec.RollupConfig, resolver.schemaLookupFunc())
	if err != nil {
		return domainschema.ResolvedRollup{}, fmt.Errorf("rollupsummary field %s: %w", fSpec.APIName, err)
	}
	calcType := canonical.CalcType
	summaryObject := canonical.SummaryObject

	// COUNT resolves directly via the domain/schema contract — no source
	// field resolution needed.
	if calcType == "COUNT" {
		resolved, err := domainschema.ResolveRollup(objectAPIName, canonical, resolver.schemaLookupFunc(), "")
		if err != nil {
			return domainschema.ResolvedRollup{}, fmt.Errorf("rollupsummary field %s: %w", fSpec.APIName, err)
		}
		return resolved, nil
	}

	summaryField := canonical.SummaryField

	// Resolve the source field's SQL type by walking the spec/schema tree.
	var summarySQLType string
	if summarySpec, ok := resolver.find(summaryObject, summaryField); ok {
		seenKey := strings.ToLower(summaryObject) + "." + strings.ToLower(summaryField)
		if seen != nil && seen[seenKey] {
			return domainschema.ResolvedRollup{}, fmt.Errorf("rollupsummary field %s detected cycle while resolving %s.%s", fSpec.APIName, summaryObject, summaryField)
		}
		nextSeen := cloneSeenKeys(seen)
		nextSeen[seenKey] = true

		summarySQLType, err = resolvePackFieldSpecStorageType(sm, summaryObject, summarySpec, resolver, nextSeen)
		if err != nil {
			return domainschema.ResolvedRollup{}, fmt.Errorf("rollupsummary field %s failed to resolve summary field %s.%s: %w", fSpec.APIName, summaryObject, summaryField, err)
		}
	} else if summaryFieldMeta, ok := resolver.findSchemaField(summaryObject, summaryField); ok {
		var err error
		summarySQLType, err = resolveSchemaFieldStorageType(sm, summaryFieldMeta)
		if err != nil {
			return domainschema.ResolvedRollup{}, fmt.Errorf("rollupsummary field %s failed to resolve summary field %s.%s from pack schema: %w", fSpec.APIName, summaryObject, summaryField, err)
		}
	} else {
		return domainschema.ResolvedRollup{}, fmt.Errorf("rollupsummary field %s summary field %s.%s not found in object spec install set", fSpec.APIName, summaryObject, summaryField)
	}

	resolved, err := domainschema.ResolveRollup(objectAPIName, canonical, resolver.schemaLookupFunc(), summarySQLType)
	if err != nil {
		return domainschema.ResolvedRollup{}, fmt.Errorf("rollupsummary field %s invalid summary field %s.%s for %s: %w", fSpec.APIName, summaryObject, summaryField, calcType, err)
	}
	return resolved, nil
}

func cloneNormalizedRollupConfig(cfg *models.RollupConfig) *models.RollupConfig {
	return cloneNormalizedRollupConfigWithResolver(cfg, objectSpecFieldResolver{})
}

// cloneNormalizedRollupConfigWithResolver canonicalizes a RollupConfig by
// delegating to domainschema.CanonicalizeRollupConfig with a SchemaLookupFunc
// adapter built from the pack resolver. This ensures pack/apply and metadata
// CRUD use the same canonicalization logic — one SSoT.
func cloneNormalizedRollupConfigWithResolver(cfg *models.RollupConfig, resolver objectSpecFieldResolver) *models.RollupConfig {
	if cfg == nil {
		return nil
	}
	lookup := resolver.schemaLookupFunc()
	canonical := domainschema.CanonicalizeRollupConfig(*cfg, lookup)
	return &canonical
}

// schemaLookupFunc builds a domainschema.SchemaLookupFunc from the pack
// resolver's object specs and installed schemas. This adapter allows the
// domain/schema canonicalization to resolve SummaryObject, SummaryField,
// and RelationshipField to live API names from the pack's view.
func (r objectSpecFieldResolver) schemaLookupFunc() domainschema.SchemaLookupFunc {
	return func(objectAPIName string) (*models.ObjectMetadata, error) {
		key := strings.ToLower(strings.TrimSpace(objectAPIName))
		apiName, _ := r.objectAPINames[key]
		union := make(map[string]models.FieldMetadata)
		order := make([]string, 0)

		if schema, ok := r.schemasByObject[key]; ok {
			if strings.TrimSpace(schema.APIName) != "" {
				apiName = schema.APIName
			}
			for _, field := range schema.Fields {
				fieldKey := strings.ToLower(strings.TrimSpace(field.APIName))
				if fieldKey == "" {
					continue
				}
				union[fieldKey] = field
				order = append(order, fieldKey)
			}
		}

		if fields, ok := r.fieldsByObject[key]; ok {
			for _, fSpec := range fields {
				fieldKey := strings.ToLower(strings.TrimSpace(fSpec.APIName))
				if fieldKey == "" {
					continue
				}
				if _, seen := union[fieldKey]; !seen {
					order = append(order, fieldKey)
				}
				var returnType *constants.SchemaFieldType
				if strings.TrimSpace(fSpec.ReturnType) != "" {
					rt := constants.SchemaFieldType(fSpec.ReturnType)
					returnType = &rt
				}
				union[fieldKey] = models.FieldMetadata{
					APIName:             fSpec.APIName,
					Type:                constants.SchemaFieldType(fSpec.Type),
					ReturnType:          returnType,
					RollupConfig:        domainschema.CloneRollupConfig(fSpec.RollupConfig),
					ReferenceTo:         append([]string(nil), fSpec.ReferenceTo...),
					WritableReferenceTo: append([]string(nil), fSpec.WritableReferenceTo...),
					IsPolymorphic:       fSpec.isPolymorphicReference(),
				}
			}
		}

		if apiName != "" && len(union) > 0 {
			mdFields := make([]models.FieldMetadata, 0, len(order))
			seen := make(map[string]struct{}, len(order))
			for _, fieldKey := range order {
				if _, done := seen[fieldKey]; done {
					continue
				}
				field, ok := union[fieldKey]
				if !ok {
					continue
				}
				mdFields = append(mdFields, field)
				seen[fieldKey] = struct{}{}
			}
			return &models.ObjectMetadata{
				APIName: apiName,
				Fields:  mdFields,
			}, nil
		}
		return nil, fmt.Errorf("object %q not found in pack resolver", objectAPIName)
	}
}

func resolveFormulaBackedStorageType(sm *metadata.SchemaManager, fSpec FieldSpec) (string, error) {
	typeName := strings.TrimSpace(fSpec.ReturnType)
	if typeName == "" {
		typeName = strings.TrimSpace(fSpec.Type)
	}
	sqlType := sm.MapFieldTypeToSQL(typeName)
	if sqlType == "" || sqlType == "INVALID_TYPE_ERROR" {
		return "", fmt.Errorf("formula field %s has unresolvable returnType %q", fSpec.APIName, fSpec.ReturnType)
	}
	return sqlType, nil
}

func resolveSchemaFieldStorageType(sm *metadata.SchemaManager, field *models.FieldMetadata) (string, error) {
	return domainschema.ResolveSchemaFieldSQLType(field, sm.MapFieldTypeToSQL)
}

func cloneSeenKeys(in map[string]bool) map[string]bool {
	out := make(map[string]bool, len(in)+1)
	for k, v := range in {
		out[k] = v
	}
	return out
}
